LL-DASH CMAF 低延迟直播

avatar
FE @字节跳动

使用 DASH 直播时一般会有几十秒的直播延迟,对于互动直播这么高的延迟根本互不动。要降低直播延迟一般会减少视频分段时长。

image.png 上图中展示了不同时长的视频片段对应的延迟,减小视频片段时长虽然可以降低延迟,但是也会增加资源消耗和视频码率,而且就算使用 1 秒的视频分段,延迟也会比下面介绍的 LLDASH 方案高。

介绍

LLDASH(Low Latency DASH)最早在 2017 年提出并成立工作组,在 2019 年 DVB 发布了 DVB-DASH with low latency 规范,基于 DVB 和 DASH IF 联合开发的这份报告 DVB and DASH-IF in 2017 on Low-Latency DASH 在 2020 年 DASH IF 发布了 Low-latency Modes for DASH 规范。

DVB(Digital Video Broadcasting)数字视频广播,是一系列数字电视国际开放标准,由 DVB Project 维护。DVB Project 是一个由 300 多个成员组成的工业组织,它是由欧洲电信标准化组织、欧洲电子标准化组织和欧洲广播联盟联合组成的联合专家组发起的。

DASH IF(DASH Industry Forum)DASH 行业论坛,它主要由流媒体公司组成,如 Akamai、谷歌、微软等。DASH IF 主要标准化互操作性,促进 MPEG-DASH 发展,并帮助其从规范过渡到真正的业务。

所以目前一共有 DVB 和 DASH IF 两套 LLDASH 规范,这两套低延迟方案非常相似只有一点不同,由于 DASH IF 较晚发布所以在 DASH IF 规范中也说明了与 DVB 不同的部分。而且这两个规范是完全向下兼容普通 DASH 直播的。

CMAF

虽然 MPEG-DASH 规范并没有限制内容格式,但是两种 LLDASH 规范中都是使用 CMAF 格式。这容易让人产生 CMAF 和低延迟划等号的误解,CMAF 本身并不会降低延迟,例如 HLS 支持 MPEG-TS 和 CMAF 两种格式,如果将普通 HLS 直播 MPEG-TS 分片换成 CMAF 分片,这并不会降低直播延迟。CMAF 最大的作用是统一播放格式,从而节省存储空间。不过 CMAF 提供了一些工具使低延迟 DASH 成为可能。

原理

LLDASH 与上篇文章介绍的 LHLS 非常相似,都是将一个分片变成一个个小 Chunk,这些小 Chunk 可以在分片完全生成之前被播放器使用 HTTP/1.1 的 Chunked transfer encoding 功能下载并缓存,从而降低直播延迟。

image.png

如上图所示,普通 DASH 直播中一个 MP4 分段只有完全编码后才能输出被请求。LLDASH 中将视频片段分割为一个个小 Chunk,编码器可以每生成一个 Chunk 就输出,传递给播放器缓存播放。

CMAF 中 ftyp 和 moov 盒子组成初始化分段,每一个 Chunk 由 moof 和 mdat 盒子组成。播放器会先请求初始化分段,然后请求最新的媒体分段,服务器会将分段的一个个 Chunk 返回给播放器播放。

image.png

播放器请求拉流时,可能如上图所示,一个视频片段被分为 3 个 Chunk。当前播放器发送请求给服务器时,视频片段还没被完全生成,服务器会保持连接不断开,每当生成一个 Chunk 就立马推送给播放器。

规范实现

对于使用 DASH IF 低延迟规范的 MPD,应该添加 http://www.dashif.org/guidelines/low-latency-live-v5MPD@profiles 属性中进行标识,下面是一个符合 DASH IF 低延迟规范的 MPD 例子。

<?xml version="1.0" encoding="utf-8"?>
<MPD
  xmlns="urn:mpeg:dash:schema:mpd:2011"
  availabilityStartTime="1970-01-01T00:00:00Z"
  id="Config part of url maybe?"
  maxSegmentDuration="PT8S"
  minBufferTime="PT1S"
  minimumUpdatePeriod="P100Y"
  profiles="urn:mpeg:dash:profile:full:2011,http://www.dashif.org/guidelines/low-latency-live-v5"
  publishTime="2021-09-14T05:25:57Z"
  timeShiftBufferDepth="PT5M"
  type="dynamic"
