概述
FFmpeg,命令行非常強大。在PC機上,調用一行FFmpeg命令,就可以對視頻文件進行剪切、合併、轉碼等功能。本文將介紹如何在Android中調用FFmpeg命令行。
編譯
前面已經有兩篇文章介紹FFmpeg的編譯:
- Android NDK交叉編譯FFmpeg
- 將FFmpeg編譯成一個libffmpeg.so庫
創建ffmpeg-cmd模塊
Step1:目錄結構
Step2:導入libffmpeg.so庫
在 ffmpeg-cmd
模塊的 build.gradle
中添加配置:
android { // 省略其他配置... defaultConfig { // 省略其他配置... // 配置cmake構建參數 externalNativeBuild { cmake { cppFlags "" abiFilters 'armeabi-v7a' } } } // 配置cmake構建腳本的路徑 externalNativeBuild { cmake { path "CMakeLists.txt" } } // 定義jniLib的目錄到libs sourceSets.main { jniLibs.srcDirs = ['libs'] } }
Step3:導入FFmpeg的相關頭文件
Step4: 導入FFmpeg的相關源碼文件
在源碼目錄的
fftools
文件夾內
Step5: 編寫CMakeLists.txt文件
cmake_minimum_required(VERSION 3.4.1) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") # 添加頭文件路徑 include_directories( ./src/main/cpp ./src/main/cpp/ffmpeg ./src/main/cpp/include ) # 定義源碼所在目錄 aux_source_directory(./src/main/cpp SRC) aux_source_directory(./src/main/cpp/ffmpeg SRC_FFMPEG) # 將 SRC_FFMPEG 添加到 SRC 中 list(APPEND SRC ${SRC_FFMPEG}) # 設置ffmpeg庫所在路徑的目錄 set(distribution_DIR ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}) # 編譯一個ffmpeg-cmd庫 add_library( ffmpeg-cmd # 庫名稱 SHARED # 庫類型 ${SRC}) # 編譯進庫的源碼 # 添加libffmpeg.so庫 add_library( ffmpeg SHARED IMPORTED ) # 指定libffmpeg.so庫的位置 set_target_properties( ffmpeg PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/libffmpeg.so ) # 查找日誌庫 find_library( log-lib log ) # 將其他庫鏈接到目標庫ffmpeg-cmd target_link_libraries( ffmpeg-cmd ffmpeg -landroid # native_window -ljnigraphics # bitmap -lOpenSLES # openSLES ${log-lib} )
修改FFmpeg源碼
ffmpeg.c
修改main方法:
// 修改前 int main(int argc, char **argv) // 修改後 int ffmpeg_exec(int argc, char **argv)
在 ffmpeg_cleanup
函數執行結束前重新初始化:
static void ffmpeg_cleanup(int ret) { // 省略其他代碼... nb_filtergraphs = 0; nb_output_files = 0; nb_output_streams = 0; nb_input_files = 0; nb_input_streams = 0; }
在 print_report
函數中添加代碼實現FFmpeg命令執行進度的回調:
static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time) { // 省略其他代碼... // 定義已處理的時長 float mss; secs = FFABS(pts) / AV_TIME_BASE; us = FFABS(pts) % AV_TIME_BASE; // 獲取已處理的時長 mss = secs + ((float) us / AV_TIME_BASE); // 調用ffmpeg_progress將進度傳到Java層,代碼後面定義 ffmpeg_progress(mss); // 省略其他代碼... }
ffmpeg.h
添加 ffmpeg_exec
方法的聲明:
int ffmpeg_exec(int argc, char **argv);
cmdutils.c
修改 exit_program
函數:
void exit_program(int ret) { if (program_exit) program_exit(ret); // 退出線程(該函數後面定義) ffmpeg_thread_exit(ret); // 刪掉下面這行代碼,不然執行結束,應用會crash //exit(ret); }
編寫JNI調用FFmpeg命令
Note:這部分代碼來自開源庫 EpMedia
C代碼:
ffmpeg_cmd.h
#include <jni.h> #ifndef _Included_FFmpeg_Cmd #define _Included_FFmpeg_Cmd #ifdef __cplusplus extern "C" { #endif JNIEXPORT jint JNICALL Java_com_github_xch168_ffmpeg_1cmd_FFmpegCmd_exec(JNIEnv *, jclass, jint, jobjectArray); JNIEXPORT void JNICALL Java_com_github_xch168_ffmpeg_1cmd_FFmpegCmd_exit(JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif void ffmpeg_progress(float progress);
ffmpeg_cmd.c
#include "ffmpeg_cmd.h" #include <jni.h> #include <string.h> #include "ffmpeg_thread.h" #include "android_log.h" #include "cmdutils.h" static JavaVM *jvm = NULL; //java虛擬機 static jclass m_clazz = NULL;//當前類(面向java) /** * 回調執行Java方法 * 參看 Jni反射+Java反射 */ void callJavaMethod(JNIEnv *env, jclass clazz,int ret) { if (clazz == NULL) { LOGE("---------------clazz isNULL---------------"); return; } //獲取方法ID (I)V指的是方法簽名 通過javap -s -public FFmpegCmd 命令生成 jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "onExecuted", "(I)V"); if (methodID == NULL) { LOGE("---------------methodID isNULL---------------"); return; } //調用該java方法 (*env)->CallStaticVoidMethod(env, clazz, methodID,ret); } void callJavaMethodProgress(JNIEnv *env, jclass clazz,float ret) { if (clazz == NULL) { LOGE("---------------clazz isNULL---------------"); return; } //獲取方法ID (I)V指的是方法簽名 通過javap -s -public FFmpegCmd 命令生成 jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "onProgress", "(F)V"); if (methodID == NULL) { LOGE("---------------methodID isNULL---------------"); return; } //調用該java方法 (*env)->CallStaticVoidMethod(env, clazz, methodID,ret); } /** * c語言-線程回調 */ static void ffmpeg_callback(int ret) { JNIEnv *env; //附加到當前線程從JVM中取出JNIEnv, C/C++從子線程中直接回到Java裏的方法時 必須經過這個步驟 (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL); callJavaMethod(env, m_clazz,ret); //完畢-脫離當前線程 (*jvm)->DetachCurrentThread(jvm); } void ffmpeg_progress(float progress) { JNIEnv *env; (*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL); callJavaMethodProgress(env, m_clazz,progress); (*jvm)->DetachCurrentThread(jvm); } JNIEXPORT jint JNICALL Java_com_github_xch168_ffmpeg_1cmd_FFmpegCmd_exec(JNIEnv *env, jclass clazz, jint cmdnum, jobjectArray cmdline) { (*env)->GetJavaVM(env, &jvm); m_clazz = (*env)->NewGlobalRef(env, clazz); //---------------------------------C語言 反射Java 相關---------------------------------------- //---------------------------------java 數組轉C語言數組---------------------------------------- int i = 0;//滿足NDK所需的C99標準 char **argv = NULL;//命令集 二維指針 jstring *strr = NULL; if (cmdline != NULL) { argv = (char **) malloc(sizeof(char *) * cmdnum); strr = (jstring *) malloc(sizeof(jstring) * cmdnum); for (i = 0; i < cmdnum; ++i) {//轉換 strr[i] = (jstring)(*env)->GetObjectArrayElement(env, cmdline, i); argv[i] = (char *) (*env)->GetStringUTFChars(env, strr[i], 0); } } //---------------------------------java 數組轉C語言數組---------------------------------------- //---------------------------------執行FFmpeg命令相關---------------------------------------- //新建線程 執行ffmpeg 命令 ffmpeg_thread_run_cmd(cmdnum, argv); //註冊ffmpeg命令執行完畢時的回調 ffmpeg_thread_callback(ffmpeg_callback); free(strr); return 0; } JNIEXPORT void JNICALL Java_com_github_xch168_ffmpeg_1cmd_FFmpegCmd_exit(JNIEnv *env, jclass type) { (*env)->GetJavaVM(env, &jvm); m_clazz = (*env)->NewGlobalRef(env, type); ffmpeg_thread_cancel(); }
ffmpeg_thread.h
#include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" #include "ffmpeg.h" #include <pthread.h> #include <string.h> int ffmpeg_thread_run_cmd(int cmdnum,char **argv); void ffmpeg_thread_exit(int ret); void ffmpeg_thread_callback(void (*cb)(int ret)); void ffmpeg_thread_cancel();
ffmpeg_thread.c
#include "libavcodec/avcodec.h" #include "ffmpeg_thread.h" #include "android_log.h" pthread_t ntid; char **argvs = NULL; int num=0; void *thread(void *arg) { //執行 int result = ffmpeg_exec(num, argvs); ffmpeg_thread_exit(result); return ((void *)0); } /** * 新建子線程執行ffmpeg命令 */ int ffmpeg_thread_run_cmd(int cmdnum,char **argv) { num=cmdnum; argvs=argv; int temp =pthread_create(&ntid,NULL,thread,NULL); if(temp!=0) { LOGE("can't create thread: %s ",strerror(temp)); return 1; } LOGI("create thread succes: %s ",strerror(temp)); return 0; } static void (*ffmpeg_callback)(int ret); /** * 註冊線程回調 */ void ffmpeg_thread_callback(void (*cb)(int ret)) { ffmpeg_callback = cb; } /** * 退出線程 */ void ffmpeg_thread_exit(int ret) { if (ffmpeg_callback) { ffmpeg_callback(ret); } pthread_exit("ffmpeg_thread_exit"); } /** * 取消線程 */ void ffmpeg_thread_cancel() { void *ret=NULL; pthread_join(ntid, &ret); }
Java代碼:
FFmpegCmd.java
public class FFmpegCmd { static { System.loadLibrary("ffmpeg-cmd"); } private static OnCmdExecListener sOnCmdExecListener; private static long sDuration; public static native int exec(int argc, String[] argv); public static native void exit(); public static void exec(String[] cmds, long duration, OnCmdExecListener listener) { sOnCmdExecListener = listener; sDuration = duration; exec(cmds.length, cmds); } /** * FFmpeg執行結束回調,由C代碼中調用 */ public static void onExecuted(int ret) { if (sOnCmdExecListener != null) { if (ret == 0) { sOnCmdExecListener.onProgress(sDuration); sOnCmdExecListener.onSuccess(); } else { sOnCmdExecListener.onFailure(); } } } /** * FFmpeg執行進度回調,由C代碼調用 */ public static void onProgress(float progress) { if (sOnCmdExecListener != null) { if (sDuration != 0) { sOnCmdExecListener.onProgress(progress / (sDuration / 1000) * 0.95f); } } } public interface OnCmdExecListener { void onSuccess(); void onFailure(); void onProgress(float progress); } }
FFmpegUtil.java
// 封裝FFmpeg命令的調用 public class FFmpegUtil { private static final String TAG = "FFmpegUtil"; public static void execCmd(CmdList cmd, long duration, final OnVideoProcessListener listener) { String[] cmds = cmd.toArray(new String[cmd.size()]); Log.i(TAG, "cmd:" + cmd); listener.onProcessStart(); FFmpegCmd.exec(cmds, duration, new FFmpegCmd.OnCmdExecListener() { @Override public void onSuccess() { listener.onProcessSuccess(); } @Override public void onFailure() { listener.onProcessFailure(); } @Override public void onProgress(float progress) { listener.onProcessProgress(progress); } }); } }
CmdList.java
public class CmdList extends ArrayList<String> { public CmdList append(String s) { this.add(s); return this; } public CmdList append(int i) { this.add(i + ""); return this; } public CmdList append(float f) { this.add(f + ""); return this; } public CmdList append(StringBuilder sb) { this.add(sb.toString()); return this; } public CmdList append(String[] ss) { for (String s:ss) { if(!s.replace(" ","").equals("")) { this.add(s); } } return this; } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (String s : this) { sb.append(" ").append(s); } return sb.toString(); } }
調用FFmpeg命令
long duration = endTime - startTime; // 構建一條視頻裁剪命令 CmdList cmd = new CmdList(); cmd.append("ffmpeg"); cmd.append("-y"); cmd.append("-ss").append(startTime/ 1000).append("-t").append(duration / 1000).append("-accurate_seek"); cmd.append("-i").append(srcFile); cmd.append("-codec").append("copy").append(destFile); FFmpegUtil.execCmd(cmd, duration, new OnVideoProcessListener() { @Override public void onProcessStart() {} @Override public void onProcessProgress(float progress) {} @Override public void onProcessSuccess() {} @Override public void onProcessFailure() {} }
源碼地址:
https://github.com/xch168/VideoEditor
參考鏈接
- https://www.jianshu.com/p/3479bba0cf28
- https://github.com/yangjie10930/EpMedia
- https://github.com/xufuji456/FFmpegAndroid