1. 程式人生 > >Android 整合 FFmpeg (三) 獲取 FFmpeg 執行進度

Android 整合 FFmpeg (三) 獲取 FFmpeg 執行進度

在以命令方式呼叫 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) + "%";
    }
}

進度效果如下:
這裡寫圖片描述

最後貼一個音訊合成效果,泡沫&童話鎮混合後的效果,感受一下 amix 命令的魅(噪)力(音)吧。