>
  <BaseURL> https://livesim.dashif.org/livesim/sts_1631597157/sid_a736b022/chunkdur_1/ato_7/testpic4_8s/
  </BaseURL>
  <ServiceDescription id="0">
    <Latency max="6000" min="2000" referenceId="0" target="4000" />
    <PlaybackRate max="1.04" min="0.96" />
  </ServiceDescription>
  <Period id="p0" start="PT0S">
    <AdaptationSet contentType="audio" lang="eng" segmentAlignment="true">
      <ProducerReferenceTime id="0" presentationTime="0" type="encoder" wallClockTime="1970-01-01T00:00:00">
        <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-iso:2014" value="http://time.akamai.com/?iso" />
      </ProducerReferenceTime>
      <SegmentTemplate 
        availabilityTimeComplete="false" 
        availabilityTimeOffset="7.000000" 
        duration="384000" 
        initialization="$RepresentationID$/init.mp4" 
        media="$RepresentationID$/$Number$.m4s" 
        startNumber="0" 
        timescale="48000" 
      />

      <Representation audioSamplingRate="48000" bandwidth="36997" codecs="mp4a.40.2" id="A48" mimeType="audio/mp4" startWithSAP="1">
        <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
      </Representation>
    </AdaptationSet>
    <AdaptationSet contentType="video" maxFrameRate="30" maxHeight="360" maxWidth="640" par="16:9" segmentAlignment="true">
      <ProducerReferenceTime id="0" presentationTime="0" type="encoder" wallClockTime="1970-01-01T00:00:00">
        <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-iso:2014" value="http://time.akamai.com/?iso" />
      </ProducerReferenceTime>
      <SegmentTemplate 
        availabilityTimeComplete="false" 
        availabilityTimeOffset="7.000000" 
        duration="122880" 
        initialization="$RepresentationID$/init.mp4" 
        media="$RepresentationID$/$Number$.m4s" 
        startNumber="0" 
        timescale="15360" 
      />

      <Representation bandwidth="303780" codecs="avc1.64001e" frameRate="30" height="360" id="V300" mimeType="video/mp4" sar="1:1" startWithSAP="1" width="640" />
    </AdaptationSet>
  </Period>
  <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-iso:2014" value="http://time.akamai.com/?iso" />
</MPD>

判断是否是 Chunked 低延迟

DASH IF 低延迟规范中定义了两种低延迟直播方法,一种是不使用上面提到的 Chunked transfer encoding 功能,而是将媒体分段切分的非常短来降低延迟,这种方法可以普通的 DASH 直播并没有什么区别,这里不再过多介绍。

另一种低延迟方法就是使用上面提到的 HTTP Chunked transfer encoding 功能,也就是当一个媒体还没完全生成好时,播放器可以请求该分段下载并缓存已经创建的 Chunk,而不是 404 请求报错。这个就是下面要介绍的低延迟模式。

有两种方法可以判断是否是低延迟模式。

  1. 根据 SegmentTemplate@availabilityTimeComplete 属性。DASH IF 低延迟规范中对于 Chunked AdaptationSet,需要将 availabilityTimeComplete 属性设置为 false,所以如果 availabilityTimeCompletefalse 时,则可以认为该媒体流是低延迟模式。
  2. DVB 还定义了描述低延迟的 EssentialPropertySupplementalProperty 元素,如果存在其中一个,并且它的 schemeIdUri 属性等于 urn:dvb:dash:lowlatency:critical:2019value 属性等于 true,则也可以认为该媒体流是低延迟模式。

延迟和播放速率

LLDASH 中定义了播放延迟和播放速率信息,这些信息在 ServiceDescription 元素中。

<ServiceDescription id="0">
    <Scope schemeIdUri="urn:dvb:dash:lowlatency:scope:2019" />
    <Latency target="3000" max="6000" min="1500" />
    <PlaybackRate max="1.5" min="0.5" />
