前言 – 历史介绍
流媒体格式及容器历史发展

CMAF
LL-HLS + CMAF 本身是一类组合其实就是, 请求协议+文件格式的组合。
CMAF 搭配不同的流媒体协议,只不过 CMAF 是最新的流媒体格式,相比之前更优秀,所以被拿来搭配
先介绍 CMAF。
CMAF,全称Common Media Application Format,是由Microsoft、Apple、MLBAM、Akamai等行业巨头向MPEG提出,并在2017年被 ISO/IEC批准的一项国际标准。CMAF通过使用基于ISO Base Media File Format(ISOBMFF)的通用分片格式来实现这一目标,以支持多个流媒体协议(如HLS和DASH)。
几个流媒体封装协议对比

CMAF的组成
CMAF文件结构如下
一种解释
摘自:https://cloud.tencent.com/developer/article/1813606

从图中数据模型组成结构可以看出,在实际Package生产过程中,CMAF包含了manifest文件中Presentation、Selection Set、Switching Set、Track以及真实的切片。
在逻辑上也可以用Header、Segment、Chunk以及Track来描述CMAF资源对象。
接下来重点介绍下这几个结构。

作为对比,这是MP4文件结构

CMAF Header:CMAF Header用于描述每个CMAF Track解析、解码和现实等相关的配置,通常是起始于一个’ftyp’类型的box,包含一个’moov’box,包含了整个set的box的序列号,基本以init.m4s形式呈现具体的内容。

CMAF Fragment:如图3中,每个Fragment通常由一个ISOBMFF段组成,可以独立解码和解密,当进行chunked传输时可以包装多个ISOBMFF段。

CMAF Track:如图4中,每个track中包含存储在CMAF指定的容器中的编码的媒体样本,包括音频,视频和字幕, 由一个CMAF头片段和其后的包含媒体样本的CMAF切片组成。CMAF序列包含存储在CMAF指定的容器中的编码的媒体样本,包括音频,视频和字幕,源自ISO基本媒体文件格式(ISOBMFF)。

CMAF Segement:如图中,在一个CMAF序列中的一个或多个CMAF Fragment可以被打包成一个CMAF Segment,每个Segment可以使用独立的资源描述符进行引用和传输。为了更高效编码,通常每个音视频Fragment长度在2-6s,为了保证CMAF低延时的效果,CMAF的Segment的长度通常不会超过10-12s。

CMAF Chunk:如图所示,CMAF Chunk是在直播编码器中,在一个CMAF Segmetn没有完整产生的情况下可以被分成不同的块进行传输分发,用这种方法能够使每一个CMAF Fragment能够渐进式编码、传输以及播放器的解码。也能够满足广播和多播的传输和识别。
除了了解上述基础的数据结构外,CMAF的媒体模型中还定义了多track集合以及自适应码率的结构、为了支持多语种&多视频角度或编码器的选择集合和延迟绑定的数据结构、多CMAF序列进行同步编码、解码的基准时间数据模型等。
我的解释
按照范围解释更好理解一点:
- CMAF Header 与 CMAF Track 平级。
- CMAF Track 就是指一个是视频流,或音频流,或字幕,或音轨。
- CMAF Track 由 Segment 组成,Segment 由 Fragment 组成,Fragment 由 Chunk组成。
- 而 Chunk,则是最小的分发单位。
CMAF Track (Video)
└── CMAF Segment 1
└── CMAF Fragment 1
└── CMAF Chunk 1
└── CMAF Chunk 2
└── CMAF Fragment 2
└── CMAF Chunk 3
└── CMAF Chunk 4
└── CMAF Segment 2
└── ...
CMAF Track (Audio)
└── CMAF Segment 1
└── CMAF Fragment 1
└── CMAF Chunk 1
└── CMAF Chunk 2
└── CMAF Fragment 2
└── CMAF Chunk 3
└── CMAF Chunk 4
└── CMAF Segment 2
└── ...
CMAF 的低延迟特性

在传统的文件切片编码器中,延迟往往由几部分产生,比如为了保证快速响应,分发历史生成的片或者为了保证传输最新切片,hold住连接,等待最新的切片生成后再分发。
分析图中的case1,为了保证对播放器的快速响应,直接分发了历史分片3,由于切片的长度为8s,生成第一个分片就会累计8s延迟,再加上当前编码器中最新未生成的3s的缓存数据,那么本次请求的延迟就是11s左右。
分析图中的case2,为了降低延迟,hold住请求5s,然后分发最新的切片,那么延迟就是8s,虽然延迟对比case1略有下降,但是用户的Qoe并不好,在最新分片生成的期间内,虽然保证了延迟都是8s,但是所有的连接都会被hold住0-8s,用户首屏的体验会比较差。
那么在CMAF中,这种被hold和延迟大的问题都会被解决,首先能够保证在任何时候都可以立刻响应,其次,即使当前的分片还没有产生,也可以用chunk编码的方式把当前片已经编码后可解码的部分立刻发出去,那么对于图中的case来说,保证立刻响应,延迟也控制在3s。
对于这种大切片的情况,实时响应的要求下,能保证延迟控制在0-8s。在实际的应用场景中,我们可以把分片长度控制小点,比如4s一个片,那么整体的用户延时能控制在0-4s,首屏也能得到保证。
传输方式的改变


