背景
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.h
和 fftools/ffmpeg.c
。ffmpeg.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 类似的使用体验。他的实现方法我感觉很巧妙,简单来说是:利用 setjmp
和 longjmp
这两个标准库函数,在 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);
}
|