</ServiceDescription>
  • Latency 元素定义直播延迟相关信息,单位是毫秒。Latency@target 为直播目标延迟。 Latency@max 为允许的最大延迟,当延迟超过该值时播放器应该直接 seek 到延迟位置。如果当前延迟小于 Latency@min 播放器应该慢放。
  • PlaybackRate 元素定义了最大和最小播放速率,正常速率是 1。当延迟超过目标延迟时播放器会进行快放追赶,当缓存过少时播放器可能进行慢放。

DVB 低延迟规范中还定义了 Scope 元素,它的 schemeIdUri 属性为 urn:dvb:dash:lowlatency:scope:2019,DASH IF 规范中没有定义该元素。

时钟同步

为了获取准确的媒体分段和直播延迟,LLDASH 规范定义 MPD 中最少存在一个 UTCTiming 元素,用于客户端与服务端时钟同步。UTCTiming@schemeIdUri 属性需要是下面 3 个中的一个。

  • urn:mpeg:dash:utc:http-xsdate:2014
  • urn:mpeg:dash:utc:http-iso:2014
  • urn:mpeg:dash:utc:http-ntp:2014 (浏览器中不支持)

UTCTiming@value 属性是服务器时间服务地址。

<UTCTiming 
    schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014"
    value="https://time.example.com"
/>

一些老规范中使用的是 2012,播放器也应该支持下面两个 schemeIdUri

  • urn:mpeg:dash:utc:http-xsdate:2012
  • urn:mpeg:dash:utc:http-iso:2012

如果 MPD 中没有 UTCTiming 元素或者时钟同步服务不可访问时,播放器可以降级为使用本地时钟。

媒体分段

LLDASH 中定义了两种获取媒体分段的方法。

  1. SegmentTemplate@media(使用 $Number$) 加 SegmentTemplate@duration
  2. SegmentTemplate@media$Time$$Number$)加 SegmentTimeline 元素

DVB-DASH 更推荐使用第一种方法。DASH IF 没有做推荐。

SegmentTemplate + SegmentTemplate@duration

下面是使用第一种方法的例子。

<Representation id="0" mimeType="video/mp4" codecs="avc1.42c028" bandwidth="6000000" width="1920" height="1080" sar="1:1">
    <SegmentTemplate 
        timescale="1000000"
        duration="2002000"
        availabilityTimeOffset="1.969"
        availabilityTimeComplete="false"
        initialization="1630889228/init-stream_$RepresentationID$.m4s"
        media="1630889228/chunk-stream_$RepresentationID$-$Number%05d$.m4s"
        startNumber="1"
    ></SegmentTemplate>
</Representation>

上面例子中我们首先将 SegmentTemplate@initialization 中的 $RepresentationID$ 替换成 Representation@id 获取到初始化分段的地址 1630889228/init-stream_0.m4s。(初始化分段还可能用其他方式提供,如使用 Initialization 元素。)

获取到初始化分段 URL 后,还需要确定第一个媒体分段的 URL。假设 NOW 变量是与服务器时钟同步后的当前时间。那么我们就可以用下面式子获取到符合目标延迟的最新完整分段地址的 $Number$

targetNumber = Math.floor(
    ((NOW - MPD@availabilityStartTime - Latency@target) / 1000 - Period@start) / 
    (SegmentTemplate@duration / SegmentTemplate@timescale)
) + SegmentTemplate@startNumber

然后再把 SegmentTemplate@media 中的 $Number$ 替换成 targetNumber,就构造好了第一个媒体分段的 URL 了。

SegmentTemplate + SegmentTimeline

<AdaptationSet 
    id="0" 
    mimeType="video/mp4" 
    width="512" 
    height="288" 
    par="16:9" 
    frameRate="30" 
    segmentAlignment="true" 
    startWithSAP="1">
    <SegmentTemplate 
        timescale="90000"        media="segment_ctvideo_cfm4s_rid$RepresentationID$_cs$Time$_w743518253_mpd.m4s"       initialization="segment_ctvideo_cfm4s_rid$RepresentationID$_cinit_w743518253_mpd.m4s"
    >
        <SegmentTimeline>
            <S t="1614755160" d="900000"/>
            <S d="900000"/>
            <S d="900000"/>
            <S d="900000"/>
            <S d="900000"/>
        </SegmentTimeline>
    </SegmentTemplate>
    <Representation id="p0va0br601091" codecs="avc1.42c015" sar="1:1" bandwidth="601091" />
