流媒体系统中生成hls切片,涉及到源流转封装为HLS流,如果源流的编码格式满足mpegts支持的格式,那么这个过程一般不涉及转码,直接转
封装的效率会更高.hls里最常见的音视频编码为h264/aac. hls码流mpegts格式的每个切片一般都从关键帧开始,这样可以保证每个切片单独播放,
尤其是直播的时候,客户端可能会从任意切片开始访问,关键帧开始的切片能立刻播放,兼容性较好.
切片程序一般都支持hls切片时长参数配置,这个参数表示每个hls切片时长. 转封装切片每个切片都从关键帧开始,但是关键帧间隔是源流固有的
属性,如果不转码,是无法改变的,除非关键帧间隔能被切片时长整除,否则是无法满足切片时长要求的.那么只能是尽量满足了,看看一些知名
开源软件怎么做的.
ffmpeg
这是个20s的片源, 关键帧间隔非常不均匀,先看看关键帧分布:
1
|
ffprobe 1min.flv -show_packets -select_streams v | grep K_ -B 10 | grep dts_time
|
1
2
3
4
5
6
7
8
9
10
|
dts_time=0.000000
dts_time=6.820000
dts_time=8.445000
dts_time=8.510000
dts_time=10.000000
dts_time=10.066000
dts_time=14.422000
dts_time=14.488000
dts_time=17.080000
dts_time=17.145000
|
再切片, 每个片段duration设置为2s, 命令如下:
1
|
ffmpeg -i 20s.flv -an -c copy -f hls -hls_list_size 0 -hls_time 2 xx.m3u8
|
切片结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:7
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.798000,
xx0.ts
#EXTINF:1.650000,
xx1.ts
#EXTINF:0.066000,
xx2.ts
#EXTINF:1.452000,
xx3.ts
#EXTINF:0.066000,
xx4.ts
#EXTINF:4.356000,
xx5.ts
#EXTINF:0.066000,
xx6.ts
#EXTINF:2.574000,
xx7.ts
#EXTINF:2.904000,
xx8.ts
#EXT-X-ENDLIST
|
看到这个结果是很诧异的,这个片段长度有的长,有的短,和设置的长度2s
的关联在哪里呢?看看代码吧.
1
2
3
4
5
6
7
8
9
10
11
12
|
int64_t end_pts = hls->recording_time * hls->number;
if (hls->has_video) {
can_split = st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
((pkt->flags & AV_PKT_FLAG_KEY) || (hls->flags & HLS_SPLIT_BY_TIME));
is_ref_pkt = st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO;
}
if (hls->fmp4_init_mode || can_split && av_compare_ts(pkt->pts - hls->start_pts, st->time_base,
end_pts, AV_TIME_BASE_Q) >= 0) {
}
|
看完代码,就明白了为什么上面的切片跳来跳去,忽大忽小了. ffmpeg切片策略是从第一个切片开始算起,设置为2s,
满足关键帧条件后判断总时长是否大于2*切片个数
,大于则做切片决策.
这种切片策略并不是单独考虑单个片段大小和设置长度大小的关系,而是从整体时长来均衡长度,后面的片段会补偿前面的,
所以对于源关键帧间隔极不均匀的情况下,切出的片段长度看起来也是极其跳跃的, 这种情况下连续多个小片断产生容易
造成直播卡顿,这种切片方式要谨慎使用.
nginx-rtmp-module
nginx-rtmp-module是较早的开源rtmp server
, 早在2013年就面世了,彼时开源项目远没有现如今这么多. 它依托于nginx框架,以性能著称,
功能上提供rtmp推拉流,转封装hls功能. 现如今流媒体服务端的开源项目已经非常多了,很多在功能上都远远超过了nginx-rtmp-module
,质量
上也不遑多让,但nginx-rtmp-module
依然有参考意义.
来看看nginx-rtmp-module
的切片策略.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
hls_fragment
Syntax: hls_fragment time
Context: rtmp, server, application
Sets HLS fragment length. Defaults to 5 seconds.
hls_fragment_slicing
Syntax: hls_fragment_slicing plain|aligned
Context: rtmp, server, application
Sets fragment slicing mode.
plain - switch fragment when target duration is reached
aligned - switch fragment when incoming timestamp is a multiple of fragment duration. This mode makes it possible to generate identical fragments on different nginx instances
Default is plain.
hls_fragment_slicing aligned;
|
hls_fragment
是设置切片长度的,它切片策略支持两种模式,plain
和aligned
plain
策略很好理解,aligned
策略不太好理解,时戳达到设置片段长度倍数,那是否还考虑关键帧这一条件呢? 看看nginx-rtmp-module
源码
ngx_rtmp_hls_update_fragment
里判断是否切片,这里传入了boundary
参数,这个参数必须是关键帧才为true
.
1
2
3
4
|
b = ctx->aframe;
boundary = frame.key && (codec_ctx->aac_header == NULL || !ctx->opened ||
(b && b->last > b->pos));
ngx_rtmp_hls_update_fragment(s, frame.dts, boundary, 1);
|
ngx_rtmp_hls_update_fragment
逻辑:
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
|
static void
ngx_rtmp_hls_update_fragment(ngx_rtmp_session_t *s, uint64_t ts, ngx_int_t boundary, ngx_uint_t flush_rate)
{
...
case NGX_RTMP_HLS_SLICING_ALIGNED:
ts_frag_len = hacf->fraglen * 90;
same_frag = ctx->frag_ts / ts_frag_len == ts / ts_frag_len;
if (f && same_frag) {
boundary = 0;
}
if (f == NULL && (ctx->frag_ts == 0 || same_frag)) {
ctx->frag_ts = ts;
boundary = 0;
}
break;
...
if (boundary || force) {
ngx_rtmp_hls_close_fragment(s);
ngx_rtmp_hls_open_fragment(s, ts, discont);
}
}
|
主要看aligned
模式,其中有个判断条件是same_frag = ctx->frag_ts / ts_frag_len == ts / ts_frag_len;
, aligned
模式下
有音视频的情况切片条件是:
- 下一帧是视频关键帧
- 时戳除以片段长度的值与上一次切片时戳除以片段长度的值不同
对齐模式下的切片策略直接用时戳除以片段长度决策是否切片, nginx-rtmp-module
计算方式能保证多个不同的server切相同的rtmp流切出的hls片段完全相同,
当然前提是保证rtmp流转发到各个不同的server端仍然保持相同的时戳, 这个算法确实是简洁精妙.