Android 获取 FFmpeg 执行进度

共 22225字,需浏览 45分钟

 ·

2021-06-11 19:51


在以命令方式调用 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(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("已执行:" + 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 命令的魅(噪)力(音)吧。


版权声明:本文为CSDN博主「王英豪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

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


项目地址:https://github.com/yhaolpz/FFmpegCmd


-- END --


推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

浏览 48
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报