文章

刨根问底:FFmpeg4 构造错误 AVCC 导致无法硬解

刨根问底:FFmpeg4 构造错误 AVCC 导致无法硬解

背景

刚做播放器的时候就发现我司的视频无法硬解,当时对比了重构前的原版 ijkplayer 发现在 iOS 上可以硬解,然后定位到问题是创建 videotoolbox 的时候传入的 AVCC 数据不对导致的,当时刚接触 FFmpeg 不久,根本没有能力解释这个问题的原因,修复的方式简单粗暴了些:

FFmpeg 在 ff_videotoolbox_avcc_extradata_create 函数里构造的 AVCC,当长度不对时就使用视频里自己带的,不用 FFmpeg 自己二次构造的,

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
/*
ffmpeg constructed avcc is wrong,but i dont't why;eg:
ff constructed avcc:
014d401effe1001b674d401eec806c1ef3fff8140013f88000000080000019078b16cb01000468ebec4c
avctx->extradata avcc:
014d401effe1001c674d401eec806c1ef3fff8140013f8800000030080000019078b16cb01000568ebec4c80
*/
if (avctx->extradata_size != vt_extradata_size) {
    char msg[256];
    {
        char buffer[128];
        sprintf(buffer, "%s", "ff avcc maybe wrong:");
        int len = (int)strlen(buffer);
        for (int i = 0; i < vt_extradata_size; i++) {
            len += sprintf(buffer + len, "%02X", vt_extradata[i]);
        }
        sprintf(msg, "%s", buffer);
    }
    {
        char buffer[128];
        sprintf(buffer, "%s", "\nuse origin avcc:");
        int len = (int)strlen(buffer);
        for (int i = 0; i < avctx->extradata_size; i++) {
            len += sprintf(buffer + len, "%02X", avctx->extradata[i]);
        }
        sprintf(msg + strlen(msg), "%s", buffer);
    }
    av_log(avctx, AV_LOG_INFO, "%s\n", msg);
    data = CFDataCreate(kCFAllocatorDefault, avctx->extradata, avctx->extradata_size);
} else {
    data = CFDataCreate(kCFAllocatorDefault, vt_extradata, vt_extradata_size);
}

因为当时修复这个问题时播放器还在灰度阶段,仅限于播放本地视频,其实是修复了我司的本地 mp4 视频,后续当播放网络视频 hls 流时没人注意到使用了软解,就这样这个逻辑一直沿用至今。

科普下 H.264 视频数据有两种常见的封装方式:

  • Annex-B 格式:使用 0x00 0x00 0x00 0x01 或 0x00 0x00 0x01 作为 NAL 单元的起始码(Start Code),常用于文件格式(如 .mp4)和实时流(如 RTSP)。
  • AVCC 格式(也称为 AVC1 或 AVC Decoder Configuration Record):不使用起始码,而是用 4 字节的长度字段(Length Prefix)标识每个 NAL 单元的长度,常用于 MP4 容器和 VideoToolbox 等解码器的输入。

拨云见日

就这样这个诡异的 80 存在了很长时间,直到我最近升级了 FFmpeg 7.1.1 之后,自测时才发现播放网络视频时(HLS/TS)使用的是软解,于是又重新开始调查这个问题,发现构造 avcc 的代码更新了,有了一个新的逻辑,计算 sps 长度时先做一个转义处理:

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
42
43
44
CFDataRef ff_videotoolbox_avcc_extradata_create(AVCodecContext *avctx)
{
    VTContext *vtctx = avctx->internal->hwaccel_priv_data;
    H264Context *h = avctx->priv_data;
    CFDataRef data = NULL;
    uint8_t *p;
    int sps_size = escape_ps(NULL, h->ps.sps->data, h->ps.sps->data_size);
    int pps_size = escape_ps(NULL, h->ps.pps->data, h->ps.pps->data_size);
    int vt_extradata_size;
    uint8_t *vt_extradata;

    vt_extradata_size = 6 + 2 + sps_size + 3 + pps_size;
    vt_extradata = av_malloc(vt_extradata_size);

    if (!vt_extradata)
        return NULL;

    p = vt_extradata;

    AV_W8(p + 0, 1); /* version */
    AV_W8(p + 1, h->ps.sps->data[1]); /* profile */
    AV_W8(p + 2, h->ps.sps->data[2]); /* profile compat */
    AV_W8(p + 3, h->ps.sps->data[3]); /* level */
    AV_W8(p + 4, 0xff); /* 6 bits reserved (111111) + 2 bits nal size length - 3 (11) */
    AV_W8(p + 5, 0xe1); /* 3 bits reserved (111) + 5 bits number of sps (00001) */
    AV_WB16(p + 6, sps_size);
    p += 8;
    p += escape_ps(p, h->ps.sps->data, h->ps.sps->data_size);
    AV_W8(p + 0, 1); /* number of pps */
    AV_WB16(p + 1, pps_size);
    p += 3;
    p += escape_ps(p, h->ps.pps->data, h->ps.pps->data_size);

    av_assert0(p - vt_extradata == vt_extradata_size);

    // save sps header (profile/level) used to create decoder session,
    // so we can detect changes and recreate it.
    if (vtctx)
        memcpy(vtctx->sps, h->ps.sps->data + 1, 3);

    data = CFDataCreate(kCFAllocatorDefault, vt_extradata, vt_extradata_size);
    av_free(vt_extradata);
    return data;
}

