This page looks best with JavaScript enabled

通过分析mobile Ffmpeg解析如何优雅的集成ffmpeg到Android应用中

 ·  ☕ 3 min read

背景

FFmpeg 是一个开源的、强大的音视频工具库,平常情况下的使用方法一般是利用编译好的 ffmpeg 程序,在 PC 上使用,需要不同的功能时只需传递不同的参数即可,而且都是一条或多条命令就能完成功能,非常方便。

比如要从视频中提取音乐,执行 ffmpeg -i input.mp4 output.mp3 就能搞定了,执行完成之后 ffmpeg 程序就退出了。也就是说 ffmpeg 命令行程序的机制就是:它是一个生命周期很简单的程序,执行完一个任务就退出。

但是,当我们想在移动应用上集成 ffmpeg,并且也希望能够如同在 PC 上那样使用,传递命令参数就能执行对应功能时,ffmpeg 的运行完成就退出的机制其实会带给我们不便。这个不便是什么呢?– 请继续浏览下文。

集成 ffmpeg 之殇

为了实现在 Android 上能和在 PC 上一样的使用方法(因为直接传递参数给 ffmpeg 程序,比自己调用 ffmpeg 的 api 实现各种功能,方便的不是一点点),我首先分析了下 PC 上使用的 ffmpeg 程序是怎么来的。

ffmpeg 命令行程序的由来

当我们在 PC 上安装了 ffmpeg 的程序之后,一般都是在命令行中就能直接调用了,它本质就是一个可直接运行的程序。那么它对应的源码在 ffmpeg 项目中的哪里呢?

➜  ~ which ffmpeg
/usr/local/bin/ffmpeg
➜  ~ ls -la /usr/local/bin/ffmpeg
-rwxr-xr-x /usr/local/bin/ffmpeg

在 FFmpeg 4.3.1 的源码中,有这么一个目录:fftools,它里面就存放了常见的 ffmpeg ffprobe ffplay 对应的源码文件。ffmpeg 对应的源码是 fftools/ffmpeg.hfftools/ffmpeg.cffmpeg.c 中的 main 方法就是 ffmpeg 命令行程序的运行入口。

main 方法的工作主要为:

  • 注册所有 ffmpeg 组件
  • 判断是否传递了输入文件路径
  • 调用方法解析传递给 main 方法的参数,并执行参数对应的功能
  • 判断是否传递了输出文件路径
  • 执行程序清理工作

源码大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int main(int argc, char **argv)
{
    int i, ret;
    init_dynload();

    register_exit(ffmpeg_cleanup);

#if CONFIG_AVDEVICE
    avdevice_register_all();
#endif
    avformat_network_init();

    /* parse options and open all input/output files */
    ret = ffmpeg_parse_options(argc, argv);
    if (ret < 0)
        exit_program(1);

    if (nb_output_files <= 0 && nb_input_files == 0) {
        show_usage();
        av_log(NULL, AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n", program_name);
        exit_program(1);
    }

    /* file converter / grab */
    if (nb_output_files <= 0) {
        av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n");
        exit_program(1);
    }

    for (i = 0; i < nb_output_files; i++) {
        if (strcmp(output_files[i]->ctx->oformat->name, "rtp"))
            want_sdp = 0;
    }

    av_log(NULL, AV_LOG_DEBUG, "%"PRIu64" frames successfully decoded, %"PRIu64" decoding errors\n", decode_error_stat[0], decode_error_stat[1]);
    if ((decode_error_stat[0] + decode_error_stat[1]) * max_error_rate < decode_error_stat[1])
        exit_program(69);

    exit_program(received_nb_signals ? 255 : main_return_code);
    return main_return_code;
}

其实这个程序,我把方法名改改,然后封装一层 JNI,让 Java 层能够调到,那么就可以在 Android 平台上运行了,使用者就如同在 PC 上使用时一样方便了。

但是,当你尝试之后,就会发现,当这段代码执行完成之后,你的 App 进程会跟着就退出了 …….,因为 exit_program 这个方法。 这个方法定义在 cmdutils.c 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static void (*program_exit)(int ret);

void exit_program(int ret)
{
    if (program_exit)
        program_exit(ret);

    exit(ret);
}

void register_exit(void (*cb)(int ret))
{
    program_exit = cb;
}

exit_program 方法会先调用 program_exit 函数指针,然后调用了系统库方法 exit。OMG,这种做法在 PC 上是没问题的,因为在 PC 上,我们通过命令行执行命令,都会新创建一个进程来执行,但是在 Android App 上,我们 JNI 调用过来,默认都是在当前进程,如果在当前进程调用了 exit,就意味着我们 App 进程要退出了。

mobile-ffmpeg 的解决方案

遇到这个坑之后,我就找了找,找到了开源的 mobile-ffmpeg,这个库就实现了 Android 上和 PC 类似的使用体验。他的实现方法我感觉很巧妙,简单来说是:利用 setjmplongjmp 这两个标准库函数,在 ffmpeg 程序要退出时,将程序的执行状态恢复到调用 ffmpeg 程序之前。

mobile-ffmpeg 实现了自己的 ffmpeg、ffprobe、 cmdutils,通过修改 ffmpeg 源码中的部分实现达到了避免程序运行 ffmpeg 指令之后进程退出的情况。

在执行 ffmpeg.c 的 main 函数代码之前,先利用 setjmp 将程序的执行状态保留:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int ffmpeg_execute(int argc, char **argv)
{

    int savedCode = setjmp(ex_buf__);
    if (savedCode == 0) {
        // 执行 ffmpeg.c main 函数中的代码
    } else {
        main_ffmpeg_return_code = (received_nb_signals || cancelRequested(executionId)) ? 255 : longjmp_value;
    }

    return main_ffmpeg_return_code;
}

在 ffmpeg 期望退出程序时,将程序的执行状态恢复到之前保留的状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void exit_program(int ret)
{
    if (program_exit)
        program_exit(ret);

    // exit disabled and replaced with longjmp, exit value stored in longjmp_value
    // exit(ret);
    longjmp_value = ret;
    longjmp(ex_buf__, ret);
}

大致流程


Yang
WRITTEN BY
Yang
Developer