Android 集成 FFmpeg (二) 以命令方式調用 FFmpeg

上一篇文章實現了 FFmpeg 編譯及 Android 端的簡單調用,成功獲取了 FFmpeg 支持的編解碼信息,而在實際使用時,需要調用 FFmpeg 內部函數,或通過命令行方式調用,但后者簡單很多。

怎么讓 FFmpeg 運行命令呢?很簡單,調用 FFmpeg 中執行命令的函數即可,這個函數位于源碼的 ffmpeg.c 文件中:

int main(int argc, char **argv)

我們的目的很簡單:將 FFmpeg 命令傳遞給 main 函數并執行。而這個傳遞過程需要編寫底層代碼實現,在這個底層接口代碼中,接收上層傳遞過來的 FFmpeg 命令 ,然后調用 ffmpeg.c 中的 main 函數執行該命令。

開始集成之前,首先回顧一下 JNI 標準接入步驟:

  1. 編寫帶有 native 方法的 Java 類
  2. 生成該類擴展名為 .h 的頭文件
  3. 創建該頭文件的 C/C++ 文件,實現 native 方法
  4. 將該 C/C++ 文件編譯成動態鏈接庫
  5. 在Java 程序中加載該動態鏈接庫

接下來按照此步驟開始集成,實現 Android 端以命令方式調用 FFmpeg ,這里假設你已經編譯過 FFmpeg 源碼,具體編譯方法可查看本系列第一篇。如果你是新手或對 Android 端集成底層庫不太熟悉,強烈建議先閱讀本系列第一篇 Android 集成 FFmpeg (一)基礎知識及簡單調用

首先新建一個文件夾 ndkBuild 作為工作空間,在 ndkBuild 目錄下新建 jni 文件夾, 作為編譯工作目錄。

1. 編寫帶有 native 方法的 Java 類

package com.jni;

public class FFmpegJni {
   
    public static native int run(String[] commands);
    
}

2. 生成該類擴展名為 .h 的頭文件

在 Android Studio 的 Terminal 中 切換到 java 目錄下,運行 javah 命令生成頭文件:

這里寫圖片描述

可以看到在 java 目錄下生成了頭文件:

這里寫圖片描述

然后將此頭文件剪切到 jni 目錄下。

3. 創建該頭文件的 C/C++ 文件,實現 native 方法

在 jni 目錄下創建對應的 C 文件 com_jni_FFmpegJni.c :

#include "android_log.h"
#include "com_jni_FFmpegJni.h"
#include "ffmpeg.h"

JNIEXPORT jint JNICALL Java_com_jni_FFmpegJni_run(JNIEnv *env, jclass obj, jobjectArray commands) {
    int argc = (*env)->GetArrayLength(env, commands);
    char *argv[argc];
    int i;
    for (i = 0; i < argc; i++) {
        jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
        argv[i] = (char*) (*env)->GetStringUTFChars(env, js, 0);
    }
    LOGD("----------begin---------");
    return main(argc, argv);
}

函數的主要作用就是將 Java 端傳遞過來的 jobjectArray 類型的 FFmpeg 命令,轉換為 main 函數所需要的參數 argc 和 argv ,然后調用之。為了將日志輸出函數簡化為簡潔的 "LOGD"、 "LOGE",需要在同級目錄下新建 android_log.h 文件:

#ifdef ANDROID
#include <android/log.h>
#ifndef LOG_TAG
#define  MY_TAG   "MYTAG"
#define  AV_TAG   "AVLOG"
#endif
#define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, MY_TAG, format, ##__VA_ARGS__)
#define LOGD(format, ...)  __android_log_print(ANDROID_LOG_DEBUG,  MY_TAG, format, ##__VA_ARGS__)
#define  XLOGD(...)  __android_log_print(ANDROID_LOG_INFO,AV_TAG,__VA_ARGS__)
#define  XLOGE(...)  __android_log_print(ANDROID_LOG_ERROR,AV_TAG,__VA_ARGS__)
#else
#define LOGE(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
#define LOGD(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
#define XLOGE(format, ...)  fprintf(stdout, AV_TAG ": " format "\n", ##__VA_ARGS__)
#define XLOGI(format, ...)  fprintf(stderr, AV_TAG ": " format "\n", ##__VA_ARGS__)
#endif

