在Hulu及Disney+流媒体平台上,自适应流媒体传输技术被广泛使用,它可以根据用户的网络情况相应地调整视频码率,为用户带来良好的观看体验。为了实现这一点,视频将被编码成不同码率的版本,并且在每个版本中,视频将被切割成一个个小的片段(segment),每个segment都应该能独立解码。如此一来,我们在播放过程中就可以做到以segment为单位地切换码率。
这样的流媒体传输方式与传统的、只需要传输一个视频文件的传输方式不同。由于视频被编码成了不同版本的流,且每个流都被切分成了一个个segment,播放器要想正常播放视频,就需要知道都有哪些流,每个流里的segment都去哪儿可以下载到。描述这些信息的方式被定义在了统一的自适应流媒体传输协议中,这使得不同的播放器也能以相同的方式去进行自适应流媒体的播放,只要它们采用了同一个协议。
自适应流媒体传输协议有很多,其中最流行的当属MPEG组织开发的DASH协议和苹果公司的HLS协议。因此,本系列技术博客计划对这两种自适应流媒体传输协议进行介绍,并将对这两种协议进行对比。在本篇博客中,我们将首先介绍HLS协议。
HLS流媒体传输协议,全称为HTTP Live Streaming,是苹果公司提出的基于HTTP的流媒体传输协议[1]。虽然写作“Live Streaming”,但它同时支持点播(VoD)和直播(Live)。由于使用的是HTTP协议,HLS支持使用传统的Web服务器和CDN来分发视听内容。目前,HLS已被多种设备和浏览器广泛支持[2]。在Safari中,还可以直接使用HTML5 的
在2017年8月,HLS规范RFC 8216[4]发布,它详细描述了HLS协议(version 7)。苹果公司正在不断更新HLS协议的第二版规范[5]。在2020年4月30日,低延迟HLS被加入到了第二版规范的草案中,它旨在减少HLS在直播场景下传输流媒体的延迟。
下面我们将对HLS协议进行简单介绍。一个简单的HLS流媒体传输系统如图所示,流媒体的传输将在客户端与服务器之间进行,客户端向服务器发起HTTP请求,首先请求的是媒体描述文件,该文件记录了本次播放中所有可以被请求的媒体的信息,客户端在解析该描述文件后,根据用户偏好,以及自适应码率算法的决策,选择合适的媒体内容,不断进行下载,直至完成本次播放。
在我们看视频的时候,我们其实是在观看某段内容,而这段内容可以使用不同的媒体去表达。例如,同样是《狮子王》这一部电影,按媒体类型来说,它可以由视频、音频、字幕来表达。而在同一个媒体类型中,它也可以有不同的版本,例如:视频内容可以有不同编码版本、不同码率版本,音频及字幕内容可以有不同语言版本,等等。
HLS协议将某个媒体类型下,表述同一内容的一条“流”称为rendition。例如,它可以是一条AAC编码的、英语版本的音频流,可以是一个AVC编码的、平均码率为400kbps的视频流。为了进行自适应流媒体传输,rendition中的媒体内容将被划分成段(segment)。
对于某个内容,我们可以准备许多条这样的rendition。首先,我们可以将它们按照不同的媒体类型归类,在HLS协议中,媒体类型主要有4种:VIDEO、AUDIO、SUBTITLES、CLOSED-CAPTIONS(较少使用)。
在每种媒体类型中,rendition可被划分成不同的rendition group,在每一个rendition group中的rendition是相互可替代的版本,例如,同一段音频的不同语言,抑或是同一段视频的不同摄像角度,等等。在播放中选择rendition group里的哪条rendition进行播放取决于播放器的设置,或者用户自己的选择,比如,播放器可能会默认选择英语版本。
而从所有的媒体类型中各选择一个rendition group组合起来,就构成了一个variant。播放器是可以在不同variant之间进行自适应切换的。因此,我们要在HLS中实现自适应码率的切换,就应该把不同码率的video rendition放入不同的rendition group,添加进不同的variant中。此外,HLS协议还要求每个rendition group里媒体的版本是一致的。例如,不允许出现在这个rendition group中有法语版本,而另一个里没有的情况,否则会播放出错。
以上就是前文所说“本次播放中所有可以被请求的媒体”,它们构成了一个HLS流的所有媒体内容。下面我们将介绍这些媒体内容是如何被HLS协议的媒体描述文件组织和描述的。
前文提到,一次播放中所有可以被请求的媒体内容可以有很多,那么播放器要如何知道:它们都是什么?都有什么样的属性?以及去哪儿下载这些内容?这些信息通通都记录在HLS协议的媒体描述文件中。
HLS协议将其媒体描述文件称为playlist,它是一个以.m3u8或者.m3u为后缀名的文本文件。文件中的每一行只可能是以下三种情况:
以#开头:若以#EXT开头,则为HLS标签(tags),它定义了一些信息;否则将为注释,不会被默认HLS播放器解析。
URI:可以是绝对地址,也可以是相对地址。若是相对地址,则是以该playlist的URI为基准的。
空行
以下是一个简单的HLS playlist的示例:
在HLS协议中,playlist分为:
media playlist,它描述和定义了一条rendition。因此,出现在其中的所有URI都是指向segment的。
multivariant playlist(旧称master playlist),它描述和定义了所有的variants和rendition groups。因此,出现在其中的所有URI都是指向一个media playlist的。
下面我们将自底向上地来看看HLS playlist是如何通过标签和URI来描述媒体内容的。所有的HLS标签都在HLS规范[4-5]中被定义。
首先,在media playlist里,我们使用指向segment的URI代表一个个segment。接下来,我们需要使用media segment标签来描述它们,或者定义一些作用于它们的参数。一些media segment标签只对紧随其后的第一个segment起作用,例如:#EXTINF标签,它定义了该segment的时长;#EXT-X-BYTERANGE标签,它定义了在使用fragment MP4(fMP4)文件时,该segment的数据处在整个fMP4文件的哪段字节范围内。下面的例子就描述了一个时长为9.009秒的segment,它的数据在movie.mp4文件的字节范围19945-116279处,共96335字节。
而有一些media segment标签对其后所有的segment都适用,直到遇到下一个此标签。例如,#EXT-X-KEY标签指定了媒体内容的解密方式,在下面的样例中,第一个key适用到segment 1到3,而第二个key适用于segment 4。
通过将segment的URI排列起来,并使用合适的media segment标签来描述它们,我们就已经初步组织好了一个rendition里的主要内容。下面,我们需要使用media playlist标签来定义一些全局参数,它们只能出现在media playlist中,并且最多只能出现一次:
#EXT-X-TARGETDURATION标签,指定了每个segment的时长的上限。
#EXT-X-MEDIA-SEQUENCE标签,指定了media playlist中,segment序号的起始值,主要用于Live场景中segment的同步,后文将会详细介绍。
#EXT-X-PLAYLIST-TYPE标签,指定了media playlist的类型,这将影响该media playlist能否被修改,能如何被修改,后文将对此进行详细介绍。
#EXT-X-ENDLIST标签,显式标出该标签则表示不允许再向该media playlist添加任何segment了。
此外,HLS playlist还有一些通用标签:
#EXTM3U标签,表示这是一个M3U8文件,它必须出现在每个playlist文件的第一行。
#EXT-X-VERSION标签,它指定了该playlist最低兼容的HLS版本号。这是因为有的HLS标签是从某个版本之后才出现的,如果播放器只支持低版本的HLS协议,很可能因为无法解析HLS playlist而出错。
那么,下面就是一个简单的media playlist的示例,它定义了一条由4个segment组成的video rendition,因为在该文件中总共有4条指向segment内容的URI。由#EXTINF标签描述的segment时长可知,这条rendition的总时长为46.166秒。#EXT-X-MEDIA-SEQUENCE标签显示segment的编号从3开始,所以这4个segment的编号将分别为3、4、5、6。类似地,在一个视频的播放过程中,我们也许还需要audio rendition、subtitle rendition等。在后文中,我们会详细讲解如何在多种媒体类型的rendition之间进行时间线的同步。
此外,一条HLS流可能由不同的内容组成,例如:普通的视频内容和广告等。这时候它们的视频文件可能由不同的码流组成。解码器需要显式地知道这些“拼接点”,这样才好做出应对。HLS协议使用#EXT-X-DISCONTINUITY标签来在media playlist里标识这些“拼接点”:如果两个segment之间有明显的不连续性(例如,码流、时间线不连续等),可以使用#EXT-X-DISCONTINUITY标签将它们隔开。N个#EXT-X-DISCONTINUITY标签将会将media playlist里的segment划分成N+1个“不连续片段”,每个“不连续片段”也有一个编号。与segment的编号类似,可以用#EXT-X-DISCONTINUITY-SEQUENCE标签来指定这个编号的起始值。这个编号可以用来辅助播放器进行不同rendition之间的时间线同步,后文将会详细介绍。
在multivariant playlist中,使用#EXT-X-MEDIA标签来定义rendition group。简单说来,每个#EXT-X-MEDIA标签都为一个rendition指定了其所在的rendition group的ID,而许多拥有相同group ID的rendition共同构成了一个rendition group。
具体来说,每个#EXT-X-MEDIA标签需要给其URI属性设置一个指向某个media playlist的URI,它指定了某个rendition;TYPE属性指定了这个rendition的媒体类型,例如:VIDEO表示它是一条视频rendition;GROUP-ID属性指定了rendition group的名字;NAME属性指定了该rendition的名字;而LANGUAGE属性指定了该rendition的语言(对于TYPE为AUDIO或SUBTITLES的rendition可能是需要的,对于VIDEO一般不需要);DEFAULT属性指定了该rendition是否为默认选择的,等等。同一个TYPE且GROUP-ID相同的所有rendition处于同一个rendition group中。
上面的样例展示了2个音频的rendition group,分别为AAC编码和EAC编码的音频,每个rendition group都有三个rendition,分别代表了英语、德语及评论音轨,而英语是默认选择的音轨。
而指定variant需要使用#EXT-X-STREAM-INF标签,每个标签都描述了一个variant。该标签一般由2行构成,第一行是标签及其属性们,第二行则是一个指向某个video rendition的URI,假如播放器不支持在video的rendition group中切换rendition时,该URI指向的video rendition将被播放。
在#EXT-X-STREAM-INF标签的属性里,属性名为某个媒体类型(VIDEO、AUDIO、SUBTITLES)的属性指定了该媒体类型的rendition group。例如,AUDIO="aac"表示音频流使用GROUP-ID为“aac”、TYPE为AUDIO的rendition group。当然,如果不为该媒体类型指定任何rendition group的话,最终的多媒体流里是不会包含该媒体类型的内容的。
其它的属性则描述了该variant的一些信息,例如,该variant多媒体流的峰值比特率(BANDWIDTH属性)、平均比特率(AVERAGE-BANDWIDTH)、编码(CODECS)、帧率(FRAME-RATE)、分辨率(RESOLUTION)等等,这些信息可以帮助播放器进行variant之间的自适应切换。
上面的样例就展示了3个variant(CODECS字段被省略了),它们分别使用了不同码率的视频流,而音频流都是前文定义的AAC编码的音频流。在播放这个multivariant playlist定义的媒体内容时,播放器可以在不同码率的视频流间自适应切换;而音频流则会使用播放器默认选择的英语音轨,或者用户选择的其它音轨,除非播放器或者用户的设置更改,否则音频流是不会发生切换的。
在这一小节中,我们将来看看HLS协议是如何在不同场景下发挥作用的。本小节主要涉及到客户端与服务器之间如何基于multivariant和media playlists进行流媒体的传输,暂不涉及到variant之间的自适应切换。
由于点播(VoD)场景相对比较简单,我们先来看看在点播场景下,HLS协议是如何工作的。正如前文所说,流媒体传输过程在客户端与服务器之间进行。在此过程中,它们将负责不同的任务。
服务器的主要职责是准备好所有的媒体内容,并准备好描述它们的multivariant 和media playlist们以供客户端下载。而客户端则需要:
下载并解析multivariant playlist,获取variant信息
选择一个variant后,下载并解析相应的media playlist
根据media playlist记录的segment信息下载并播放媒体内容
若发生variant/rendition的切换,下载新的media playlist,重复步骤2-3。
在点播场景下,需要注意的主要是播放器如何在不同媒体类型的rendition之间保持时间线的同步。它的作用有许多,例如:音频流里的第10秒如何对应到视频流里的相应画面上;如果我拖动到进度条上的某个位置,如何找到这个位置的所有媒体内容,等等。之所以需要同步,原因是两条rendition提供的内容并不一定是完全对齐的,播放器需要根据media playlist里提供的时间线信息去进行rendition之间的同步。
在media playlist里,我们可以通过#EXT-X-PROGRAM-DATE-TIME标签来为该rendition提供时间线信息。它是一个media segment标签,指定了其后紧跟着的segment的第一帧所对应的时间戳。不是每个segment都需要一个此标签的,对于第一个#EXT-X-PROGRAM-DATE-TIME标签之前的所有segment,可以通过第一个#EXT-X-PROGRAM-DATE-TIME标签的值倒推(例如:下图示例中segment 0的起始时间为00-2s=-02),而对于其它的没有该标签的segment,则可以根据在它前面的最近一个#EXT-X-PROGRAM-DATE-TIME标签的时间戳来推断(例如:下图示例中segment 3的起始时间为08+2s=10)。
需要注意的是,由于“不连续片段”之间的时间线可能也是不连续的。比如,在某段时间内可能只有音频流有内容,而视频流没有内容,此时视频流对应的media playlist就可能出现不连续的时间线,需要用#EXT-X-DISCONTINUITY标签分隔开。如果我们用#EXT-X-PROGRAM-DATE-TIME标签进行时间同步,那么每个“不连续片段”中最好都至少有1个此标签。
同时,协议要求整个HLS流中的所有rendition必须处于相同的时间体系,这意味着同一块内容在不同的rendition中的时间戳是相同的。这样一来,rendition之间就可以通过这个时间戳来进行时间线的同步了。
除了时间戳之外,对于拥有多个“不连续片段”的rendition来说,还需要同时结合“不连续片段”的编号来进行时间线的同步。具体说来,协议要求“不连续片段”的划分及其编号在所有的rendition中都保持一致。例如,不允许出现内容的前10秒在这个rendition里属于“不连续片段” 0,而在另一个rendition里属于“不连续片段” 1。因此,我们可以通过同一个编号在多个rendition中定位到对应的“不连续片段”,并在该“不连续片段”中使用时间戳进行同步。
需要注意的是,除了“不连续片段”的编号外,每个segment也会有一个序号,该序号是不可以用于时间线同步的。协议允许表述同一块内容的segment,在不同的rendition里拥有不同的segment序号。
直播场景与点播场景最大的不同是:在直播场景下,媒体内容是随着直播事件的进行在不断生成的。这一特点为客户端和服务器都带来了新的任务。
对服务器来说,它需要准备好新增的媒体内容,并将其更新到描述文件中。这就意味着,服务器需要对playlist进行修改,由于只有media playlist会包含有关具体媒体内容(即segment)的信息,因此需要修改的也只有media playlist。而media playlist的修改需要从#EXT-X-PLAYLIST-TYPE标签谈起,此标签只有两个可选值:
VOD:表示不允许向media playlist增加或者删除任何segment
EVENT:表示只允许向media playlist末尾增加segment或者#EXT-X-ENDLIST标签,且不允许删除任何segment。而在#EXT-X-ENDLIST标签存在后,也将不允许再增加segment
当不指定#EXT-X-PLAYLIST-TYPE标签时,服务器对media playlist的修改限制是最宽松的,除了可以在media playlist的末尾增加segment之外,还可以从media playlist的开头删除segment,这么做可以防止直播时间较长时media playlist长得过大,增加下载开销。这种模式也被称作“滑动窗口”模式。需要注意的是,不论何时,在中间增加或删除segment都是不允许的。
除非#EXT-X-PLAYLIST-TYPE=VOD,或者“#EXT-X-PLAYLIST-TYPE=EVENT且存在#EXT-X-ENDLIST”,否则HLS协议对服务器更新media playlist的要求是强制的。协议要求服务器必须在target duration的1.5倍时间内新增一个segment,或者添加#EXT-X-ENDLIST标签。否则客户端可以认为服务器发生了错误。
而对于客户端来说,由于服务器端对media playlist进行了更新,它就需要不断地刷新当前正在使用的media playlist,看看是否有更新的内容需要播放。更具体地说,同样地,除非#EXT-X-PLAYLIST-TYPE=VOD,或者“#EXT-X-PLAYLIST-TYPE=EVENT且存在#EXT-X-ENDLIST”,否则客户端都应该每隔一定时间后刷新一下media playlist。如果media playlist在target duration的1.5倍时间内都没有发生变化,客户端可以认为服务器出现了问题,这时候它可以选择切换variant,或者中断播放。
在对media playlist一通增增减减后,现在问题来了,作为手里同时有新旧两种media playlist的客户端,我们应该怎么把segment都一一对应上呢?答案就是靠segment序号。使用的标签是我们前文略过没有展开讲解的#EXT-X-MEDIA-SEQUENCE和#EXT-X-DISCONTINUITY-SEQUENCE标签。
对于同一个rendition,在新旧两个版本的media playlist之间同步的原理就是:同一个segment的序号相同,且所属的“不连续片段”的序号也相同。对于可能删除segment的media playlist,服务器必须给其添加 上述两个标签,指定序号的起始值。当删除一个segment之后,#EXT-X-MEDIA-SEQUENCE标签对应的起始值就需要加1;如果某个“不连续片段”里的所有segment都被删除之后,那将它与下一个“不连续片段”分隔开的#EXT-X-DISCONTINUITY标签也可以被删除,删除之后同样需要在#EXT-X-DISCONTINUITY-SEQUENCE中将起始编号加1,如下图所示。
通过序号的对应关系找到同一个segment之后,客户端依然需要检查该segment在新旧两个版本里的URI、字节范围是否一致,来判断media playlist的更新是否出错。
本文主要介绍了流行的自适应流媒体传输协议之一的HLS协议,聊到了在HLS协议里如何使用其媒体描述文件(multivariant and media playlists)去描述一个HLS流所涉及到的所有媒体内容,以及在点播与直播场景下,客户端与服务器之间如何根据这些描述文件去进行流媒体传输。在以后的文章中,我们将对DASH协议也进行介绍,并对这两个协议进行对比。
[1]HTTP Live Streaming: https://developer.apple.com/streaming/
[2]支持HLS的播放器与服务器:https://en.wikipedia.org/wiki/HTTP_Live_Streaming#Supported_players_and_servers
[3]苹果公司提供的HLS demo(通过HTML5的标签):https://developer.apple.com/streaming/examples/advanced-stream-hevc.html
[4]RFC 8216: https://datatracker.ietf.org/doc/html/rfc8216
[5]HTTP Live Streaming 2nd Edition draft: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis
Lemei Huang,迪士尼流媒体视频优化团队高级算法工程师
媒体工程部门(Media Engineering Org.)为迪士尼流媒体构建播放内容的管理、编码、播放和优化系统,致力于给我们的观众提供最好的播放观看体验。
团队覆盖视频、音频和元数据内容从采集、处理到发布的全部过程,支持运维工具、可扩展的自动化流程,包括整个链路的后端服务、前端播放器和性能优化。媒体工程覆盖视频、音频和元数据内容从采集、处理到发布的全部过程,支持运维工具、可扩展的自动化流程,包括整个链路的后端服务、前端播放器和性能优化。