建设校园网站,北京开发网站建设,asp企业网站模版,宜昌云网站建设一#xff0c;背景
作为一个音视频开发者#xff0c;在日常工作中经常会使用ffmpeg 命令来做很多事比如转码ffmpeg -y -i test.mov -g 150 -s 1280x720 -codec libx265 -r 25 test_h265.mp4 #xff0c;水平翻转视频#xff1a;ffmpeg -i src.mp4 -vf hflip -acodec copy …一背景
作为一个音视频开发者在日常工作中经常会使用ffmpeg 命令来做很多事比如转码ffmpeg -y -i test.mov -g 150 -s 1280x720 -codec libx265 -r 25 test_h265.mp4 水平翻转视频ffmpeg -i src.mp4 -vf hflip -acodec copy -vcodec h264 -b 22000000 out.mp4视频截取ffmpeg -i input.wmv -ss 00:00:30.0 -c copy -t 00:00:10.0 output.wmv 等等一个简单的命令就可以解决很多事情如果通过执行一些命令就能完成日常开发工作那么能极大的提升我们的开发效率但是这些命令只能在PC上使用在移动端是无法直接使用的这也就引出了这篇文章的所要讲的内容–FFmpeg命令行工具编译
编译好的工程https://github.com/bookzhan/bzffmpegcmd 想偷懒的可以直接跳过本文直接使用或者直接看源码记得给一个Start不过建议完整看完本文你收获的会更多
由于ffmpeg命令是一个功能完备且比较独立的模块因此在开发中我们一般都编译为一个独立的SO在需要的地方作为动态库引入就好了话不多说我们来看看FFmpeg官方在PC上实现ffmpeg命令的过程
二FFmpeg实现ffmpeg命令的方式 本文使用的FFmpeg版本为6.0其它版本大同小异 通过查看源码不难发现FFmpeg实现ffmpeg命令是通过fftools/ffmpeg.c文件来实现的通常这种.c都有一个入口函数也就是我们常见的main函数在ffmpeg.c的入口函数为int main(int argc, char **argv) 其中argc是args count的缩写在c函数中传指针都需要指定指针的长度根据这个长度来防止访问越界char **argv是一个二级指针里面存放的是参数类似于ffmpge, -i , test.mov, out.mp4的字符串 进一步查看main函数就可以发现这个函数很短但是基本流程都包括了详见下面的注释
int main(int argc, char **argv)
{int ret;BenchmarkTimeStamps ti;init_dynload();//加载动态库的用于处理Windowsdll库的register_exit(ffmpeg_cleanup);//程序结束的回调setvbuf(stderr,NULL,_IONBF,0); /* win32 runtime needs this */av_log_set_flags(AV_LOG_SKIP_REPEATED);parse_loglevel(argc, argv, options);
#if CONFIG_AVDEVICEavdevice_register_all();//老版本还有很多需要注册的包括编码器解码器解复用等新版的不需要处理了
#endifavformat_network_init();//只是需要初始化一次就好了show_banner(argc, argv, options);/* 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);}current_time ti get_benchmark_time_stamps();if (transcode() 0)//核心流程exit_program(1);if (do_benchmark) {int64_t utime, stime, rtime;current_time get_benchmark_time_stamps();utime current_time.user_usec - ti.user_usec;stime current_time.sys_usec - ti.sys_usec;rtime current_time.real_usec - ti.real_usec;av_log(NULL, AV_LOG_INFO,bench: utime%0.3fs stime%0.3fs rtime%0.3fs\n,utime / 1000000.0, stime / 1000000.0, rtime / 1000000.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;
}三ffmpeg.c文件编译
如上所示我们之需要把ffmpeg.c的main函数调用起来就好听起来是不是很简单[手动狗头]那我们就来编译首先请按照Android音视频开发实战01-环境搭建 把Native开发的环境搭建起来包括ffmpeg的include的文件特别是config.h文件以及ffmpeg so文件最终的文件结构如下
3.1 依赖文件处理
fftools 文件夹里面的文件很多我们没有必要全部copy进去先把ffmpeg.hffmpeg.c文件copy进去然后看看哪里有报错就把报错的文件的文件copy进去最终需要的文件如下里面cpp和ffmpeg_cmd文件是后来新建的请先忽略
3.2 调用main函数
我们可以写一个jni函数把main函数直接调用起来不会jni的可以参考音视频开发实战02-JNI写一个命令然后执行 我们把main函数调用起来之后会发现命令执行成功了但是app退出了类似发生crash了入坑了 没得办法只能一步步看源码此处省略10086个字最终在这个函数中发现了猫腻如下 没错ffmpeg.c文件在运行过程中有很多地方调用了这个函数退出的原因就在于执行了exit函数exit在Linux系统中的实现就是退出进程但是Android App运行起来后就一个主进程退出后整个App就退出了如果作为电脑的命令行工具那么没有问题每一次执行都是新开一个进程执行完后进程释放但是作为作为Android应用那就不行了我们注释掉之后程序能够正常运行不再退出。
3.3 程序健壮性处理
我们在接入一个陌生库的时候步骤一般如下
先看License看协议是否符合开源规范常见的开源协议可以参考这篇文章https://www.cnblogs.com/findumars/p/9874836.html导入SDK成功跑起来异常参数调用测试重复调用测试多线程调用测试内存泄漏检查代码review确保没有高危代码
123没什么好说的我们做后面的测试
3.3.1 重复调用测试
我们在重复调用main函数之后你会惊奇的发现程序会crashFFmpeg会这么坑我不可能绝对不可能接着看代码吧此处省略10086个字最终你会发现ffmpeg.c文件里面的变量都是静态变量如果是想PC那样作为进程来调用那么自然没有问题每次进程起来这些变量就相当于是初始值如果是面向对象编程也不存在这样的问题每次new 一个Class那么这些变量也就恢复初始值了嗨~吃了没有对象的亏那么现在只能在每次程序运行完成后把这些变量的值重置。在ffmpeg_cleanup函数中把这些变量重置如下
static void ffmpeg_cleanup(int ret) {//...progress_avio NULL;input_files NULL;nb_input_files 0;output_files NULL;nb_output_files 0;filtergraphs NULL;nb_filtergraphs 0;ffmpeg_exited 1;
}3.3.2 多线程调用测试
在3.3.1中我们知道ffmpeg.c中有很多变量是静态的那么在我们处理完后单线程调用肯定是没有什么问题的但是在多线程调用的情况下那么这些变量的读写就会串掉随手测试一把就会发现疯狂的crash加锁C语言的加锁一般都是使用pthread提供的pthread_mutex_lock其中cmdLock作为静态变量全局唯一如下 if (!cmdLockHasInit) {pthread_mutex_init(cmdLock, NULL);//初始化cmdLockHasInit 1;}pthread_mutex_lock(cmdLock);//...处理逻辑pthread_mutex_unlock(cmdLock);3.3.3 内存泄漏检查
内存泄漏检查没有太多好说的重复运行多次后观察内存增长情况就好了这里经过测试ffmpeg.c没有什么问题
3.3.4 代码review确保没有高危代码
这一步不可少这一步是确保代码健壮性的重要保障即使常规case已经测试过了这一步也可以提前做不过我喜欢放在全部run起来之后再做一开始就review代码很容易劝退。我们这里review代码不需要很仔细重点要关注流程。 在我review代码的过程中发现ffmpeg.c有很多地方调用了exit_program方法特别是在状态不对发生错误的时候在原先的实现中exit_program是直接把整个进程退出了那么exit_program之后的代码就不会执行但是我们不能退出进程而且要确保exit_program方法执行完后面的代码不能被调用因为很多资源都被释放状态已经不对了代码往下执行会发生不可预知的问题。 因此我们需要修改调用exit_program的地方改成retrun exit_program(), 同时让exit_program的返回值改成int把传入的错误码再返回回去确保错误码能够被传递到调用方需要修改的地方很多具体的请直接查看代码。
四程序封装
4.1 支持以字符串的方式调用ffmpeg
我们可以看到ffmpeg.c的main函数的入参是一个二级指针可以理解为一个二维数组调用的时候很不方便我们希望在使用的时候和在PC命令工具里面一样输入一个ffmpeg命令就可以直接使用那么就涉及到命令的解析如下
char *pCommand (char *) command;int stingLen (int) (strlen(command) 1);char *argv[stingLen];char *buffer NULL;int index 0;int isStartYH 0;for (int i 0; i stingLen; i) {char str *pCommand;pCommand;if (NULL buffer) {buffer malloc(512);memset(buffer, 0, 512);argv[index] buffer;}//保证引号成对出现if (str ) {if (isStartYH) {isStartYH 0;} else {isStartYH 1;}continue;}if (str ! || isStartYH) {*buffer str;buffer;} else {buffer NULL;}}//手动告诉它结束了,防止出现意外argv[index] 0;int ret exe_ffmpeg_cmd(index, argv, handle, progressCallBack, totalTime);for (int i 0; i index; i) {free(argv[i]);}经过这样处理之后我们输入类似ffmpeg -i src.mp4 out.mp4之后就可以自动解析参数传入main函数了
4.2 支持进度回调
由于FFmpeg的命令一般都是处理音视频的相对来说耗时较长如果没有进度的话是很让人抓狂的一件事ffmpeg处理音视频的流程一般来说很固定如下
读取文件读取文件视频流音频流元信息分配解码器初始化输出文件添加视频流音频流初始化编码器解复用循环读取音视频信息解码编码复用-写音视频数据完成 我们要做进度回调的话一般都是在第10步去做处理根据写入的音视频数据的时间戳/视频的总时间那么就能得到我们想要的视频处理进度了结合音视频开发实战02-JNI 所讲的回调函数的写法我们可以很容易的实现
static int write_packet(Muxer *mux, OutputStream *ost, AVPacket *pkt)
{//...//回调处理enum AVMediaType mediaType;if (ost-hasVideoStream) {mediaType AVMEDIA_TYPE_VIDEO;} else {mediaType AVMEDIA_TYPE_AUDIO;}if (NULL ! ost-st NULL ! pkt pkt-dts 0 ost-duration 0 NULL ! ost-progressCallBack mediaType ost-st-codecpar-codec_type) {if (ost-writePacketCount % 2 0) {int64_t temp pkt-dts * 1000 * ost-st-time_base.num /ost-st-time_base.den;float progress temp * 1.0f / ost-duration;ost-progressCallBack(ost-callBackHandle, 0, progress);}ost-writePacketCount;}//回调处理结束
}核心代码到这里就结束了还有一些其他的封装就不再这里讲了具体的可以去git库里面查看