MP4文件转换为CMAF格式示例
ffmpeg -i walking-dead.mp4 -c:v libx264 -c:a aac -f dash -seg_duration 2 -use_template 1 -use_timeline 1 -init_seg_name 'init-$RepresentationID$.m4s' -media_seg_name 'chunk-$RepresentationID$-$Number$.m4s' -adaptation_sets "id=0,streams=v id=1,streams=a" manifest.mpd
文件展示:

manifest.mpd文件内容:

中间
所以看到 CMAF 只是一种流媒体文件格式,需要搭配流式媒体传输协议才可以。
而我们看到CMAF 文件结构对于低延迟的设计,其实最主要用在直播领域。
目前与 CMAF 搭配的有 LL-DASH 和 LL-HLS。
LL-HLS
此处忽略HLS不讲。
传统 HLS 延迟太高,且苹果久久没有更新,所以先出现了社区版的 LHLS。
在 2019 年 WWDC 上苹果介绍了他们官方的 HLS 低延迟解决方案,苹果发布的低延迟方案并没有借鉴社区低延迟方案的成果,而是重新设计了一套低延迟方案。苹果的目标是 1 到 2 秒低延迟,支持大规模用户的直播,并且可以完全向下兼容。
目前苹果推动 LL-HLS 成为 HLS 2.0版方案,当前处于 Internet-Draft 阶段。
在讲 LL-HLS 之前,先说一下HLS延时高的原因。
HLS延时高的原因
HLS协议规定直播时,客户端不应该选择开始时间到最后一个 segment 结束时间间隔小于最后一个 segment 时长加两倍的目标时长的 segment 作为首个 segment 进行播放。
也就是说,一般而言,客户端应该从 m3u8 文件中倒数第三个或倒数第四个 segment 开始播放。如下图所示,客户端应该选择标号为3的 segment 作为起始的segment 播放。由此可见,HLS直播系统至少会产生3个 segment 时长之和的时延,假设每个 segment 时长6s,再加上客户端会有缓存(假设为1s)和传输时延,总体的时延可能会达到20s。

LL-HLS 新功能
Partial Media Segment – 更小的分段
LLHLS 将一个视频片段再细分称为小分段,一个视频片段由多个小分段组成。原先需要等待一个视频片段完全被生成才能下载,比如一个片段是 6 秒种,客户端就需要等待 6 秒这个分片被生成才能下载它。
现在服务端将一个片段分成多个小分段,比如一个小分段是 200 毫秒,那么一个视频片段包含 30 个小分段,客户端只需等待 200 毫秒就可以一个个下载这些小分段。

可以发现这种方式和社区方案非常相似,社区方案是将一个视频分段分成一个个小 Chunk,通过 HTTP/1.1 的 Chunked transfer encoding 功能下载到客户端。而 LLHLS 是将一个视频片段分成一个个小分段,通过普通 HTTP 请求去下载这些小分段。
与小分段相关的标签有
EXT-X-PART-INF 和EXT-X-PART 两个标签。
EXT-X-PART-INF
EXT-X-PART-INF 提供了播放列表中小分段的信息,如果播放列表中存在
EXT-X-PART 标签,那么必须提供这个标签。这个标签只有一个必传属性
PART-TARGET,它的值是浮点数,单位是秒。和
EXT-X-TARGETDURATION 标签类型,这个属性表示的是小分段的目标时长。
EXT-X-PART
EXT-X-PART 标签与 EXTINF 相似,它是用来声明一个小分段,它一共有 5 个属性。
- URI
小分段的资源链接。
- DURATION
小分段时长。
- INDEPENDENT 如果小分段中包含关键帧,可以将这个字段设置为
YES。
- BYTERANGE 如果要使用 HTTP Range 请求,可以使用该属性,它的值与
EXT-X-BYTERANGE 标签一样。
- GAP 如果这个小分段不可使用,可以将这个属性设置为
YES。
需要注意,如果该标签包含了
GAP=YES 属性,那么客户端就不应该去请求这个资源,客户端需要自己解决如何跳过这个 gap,苹果播放器的做法是延长上一帧的播放时长。
下面是一个完整 LL-HLS 播放列表的例子。
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:6
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.0,CAN-SKIP-UNTIL=12.0
#EXT-X-PART-INF:PART-TARGET=0.33334
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4.00008,
fileSequence266.mp4
#EXTINF:4.00008,
fileSequence267.mp4
#EXTINF:4.00008,
fileSequence268.mp4
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4"
#EXTINF:4.00008,
fileSequence271.mp4
#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z
#EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4"
#EXTINF:4.00008,
fileSequence272.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.3.mp4"
#EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=2
#EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=1
可以发现 LLHLS 播放列表中有非常多的 Part 小分段,为了防止生成太多的小分段,服务端将会定期清理老的小分段。
预加载(Preload Hints)
大致意思
LL-HLS是在上述带来时延的三个 segment 中,第一个封装完成,第二个正在封装,第三个还开始没封装的时候,就把三个的 url 都写入 m3u8 文件,如下图所示。这时候客户端发现 m3u8 文件里已经有三个 segment 的 url 了,就开始播放第一个segment 了。这样,就减少了几乎是第二个和第三个 segment 时长之和(12s左右)的延时。
与该功能相关的标签是
EXT-X-PRELOAD-HINT,它后面跟一个属性列表,一共有 4 个属性。
- TYPE 属性有两个值,
PART 表示是小分段,
MAP 表示是媒体初始部分(与
EXT-X-MAP 相似)。
- URI
资源的 url。
- BYTERANGE-START
如果是一个资源的一部分,这个属性用来指定开始部分。
- BYTERANGE-LENGTH 这个表示资源的字节长度,与
BYTERANGE-START 配合使用。
当客户端碰到这个标签时,可以选择是否直接请求这个资源,服务器会和上面请求长连接中一样维持这个请求,直到整个资源数据可用时才返回资源。当然也有可能直接返回 404。

