Android 集成 FFmpeg ③ 獲取 FFmpeg 執行進度 [復制鏈接]

2019-1-18 22:23
鴻宇 閱讀:205 評論:0 贊:0
引言
在以命令方式調用 FFmpeg 的時候,可能會執行一些比較耗時的任務,這時如果沒有進度展示,用戶可能會以為程序崩潰了,體驗十分不好.能不能在以命令方式調用 FFmpeg 時實時獲取執行進度呢?谷歌關鍵詞 “Android FFmpeg 命令” 可以得到很多教程,但加上關鍵詞 “進度”就沒有相關文章了,看來以命令方式調用 FFmpeg 實時獲取執行進度這個需求沒有前人的肩膀可站,要開動自己的小腦筋了.
首先來分析一下,以命令方式調用就是把一條命令交給 FFmpeg 執行,具體就是 ffmpeg.c 的 main 函數,待 main 函數執行完畢才會返回,執行過程相當于一個黑盒,執行進度顯然是無法獲取的.網上也沒有相關文章,難道只有以函數方式調用 FFmpeg 才能獲取到執行進度嗎?當我快要下這樣的定論時,看到了 FFmpeg 的 log 信息:

這是在執行混合音頻命令時 FFmpeg 的日志輸出,其中的 time 信息表示當前已合成的音頻時長,這不就是進度信息嗎!下面就針對混合音頻命令獲取實時執行進度.要做的就是提取日志中的進度信息,傳遞給 Android 層,首先回顧一下這些日志信息是怎樣輸出到 logcat 的,在Android 集成 FFmpeg(二) 以命令方式調用中有詳細說明,這里只關注關鍵方法 log_callback_null ,位于 ffmpeg.c 中:

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);
    }
}

日志信息都是通過第 13 行的 XLOGD 方法輸入到 logcat 中的,我們需要的進度信息就在 line 字符串中,那只要在此處把進度提取出來傳遞給 Android 層就行了,在 XLOGD 方法下添加一個傳遞方法:

        XLOGD("%s", line);
        callJavaMethod(line);//傳遞進度信息

需要明白 JNI 不僅可以實現 java 調用底層代碼, c/c++ 也可以主動調用 java 代碼,我在Android 集成 FFmpeg (一) 基礎知識及簡單調用 中對此也有說明. callJavaMethod 方法要做的就是主動調用 java 層的方法,從而實現進度信息的回調. callJavaMethod 方法直接在 com_jni_FFmpegJni.c 接口文件中定義即可,在實現此方法前先明確要做什么.首先要對日志信息進行處理,把進度提取出來,日志信息形如:

 frame=    1 fps=0.0 q=0.0 size=       0kB time=00:01:02.71 bitrate=   0.0kbits/s speed=2.88x

把關鍵的已處理時長 “00:01:02” 轉換成秒數 “62” 就足夠了,代碼如下:

void callJavaMethod(char *ret) {
   int result = 0;
   char timeStr[10] = "time=";
  char *q = strstr(ret, timeStr);
  if(q != NULL){ //日志信息中若包含"time="字符串
      char str[14] = {0};
      strncpy(str, q, 13);
      int h =(str[5]-'0')*10+(str[6]-'0');
      int m =(str[8]-'0')*10+(str[9]-'0');
      int s =(str[11]-'0')*10+(str[12]-'0');
      result = s+m*60+h*60*60;
   }else{
      return;
   }
   //已執行時長 result

}

其中的 strstr 為 < string.h > 中的方法,表示找出 timeStr 字符串在 ret 字符串中第一次出現的位置,并返回該位置的指針,如找不到,返回空指針。也就是說,如果日志信息中包含”time=”字符串,q 指針就指向字符 “t”,然后根據 “time=00:01:02” 這種固定格式,將總秒數提取出來,strncpy 及其他語法方法就不再細說了,不熟悉的話可以復習 c 語言.

獲取到進度信息后,就可以調用 java 層的方法了,首先在 FFmpegJni.java 中定義待調用方法:

    public static void onProgress(int second) {

    }

然后在com_jni_FFmpegJni.c 的 callJavaMethod 方法中調用,代碼很簡單,只需兩行:

   //獲取java方法
    jmethodID methodID = (*m_env)->GetStaticMethodID(m_env, m_clazz, "onProgress", "(I)V");
    //調用該方法
    (*m_env)->CallStaticVoidMethod(m_env, m_clazz, methodID,result);