于是我尝试将之前使用原始 avcc 的 patch 删了,重新编译竟然发现 ts 也能硬解了,之前的本地 mp4 也能!这太好了呀,然后查看提交记录发现,早就有人改了,跟我发现无法硬解问题是同一年。也就是说 5、6、7 代都没有这个问题了,于是我删了 patch 重新打了 FFmpeg5、6、7代。

尝试反哺 4 代

那 4 代呢?毕竟我的编译脚本还维护 4 代,为了搞清楚这个问题的根源,我决定将代码切到升级到 5 代之前,下面是 4 代FFmpeg 构造 avcc 的源码:

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
static CFDataRef ff_videotoolbox_avcc_extradata_create(AVCodecContext *avctx)
{
    VTContext *vtctx = avctx->internal->hwaccel_priv_data;
    H264Context *h = avctx->priv_data;
    CFDataRef data = NULL;
    uint8_t *p;
    int vt_extradata_size = 6 + 2 + h->ps.sps->data_size + 3 + h->ps.pps->data_size;
    uint8_t *vt_extradata = av_malloc(vt_extradata_size);
    if (!vt_extradata)
        return NULL;

    p = vt_extradata;

    AV_W8(p + 0, 1); /* version */
    AV_W8(p + 1, h->ps.sps->data[1]); /* profile */
    AV_W8(p + 2, h->ps.sps->data[2]); /* profile compat */
    AV_W8(p + 3, h->ps.sps->data[3]); /* level */
    AV_W8(p + 4, 0xff); /* 6 bits reserved (111111) + 2 bits nal size length - 3 (11) */
    AV_W8(p + 5, 0xe1); /* 3 bits reserved (111) + 5 bits number of sps (00001) */
    AV_WB16(p + 6, h->ps.sps->data_size);
    memcpy(p + 8, h->ps.sps->data, h->ps.sps->data_size);
    p += 8 + h->ps.sps->data_size;
    AV_W8(p + 0, 1); /* number of pps */
    AV_WB16(p + 1, h->ps.pps->data_size);
    memcpy(p + 3, h->ps.pps->data, h->ps.pps->data_size);

    p += 3 + h->ps.pps->data_size;
    av_assert0(p - vt_extradata == vt_extradata_size);

    // save sps header (profile/level) used to create decoder session,
    // so we can detect changes and recreate it.
    if (vtctx)
        memcpy(vtctx->sps, h->ps.sps->data + 1, 3);

    data = CFDataCreate(kCFAllocatorDefault, vt_extradata, vt_extradata_size);
    av_free(vt_extradata);
    return data;
}

我直接搬运了 7 代的 ff_videotoolbox_avcc_extradata_create 方法,得到 avcc 还是不对:

1
014d401effe1001c674d401eec806c1ef3fff8140013f8800000030080000019078b16cb01000568ebec4c

但是呢,比 4 代有进步,因为这个结果和 7 代就差末尾的 80 了,要知道 4 代时,除了末尾的 80 还有中间几个字节也不同。

经过查阅源码,断定 pps 信息是在 ff_h264_decode_picture_parameter_set 这个函数里解析的,原本想直接把 7 代最新代码拿过来,发现编译报错,所以硬着头皮开两个 Xcode,左边是 7 代,右边是 4 代,一行行地开始比对,直到对比到 pps->sps_id = get_ue_golomb_31(gb); 时,发现逻辑不一样了:

在 7 代的代码里发现了 0x80,这不就我苦苦寻找的么,把这行代码复制到 4 代上之后,ff_videotoolbox_avcc_extradata_create 方法构造出来的 avcc 终于对了,最后也带上了 80,播放后查看解码器确定走了 videotoolbox 硬解。

下面给大家看下,就这个代码,补上了 80:

1
2
3
// Re-add the removed stop bit (may be used by hwaccels).
if (!(bit_length & 7) && pps->data_size < sizeof(pps->data))
    pps->data[pps->data_size++] = 0x80;

注释写得很对,硬件加速的确需要这个 0x80,这个逻辑是 22 年 6 月提交的。

接下来就是更新 FFmpeg ,让我司的 ts 能硬解,还有就是将上述修改打成 patch,重新编译 4 代 FFmpeg 给大家用。

本文由作者按照 CC BY 4.0 进行授权