分段传输(Partial Segment Transfer)
分块就是把数据分成一块一块的再发送出去,浏览器收到后再组装起来,这种“化整为零”的思路在 HTTP 协议里就是“chunked”分块传输编码,在响应报文里用头字段“Transfer-Encoding: chunked”来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。
每个分块包含两个部分,长度头和数据块,长度头是以 CRLF(回车换行,即rn)结尾的一行明文,用 16 进制数字表示长度,数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF,最后用一个长度为 0 的块表示结束,即“0rnrn”。

播放列表的新功能
- 支持播放列表增量更新
- 支持阻止播放列表重新加载
标签:#EXT-X-SERVER-CONTROL
快速码率切换
LL-HLS播放过程中有时候会遇到#EXT-X-RENDITION-REPORT, 这说明接下来需要加载不一样的类型的视频了, 可能是分辨率/码率/格式发生了变化, LAST-MSN表示是在哪一个MSN结束之后开始加载这个新的索引文件.
标签:#EXT-X-RENDITION-REPORT
综上
CMAF 是一种个视频格式,可以用在点播和直播中。
LL-HLS 则是一种协议,LL-HLS 搭配 CMAF 用在直播中。
对于点播来说,用不着使用CMAF的低延迟功能,可以当做静态视频文件处理,这样按照一般文件处理就可以。
对于直播,采用 Nginx + 后端服务的方式,Nginx只负责中转,这样Nginx也可以不用直接处理 LL-HLS 协议
开源方案
只找到一个:
https://github.com/kaltura/media-framework/
已有架构
https://cloud.tencent.com/developer/article/1358728

有两种方法
1. 使用客户端的PULL+Chunked是一个简单方法(但和现在的分片获取没有本质区别)
2. 使用HTTP2 PUSH方法,没有资源ready时不返回404而是等有资源时再PUSH(其链接不断)
PUSH不是必须的办法。即使GET方法也能做到不返404 hang住等数据ready了发送
PUSH剩下的好处是连续推送分段
2.1 需要能解析m3u8文件,能根据用户请求的文件找到后续的文件持续推送分片文件(有可能用户会发起后续的请求造成带宽浪费)
2.2 整个链路上的cache能够接收push的结果,缓存数据 (或者只在最边缘点做这个功能)
2.3 分片源站还是以完整文件的形式存储文件,因此无法做到编码器生产的文件能流式推送到直播CDN
存在的问题
- golang暂时不支持client端读取push的数据。下面是个补丁
https://go-review.googlesource.com/c/net/+/181497/9/http2/transport_test.go
- golang解析M3U8文件
https://github.com/grafov/m3u8
编码器将使用HTTP 1.1分块传输编码将编码的CMAF块推送到origin处以进行重新分发。例如,一个产生4s 30fps的segment的编码器将每4秒发一个HTTP POST(每个segment一个),然后在接下来的4s内,构成该切片的每个33ms长的120个chunk将被发送到开放连接的云网络。请注意,编码器不会为每个chunk进行POST。
剩余chunk的传输是基于媒体播放器驱动的pull行为,媒体播放器读取描述媒体内容的manifest或playlist,计算它希望开始播放的live edge(稍后将详细说明),然后开始请求segment,manifest必须表明segment数据的早期可用性。在MPEG DASH中,这是通过MPD @ availabilityTimeOffset参数完成的。在HLS中,引用该segment的播放列表应该在segment的第一个chunk被释放后发布,而不是在该segment的最后一个chunk后发布。
Reference
CMAF
https://www.iso.org/obp/ui/es/#iso:std:iso-iec:23000:-19:ed-3:v1:en
https://cloud.tencent.com/developer/article/1051357
https://www.nxrte.com/jishu/21525.html
https://cloud.baidu.com/article/3241229
https://www.nxrte.com/jishu/24829.html
https://cloud.tencent.com/developer/article/1813606
https://brands.cnblogs.com/tencentcloud/p/11580
LL-HLS
https://blog.csdn.net/u011686167/article/details/131155501
https://cloud.tencent.com/developer/article/1812563
https://juejin.cn/post/7012155300916658189