其中 m_env, m_clazz 定義在 com_jni_FFmpegJni.c 中,在 java 層進入 c 語言層時賦值,如下:

static jclass m_clazz = NULL;//當前類(面向java)
static JNIEnv *m_env = NULL;

JNIEXPORT jint JNICALL Java_com_jni_FFmpegJni_run(JNIEnv *env, jclass clazz, jobjectArray commands) {

    //獲取java虛擬機,在jni的c線程中不允許使用共用的env環境變量 但JavaVM在整個jvm中是共用的 可通過保存JavaVM指針,到時候再通過JavaVM指針取出JNIEnv *env
    (*env)->GetJavaVM(env, &jvm);
  //獲取調用此方法的java類,ICS之前(你可把NDK sdk版本改成低于11) 可以寫m_clazz = clazz直接賦值,  然而ICS(sdk11) 后便改變了這一機制,在線程中回調java時 不能直接共用變量 必須使用NewGlobalRef創建全局對象
    m_clazz = (*env)->NewGlobalRef(env, clazz);
    m_env = env;

   //以命令方式調用 FFmpeg
    ...
}

這樣就可以實現 c 語言中調用 java 方法了,進度以形參傳遞到 Java 層,修改 onProgress 方法測試一下:

    public static void onProgress(int second) {
        Log.d("AAA", "已執行時長:" + second);
    }

如圖,已經成功的將包含”time=00:01:02” 格式的日志進行處理,轉換為總秒數(已合成時長),作為進度信息傳遞給 Java 層。需要的注意的是,這種方式將處理包括 “time=”日志的所有命令,不僅局限于合成音頻,那如果要只在合成音頻時輸出進度呢?

合成音頻命令的關鍵詞為”amix”,FFmpeg 開始執行這個命令時,會輸出包含 “amix” 字符串的日志信息,那我們就可以再次使用 strstr 方法過濾日志信息,com_jni_FFmpegJni.c 完整代碼如下:

#include "android_log.h"
#include "com_jni_FFmpegJni.h"
#include "ffmpeg.h"
#include <string.h>

static JavaVM *jvm = NULL;//java虛擬機
static jclass m_clazz = NULL;//當前類(面向java)
static JNIEnv *m_env = NULL;
static char amixStr[10] = "amix";
static char timeStr[10] = "time=";
static char amixing = 0;  //0:沒遇到  1:遇到

/**
 * 回調執行Java方法
 */
void callJavaMethod(char *ret) {
    char *p = strstr(ret, amixStr);
    if(p != NULL){
      //LOGE("遇到amix");
      amixing = 1;
    }
    int ss=0;

    if(amixing == 1){
       char *q = strstr(ret, timeStr);
       if(q != NULL){
          //LOGE("遇到time=");
          char str[14] = {0};
          strncpy(str, q, 13);
          int h =(str[5]-'0')*10+(str[6]-'0');
      int m =(str[8]-'0')*10+(str[9]-'0');
      int s =(str[11]-'0')*10+(str[12]-'0');
      ss = s+m*60+h*60*60;
       }else{
          return;
       }
    }else{
      return;
    }

    if (m_clazz == NULL) {
        LOGE("---------------clazz isNULL---------------");
        return;
    }
    //獲取方法ID (I)V指的是方法簽名 通過javap -s -public FFmpegCmd 命令生成
    jmethodID methodID = (*m_env)->GetStaticMethodID(m_env, m_clazz, "onProgress", "(I)V");
    if (methodID == NULL) {
        LOGE("---------------methodID isNULL---------------");
        return;
    }
    //調用該java方法
    (*m_env)->CallStaticVoidMethod(m_env, m_clazz, methodID,ss);
}