其中 XLOGD 和 XLOGE 方法是為了將 FFmpeg 內部日志信息自動輸出到 logcat,后面會用到。除 android_log.h 之外,很顯然,還需要添加 ffmpeg.c 、ffmpeg.h 文件,實際上 ffmpeg.c 的 main 函數中還會調用到其他文件,所以需要從源碼中拷貝 ffmpeg.h、ffmpeg.c、ffmpeg_opt.c、ffmpeg_filter.c、cmdutils.c、cmdutils.h 以及 cmdutils_common_opts.h 共 7 個文件到 jni 目錄下。

此時 jni 目錄下應該有以下 10 個文件:

這里寫圖片描述

接下來還要修改 ffmpeg.c 、cmdutils.c 以及 cmdutils.h 三個文件使其適用于 Android 端調用,按功能分為以下三點:

1.日志輸出到 logcat (修改 ffmpeg.c)

在執行命令過程中,FFmpeg 內部的日志系統會輸出很多有用的信息,但是在 Android 的 logcat 中是看不到的,所以需要修改源碼將 FFmpeg 內部日志輸出 logcat 中,方便調試,其實這是十分必要的。修改方法很簡單,只需修改 ffmpeg.c 文件三處:

引入 android_log.h 頭文件:

#include "android_log.h"

修改 log_callback_null 方法為下:(原方法為空)

static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl)
{
    static int print_prefix = 1;
    static int count;
    static char prev[1024];
    char line[1024];
    static int is_atty;
    av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);
    strcpy(prev, line);
    if (level <= AV_LOG_WARNING){
        XLOGE("%s", line);
    }else{
        XLOGD("%s", line);
    }
}

設置日志回調方法為 log_callback_null:(main 函數開始處)

int main(int argc, char **argv)
{
    av_log_set_callback(log_callback_null);
    int i, ret;
    ......

2.執行命令后清除數據(修改 ffmpeg.c)

由于 Android 端執行一條 FFmpeg 命令后并不需要結束進程,所以需要初始化相關變量,否則執行下一條命令時就會崩潰。首先找到 ffmpeg.c 的 ffmpeg_cleanup 方法,在該方法的末尾添加以下代碼:

    nb_filtergraphs = 0;
    nb_output_files = 0;
    nb_output_streams = 0;
    nb_input_files = 0;
    nb_input_streams = 0;

然后在 main 函數的最后調用 ffmpeg_cleanup 方法,如下:

    ......
    ffmpeg_cleanup(0);
    return main_return_code;
}

3.執行結束后不結束進程(修改 cmdutils.c、cmdutils.h)

FFmpeg 在執行過程中出現異常或執行結束后會自動銷毀進程,而我們在 Android 中調用時,只想讓它作為一個普通的方法,不需要銷毀進程,只需要正常返回就可以了,這就需要修改 cmdutils.c 中的 exit_program 方法,源碼中為:

void exit_program(int ret)
{
    if (program_exit)
        program_exit(ret);

    exit(ret);
}

修改為:

int exit_program(int ret)
{
   return ret;
}

此處修改了方法的返回值類型,所以還需要修改對應頭文件中的方法聲明,即將 cmdutils.h 中的:

void exit_program(int ret) av_noreturn;

修改為:

int exit_program(int ret);

到這里需要修改項都已修改完畢,網上教程實現 FFmpeg 內部日志輸出到 logcat 的并不多,但這一步是十分有必要的。很多教程中需要將 ffmpeg 中的 main 方法名字修改為 "run" 、"exec" 等等,其實完全沒必要,為什么要對方法名這么在意,乃至不惜徒增新手學習的復雜度呢? 我不知道修改的原因和意義所在。 有些教程中需要把 config.h 文件也拷貝到 jni 目錄下,而我并沒有拷貝,那么到底需不需要呢?FFmpeg 的命令數不勝數,我只能說我執行過的命令都不需要拷貝 config.h ,盡管源碼 ffmpeg.c 中就聲明了引入 config.h 文件。

