记录一次issue分析并给ffmpeg官方提交patch的过程
issue 描述
版本: ffmpeg5.0以下版本
使用ffmpeg生成hls切片
1
|
ffmpeg -i xx.flv -c copy -f hls -hls_flags single_file -hls_list_size 0 xx.m3u8
|
这是个典型的使用ffmpeg生成hls切片的命令,使用了单文件模式.
生成切片后使用native播放器播放hls流,发现会跳过有些切片,这些切片解封装后无法获取对应的h264码流.
issue 分析
这个问题需要单独分析一下这些问题切片,single_file
模式的切片方式m3u8列表生成是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:13
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.820000,
#EXT-X-BYTERANGE:93436@0
xx.ts
#EXTINF:1.625000,
#EXT-X-BYTERANGE:23876@93436
xx.ts
#EXTINF:0.065000,
#EXT-X-BYTERANGE:9776@117312
xx.ts
#EXTINF:1.490000,
#EXT-X-BYTERANGE:21244@127088
xx.ts
#EXTINF:0.066000,
#EXT-X-BYTERANGE:9776@148332
xx.ts
#EXTINF:4.356000,
#EXT-X-BYTERANGE:41360@158108
xx.ts
...
...
|
其中第四个切片是问题切片,首先需要把这些切片从单文件中抽离出来
写个python脚本:
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
|
#!/usr/bin/env python
import sys
infolist = []
with open(sys.argv[1], 'r') as f:
for line in f.readlines():
line = line.strip('\n')
if "BYTERANGE" in line:
_, info = line.split(':')
length, start = info.split('@')
infolist.append((int(length), int(start)))
f.close()
expect = 0
i = 0
with open(sys.argv[2], 'rb') as f:
for (length, start) in infolist:
if expect != start:
print("expect is not equal start, may be error")
with open(sys.argv[2] + '-' +str(i), 'wb') as output:
output.write(f.read(length))
output.close()
i+=1
expect += length
f.close()
|
抽离后单独播放切片,发现ffplay播放正常,vlc播放无内容.
ffprobe分析一下切片,正常的切片和异常切片有点区别
正常切片:
1
|
Stream #0:0[0x100]: Video: h264 (Main) ([27][0][0][0] / 0x001B), yuv420p(progressive), 720x416, 15 fps, 15.17 tbr, 90k tbn
|
异常切片:
1
|
Stream #0:0[0x100]: Video: h264 (Main), yuv420p(progressive), 720x416, 15 fps, 15.38 tbr, 90k tbn
|
其中差别就在括号里的数据([27][0][0][0] / 0x001B)
, 异常切片是没有这个数据的,这一串数据是什么意思呢,熟悉ts封装的应该很快能猜到,27
代表mpegts的pmt表的id,0x1B
是h264在mpegts中的codecid
.
使用ts分析工具验证一下,确实是这样.并且发现异常切片是因为没有写入pat
和pmt
表头,所以解析工具无法识别后续的数据包.vlc单独播放这样的ts也是不行的,但是为什么ffmpeg工具链仍然能分析出codec
格式,并且ffplay也能正常播放呢?那是因为ffmpeg库有猜测功能,它能继续提取mpegts里的pes payload
并猜测分析它的codec
格式.而一般的解析工具是不行的.
问题的直接原因找到了. 还需要分析一下为什么ffmpeg切片会形成这样的片段.
ffmpeg切片分析
要分析切片时某些片段为什么没有写入pat
和pmt
表,直接看写入逻辑,libavformat/mpegtsenc.c
中的retransmit_si_info
1
2
3
4
5
6
7
8
9
|
if ((pcr != AV_NOPTS_VALUE && ts->last_pat_ts == AV_NOPTS_VALUE) ||
(pcr != AV_NOPTS_VALUE && pcr - ts->last_pat_ts >= ts->pat_period) ||
force_pat) {
if (pcr != AV_NOPTS_VALUE)
ts->last_pat_ts = FFMAX(pcr, ts->last_pat_ts);
mpegts_write_pat(s);
for (i = 0; i < ts->nb_services; i++)
mpegts_write_pmt(s, ts->services[i]); ‣service: ts->services[i]
}
|
主要条件是pcr - ts->last_pat_ts >= ts->pat_period
和force_pat
, 第一个条件是定时写入,是配置选项里的,我们主要探究ts片段开头为什么没有写入pat
和pmt
表,这里肯定不是定时机制写入的,所以关注第二个条件, 来看第二个条件force_pat
是什么情况下为true的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
int force_pat = st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && key && !ts_st->prev_payload_key;
int force_sdt = 0;
int force_nit = 0;
av_assert0(ts_st->payload != buf || st->codecpar->codec_type != AVMEDIA_TYPE_VIDEO);
if (ts->flags & MPEGTS_FLAG_PAT_PMT_AT_FRAMES && st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
force_pat = 1;
}
if (ts->flags & MPEGTS_FLAG_REEMIT_PAT_PMT) {
force_pat = 1;
force_sdt = 1;
force_nit = 1;
ts->flags &= ~MPEGTS_FLAG_REEMIT_PAT_PMT;
}
|
三个条件force_pat
会置1,也就是会添加表头
- video帧并且是key帧并且上一个
pre_payload_key
为false,也就是上一个帧不是key帧
- MPEGTS_FLAG_PAT_PMT_AT_FRAMES标志设置了,这个标志对应mpegts的"pat_pmt_at_frames"选项
- MPEGTS_FLAG_REEMIT_PAT_PMT标志,这个标志对应mpegts的"resend_headers"选项
这个issue中一些切片没有表头,肯定是没有击中这几个条件.
这三个条件中的第二个可以忽略,因为没有地方设置第二个标志.
再来看libavformat/hlsenc.c
,调试发现single_file
模式下, hlsenc切片时并不会设置resend_headers
这个选项,对应于上述三个条件的第三条.在非single_file
模式下则会在每个切片开头调用hls_start
,并设置resend_headers
.也就是非single_file
模式下切片不会有这个缺失表头的问题。
三个条件排除了两个的影响,只剩下第一个了,也就是说这个issue是受第一个条件影响产生的.
来看第一个条件,如果是视频帧并且是关键帧,将会插入pat pmt
表,它还有个尾巴:前一帧必须不是key帧.好了,这下子问题应该找到了,那些没有pat pmt
表的切片很可能是因为不符合这个尾巴条件,也就是前面一帧是key帧.这也解释了为什么single_file
模式下缺失表头是概率性的,而且是小概率,切出来的大部分切片都是包含表头的,只有少数是没有的.验证一下这个问题,使用ffprobe分析一下问题切片的前面一个切片的最后一帧,果然是关键帧, 也就是这个视频源连续两个关键帧挨着,并且这两个挨着的关键帧中的第二个刚好是新切片的第一个视频帧. 满足上述条件则这个切片将不会添加表头,也就造成了这个问题.
issue 修复
这个问题是ffmpeg切片single_file
模式下没有强制在每个分片头添加pat pmt
表造成的,标准文档也并没有明确给出这种模式下的切片标准。这个issue如果客户端能够复用hls码流开始的pat pmt
表去解析整个码流,那么也就能兼容这个问题,因为这些插入的pat pmt
表其实都是一样的,没有任何变化。事实上在现实世界里,很多客户端都能兼容这个问题,例如hls.js
.但是依然有一些客户端没有做这种兼容处理。
修复这个问题有几种办法,一是按照上述方式客户端解析时兼容一下,还有就是切片时single_file
模式也强制在分片头加上pat pmt
表. 第二种方式最好的办法是修改ffmpeg源码,修改方式很简单,在libavformat/hlsenc.c
中找到single_file
模式时分片的地方,加上强制生成表头标志.
ffmpeg patch提交
最终生成的patch如下:
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
|
commit 8d4bdf99e5650019e25174de3ff41c8c3396a904
Author: huheng <heng.hu.1989@gmail.com>
Date: Mon Jun 27 21:30:11 2022 +0800
avformat/hlsenc: Add resend_headers option
----
Add pat and pmt table at start of each segment in single_file mode enhanced
compatibility of hls stream. Because some hls clients separate parsing segment
of hls stream, the absence of pat/pmt will cause parsing to fail.
----
Reviewed-by: Steven Liu <liuqi05@kuaishou.com>
Signed-off-by: huheng <heng.hu.1989@gmail.com>
diff --git a/libavformat/hlsenc.c b/libavformat/hlsenc.c
index 7c097b4bf2..6f49ae1aa2 100644
--- a/libavformat/hlsenc.c
+++ b/libavformat/hlsenc.c
@@ -2635,6 +2635,9 @@ static int hls_write_packet(AVFormatContext *s, AVPacket *pkt)
vs->start_pos += vs->size;
if (hls->key_info_file || hls->encrypt)
ret = hls_start(s, vs);
+ if (hls->segment_type == SEGMENT_TYPE_MPEGTS && oc->oformat->priv_class && oc->priv_data) {
+ av_opt_set(oc->priv_data, "mpegts_flags", "resend_headers", 0);
+ }
} else if (hls->max_seg_size > 0) {
if (vs->size + vs->start_pos >= hls->max_seg_size) {
vs->sequence++;
|
修改开源代码,同步到上游是美德.所以就有了我的第一个ffmpeg patch.
这个patch是hlsenc.c
的maintainer steven liu
, ffmpeg中文社区网名"大师兄"的刘歧老哥reveiw的,感谢他指出了我插入发送表头选项前应该判断hls切片格式是mpegts,很高兴他接受了这个patch.
这个patch将会应用在ffmpeg5.1版本上.