最近使用ffmpeg调用videotoolbox进行硬编码,在iOS10系统下测试时,发现运行一两分钟就会崩溃。
同时控制台会输出
[h264_videotoolbox @ 0x12c92d600] Error encoding frame: 0
[h264_videotoolbox @ 0x12c92d600]
经过查阅ffmpeg源码和videotoolbox相关文档,解决了这个问题,记录下解决过程。
ffmpeg调用videotoolbox硬编码简单流程
1 |
|
ffmpeg调用videotoolbox硬编码的代码调用和普通的软编码类似,只是avcodec的获取方式通过字符串”h264_videotoolbox”来获取,然后根据codec创建AVCodecContext,在codecContext设置好一系列编码参数后,创建一个AVFrame,调用av_image_fill_arrays将原始视频数据写入AVFrame中,然后调用avcodec_send_frame函数将AVFrame传给codecContext进行编码。
定位错误
首先根据崩溃线程所在队列名称,可以看出是和videotoolbox硬编码相关的,如果了解过videotoolbox使用流程的话,基本可以确定是在硬编码的回调函数中。打开ffmpeg中videotoolbox硬编码的实现文件videotoolboxenc.c,找到硬编码的回调函数vtenc_output_callback,可以看到崩溃是控制台输出的位置,基本确定崩溃是在vtenc_output_callback中。
1 |
|
分析vtenc_output_callback函数
这个回调函数首先检查了VTEncContext这个结构体的async_error值是否不为0,这个async_error是用于记录错误值,并最终返回为avcodec_send_frame函数的结果。之后是检查status和sample_buffer,根据控制台输出,可以判断在崩溃前,函数进入到了这个if判断,并且由于status打印的是0,可以知道进入这里是sample_buffer为空。最后将sample_buffer传入一个队列,用于后续的处理。
通过增加打印日志找到崩溃前的代码执行顺序
回调函数中打印日志
1 |
|
首先在进入回调后增加打印,然后在出错误的两个判断中也增加,然后重新编译,运行程序。 发生崩溃后,打印日志如下:
vtencoutputcallback 522
vtencoutputcallback 522
Error encoding frame: 0
vtencoutputcallback 542
[h264_videotoolbox @ 0x14e10be00] vtencoutputcallback 522
vtencoutputcallback 528
vtencoutputcallback 534
根据日志打印顺序可以分析出,一开始是正常的执行回调函数,直到sample_buffer某次为空后,会先进入(status | !sample_buffer)这个判断并打印Error encoding frame: 0,然后下次进入回调的时候,再进入(vtctx->async_error)这个判断,然后执行完CFRelease(sample_buffer)后崩溃。 |
从async_error入手
因为崩溃的时候async_error不为0,所以想到看看async_error会影响哪些地方。
1 |
|
然后在调用avcodec_send_frame处打印出错时的结果。
1 |
|
崩溃后,日志输出:
vtencoutputcallback 522
vtencoutputcallback 522
vtencoutputcallback 522
Error encoding frame: 0
vtencoutputcallback 542
[h264_videotoolbox @ 0x157101e00] vtencqpop 241vtencframe 2283 status -542398533
vtencframe 2301 end_nopkt
result = -542398533
vtencoutputcallback 522
vtencoutputcallback 528
vtencoutputcallback 534
可以看到,在出现空的sample_buffer后,就将错误值返回给了上层调用,然后在上层已经调用 avcodec_free_context(&ctx);释放context的情况下,再次进入了编码回调,并且进入(vtctx->async_error)这个判断。因此就要看看在释放context的过程中发生了什么。
分析AVCodecContext释放过程
1 |
|
在编码器关闭函数vtenc_close中,每行增加打印,运行程序,崩溃后控制台输出:
vtencoutputcallback 522
vtencoutputcallback 522
Error encoding frame: 0
vtencoutputcallback 542
[h264_videotoolbox @ 0x12604aa00] vtencqpop 241vtencframe 2283 status -542398533
vtencframe 2301 end_nopkt
result = -542398533
vtencclose 2412
vtencclose 2414
vtencoutputcallback 522
vtencoutputcallback 528
vtencoutputcallback 534
根据打印可以看出,执行到 VTCompressionSessionCompleteFrames后就崩溃在CFRelease(sample_buffer);之后。
尝试注释CFRelease(sample_buffer);解决问题
查找了苹果的官方文档,VTCompressionSessionCompleteFrames的作用是强制让编码器完成编码。
Forces the compression session to complete the encoding of frames.
猜测是在回调中sample_buffer已经被释放,而编码器执行了VTCompressionSessionCompleteFrames方法后,可能在回调结束后又对sample_buffer指针发送消息,因此造成野指针崩溃。同时也查阅硬件编码的一些使用示例,确认在编码回调函数中CFRelease(sample_buffer)是不必要的。因此尝试注释这句话,重新运行后不再崩溃。
iOS系统差异以及一点优化
为什么只在iOS10及以下系统中出现这个问题呢?导致这个问题出现的首要原因是某些情况下sample_buffer会为空,经过测试,在iOS11和12中都不会有这样的情况。而在出现这种情况后,ffmpeg的回调函数是记录一个错误然后返回错误值给调用方,然后调用方销毁context再重新创建,这样对性能是一个损耗。其实在sample_buffer为空的时候不处理,后续不为空的时候继续即可,这样就避免了无谓的销毁和创建context,最终修改的代码如下:
1 |
|
补充:一个有点弯路的解决方法
在找到正确的解决方法之前,还曾经做过一个尝试:既然出错后销毁会有问题,那我不销毁,继续使用context呢?
于是经过尝试,发现vtctx->async_error会一直不为0,也就是说设置过一次后,就无法继续,只有在关闭的时候才会置0,因此尝试在将结果返回给调用方时,也将vtctx->async_error置为0。代码如下:
1 |
|
这样之后,经过尝试,也不会崩溃。但一来这种改动违背了作者的初衷,既然作者没有将vtctx->async_error置为0,也就是说作者希望出错后就重新创建,二来可能对其他的错误处理造成影响,因此不考虑使用这种方式解决。