4. 將該 C/C++ 文件編譯成動態鏈接庫

在 jni 目錄下創建 Android.mk 文件 :

LOCAL_PATH:= $(call my-dir)

#編譯好的 FFmpeg 頭文件目錄
INCLUDE_PATH:=/home/yhao/sf/ffmpeg-3.3.3/Android/arm/include

#編譯好的 FFmpeg 動態庫目錄
FFMPEG_LIB_PATH:=/home/yhao/sf/ffmpeg-3.3.3/Android/arm/lib

include $(CLEAR_VARS)
LOCAL_MODULE:= libavcodec
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavcodec-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavformat
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavformat-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libswscale
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswscale-4.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavutil
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavutil-55.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libavfilter
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavfilter-6.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
 
include $(CLEAR_VARS)
LOCAL_MODULE:= libswresample
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswresample-2.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= libpostproc
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libpostproc-54.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= libavdevice
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavdevice-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := com_jni_FFmpegJni.c \
                  cmdutils.c \
                  ffmpeg.c \
                  ffmpeg_opt.c \
                  ffmpeg_filter.c   
LOCAL_C_INCLUDES := /home/yhao/sf/ffmpeg-3.3.3
LOCAL_LDLIBS := -lm -llog
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice
include $(BUILD_SHARED_LIBRARY)

此處使用的 FFmpeg 為本系列上篇文章中的編譯配置,其中引入了 libmp3lame 庫以支持 mp3 格式編碼,與上篇文章不同的是最后對 ffmpeg 動態庫的編譯,加入了 ffmpeg.c 、cmdutils.c 等文件。

然后在 jni 目錄下創建 Application.mk 文件:

APP_ABI := armeabi-v7a
APP_PLATFORM=android-14
NDK_TOOLCHAIN_VERSION=4.9

這時 jni 目錄應該有以下 12 個文件:

這里寫圖片描述

一切準備就緒,在 jni 目錄下運行 ndk 編譯命令:

ndk-build

然后就可以在 ndkBuild 目錄下看到生成的 libs 和 obj 文件夾了。

5.在Java 程序中加載該動態鏈接庫

將 libs 目錄下生成的 armeabi-v7a 動態庫拷貝到 Android 工程中,此時工程應該是這樣的:

這里寫圖片描述

在 FFmpegJni.java 中加載動態庫:

package com.jni;

public class FFmpegJni {
    static {
        System.loadLibrary("avutil-55");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("swresample-2");
        System.loadLibrary("swscale-4");
        System.loadLibrary("postproc-54");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("ffmpeg");
    }
    public static native int run(String[] commands);
}

記得在應用的 build.gradle 文件中 android 節點下添加動態庫加載路徑:

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

OK,至此集成工作就全部完成了,在程序中調用 run 方法,就能以命令方式調用 FFmpeg 。

接下來以剪切 mp3 文件為例,驗證是否集成成功,首先需要準備一個 mp3 文件,這里我提供兩首歌:泡沫、童話鎮,下載解壓后直接將其放到手機根目錄下即可。

直接給出 MainActivity 代碼:

import com.jni.FFmpegJni;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if ( ActivityCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ) {
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);
        }
    }

    public void run(View view) {
        String dir = Environment.getExternalStorageDirectory().getPath() + "/ffmpegTest/";

        //ffmpeg -i source_mp3.mp3 -ss 00:01:12 -t 00:01:42 -acodec copy output_mp3.mp3
        String[] commands = new String[10];
        commands[0] = "ffmpeg";
        commands[1] = "-i";
        commands[2] = dir+"paomo.mp3";
        commands[3] = "-ss";
        commands[4] = "00:01:00";
        commands[5] = "-t";
        commands[6] = "00:01:00";
        commands[7] = "-acodec";
        commands[8] = "copy";
        commands[9] = dir+"paomo_cut_mp3.mp3";

        int result = FFmpegJni.run(commands);
        Toast.makeText(MainActivity.this, "命令行執行完成 result="+result, Toast.LENGTH_SHORT).show();
    }
}