JNIEXPORT jint JNICALL Java_com_jni_FFmpegJni_run(JNIEnv *env, jclass clazz, jobjectArray commands) {

    //獲取java虛擬機,在jni的c線程中不允許使用共用的env環境變量 但JavaVM在整個jvm中是共用的 可通過保存JavaVM指針,到時候再通過JavaVM指針取出JNIEnv *env
    (*env)->GetJavaVM(env, &jvm);
    //獲取調用此方法的java類,ICS之前(你可把NDK sdk版本改成低于11) 可以寫m_clazz = clazz直接賦值,  然而ICS(sdk11) 后便改變了這一機制,在線程中回調java時 不能直接共用變量 必須使用NewGlobalRef創建全局對象
    m_clazz = (*env)->NewGlobalRef(env, clazz);
    m_env = env;

    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);
    }
    amixing = 0;
    int ret = main(argc, argv);
    amixing = 0;
    return ret;
}

接下來再完善一下 FFmpegJni.java,針對本案例,我把合成音頻命令和進度回調進行了簡單封裝,完整代碼如下:

public class FFmpegJni {
    private static OnAmixProgressListener mOnAmixProgressListener;

    public static void onProgress(int second) {
        if (mOnAmixProgressListener != null && second >= 0) {
            mOnAmixProgressListener.onProgress(second);
        }
    }

    public interface OnAmixProgressListener {
        void onProgress(int second);
    }

    public static void mixAudio(String srcAudioPath, List<String> audioPathList, String outputPath, OnAmixProgressListener onAmixProgressListener) {
        mOnAmixProgressListener = onAmixProgressListener;
        _mixAudio(srcAudioPath, audioPathList, outputPath);
    }

    private static void _mixAudio(String srcAudioPath, List<String> audioPathList, String outputPath) {
        ArrayList<String> commandList = new ArrayList<>();
        commandList.add("ffmpeg");
        commandList.add("-i");
        commandList.add(srcAudioPath);
        for (String audioPath : audioPathList) {
            commandList.add("-i");
            commandList.add(audioPath);
        }
        commandList.add("-filter_complex");
        commandList.add("amix=inputs=" + (audioPathList.size()+1) + ":duration=first:dropout_transition=1");
        commandList.add("-f");
        commandList.add("mp3");
        commandList.add("-ac");//聲道數
        commandList.add("1");
        commandList.add("-ar"); //采樣率
        commandList.add("24k");
        commandList.add("-ab");//比特率
        commandList.add("32k");
        commandList.add("-y");
        commandList.add(outputPath);
        String[] commands = new String[commandList.size()];
        commandList.toArray(commands);
        run(commands);
    }

    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);
}

有了當前已合成時長,再結合總時長,就能得到命令執行的百分比進度了,MainActivity.java 如下:

public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.textView);
        mButton = (Button) findViewById(R.id.button);
        if (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        }

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        String dir = Environment.getExternalStorageDirectory().getPath() + "/ffmpegTest/";
                        String srcAudio = dir + "paomo.mp3";
                        String audio1 = dir + "tonghuazhen.mp3";
                        String outputAudio = dir + "outputAudio.mp3";
                        List<String> audioPaths = new ArrayList<>();
                        audioPaths.add(audio1);
                        final int duration = getDuration(srcAudio);
                        FFmpegJni.mixAudio(srcAudio, audioPaths, outputAudio, new FFmpegJni.OnAmixProgressListener() {
                            @Override
                            public void onProgress(int second) {
                                final String percent = format((second / (float) duration) * 100);
                                Log.d("FFMPEG", "second=" + second + " duration=" + duration +
                                        " percent=" + percent);
                                mTextView.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        mTextView.setText("已執行:" + percent);
                                    }
                                });
                            }
                        });
                    }
                }).start();
            }
        });
    }

    public int getDuration(String audioPath) {
        MediaPlayer player = new MediaPlayer();
        try {
            player.setDataSource(audioPath);
            player.prepare();
        } catch (IOException e) {
            e.printStackTrace();
        }
        int duration = (int) Math.round(player.getDuration() / 1000.0);
        player.release();
        return duration;
    }

    public static String format(float value) {
        return String.format("%.2f", value) + "%";
    }
}

進度效果如下: 

總結

至此Android集成FFmpeg以基本完成,如有不足之處歡迎加入Android開發技術交流Q群:150923287一起探討學習!

另外后續會繼續更新Android中關于底層以及架構方面的技術文!

分享到:
我來說兩句
facelist
您需要登錄后才可以評論 登錄 | 立即注冊
所有評論(0)

領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

两码中特期期