记ffmpeg在iOS10下硬编码视频崩溃问题

最近使用ffmpeg调用videotoolbox进行硬编码,在iOS10系统下测试时,发现运行一两分钟就会崩溃。

ffmpeg_crash_thread01.png

同时控制台会输出

[h264_videotoolbox @ 0x12c92d600] Error encoding frame: 0
[h264_videotoolbox @ 0x12c92d600]

经过查阅ffmpeg源码和videotoolbox相关文档,解决了这个问题,记录下解决过程。

ffmpeg调用videotoolbox硬编码简单流程

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
	//找到硬编码codec
	AVCodec*codec = avcodec_find_encoder_by_name("h264_videotoolbox");
	//创建codecContext
	AVCodecContext*ctx = avcodec_alloc_context3(codec);
	//设置参数
	...
	//创建AVFrame
	AVFrame *pYuvFrame = av_frame_alloc();
	//拷贝原始数据给frame
	av_image_fill_arrays(pYuvFrame->data, pYuvFrame->linesize, videoBuffer, AV_PIX_FMT_YUV420P, width, height, 1);
	//将frame交给context编码
	int result = avcodec_send_frame(ctx, pYuvFrame);
    if (result < 0) {
        //编码错误,释放context
        avcodec_free_context(&ctx);
        ...
        return false;
    }
    //没有错误,取出编码后数据,做后续处理
    AVPacket *pkt = av_packet_alloc();
    while (true) {
    	//将编码后数据写入AVPacket
        result = avcodec_receive_packet(ctx, pkt);
        if (result < 0) {
            break;
        }
        
        ...
    }

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
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
static void vtenc_output_callback(
    void *ctx,
    void *sourceFrameCtx,
    OSStatus status,
    VTEncodeInfoFlags flags,
    CMSampleBufferRef sample_buffer)
{
    AVCodecContext *avctx = ctx;
    VTEncContext   *vtctx = avctx->priv_data;
    ExtraSEI *sei = sourceFrameCtx;

    if (vtctx->async_error) {
        if(sample_buffer) {
            CFRelease(sample_buffer);
        }
        return;
    }

    if (status || !sample_buffer) {
        av_log(avctx, AV_LOG_ERROR, "Error encoding frame: %d\n", (int)status);
        set_async_error(vtctx, AVERROR_EXTERNAL);
    }

   ...
    vtenc_q_push(vtctx, sample_buffer, sei);
}

分析vtenc_output_callback函数

这个回调函数首先检查了VTEncContext这个结构体的async_error值是否不为0,这个async_error是用于记录错误值,并最终返回为avcodec_send_frame函数的结果。之后是检查status和sample_buffer,根据控制台输出,可以判断在崩溃前,函数进入到了这个if判断,并且由于status打印的是0,可以知道进入这里是sample_buffer为空。最后将sample_buffer传入一个队列,用于后续的处理。

通过增加打印日志找到崩溃前的代码执行顺序

回调函数中打印日志

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
static void vtenc_output_callback(
    void *ctx,
    void *sourceFrameCtx,
    OSStatus status,
    VTEncodeInfoFlags flags,
    CMSampleBufferRef sample_buffer)
{
    printf("vtencoutputcallback %d\n",__LINE__);//522
    AVCodecContext *avctx = ctx;
    VTEncContext   *vtctx = avctx->priv_data;
    ExtraSEI *sei = sourceFrameCtx;

    if (vtctx->async_error) {
        printf("vtencoutputcallback %d\n",__LINE__);//528
        if(sample_buffer) {
            CFRelease(sample_buffer);
        }
        printf("vtencoutputcallback %d\n",__LINE__);//534
        return;
    }

    if (status || !sample_buffer) {
        av_log(avctx, AV_LOG_ERROR, "Error encoding frame: %d\n", (int)status);
        set_async_error(vtctx, AVERROR_EXTERNAL);
        
        printf("vtencoutputcallback %d\n",__LINE__);//542
        return;
    }

    ...

    vtenc_q_push(vtctx, sample_buffer, sei);
}

