1. Android 獲取 FFmpeg 執(zhí)行進(jìn)度

        共 22225字,需瀏覽 45分鐘

         ·

        2021-06-11 19:51


        在以命令方式調(diào)用 FFmpeg 的時(shí)候,可能會(huì)執(zhí)行一些比較耗時(shí)的任務(wù),這時(shí)如果沒有進(jìn)度展示,用戶可能會(huì)以為程序崩潰了,體驗(yàn)十分不好。


        能不能在以命令方式調(diào)用 FFmpeg 時(shí)實(shí)時(shí)獲取執(zhí)行進(jìn)度呢?

        谷歌關(guān)鍵詞 “Android FFmpeg 命令” 可以得到很多教程,但加上關(guān)鍵詞 "進(jìn)度"就沒有相關(guān)文章了,看來以命令方式調(diào)用 FFmpeg 實(shí)時(shí)獲取執(zhí)行進(jìn)度這個(gè)需求沒有前人的肩膀可站,要開動(dòng)自己的小腦筋了。


        首先來分析一下,以命令方式調(diào)用就是把一條命令交給 FFmpeg 執(zhí)行,具體就是 ffmpeg.c 的 main 函數(shù),待 main 函數(shù)執(zhí)行完畢才會(huì)返回,執(zhí)行過程相當(dāng)于一個(gè)黑盒,執(zhí)行進(jìn)度顯然是無法獲取的。


        網(wǎng)上也沒有相關(guān)文章,難道只有以函數(shù)方式調(diào)用 FFmpeg 才能獲取到執(zhí)行進(jìn)度嗎?當(dāng)我快要下這樣的定論時(shí),看到了 FFmpeg 的 log 信息:



        這是在執(zhí)行混合音頻命令時(shí) FFmpeg 的日志輸出,其中的 time 信息表示當(dāng)前已合成的音頻時(shí)長,這不就是進(jìn)度信息嗎!


        下面就針對(duì)混合音頻命令獲取實(shí)時(shí)執(zhí)行進(jìn)度.要做的就是提取日志中的進(jìn)度信息,傳遞給 Android 層,首先回顧一下這些日志信息是怎樣輸出到 logcat 的,在Android 集成 FFmpeg(二) 以命令方式調(diào)用中有詳細(xì)說明,這里只關(guān)注關(guān)鍵方法 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 中的,我們需要的進(jìn)度信息就在 line 字符串中,那只要在此處把進(jìn)度提取出來傳遞給 Android 層就行了,在 XLOGD 方法下添加一個(gè)傳遞方法:

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

        需要明白 JNI 不僅可以實(shí)現(xiàn) java 調(diào)用底層代碼, c/c++ 也可以主動(dòng)調(diào)用 java 代碼,我在Android 集成 FFmpeg (一) 基礎(chǔ)知識(shí)及簡單調(diào)用 中對(duì)此也有說明. callJavaMethod 方法要做的就是主動(dòng)調(diào)用 java 層的方法,從而實(shí)現(xiàn)進(jìn)度信息的回調(diào). 
        callJavaMethod 方法直接在 com_jni_FFmpegJni.c 接口文件中定義即可,在實(shí)現(xiàn)此方法前先明確要做什么.首先要對(duì)日志信息進(jìn)行處理,把進(jìn)度提取出來,日志信息形如:
         frame=    1 fps=0.0 q=0.0 size=       0kB time=00:01:02.71 bitrate=   0.0kbits/s speed=2.88x

        ?

        把關(guān)鍵的已處理時(shí)長 “00:01:02” 轉(zhuǎn)換成秒數(shù) “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;
           }
           //已執(zhí)行時(shí)長 result

        }

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

        獲取到進(jìn)度信息后,就可以調(diào)用 java 層的方法了,首先在 FFmpegJni.java 中定義待調(diào)用方法:

        public static void onProgress(int second) {

        }


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


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


        其中 m_env, m_clazz 定義在 com_jni_FFmpegJni.c 中,在 java 層進(jìn)入 c 語言層時(shí)賦值,如下:

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

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

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

           //以命令方式調(diào)用 FFmpeg
            ...
        }


        這樣就可以實(shí)現(xiàn) c 語言中調(diào)用 java 方法了,進(jìn)度以形參傳遞到 Java 層,修改 onProgress 方法測試一下:

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


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

        合成音頻命令的關(guān)鍵詞為"amix",F(xiàn)Fmpeg 開始執(zhí)行這個(gè)命令時(shí),會(huì)輸出包含 “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虛擬機(jī)
        static jclass m_clazz = NULL;//當(dāng)前類(面向java)
        static JNIEnv *m_env = NULL;
        static char amixStr[10] = "amix";
        static char timeStr[10] = "time=";
        static char amixing = 0;  //0:沒遇到  1:遇到

        /**
         * 回調(diào)執(zhí)行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;
            }
            //調(diào)用該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虛擬機(jī),在jni的c線程中不允許使用共用的env環(huán)境變量 但JavaVM在整個(gè)jvm中是共用的 可通過保存JavaVM指針,到時(shí)候再通過JavaVM指針取出JNIEnv *env
            (*env)->GetJavaVM(env, &jvm);
            //獲取調(diào)用此方法的java類,ICS之前(你可把NDK sdk版本改成低于11) 可以寫m_clazz = clazz直接賦值,  然而ICS(sdk11) 后便改變了這一機(jī)制,在線程中回調(diào)java時(shí) 不能直接共用變量 必須使用NewGlobalRef創(chuàng)建全局對(duì)象
            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,針對(duì)本案例,我把合成音頻命令和進(jìn)度回調(diào)進(jìn)行了簡單封裝,完整代碼如下:


        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");//聲道數(shù)
                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);
        }


        有了當(dāng)前已合成時(shí)長,再結(jié)合總時(shí)長,就能得到命令執(zhí)行的百分比進(jìn)度了,

        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(thisnew 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("已執(zhí)行:" + 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) + "%";
            }
        }


        進(jìn)度效果如下:


        最后貼一個(gè)音頻合成效果,泡沫&童話鎮(zhèn)混合后的效果,感受一下 amix 命令的魅(噪)力(音)吧。


        版權(quán)聲明:本文為CSDN博主「王英豪」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。

        原文鏈接:https://blog.csdn.net/yhaolpz/article/details/78350435


        項(xiàng)目地址:https://github.com/yhaolpz/FFmpegCmd


        -- END --


        推薦:

        Android FFmpeg 實(shí)現(xiàn)帶濾鏡的微信小視頻錄制功能

        全網(wǎng)最全的 Android 音視頻和 OpenGL ES 干貨,都在這了

        瀏覽 57
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 91 中国 精品 | 男男军人警察裸体gay视频 | 亚洲女人网 | 揉捏小核高潮不停颤抖 | 幸福宝导航 |