記得在 AndroidManifest.xml 中聲明權限:

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

運行之后,播放生成的 paomo_cut_mp3 試試,剪切成功~ 但是一個例子不夠過癮,再來一個延時播放的命令:

        String[] commands = new String[12];
        commands[0] = "ffmpeg";
        commands[1] = "-i";
        commands[2] = dir+"paomo.mp3";
        commands[3] = "-filter_complex";
        commands[4] = "adelay=5000|5000";
        commands[5] = "-ac"; //聲道數
        commands[6] = "1";
        commands[7] = "-ar"; //采樣率
        commands[8] = "24k";
        commands[9] = "-ab"; //比特率
        commands[10] = "32k";
        commands[11] = dir+"adelay_output.mp3";

更簡單的方法實現

本文的核心工作是 ffmpeg.c 等文件的修改及編譯,其實這些文件的修改是一勞永逸的。無論你的 FFmpeg 如何配置編譯選項,不管是支持 mp3 編碼還是支持 h264 編碼,對 ffmpeg.c 等文件的修改內容都是固定的,所以我把這個工作目錄上傳到了 github ,方便大家直接拿來使用。

github 地址 : https://github.com/yhaolpz/ffmpeg-command-ndkBuild

接下來演示一下如何使用,首先要將 ffmpeg-command-ndkBuild 克隆到你的電腦上,注意這里默認你已經編譯過 FFmpeg,編譯方法見本系列第一章。

  1. 編寫帶有 native 方法的 Java 類

package com.jni;

public class FFmpegJni {
   
    public static native int run(String[] commands);
    
}

如果你編寫的 Java 類跟上面這個類的包名、類名和方法都相同,那就直接跳到第 4 步,因為你可以直接使用 jni 目錄中的 com_jni_FFmpegJni.c 和 com_jni_FFmpegJni.h 。

  1. 生成該類擴展名為 .h 的頭文件

在 Android Studio 的 Terminal 中 切換到 Java 目錄下,運行 javah 命令生成頭文件:

javah -classpath .  com.包名.類名

將該頭文件拷貝到 jni 目錄下,并且刪除 com_jni_FFmpegJni.h 文件。

  1. 創建該頭文件的 C/C++ 文件,實現 native 方法

這里不需要重新創建 C 文件,直接在 com_jni_FFmpegJni.c 基礎上修改即可。

  • 修改第2行 com_jni_FFmpegJni.h 為你自己的頭文件名
  • 修改 Java_com_jni_FFmpegJni_run 方法名為你自己的頭文件中的方法名
  • 修改 com_jni_FFmpegJni.c 文件名為你自己的頭文件名
  • 修改 Android.mk 中文件最后的 com_jni_FFmpegJni.c 為你自己的 C 文件名
  1. 將該 C/C++ 文件編譯成動態鏈接庫

  • 修改 Android.mk 中的 INCLUDE_PATH 、FFMPEG_LIB_PATH 為你自己編譯好的 FFmpeg 動態庫路徑
  • 修改 Android.mk 倒數第四行的 LOCAL_C_INCLUDES 為你自己的 FFmpeg 源碼路徑。

OK~ 直接運行 ndk-build 命令編譯吧,最后一步 "在Java 程序中加載該動態鏈接庫" 以及調用案例在上文中已經描述的很詳細了,就不再贅述了。

總結

本文延續第一篇的規則,將編譯工作置于 Android 工程之外進行,直到生成最后可用的 so 庫再移植到 Android 工程中,我相信這樣對 jni 的理解會更清晰一些。

本文中還有兩個問題待解決,在 Application.mk 文件中通過 NDK_TOOLCHAIN_VERSION=4.9 指定編譯器為 4.9 版本的 gcc,若不指定,將默認使用 clang 編譯器,這時編譯會報錯,提示缺少一些文件。第二個問題就是 Application.mk 中通過 APP_ABI := armeabi-v7a 指定生成 armv7 架構動態庫,若改成 armeabi 架構編譯則會報錯,提示不支持該模式,后續盡快解決這兩個問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容