首先在进入回调后增加打印,然后在出错误的两个判断中也增加,然后重新编译,运行程序。 发生崩溃后,打印日志如下:

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
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
static int vtenc_q_pop(VTEncContext *vtctx, bool wait, CMSampleBufferRef *buf, ExtraSEI **sei)
{
    BufNode *info;

    pthread_mutex_lock(&vtctx->lock);

    if (vtctx->async_error) {
        pthread_mutex_unlock(&vtctx->lock);
        printf("vtencqpop %d",__LINE__);//241
        return vtctx->async_error;
    }

    ...
}

static av_cold int vtenc_frame(
    AVCodecContext *avctx,
    AVPacket       *pkt,
    const AVFrame  *frame,
    int            *got_packet)
{
    ...

    status = vtenc_q_pop(vtctx, !frame, &buf, &sei);
    if (status)
    {
        printf("vtencframe %d status %d\n",__LINE__,status);//2283
        goto end_nopkt;
    }
    if (!buf)   goto end_nopkt;

    ...
    return 0;

end_nopkt:
	printf("vtencframe %d end_nopkt\n",__LINE__,status);//2301
    av_packet_unref(pkt);
    return status;
}

然后在调用avcodec_send_frame处打印出错时的结果。

1
2
3
4
5
6
7
8
9
	int result = avcodec_send_frame(ctx, pYuvFrame);
    if (result < 0) {
    	printf("result = %d\n",result);
        //编码错误,释放context
        avcodec_free_context(&ctx);
        ...
        return false;
    }

崩溃后,日志输出:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
static av_cold int vtenc_close(AVCodecContext *avctx)
{
    
	VTEncContext *vtctx = avctx->priv_data;
    printf("vtencclose %d\n",__LINE__);//2412
    if(!vtctx->session) return 0;
    printf("vtencclose %d\n",__LINE__);//2414
    VTCompressionSessionCompleteFrames(vtctx->session,
                                       kCMTimeIndefinite);
    printf("vtencclose %d\n",__LINE__);//2417
    clear_frame_queue(vtctx);
	 ...
    return 0;
}

在编码器关闭函数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
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
static void vtenc_output_callback(
    void *ctx,
    void *sourceFrameCtx,
    OSStatus status,
    VTEncodeInfoFlags flags,
    CMSampleBufferRef sample_buffer)
{
    
    if (!CMSampleBufferDataIsReady(sampleBuffer))
    {
//        NSLog(@"didCompressH264 data is not ready ");
        return;
    }
    AVCodecContext *avctx = ctx;
    VTEncContext   *vtctx = avctx->priv_data;
    ExtraSEI *sei = sourceFrameCtx;

    if (vtctx->async_error) {
        if(sample_buffer) {
//            CFRelease(sample_buffer);
        }
        return;
    }

   ...
}

补充:一个有点弯路的解决方法

在找到正确的解决方法之前,还曾经做过一个尝试:既然出错后销毁会有问题,那我不销毁,继续使用context呢?
于是经过尝试,发现vtctx->async_error会一直不为0,也就是说设置过一次后,就无法继续,只有在关闭的时候才会置0,因此尝试在将结果返回给调用方时,也将vtctx->async_error置为0。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int vtenc_q_pop(VTEncContext *vtctx, bool wait, CMSampleBufferRef *buf, ExtraSEI **sei)
{
    BufNode *info;

    pthread_mutex_lock(&vtctx->lock);

    if (vtctx->async_error) {
        
        int err = vtctx->async_error;
        vtctx->async_error = 0;
        pthread_mutex_unlock(&vtctx->lock);
        return err;
    }
    ...
}

这样之后,经过尝试,也不会崩溃。但一来这种改动违背了作者的初衷,既然作者没有将vtctx->async_error置为0,也就是说作者希望出错后就重新创建,二来可能对其他的错误处理造成影响,因此不考虑使用这种方式解决。