刨根问底: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 给大家用。