Android 集成 FFmpeg 获取 FFmpeg 执行进度

Posted 王英豪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 命令的魅(噪)力(音)吧。

关注公众号,Get 更多知识点

以上是关于Android 集成 FFmpeg 获取 FFmpeg 执行进度的主要内容,如果未能解决你的问题,请参考以下文章

Mac 平台 Android FFmpeg 编译与集成实践

metaRTC4.0集成ffmpeg编译

Android 集成 FFmpeg 获取 FFmpeg 执行进度

Mac中编译FFmpeg教程(Android版)

使用 ffmpeg 将低延迟 RTSP 视频流式传输到 android

什么是ffm模型?