流媒体系统中生成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是设置切片长度的,它切片策略支持两种模式,plainaligned

  • plain策略的解释是片段达到设定长度就切分.

  • aligned策略意思是时间戳达到设置片段长度的倍数时切片,这个策略能让不同nginx服务器切出一摸一样的片段来.

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端仍然保持相同的时戳, 这个算法确实是简洁精妙.