在某一天,我发现我需要一个视频剪切操作,然后翻遍百度,看到一个还算比较靠谱的东西FFmpeg,然后又百度其使用方法,一大堆编译so库,一大堆命令,简直令人窒息!为此,我打开google开始我的上网之旅….算了,不逼逼了,直接接入正题:
FFmpeg定义:FFmpeg是一个视频解码的东西,它包括8个库:
1)avcodec:编解码(最重要的库)
2)avformat:封装格式处理
3)avfiler:滤镜特效处理
4)avdevice:各种设备的输入输出
5)avutil:工具库(大部分库都需要这个库的支持)
6)postpro:后加工
7)swresaple:音频采样数据格式转换
8)swscale:视频像素格式转换
今天我所需要的是视频剪切功能,我就不管哪个库了,先来一堆操作:
//视频剪切类
implementation ‘nl.bravobit:android-ffmpeg:1.1.5’
我们先来导入一个库,别问我哪来的,我翻了三天google翻出来的,刺激;
然后第一步:
if (!FFmpeg.getInstance(context).isSupported) {
Log.e(“TAG”, “MainApplication Android cup arch not supported!”)
}
我们先要判断是否支持,这个方法里面是创建FFmpeg的文件夹,会自动创建,只要写这么一句,最好是放在Application中写,做于初始化操作,
FFmpeg的剪切视频的操作主要靠命令:
package com.airiche.nixplay.Utils.VideoUtrl
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.util.Log
import com.airiche.nixplay.Utils.VideoUtrl.caliback.SingleCallback
import com.airiche.nixplay.Utils.VideoUtrl.interfaces.VideoTrimListener
import com.airiche.nixplay.Utils.VideoUtrl.thread.BackgroundExecutor
import nl.bravobit.ffmpeg.ExecuteBinaryResponseHandler
import nl.bravobit.ffmpeg.FFmpeg
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object VideoTrimmerUtil {
//剪切视频 传入 上下文对象 所需剪切的视频地址 保存的地址 开始时间 结束时间 一个接口类得到是否成功剪切
fun trim(
context: Context,
inputFile: String,
outputFile: String,
startMs: Long,
endMs: Long,
callback: VideoTrimListener
) {
var outputFile = outputFile
Log.e(
“TAG”, “context:” + context + ” inputFile:” + inputFile +
” outputFile:” + outputFile + ” startMs:” + startMs + ” endMs:” + endMs
+ ” callback:” + callback
)
val timeStamp = SimpleDateFormat(“yyyyMMdd_HHmmss”, Locale.getDefault()).format(Date())
val outputName = “trimmedVideo_$timeStamp.mp4”
outputFile = “$outputFile/$outputName”
val start = convertSecondsToTime(startMs / 1000)
val duration = convertSecondsToTime((endMs – startMs) / 1000)
val cmd = “-ss $start -t $duration -accurate_seek -i $inputFile -codec copy -avoid_negative_ts 1 $outputFile”
Log.e(“TAG”, “cmd:$cmd”)
val command = cmd.split(” “.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
try {
val tempOutFile = outputFile
FFmpeg.getInstance(context).execute(command, object : ExecuteBinaryResponseHandler() {
override fun onSuccess(s: String?) {
Log.e(“TAG”, “文件地址是;$tempOutFile”)
callback.onFinishTrim(tempOutFile)
}
override fun onStart() {
callback.onStartTrim()
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun convertSecondsToTime(seconds: Long): String {
var timeStr: String? = null
var hour = 0
var minute = 0
var second = 0
if (seconds <= 0) {
return “00:00”
} else {
minute = seconds.toInt() / 60
if (minute < 60) {
second = seconds.toInt() % 60
timeStr = “00:” + unitFormat(minute) + “:” + unitFormat(second)
} else {
hour = minute / 60
if (hour > 99) return “99:59:59”
minute = minute % 60
second = (seconds – (hour * 3600).toLong() – (minute * 60).toLong()).toInt()
timeStr = unitFormat(hour) + “:” + unitFormat(minute) + “:” + unitFormat(second)
}
}
return timeStr
}
private fun unitFormat(i: Int): String {
var retStr: String? = null
if (i >= 0 && i < 10) {
retStr = “0” + Integer.toString(i)
} else {
retStr = “” + i
}
return retStr
}
}
log打印值大概是:E/TAG: context:com.airiche.nixplay.MainApplication@b404949 inputFile:/storage/emulated/0/Pictures/VIDEO_20191202_171820.mp4 outputFile:/storage/emulated/0/Android/data/com.airiche.nixplay/cache startMs:0 endMs:15000 callback:com.airiche.nixplay.Activity.PhotoUploadActivity$onResume$1@3a2ce32
E/TAG: cmd:-ss 00:00 -t 00:00:15 -accurate_seek -i /storage/emulated/0/Pictures/VIDEO_20191202_171820.mp4 -codec copy -avoid_negative_ts 1 /storage/emulated/0/Android/data/com.airiche.nixplay/cache/trimmedVideo_20191203_114119.mp4
文件地址是;/storage/emulated/0/Android/data/com.airiche.nixplay/cache/trimmedVideo_20191203_114119.mp4
在代码中有个回调接口,那个接口是我需要在主界面做操作的接口,你们可以自定义,我也贴出来:
interface VideoTrimListener {
fun onStartTrim()
fun onFinishTrim(url: String)
fun onCancel()
}
注意啊你们,我的代码都是使用的kotlin代码,等会再贴一个java的,我们现在来直接使用:
VideoTrimmerUtil.trim(
BaseUtils.context,//这个就是上下文对象,直接在activity中使用this即可
“/storage/emulated/0/Pictures/VIDEO_20191202_171820.mp4”,//这个是你的视频地址,最好是获取到
StorageUtils.cacheDir!!,//这个是保存的地址,等会我贴一下这个代码
0,//截取的视频的开始时间
15000,//需要截取的终止时间
//这个就是我的接口了,你们可以看着改的
object : VideoTrimListener {
override fun onStartTrim() {
Log.e(“TAG”, “onStartTrim;”)
}
override fun onFinishTrim(url: String) {
Log.e(“TAG”, “onFinishTrim;$url”)
}
override fun onCancel() {
Log.e(“TAG”, “onCancel;”)
}
})
使用以后就会看到上面的log了!
,我们贴一下获取保存地址的代码:
package com.airiche.nixplay.Utils.VideoUtrl
import android.annotation.SuppressLint
import android.content.Context
import android.os.Environment
import android.text.TextUtils
import android.util.Log
import com.airiche.nixplay.BuildConfig
import java.io.File
import java.io.IOException
import java.util.Locale
object StorageUtils {
private val TAG = “StorageUtils”
private val APP_DATA_PATH = “/Android/data/” + BuildConfig.APPLICATION_ID
private var sDataDir: String? = null
private var sCacheDir: String? = null
//判断文件目录是否存在
val appDataDir: String?
get() {
if (TextUtils.isEmpty(sDataDir)) {
try {
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
sDataDir = android.os.Environment.getExternalStorageDirectory().path + APP_DATA_PATH
if (TextUtils.isEmpty(sDataDir)) {
sDataDir = BaseUtils.context.filesDir.absolutePath
}
} else {
sDataDir = BaseUtils.context.filesDir.absolutePath
}
} catch (e: Throwable) {
e.printStackTrace()
sDataDir = BaseUtils.context.filesDir.absolutePath
}
val file = File(sDataDir!!)
if (!file.exists()) {
file.mkdirs()
}
}
return sDataDir
}
val cacheDir: String?
get() {
if (TextUtils.isEmpty(sCacheDir)) {
var file: File? = null
val context = BaseUtils.context
try {
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
file = context.externalCacheDir
if (file == null || !file.exists()) {
file = getExternalCacheDirManual(context)
}
}
if (file == null) {
file = context.cacheDir
if (file == null || !file.exists()) {
file = getCacheDirManual(context)
}
}
Log.w(TAG, “cache dir = ” + file.absolutePath)
sCacheDir = file.absolutePath
} catch (ignored: Throwable) {
}
}
return sCacheDir
}
private fun getExternalCacheDirManual(context: Context): File? {
val dataDir = File(File(Environment.getExternalStorageDirectory(), “Android”), “data”)
val appCacheDir = File(File(dataDir, context.packageName), “cache”)
if (!appCacheDir.exists()) {
if (!appCacheDir.mkdirs()) {//
Log.w(TAG, “Unable to create external cache directory”)
return null
}
try {
File(appCacheDir, “.nomedia”).createNewFile()
} catch (e: IOException) {
Log.i(TAG, “Can’t create \”.nomedia\” file in application external cache directory”)
}
}
return appCacheDir
}
@SuppressLint(“SdCardPath”)
private fun getCacheDirManual(context: Context): File {
val cacheDirPath = “/data/data/” + context.packageName + “/cache”
return File(cacheDirPath)
}
// 功能描述:删除文件夹下所有文件和文件夹
fun delFiles(path: String): Boolean {
val cacheFile = File(path)
if (!cacheFile.exists()) {
return false
}
val files = cacheFile.listFiles()
for (i in files!!.indices) {
// 是文件则直接删除
if (files[i].exists() && files[i].isFile) {
files[i].delete()
} else if (files[i].exists() && files[i].isDirectory) {
// 递归删除文件
delFiles(files[i].absolutePath)
// 删除完目录下面的所有文件后再删除该文件夹
files[i].delete()
}
}
return true
}
fun sizeOfDirectory(dir: File): Long {
if (dir.exists()) {
var result: Long = 0
val fileList = dir.listFiles()
for (i in fileList!!.indices) {
// Recursive call if it’s a directory
if (fileList[i].isDirectory) {
result += sizeOfDirectory(fileList[i])
} else {
// Sum the file size in bytes
result += fileList[i].length()
}
}
return result // return the file size
}
return 0
}
/**
* @param length 长度 byte为单位
* 将文件大小转换为KB,MB格式
*/
fun getFileSize(length: Long): String {
val MB = 1024 * 1024
if (length < MB) {
val resultKB = length * 1.0 / 1024
return String.format(Locale.getDefault(), “%.1f”, resultKB) + “Kb”
}
val resultMB = length * 1.0 / MB
return String.format(Locale.getDefault(), “%.1f”, resultMB) + “Mb”
}
fun isFileExist(path: String): Boolean {
if (TextUtils.isEmpty(path)) return false
val file = File(path)
return file.exists()
}
/**
* @param path 路径
* @return 是否删除成功
*/
fun deleteFile(path: String): Boolean {
return if (TextUtils.isEmpty(path)) true else deleteFile(
File(
path
)
)
}
/**
* @return 是否删除成功
*/
fun deleteFile(file: File?): Boolean {
if (file == null || !file.exists()) return true
if (file.isFile) {
return file.delete()
}
if (!file.isDirectory) {
return false
}
for (f in file.listFiles()!!) {
if (f.isFile) {
f.delete()
} else if (f.isDirectory) {
deleteFile(f)
}
}
return file.delete()
}
}
我直接贴这个类吧.省的你们还要用!
接下来我贴一下java的代码,就不做多余解释了:
if (!FFmpeg.getInstance(context).isSupported()) {
Log.e(“ZApplication”, “Android cup arch not supported!”);
}
VideoTrimmerUtil.trim(BaseUtils.getContext(),
“/storage/emulated/0/Pictures/VIDEO_20191202_171820.mp4”,
StorageUtil.getCacheDir(),
0,
15000,
new VideoTrimListener() {
@Override
public void onStartTrim() {
Log.e(“TAG”, “onStartTrim;” );
}
@Override
public void onFinishTrim(String url) {
Log.e(“TAG”, “onFinishTrim;”+url );
}
@Override
public void onCancel() {
Log.e(“TAG”, “onCancel;” );
}
});
package com.iknow.android.features.trim;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import com.iknow.android.interfaces.VideoTrimListener;
import iknow.android.utils.DeviceUtil;
import iknow.android.utils.UnitConverter;
import iknow.android.utils.callback.SingleCallback;
import iknow.android.utils.thread.BackgroundExecutor;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import nl.bravobit.ffmpeg.ExecuteBinaryResponseHandler;
import nl.bravobit.ffmpeg.FFmpeg;
public class VideoTrimmerUtil {
public static void trim(Context context, String inputFile, String outputFile, long startMs, long endMs, final VideoTrimListener callback) {
Log.e(“TAG”, “context:” + context + ” inputFile:” + inputFile +
” outputFile:” + outputFile + ” startMs:” + startMs + ” endMs:” + endMs
+ ” callback:” + callback);
final String timeStamp = new SimpleDateFormat(“yyyyMMdd_HHmmss”, Locale.getDefault()).format(new Date());
final String outputName = “trimmedVideo_” + timeStamp + “.mp4”;
outputFile = outputFile + “/” + outputName;
String start = convertSecondsToTime(startMs / 1000);
String duration = convertSecondsToTime((endMs – startMs) / 1000);
String cmd = “-ss ” + start + ” -t ” + duration + ” -accurate_seek” + ” -i ” + inputFile + ” -codec copy -avoid_negative_ts 1 ” + outputFile;
Log.e(“TAG”, “cmd:” + cmd);
String[] command = cmd.split(” “);
try {
final String tempOutFile = outputFile;
FFmpeg.getInstance(context).execute(command, new ExecuteBinaryResponseHandler() {
@Override
public void onSuccess(String s) {
Log.e(“TAG”, “文件地址是;” + tempOutFile);
callback.onFinishTrim(tempOutFile);
}
@Override
public void onStart() {
Log.e(“TAG”, “onStart;” );
callback.onStartTrim();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private static String convertSecondsToTime(long seconds) {
String timeStr = null;
int hour = 0;
int minute = 0;
int second = 0;
if (seconds <= 0) {
return “00:00”;
} else {
minute = (int) seconds / 60;
if (minute < 60) {
second = (int) seconds % 60;
timeStr = “00:” + unitFormat(minute) + “:” + unitFormat(second);
} else {
hour = minute / 60;
if (hour > 99) return “99:59:59”;
minute = minute % 60;
second = (int) (seconds – hour * 3600 – minute * 60);
timeStr = unitFormat(hour) + “:” + unitFormat(minute) + “:” + unitFormat(second);
}
}
return timeStr;
}
private static String unitFormat(int i) {
String retStr = null;
if (i >= 0 && i < 10) {
retStr = “0” + Integer.toString(i);
} else {
retStr = “” + i;
}
return retStr;
}
}
@SuppressWarnings({“ResultOfMethodCallIgnored”, “FieldCanBeLocal”})
public class StorageUtil {
private static final String TAG = “StorageUtil”;
private static String APP_DATA_PATH = “/Android/data/” + BuildConfig.APPLICATION_ID;
private static String sDataDir;
private static String sCacheDir;
public static String getAppDataDir() {
if (TextUtils.isEmpty(sDataDir)) {
try {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
sDataDir = android.os.Environment.getExternalStorageDirectory().getPath() + APP_DATA_PATH;
if (TextUtils.isEmpty(sDataDir)) {
sDataDir = BaseUtils.getContext().getFilesDir().getAbsolutePath();
}
} else {
sDataDir = BaseUtils.getContext().getFilesDir().getAbsolutePath();
}
} catch (Throwable e) {
e.printStackTrace();
sDataDir = BaseUtils.getContext().getFilesDir().getAbsolutePath();
}
File file = new File(sDataDir);
if (!file.exists()) {//判断文件目录是否存在
file.mkdirs();
}
}
return sDataDir;
}
public static String getCacheDir() {
if (TextUtils.isEmpty(sCacheDir)) {
File file = null;
Context context = BaseUtils.getContext();
try {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
file = context.getExternalCacheDir();
if (file == null || !file.exists()) {
file = getExternalCacheDirManual(context);
}
}
if (file == null) {
file = context.getCacheDir();
if (file == null || !file.exists()) {
file = getCacheDirManual(context);
}
}
Log.w(TAG, “cache dir = ” + file.getAbsolutePath());
sCacheDir = file.getAbsolutePath();
} catch (Throwable ignored) {
}
}
return sCacheDir;
}
private static File getExternalCacheDirManual(Context context) {
File dataDir = new File(new File(Environment.getExternalStorageDirectory(), “Android”), “data”);
File appCacheDir = new File(new File(dataDir, context.getPackageName()), “cache”);
if (!appCacheDir.exists()) {
if (!appCacheDir.mkdirs()) {//
Log.w(TAG, “Unable to create external cache directory”);
return null;
}
try {
new File(appCacheDir, “.nomedia”).createNewFile();
} catch (IOException e) {
Log.i(TAG, “Can’t create \”.nomedia\” file in application external cache directory”);
}
}
return appCacheDir;
}
@SuppressLint(“SdCardPath”)
private static File getCacheDirManual(Context context) {
String cacheDirPath = “/data/data/” + context.getPackageName() + “/cache”;
return new File(cacheDirPath);
}
// 功能描述:删除文件夹下所有文件和文件夹
public static boolean delFiles(String path) {
File cacheFile = new File(path);
if (!cacheFile.exists()) {
return false;
}
File[] files = cacheFile.listFiles();
for (int i = 0; i < files.length; i++) {
// 是文件则直接删除
if (files[i].exists() && files[i].isFile()) {
files[i].delete();
} else if (files[i].exists() && files[i].isDirectory()) {
// 递归删除文件
delFiles(files[i].getAbsolutePath());
// 删除完目录下面的所有文件后再删除该文件夹
files[i].delete();
}
}
return true;
}
public static long sizeOfDirectory(File dir) {
if (dir.exists()) {
long result = 0;
File[] fileList = dir.listFiles();
for (int i = 0; i < fileList.length; i++) {
// Recursive call if it’s a directory
if (fileList[i].isDirectory()) {
result += sizeOfDirectory(fileList[i]);
} else {
// Sum the file size in bytes
result += fileList[i].length();
}
}
return result; // return the file size
}
return 0;
}
/**
* @param length 长度 byte为单位
* 将文件大小转换为KB,MB格式
*/
public static String getFileSize(long length) {
int MB = 1024 * 1024;
if (length < MB) {
double resultKB = length * 1.0 / 1024;
return String.format(Locale.getDefault(), “%.1f”, resultKB) + “Kb”;
}
double resultMB = length * 1.0 / MB;
return String.format(Locale.getDefault(), “%.1f”, resultMB) + “Mb”;
}
public static boolean isFileExist(String path) {
if (TextUtils.isEmpty(path)) return false;
File file = new File(path);
return file.exists();
}
/**
* @param path 路径
* @return 是否删除成功
*/
public static boolean deleteFile(String path) {
if (TextUtils.isEmpty(path)) return true;
return deleteFile(new File(path));
}
/**
* @return 是否删除成功
*/
public static boolean deleteFile(File file) {
if (file == null || !file.exists()) return true;
if (file.isFile()) {
return file.delete();
}
if (!file.isDirectory()) {
return false;
}
for (File f : file.listFiles()) {
if (f.isFile()) {
f.delete();
} else if (f.isDirectory()) {
deleteFile(f);
}
}
return file.delete();
}
}
导包之类的直接使用上面kotlin的导包,这就是视频剪切的简单应用了,有不同见解随时可以提,别的功能比如合成之类的,我会慢慢完善!
————————————————
版权声明:本文为CSDN博主「尹大喵」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36333309/article/details/103364358