</AdaptationSet>

SegmentTimeline 用来表示各个媒体分段的媒体时间和时长,用来替换SegmentTemplate@duration 属性。SegmentTimeline 有一堆 S 子元素,S 元素主要有 S@tS@dS@r 三个属性。

属性名描述
S@t分段媒体时间,如果不存在则等于它上一个分段 SpSp@t + Sp@d
S@d分段时长
S@r与该分段相同时长分段的的重复次数,默认为 0,如果为负数则表示重复到下一个 S 元素或 Period 结束

和第一种方法一样首先我们需要去请求初始化分段,这里不在详细介绍。

要计算出符合目标延迟的最新完整分段的地址,首先需要计算出目标 S@t 值,再在 SegmentTimeline 找到这个分段并计算出它的 URL。

targetT = (NOW - MPD@availabilityStartTime - Latency@target) / 1000 -

          Period@start + (SegmentTemplate@presentationTimeOffset / SegmentTemplate@timescale)

计算出目标 targetT 后,我们就可以迭代 SegmentTimelineS 元素,如果存在 S@r 属性则需要进行展开,找到 (S@t + S@d) / SegmentTemplate@timescale > targetTS 元素,这个 S 元素就是我们要找的目标 S 元素。

然后把 SegmentTemplate@media 中的 $Time$ 替换成目标 S 元素的 S@t 属性,$Number$ 替换成目标 S 元素在展开后的 SegmentTimeline 元素中的下标再加上 SegmentTemplate@startNumber。这样就构造好了目标媒体分段的 URL 了。

Resync

Resync 被定义与 MPEG DASH ISO/IEC 23009-1:2020/Amd.1 规范中。它定义了分段的同步点信息,通过它播放器可以进行快速随机访问,可是实现类似与 LLHLS 中的 EXT-X-PART

它可以放在 AdaptationSetRepresentation 元素下。

<Resync type="2" dT="1000000" dImin="0.1" dImax="0.15” marker="TRUE"/>
  • type 等于 2 表示可以被随机访问,0 表示 CMAF Chunk 不保证能随机访问
  • dT 表示在 timescale 下最大随机访问点的时间距离。
  • dImin 表示最小两个随机点之间的字节距离 dImin * bandwidth
  • dImax 表示最大两个随机点之间的字节距离 dImax * bandwidth
  • marker 为 true,表示播放器可以解析 CMAF 盒子找到同步点

低延迟 ABR 算法

ABR(自适应码率)是 DASH 播放器的一个关键功能,它可以让视频在复杂的网络条件下动态切换码率和播放速率,而不是中断播放降低用户体验。

在低延迟下,一些基于带宽估算的 ABR 算法都不太好用。这是因为使用 Chunked transfer encoding 时,一个视频分段并没有被完全生成,对于一个 5 秒的视频分段,一个 http 请求可能需要花费 5 秒,这个时间并不是准确的下载时间。 在 2020 年 Twitch 和 ACM 合作举办了低延迟下的 ABR 算法大挑战 Adaptation Algorithms for Near-Second Latency。比赛的第一名是 Unified Streaming 的 L2A-LL(Learn2Adapt-LowLatency) 算法 ,第二名是新加坡国立大学的 LoL(Low-on-Latency)算法。由于 Twitch 播放器不是开源的,比赛是基于 dash.js 播放器,目前 dash.js 也集成了这两种 ABR 算法。

总结

LLDASH 和 LHLS 非常相似,都是使用 HTTP/1.1 的 Chunked transfer encoding 功能来降低延迟提供 1 到 6 秒的低延迟直播,而且可以复用现有的 CDN 网络支持大规模用户观看直播。不过 Chunked transfer encoding 功能需要浏览器支持 fetch API,所以在 IE 上会降级为使用 XHR 的普通 DASH 直播。