第5章

HLS流媒体协议








7min


HLS与RTMP都是流媒体协议。RTMP由Adobe开发,广泛应用于低延时直播,也是编码器和服务器对接的实际标准协议,在PC(Flash)上有最佳观看体验和最佳稳定性。HLS由苹果公司开发,本身是Live(直播)的,但也支持VoD(点播)。HLS是苹果平台的标准流媒体协议,和RTMP在PC上一样支持得非常完善。HLS协议的详细内容可以参考网址https://datatracker.ietf.org/doc/html/draftpantoshttplivestreaming08。

5.1HLS协议简介

HLS,全称HTTP Live Streaming,是一种由苹果公司提出的基于HTTP的流媒体网络传输协议,是QuickTime X和iPhone软件系统的一部分。它的工作原理是把整个流分成一个个小的基于HTTP的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的extended m3u/m3u8 playlist文件,用于寻找可用的媒体流。HLS只请求基本的HTTP报文,与实时传输协议(RTP)不同,HLS可以穿过任何允许HTTP数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。HLS的网络框架结构如图51所示。

(1) 服务器将媒体文件转换为m3u8及TS分片; 对于直播源,服务器需要实时动态更新。

(2) 客户端请求m3u8文件,根据索引获取TS分片; 点播与直播服务器不同的地方是,直播的 m3u8 文件会不断更新,而点播的 m3u8 文件是不会变的,只需客户端在开始时请求一次。

5.1.1HLS的索引文件的嵌套

HLS协议中的索引文件可以嵌套,一般只有一级索引和二级索引; 媒体流封装的分片格式只支持MPEG2传输流(TS)、WebVTT文件或Packed Audio文件。

索引文件(m3u8)和媒体分片(TS)之间的关系如图52所示。一级m3u8嵌套二级m3u8,二级m3u8描述TS分片。



图51HLS框架




图52HLS索引文件的嵌套关系






5.1.2HLS服务器端和客户端工作流程

在服务器端,流媒体文件被切割成一个一个的小分片,这些小分片有着相同的时长(常为10s,也可以灵活设置),每个小分片是一个TS文件。同时会产生一个索引文件(m3u8),索引文件里存放了TS文件的URL。

客户端请求方式分两种,一种是点播(VoD),一种是直播(Live)。

(1) VoD: 全称Video on Demand,即视频点播,有求才播放。客户端一次获取整个m3u8文件,按照里面的URL获取TS文件,采用HTTP。

(2) Live: 由于m3u8文件是实时更新的,所以客户端每隔一段时间会获取m3u8文件,再根据里面的URL获取TS文件,采用HTTP。

客户端与服务器端通过HTTP进行交互,以两级m3u8嵌套为例,客户端先GET请求到一级m3u8,一级m3u8里面包含了服务器端可以用于传播的一个或多个不同带宽的URL,这个URL可以获取二级m3u8; 二级m3u8包含了多个TS分片的duration及其URL,最后通过这个URL下载TS分片。HLS服务器端和客户端的交互流程如图53所示。



图53HLS服务器端和客户端的交互流程


5.1.3HLS优势及劣势

HLS的优势主要包括以下几点:

(1) 客户端支持简单,只需支持 HTTP 请求,HTTP 协议无状态,只需按顺序下载媒体片段。

(2) 使用 HTTP 协议网络兼容性好,HTTP 数据包也可以方便地通过防火墙或者代理服务器。

(3) 当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源(多码流自适应),允许流媒体会话适应不同的数据速率。

HLS的劣势: 因其自身的实现方式,HLS存在延迟(最少有一个分片),对于直播等对实时敏感的场景,延迟比较大,用户体验不太好。

5.1.4HLS主要的应用场景

HLS主要的应用场景,包括以下几个方面。

(1) 跨平台: PC主要的直播方案是RTMP,而如果选一种协议能跨PC/Android/iOS,那就是HLS。

(2) iOS上苛刻的稳定性要求: iOS上最稳定的是HLS,稳定性不差于RTMP在PC Flash上的表现。

(3) 友好的CDN分发方式: 目前CDN对于RTMP也是基本协议,但是HLS分发的基础是HTTP,所以CDN的接入和分发会比RTMP更加完善。能在各种CDN之间切换,RTMP也能,只是可能需要对接测试。

(4) 简单: HLS作为流媒体协议非常简单,苹果公司的产品支持也很完善。Android对HLS的支持也会越来越完善。

5.2HLS协议详细讲解

HLS协议规定,视频的封装格式是TS; 视频的编码格式为H.264; 声频编码格式为MP3、AAC或者AC3。除了TS视频文件本身,还定义了用来控制播放的m3u8文件(文本文件)。

HLS需要提供一个m3u8播放地址,苹果公司的Safari浏览器直接就能打开m3u8地址,例如http://demo.srs.com/live/livestream.m3u8。Android的浏览器不能直接打开,需要使用HTML5的video标签,然后在浏览器中打开这个页面即可,代码如下: 



//chapter5/hls.h5.video.html

<!-- livestream.html -->

<video width="640" height="360"

autoplay controls autobuffer 

src="http://demo.srs.com/live/livestream.m3u8"

type="application/vnd.apple.mpegurl">

</video>





5.2.1m3u8简介

HLS协议中的m3u8,是一个包含TS列表的文本文件,目的是告诉客户端或浏览器可以播放这些TS文件。m3u8的一些主要标签解释如下。

(1) EXTM3U: 每个m3u8文件的第一行必须是这个tag,提供标识作用。

(2) EXTXVERSION: 用以标示协议版本。例如这里是3,表明这里用的是HLS协议的第3个版本,此标签只能有0或1个,不写代表使用版本1。

(3) EXTXTARGETDURATION: 所有切片的最大时长,如果不设置这个参数,则有些苹果设备就会无法播放。

(4) EXTXMEDIASEQUENCE: 切片的开始序号。每个切片都有唯一的序号,相邻序号+1。这个编号会继续增长,保证流的连续性。

(5) EXTINF: TS切片的实际时长。

(6) EXTXPLAYLISTTYPE: 类型,VoD表示点播,Live表示直播。

(7) EXTXENDLIST: 文件结束符号,表示不再向播放列表文件添加媒体文件。

一个典型的m3u8示例文件,代码如下: 



//chapter5/hls.sample1.m3u8

#EXTM3U//开始标识

#EXT-X-VERSION:3	  //版本为3

#EXT-X-ALLOW-CACHE:YES   //允许缓存

#EXT-X-TARGETDURATION:13  //切片最大时长

#EXT-X-MEDIA-SEQUENCE:430  //切片的起始序列号

#EXT-X-PLAYLIST-TYPE:VoD  //VoD表示点播

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000

http://example.com/low.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000

http://example.com/mid.m3u8

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000

http://example.com/hi.m3u8

#EXTINF:11.800

news-430.ts

#EXTINF:10.120

news-431.ts

#EXT-X-DISCONTINUITY

#EXTINF:11.952

news-430.ts

#EXTINF:12.640

news-431.ts

#EXTINF:11.160

news-432.ts

#EXT-X-DISCONTINUITY

#EXTINF:11.751

news-430.ts

#EXTINF:2.040

news-431.ts

#EXT-X-ENDLIST	  //结束标识





(1) BANDWIDTH用于指定视频流的比特率。

(2) #EXTXSTREAMINF的下一行是二级index文件的路径,可以用相对路径,也可以用绝对路径。上文例子中用的是相对路径。这个文件中记录了不同比特率视频流的二级index文件路径,客户端可以判断自己的现行网络带宽,以此来决定播放哪一个视频流,也可以在网络带宽变化时平滑地切换到和带宽匹配的视频流。二级文件实际负责给出TS文件的下载网址,这里同样使用了相对路径。

5.2.2HLS播放模式

点播VoD的特点是当前时间点可以获取所有index文件和TS文件,二级index文件中记录了所有TS文件的地址。这种模式允许客户端访问全部内容。在上面的例子中是一个点播模式下的m3u8的结构。

Live模式是实时生成m3u8和TS文件。它的索引文件一直处于动态变化中,播放时需要不断下载二级index文件,以获得最新生成的TS文件播放视频。如果一个二级index文件的末尾没有#EXTXENDLIST标志,则说明它是一个Live视频流。

客户端在播放VoD模式的视频时其实只需下载一次一级index文件和二级index文件便可以得到所有TS文件的下载网址,除非客户端进行比特率切换,否则无须再下载任何index文件,只需按顺序下载TS文件并播放就可以了。

Live模式下略有不同,因为播放的同时,新TS文件也在被生成中,所以客户端实际上是下载一次二级index文件,然后下载TS文件,再下载二级index文件(此时这个二级index文件已经被重写,记录了新生成的TS文件的下载网址),然后下载新TS文件,如此反复进行播放。

5.2.3TS文件

TS文件是传输流文件(MPEG2Transport Stream),视频编码主要格式为H.264/MPEG4,声频为AAC/MP3。TS文件分为3层,包括TS层(Transport Stream)、PES层(Packet Elemental Stream)和ES层(Elementary Stream)。ES层是原始的音视频压缩数据,PES层是在音视频数据ES上加了时间戳(PTS/DTS)等对数据帧的说明信息,TS层是在PES层加入数据流的识别和传输所需的信息,这3层结构如图54所示。



图54ES/PES/TS三层结构


(1) TS包大小固定为188B,TS层分为3个部分,即TS Header、Adaptation Field和Payload。TS Header固定为4B; Adaptation Field可能存在也可能不存在,主要作用是给不足188B的数据做填充; Payload是PES数据。

(2) PES是在每个视频/声频帧上加入了时间戳等信息,PES包的内容很多,通常只留下最常用的。

(3) ES层指的是音视频数据,例如H.264视频。

5.3m3u8格式讲解

HLS协议很大一部分内容是对m3u8文本协议的描述。m3u8即播放索引文件,也称为Playlist,是由多个独立行组成的文本文件,必须通过URI(.m3u8或.m3u)或者HTTP ContentType 来识别(application/vnd.apple.mpegurl或audio/mpegurl)。

m3u8文件实际上是一个播放列表(Playlist),可能是一个媒体播放列表(Media Playlist),也可能是一个主列表(Master Playlist),但无论是哪种播放列表,其内部文字使用的都是UTF8编码。当m3u8文件作为媒体播放列表(Media Playlist)时,其内部信息记录的是一系列媒体片段资源,按顺序播放该片段资源,即可完整展示多媒体资源,由此可知,整个视频的总时长是各个TS切片资源的时长之和。

m3u8的每行由用\n 或者\r\n来标识换行。每一行可以是一个URI、空白行或是一个以#号开头的字符串。以#开头的是tag或者注释,以#EXT开头的是tag,其余的为注释,在解析时应该忽略。URI表示一个TS分片地址或是Playlist地址。URI可以用绝对地址或者相对地址,如果使用相对地址,则是相对于当前Playlist的地址。有些tag带有属性值,多个属性用逗号分隔。

一个常见的一级m3u8示例文件,代码如下: 



//chapter5/hls.firstLevel.sample.m3u8

#EXTM3U

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=700,000

http://xxx.itv.cmvideo.cn/low.m3u8?channel-id=bstvod&Contentid=4007432528

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1300,000

http://xxx.itv.cmvideo.cn/mid.m3u8?channel-id=bstvod&Contentid=4007432527








#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2300,000

http://xxx.itv.cmvideo.cn/high.m3u8?channel-id=bstvod&Contentid=4007432526





一个常见的二级m3u8示例文件,代码如下: 



//chapter5/hls.secondLevel.sample.m3u8

#EXTM3U

#EXT-X-VERSION:1

#EXT-X-TARGETDURATION:11

#EXT-X-MEDIA-SEQUENCE:19674922

#EXT-X-PROGRAM-DATE-TIME:2019-03-28T04:33:40Z

#EXTINF:10,

19674922.ts?

#EXT-X-PROGRAM-DATE-TIME:2019-03-28T04:33:50Z

#EXTINF:10,

19674923.ts?

#EXT-X-PROGRAM-DATE-TIME:2019-03-28T04:34:00Z

#EXTINF:10,

19674924.ts?





Master Playlist是指一级m3u8; Media Playlist是指二级m3u8,携带TS分片URL的m3u8; Media Segment是指TS分片; Attribute Lists是指属性列表,是一个用逗号分隔的 attribute/value 对列表,格式为AttributeName=AttributeValue。

tag以#EXT开头,主要分为以下几类。

1. Basic Tags

Basic Tags,可以用在Media Playlist和Master Playlist里面。EXTM3U必须出现在文件的第一行,标识这是一个Extended M3U Playlist文件。EXTXVERSION表示Playlist兼容的版本。

2. Media Segment Tags

Media Segment Tags,每个Media Segment通过一系列的Media Segment Tags跟一个URI来指定。有的Media Segment Tags只应用于下一个Segment,有的则是应用于所有下面的Segments。一个Media Segment Tag只能出现在Media Playlist里面。

(1) EXTINF:  用于指定Media Segment的duration。

(2) EXTXBYTERANGE: 用于指定URI的subrange。

(3) EXTXDISCONTINUITY: 表示后续分片属性发生变化,如文件格式/编码/序号。

(4) EXTXKEY: 表示Media Segment已加密,该值用于解密。

(5) EXTXMAP: 表示Media Segment的头部信息,例如PAT/PMT或者WebVTT头。

(6) EXTXPROGRAMDATETIME: 和Media Segment 的第1个sample一起来确定时间戳。

3. Media Playlist Tags

Media Playlist Tags,用于描述Media Playlist的全局参数。同样地,Media Playlist Tags只能出现在Media Playlist里面。

(1) EXTXTARGETDURATION: 用于指定最大的Media Segment Duration。

(2) EXTXMEDIASEQUENCE: 用于指定第1个Media Segment的序号。

(3) EXTXDISCONTINUITYSEQUENCE: 用于不同的Variant Stream之间同步。

(4) EXTXENDLIST: 表示Media Playlist结束。

(5) EXTXPLAYLISTTYPE: 可选,指定整个Playlist的类型。

(6) EXTXIFRAMESONLY: 表示每个Media Segment均为Iframe。

4. Master Playlist Tags

Master Playlist Tags,用于定义Variant Streams、Renditions和其他显示的全局参数。Master Playlist Tags只能出现在Master Playlist 中。

(1) EXTXMEDIA: 用于关联同一个内容的多个Media Playlist的多种翻译。

(2) EXTXSTREAMINF: 用于指定下级Media Playlist的相关属性。

(3) EXTXIFRAMESTREAMINF: 与EXTXSTREAMINF类似,但指向的下级Media Playlist包含Media Segment均为Iframe。

(4) EXTXSESSIONDATA: 可以随意存放一些 session 数据。

5. Media or Master Playlist Tags

Media or Master Playlist Tags,这里的Tags可以出现在Media Playlist或者Master Playlist中。但是如果同时出现在同一个Master Playlist和Media Playlist中,则必须为相同值。

(1) EXTXINDEPENDENTSEGMENTS: 表示每个 Media Segment 可以独立解码。

(2) EXTXSTART: 标识一个优选的点来播放这个 Playlist。

5.4TS与PS格式简介

据传输媒体的质量不同,MPEG2中定义了两种复合信息流,即传送流(Transport Stream,TS)和节目流(Program Stream,PS)。TS流与PS流的区别在于TS流的包结构是固定长度的,而PS流的包结构是可变长度的。PS包与TS包在结构上的这种差异,导致了它们对传输误码具有不同的抵抗能力,因而应用的环境也有所不同。TS码流由于采用了固定长度的包结构,当传输误码破坏了某一TS包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免了信息丢失,而PS包由于长度是变化的,一旦某一PS包的同步信息丢失,接收机无法确定下一包的同步位置,这就会造成失步,导致严重的信息丢失,因此,在信道环境较为恶劣或传输误码较高时,一般采用TS码流,而在信道环境较好和传输误码较低时,一般采用PS码流。由于TS码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的MPEG2码流基本上采用了TS码流的包格式。MPEG2PS主要应用于存储具有固定时长的节目,如DVD电影,而MPEGTS则主要应用于实时传送的节目,例如实时广播的电视节目。

5.4.1ES、PES、PS、TS

MPEG是活动图像专家组(Moving Picture Expert Group)的缩写,于1988年成立。目前MPEG已颁布了3个活动图像及声音编码的正式国际标准,分别称为MPEG1、MPEG2和MPEG4,而MPEG7和MPEG21仍在研究中。

MPEG2是运动图像专家组(MPEG)组织制定的视频和声频有损压缩标准之一,它的正式名称为“基于数字存储媒体运动图像和语音的压缩标准”。与MPEG1标准相比,MPEG2标准具有更高的图像质量、更多的图像格式和传输码率的图像压缩标准。MPEG2标准不是MPEG1的简单升级,而是在传输和系统方面做了更加详细的规定和进一步的完善。它是针对标准数字电视和高清晰电视在各种应用下的压缩方案,传输速率为3Mb/s~10Mb/s。MPEG2标准目前分为9个部分,统称为ISO/IEC 13818国际标准。各部分的内容描述如下。

(1) 第1部分,ISO/IEC 138181,System: 系统,描述多个视频、声频和数据基本码流合成传输码流和节目码流的方式。

(2) 第2部分,ISO/IEC 138182,Video: 视频,描述视频编码方法。

(3) 第3部分,ISO/IEC 138183,Audio: 声频,描述与MPEG1声频标准反向兼容的声频编码方法。

(4) 第4部分,ISO/IEC 138184,Compliance: 符合测试,描述测试一个编码码流是否符合MPEG2码流的方法。

(5) 第5部分,ISO/IEC 138185,Software: 软件,描述了MPEG2标准的第1、2、3部分的软件实现方法。

(6) 第6部分,ISO/IEC 138186,DSMCC: 数字存储媒体命令与控制,描述交互式多媒体网络中服务器与用户间的会话信令集。

以上6个部分均已获得通过,成为正式的国际标准,并在数字电视等领域中得到了广泛的实际应用。此外,MPEG2标准还有3个部分,其中第7部分规定不与MPEG1声频反向兼容的多通道声频编码; 第8部分现已停止; 第9部分规定了传送码流的实时接口。

下面介绍几个重要概念,包括ES、PES、PTS与DTS、PS、TS等。

(1) ES,即原始流(Elementary Streams),是直接从编码器出来的数据流,可以是编码过的视频数据流(H.264/MJPEG等)、声频数据流(AAC/AC3/MP3)或其他编码数据流的统称。ES流经过PES打包器之后,被转换成PES包。ES是只包含一种内容的数据流,如只含视频或只含声频等,打包之后的PES也是只含一种性质的ES,如只含视频ES的PES,或只含声频ES的PES等。每个ES都由若干个存取单元(Access Unit,AU)组成,每个视频AU或声频AU都由头部和编码数据两部分组成,1个AU相当于编码的1幅视频图像或1个声频帧,也可以说,每个AU实际上是编码数据流的显示单元,即相当于解码的1幅视频图像或1个声频帧的取样。

(2) PES,即分组的ES(Packetized Elementary Stream),是用来传递ES的一种数据结构。PES流是ES流经过PES打包器处理后形成的数据流,在这个过程中完成了将ES流分组、打包、加入包头信息等操作(对ES流的第一次打包)。PES流的基本单位是PES包。PES包由包头和Payload组成。

(3) PTS与DTS,PTS即显示时间标记(Presentation Timestamp)表示显示单元出现在系统目标解码器(H.264、MJPEG等)的时间。DTS即解码时间标记(Decoding Time Stamp)表示将存取单元的全部字节从解码缓存器移走的时间。PTS/DTS是打在PES包的包头里面的,这两个参数是解决音视频同步显示,防止解码器输入缓存上溢或下溢的关键。每个I(关键帧)、P(预测帧)、B(双向预测帧)帧的包头都有一个PTS和DTS,但PTS与DTS对于B帧不一样; 对于I帧和P帧,显示前一定要存储于视频解码器的重新排序缓存器中,经过延迟(重新排序)后再显示,所以一定要分别标明PTS和DTS。

(4) PS,即节目流(Program Stream),由PS包组成,而一个PS包又由若干个PES包组成(到这里,ES经过了两层的封装)。PS包的包头中包含了同步信息与时钟恢复信息。1个PS包最多可包含具有同一时钟基准的16个视频PES包和32个声频PES包。

(5) TS,即传输流(Transport Stream),由定长的TS包组成(188B),而TS包是对PES包的一个重新封装(到这里,ES也经过了两层的封装)。PES包的包头信息依然存在于TS包中。单一性是指TS流的基本组成单位,是长度为188B的TS包。混合性是指TS流由多种数据组合而成,1个TS包中的数据可以是视频数据、声频数据、填充数据、PSI/SI表格数据等(由唯一的PID对应)。

5.4.2PS/TS编码基本流程

从ES到PES再到PS/TS的基本编码流程如图55所示。



图55ES/PES/TS编码流程


(1) A/D转换后,通过MPEG2压缩编码得到的ES基本流。这个数据流很大,并且只是I、P、B的这些视频帧或声频取样信息。

(2) 通过PES打包器,打包并在每个帧中插入PTS/DTS标志,变成PES。原来是流的格式,现在成了数据包的分割形式。

(3) PES根据需要打包成PS或TS包进行存储(DVD)或传输(DVB)。因每路音/视频只包含一路的编码数据流,所以每路PES也只包含相应的数据流。

5.4.3PS/TS码流小结

MPEG2作为一个数字视声频的一种压缩标准一直被广泛地运用于多媒体、数字存储及数字传输(如数字电视)等领域。其规范主要包括声频编码、视频编码、系统、数字存储规范、复用和测试等几个部分。其中音、视频和系统(音视频同步)为主要部分,解决音视频的压缩问题并提供一种不同码流间的复用规范。根据传输媒体质量的不同,MPEG2中定义了两种复合信息流,即传送流(TS)和节目流(PS)。在MPEG2系统中,信息复合/分离的过程称为系统复接/分接,由视频、声频的ES流和辅助数据复接生成的用于实时传输的标准信息流(例如实时广播的电视节目)称为MPEG2传送流(MPEG2TS),而MPEG2节目流(MPEG2PS)主要应用于存储具有固定时长的节目,如DVD电影。

TS流与PS流的区别在于TS流的包结构是固定长度的,而PS流的包结构是可变长度的。PS包与TS包在结构上的这种差异,导致了它们对传输误码具有不同的抵抗能力,因而应用的环境也有所不同。TS码流由于采用了固定长度的包结构,当传输误码破坏了某一TS包的同步信息时,接收机可在固定的位置检测它后面包中的同步信息,从而恢复同步,避免了信息丢失,而PS包由于长度是可变化的,一旦某一PS包的同步信息丢失,接收机无法确定下一包的同步位置,这就会造成失步,导致严重的信息丢失,因此,在信道环境较为恶劣或传输误码较高时,一般采用TS码流,而在信道环境较好和传输误码较低时,一般采用PS码流。由于TS码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的MPEG2码流基本上采用了TS码流的包。节目流主要用于误码相对较低的演播室和数字存储(如DVD)中; 传输流主要用于传输中,它有固定长度的明显特点。这种数据结构运用于数字视频广播(Digital Video Broadcasting,DVB)的传输层中。

ES流是音、视频信号经过编码器之后或数据信号的基本码流。ES是只包含一种内容的数据流,如只含视频或只含声频等,打包之后的PES也是只含一种性质的ES,如只含视频的ES的PES或只含声频ES的PES。PES(Packetized Elementary Stream)是打包的基本码流,ES经过打包后的码流,其长度可变。视频一般一帧一个包,声频长度一般不超过64KB。

ES、PES、PS、TS的关系,如图56所示。



图56ES、PES、TS、PS关系框架图


5.5TS码流详细讲解

学习数字电视机顶盒的开发,从MPEG2到DVB会出现很多数据结构,如PAT、PMT、CAT等。数字电视机顶盒接收的是一段段的码流,称为传输流(Transport Stream,TS),每个TS流都携带一些信息,如Video、Audio、PAT、PMT等信息,因此,首先需要了解TS流是什么,以及TS流是怎样形成、有着怎样的结构,下面开始详细解释这些内容。

ES流是基本码流,不分段的声频、视频或其他信息的连续码流。PES流把基本流ES分割成段,并加上相应头文件打包形成的打包基本码流。PS流将具有共同时间基准的一个或多个PES组合(复合)而成单一数据流(用于播放或编辑系统,如m2p)。TS流将具有共同时间基准或独立时间基准的一个或多个PES组合(复合)而成单一数据流(用于数据传输)。由于TS码流具有较强的抵抗传输误码的能力,因此目前在传输媒体中进行传输的MPEG2码流基本上采用了TS码流的包。

TS流的产生过程,如图57所示。



图57TS流的产生过程


可以看出,视频ES和声频ES通过打包器和共同或独立的系统时间基准形成一个个PES,通过TS复用器复用形成传输流。注意这里的TS流是位流格式,也就是说TS流是可以按位读取的。

5.5.1TS包格式

TS流是基于Packet的位流格式,每个包是188B(或204B,在188B后加上了16B的CRC校验数据,其他格式相同)。整个TS流组成形结构如图58所示。



图58TS流与TS包


TS Packet Header(包头)字段结构,代码如下: 



//chapter5/ts.packet.header.txt

struct ts_header{

char    syn_Byte:8;                            //包头同步字节,0x47

char    transport_error_indicator:1;        //传送数据包差错指示器

char    payload_unit_start_indicator:1;    //有效净荷单元开始指示器

char    transport_priority:1;                //传送优先级

int     PID:13;                                 //包ID

char    transport_scrambling_control:2;    //传送加扰控制

char    adaptation_field_control:2;         //调整字段控制

char    continuity_counter:4;               //连续计数器 0~15

};





(1) syn_Byte: 值为0x47,是MPEG2TS的传送包标识符。

(2) transport_error_indicator: 值为1时,表示相关的传送包中至少有一个不可纠正的错误位,只有错误位纠正后,该位才能置0。

(3) payload_unit_start_indicator: 表示TS包的有效净荷带有PES或PSI数据的情况; 当TS包的有效净荷带有PES包数据时,payload_unit_start_indicator为1,表示TS包的有效净荷以PES包的第1字节开始; payload_unit_start_indicator为0,表示TS包的开始不是PES包。

(4) 当TS包带有PSI数据时,payload_unit_start_indicator为1,表示TS包带有PSI部分的第1字节,即第1字节带有指针pointer_field,payload_unit_start_indicator为0,表示TS包不带有一个PSI部分的第1字节,即在有效净荷中没有指针pointer_field; 对于空包的包,play_unit_start_indicator应该置为0。

(5) transport_priority: 置1表示相关的包比其他具有相同PID但transport_priority为0的包有更高的优先级。

(6) PID是TS流中唯一识别标志,Packet Data是什么内容由PID决定。如果一个TS流中的1个Packet的Packet Header中的PID是0x0000,则这个Packet的Packet Data是DVB的PAT表,而非其他类型数据(如Video、Audio)或其他业务信息。

一些TS表的PID值是固定的,不允许用于更改,如表51所示。

表51TS表的名称及对应的PID值



表PID值表PID值

PAT0x0000EIT/ST0x0012

CAT0x0001RST/ST0x0013

TSDT0x0002TDT/TOT/ST0x0014


TS流最经典的应用是平时生活中的数字高清电视。电视码流是TS封装格式的码流,电视码流发送过来后,就会由机顶盒进行解封装和解码,然后传给电视机进行播放。这里就有一个问题,例如看电视,有很多的频道和节目,那么对应码流是怎么区分的呢?TS流引入了PAT和PMT两张表格的概念来解决这个问题。

TS流是以188B为一包,可以称为TS Packet。这个TS Packet有可能是音视频数据,也有可能是表格(PAT/PMT/...)。举例说明,TS流的包顺序,代码如下: 



PAT,PMT,DATA,DATA,,,,,,PAT,PMT,DATA,DATA,,,,,,





每隔一段时间,发送一张PAT表,紧接着发送一张PMT表,接着发送DATA(音视频)数据。PAT表格里面包含所有PMT表格的信息,一个PMT表格对应一个频道,例如中央电视台综合频道,而一个PMT里面包含所有节目的信息,例如CCTV1到CCTV14。在实际情况中有很多频道,所以PMT表格可不止一张,有可能是如下形式,代码如下: 



PAT,PMT,PMT,PMT,,,DATA,DATA,,,,PAT,PMT,PMT,,,DATA,DATA





除了这个设定外,每个频道或节目都有自己的标识符(PID),这样当得到一个DATA,解析出里面的PID,就知道是什么节目了,并且也知道所属频道是什么了。在看电视时,会收到所有节目的DATA,当选择某个节目时,机顶盒会把这个节目的DATA单独过滤出来,其他的舍弃。

TS Packet的长度是188B,分为TS Header和TS Body。其中TS Header里面会有个PID字段,标识着当前TS Body的类型。TS Body有可能是表格,也有可能是DATA,表格很好理解,接下来讲解DATA的结构。DATA包其实是PES包,而PES包是对ES的封装,PES包分为PES头加ES。这里的ES是原始流,是指经过压缩后的H.264、AAC等格式的音视频数据。

这里介绍一下帧数据、PES包、TS Packet包的对应关系。一帧数据封装成一个PES包(含PES头和ES),这个PES包如果小于188B,则一个TS Packet就可以放下。最终TS Packet包的格式是TS Header+Padding+PES Packet(PES Header+ES)。Padding为填充字节的意思,如果TS Header加上PES包后仍不满188B,则此时需要填充,使其凑满188B后再发送。

视频帧是很大的,往往大于188B,同样需要把一个视频帧放入一个PES包,然后分别放在几个TS Packet包即可。PES头加上这些部分ES,是一个PES包,伪代码如下: 



第1个TS Packet:TS Header+PES头+部分ES

第2个TS Packet:TS Header+部分ES

...

最后一个TS Packet:TS Header+填充字节+部分ES





5.5.2TS码流分析工具

Tsr(TS码流分析工具)是一款非常好用的解析软件,是专门为TS码流所设计的。读者可通过它对多种TS流文件进行查询,进而了解其中所包含的详细信息,以便用户后续处理。该软件的启动界面如图59所示。



图59TS码流分析工具


TS流是分包发送的,每个包长为188B。在TS流里可以填入很多类型的数据,如视频、声频、自定义信息等。它的包的结构: 包头为4B,负载为184B。将一个TS视频文件拖曳到该软件中,即可自动分析TS格式,如图510所示。



图510TS文件的码流结构


5.5.3TS码流结构分析

MPEG2中规定TS传输包的长度为188B,包头为4B,负载为184B,但通信媒介会为包添加错误校验字节,从而有了不同于188B的包长。

(1) 在DVB规定中,使用204B作为包长。通过调制器时,在每个传输包后增加了16B的里德所罗门前向纠错码,因而形成了204B的数据包,调制后总存在204B的数据包。调制之前在复用器插入RS码或虚构的RS码。

(2) 在ATSC规定中,使用208B作为包长,即添加20B的 RS(ReedSolomon)前向纠错码。与DVB不同,ATSC规定RS码只能出现在调制的TS流中。所有的TS包都分为包头和净荷(负载、Payload)部分。TS包中可以填入很多东西(填入的东西都被填到净荷部分),包括视频、声频、数据(包括PSI、SI及其他任何形式的数据)。TS包的语法结构如图511所示。



图511传输包(TS)码流语法结构


1. TS包头

TS包的包头提供关于传输方面的信息,包括同步、有无差错、有无加扰、PCR(节目参考时钟)等标志。TS包的包头长度不固定,前32b(4字节)是固定的,后面可能跟有自适应字段(适配域),因此32b(4字节)是最小包头。TS包也可以是空包,空包用来填充TS流,可能在重新进行多路复用时被插入或删除。

TS包头字段结构如下: 

(1) sync_Byte (同步字节): 固定为0100 0111(0x47),该字节由解码器识别,使包头和有效负载可相互分离。

(2) transport_error_indicator(传输差错指示): 1表示在相关的传输包中至少有一个不可纠正的错误位。当被置为1后,在错误被纠正之前不能重置为0。

(3) payload_unit_start_indicator(开始指示): 为1时,在前4字节之后会有一个调整字节,其数值为后面调整字段的长度length,因此有效载荷开始的位置应再偏移1+[length]字节。为0时,则在4字节之后,直接是有效载荷。

(4) transport_priority(传输优先级): 1表明优先级比其他具有相同PID,但此位没有被置为1的分组高。

(5) PID: 指示存储与分组有效负载中数据的类型。PID值0x0000~0x000F保留。其中0x0000为PAT保留; 0x0001为CAT保留; 0x1fff为分组保留,即空包。

(6) transport_scrambling_control(加扰控制): 表示TS流分组有效负载的加密模式。空包为 00,如果传输包的包头中包括调整字段,则不应被加密。

(7) adaptation_field_control(自适应字段控制): 表示包头是否有调整字段或有效负载。00表示为ISO/IEC未来使用保留; 01表示仅含有效载荷,无调整字段; 10表示无有效载荷,仅含调整字段; 11表示调整字段后为有效载荷,调整字段中的前一字节表示调整字段的长度length,有效载荷开始的位置应再偏移[length]字节。空包应为10。

(8) continuity_counter(连续计数器): 随着每个具有相同PID的TS流分组而增加,当它达到最大值后又回复到0。范围为0~15。

(9) adaptation_field(自适应字段): 根据自适应控制字段填充负载。

2. 节目专用信息PSI

在系统复用时,视频、声频的ES流需进行打包形成视频、声频的 PES流,辅助数据(如图文电视信息)不需要打成PES包。PES包非定长,声频的PES包小于或等于64K,视频一般为一帧一个PES包。一帧图像的PES包通常要由许多个TS包来传输。

MPEG2中规定,一个PES包必须由整数个TS包来传输。如果承载一个PES包的最后一个TS包没能装满,则用填充字节来填满; 目前一个新的PES包形成时,需用新的TS包来开始传输。

节目专用信息(Program Specific Information,PSI),用来管理各种类型的TS数据包,需要有些特殊的TS包来确立各个TS数据包之间的关系。这些特殊的TS包里所包含的信息是节目专用信息。在不同的标准中它有不同的名字: MPEG2中称为PSI; DVB标准根据实际需要,对PSI扩展,称为SI信息; ATSC标准中称为PSIP信息。

MPEG2中,规定的对PSI信息的描述方法有以下几种: 

(1) 表(Table),对节目信息的结构性描述,包括节目关联表(Program Association Table,PAT)、节目映射表(Program Map Tables,PMT)、条件接收表(Conditional Access Table,CAT)、加密解密、网络信息表(Network Information Table,NIT)、传送流描述表(Transport Stream Description Table,TSDT)等。其中,PAT、CAT、NIT的PID值是固定的,分别为0x0000、0x0001、0x0010,而PMT的PID值是在PAT中定义的。

(2) 节(Section),将表格的内容映射到TS流中,包括专用段 Private_section等。

(3) 描述符(Descriptor),提供有关节目构成(视频流、声频流、语言、层次、系统时钟和码率等多方面)的信息;  ITUT Rec.H.222.0|ISO /IEC 138181 中定义的 PSI表可被分成一段或多段置于传输流分组中。一段是一个语法结构,用来将 ITUT Rec.H.222.0|ISO /IEC 138181 中定义的 PSI表映射到传输流分组中。

5.5.4PAT及PMT表格式

下面介绍两个重要的表结构,包括PAT和PMT。

1. PAT

TS流中包含一个或者多个PAT。PAT由PID为0x0000的TS包传送,其作用是为复用的每一路传送流提供所包含的节目和节目编号,以及对应节目的PMT的位置,即PMT的TS包的PID值,同时还提供NIT的位置,即NIT的TS包的PID的值。TS码流解析从PAT开始。PAT定义了当前TS流中所有的节目,其PID为0x0000,它是PSI的根节点,要查找节目必须从PAT开始查找。PAT主要包含频道号码和每个频道对应的 PMT 的 PID 号码, 这些信息在处理PAT时会保存起来,以后会使用这些数据。下面给出PAT的字段结构,代码如下: 



//chapter5/ts_pat_program.txt

typedef struct TS_PAT_Program  

{  

unsigned program_number :	16;  //节目号  

unsigned program_map_PID:	13;  //节目映射表的PID,每个节目对应一个   

}TS_PAT_Program;





PAT数据包分为两部分,包括PAT数据包头部(前8B)和循环部分,代码如下: 



//chapter5/ts_program_association_section.txt

/*头部部分 8字节*/

program_association_section()

{  

unsigned table_id: 8;   //固定为0x00,标志是该表为PAT

unsigned section_syntax_indicator    : 1; //段语法标志位,固定为1  

unsigned '0'		: 1; //0  

unsigned reserved_1	: 2; //保留位  

unsigned section_length	: 12; //段长度字节  



//该传输流的ID,区别于一个网络中其他多路复用的流

unsigned transport_stream_id: 16;   

unsigned reserved_2	: 2;   //保留位

unsigned version_number	: 5; //范围为0~31,表示PAT的版本号 

unsigned current_next_indicator: 1; //发送的当前PAT是否有效








//PAT可能分为多段传输,第一段为00,以后每个分段加1,最多可能有256个分段

//给出section号,在sub_table中,第1个section的section_number为"0x00"

//每增加一个section,section_number加1

unsigned section_number	: 8; //分段的号码



//最后一个分段的号码,sub_table中最后一个section的section_number

unsigned last_section_number    : 8;  



/*循环部分 4个Byte*/

for(i=0;i<N;i++)

{

program_number     		:16;  //节目号

Reserved				:3;   //保留位

//网络信息表(NIT)的PID,节目号为0时对应的PID为network_PID

//其余情况是program_map_PID(PMT的PID)

network_id 或 program_map_PID	:13; 

}

CRC_32                                	:32; //校验码     

}





各个字段的含义如下。 

(1) table_id: 固定为0x00,标志该表是PAT。

(2) section_syntax_indicator: 段语法标志位,固定为1。

(3) section_length: 表示此字节后面有用的字节数,包括CRC32。

(4) 节目套数: (section_length-9)/4。

(5) transport_stream_id: 表示该TS流的ID,区别于同一个网络中其他多路复用流。

(6) version_number: 表示PAT的版本号。

(7) current_next_indicator: 表示发送的PAT是当前有效还是下一个有效。

(8) section_number: 表示分段的号码。PAT可能分多段传输,第一段为0,以后每个分段加1,最多可能有256个分段。

(9) last_section_number: 表示PAT最后一个分段的号码。

(10) program_number: 节目号。

(11) network_PID: 网络信息表(NIT)的PID,节目号为0时对应的ID为network_PID。

(12) program_map_PID: 节目映射表(PMT)的PID,节目号为大于或等于1时,对应的ID为program_map_PID。一个PAT中可以有多个program_map_PID。

(13) CRC_32: 32位字段,CRC32校验码Cyclic Redundancy Check。

使用Tsr码流分析工具,可以很方便地解析出对应的PAT结构,如图512所示。



图512PAT结构


PAT的解析函数,代码如下: 



//chapter5/ts.pat.analysis.c

//TS_PAT结构体参考program_association_section()

//C语言的位操作符

int adjust_PAT_table( TS_PAT * packet, unsigned char * buffer)  

{  

packet->table_id	= buffer[0];  

packet->section_syntax_indicator	= buffer[1] >> 7;  

packet->zero		= buffer[1] >> 6 & 0x1;  

packet->reserved_1		= buffer[1] >> 4 & 0x3;  

packet->section_length	= (buffer[1] & 0x0F) << 8 | buffer[2];   



packet->transport_stream_id	= buffer[3] << 8 | buffer[4];  



packet->reserved_2			= buffer[5] >> 6;  

packet->version_number		= buffer[5] >> 1 &  0x1F;  

packet->current_next_indicator	= (buffer[5] << 7) >> 7;  

packet->section_number		= buffer[6];  

packet->last_section_number= buffer[7];   



int len = 0;  

len = 3 + packet->section_length;  

packet->CRC_32 = (buffer[len-4] & 0x000000FF) << 24  

| (buffer[len-3] & 0x000000FF) << 16  

| (buffer[len-2] & 0x000000FF) << 8   

| (buffer[len-1] & 0x000000FF);   








int n = 0;  

///循环次数

for ( n = 0; n < packet->section_length - 12; n += 4 )  

{  

unsigned  program_num = buffer[8 + n ] << 8 | buffer[9 + n ];    

packet->reserved_3 		= buffer[10 + n ] >> 5;   



packet->network_PID = 0x00;  

if ( program_num == 0x00)  

{    

packet->network_PID = (buffer[10+n ] & 0x1F) << 8 | buffer[11+n ];  



TS_network_Pid = packet->network_PID; //记录该TS流的网络PID  



TRACE(" packet->network_PID %0x /n/n", packet->network_PID );  

}  

else  

{  

TS_PAT_Program PAT_program;  //队列

PAT_program.program_map_PID = 

(buffer[10 + n] & 0x1F) << 8 | buffer[11 + n];  

PAT_program.program_number = program_num;  

packet->program.push_back( PAT_program );  

//向全局PAT节目数组中添加PAT节目信息

TS_program.push_back( PAT_program );       

}           

}  

return 0;  

}





在上述代码中,从for循环开始,描述了当前流中的频道数目和每个频道对应的PMT的PID值。解复用程序需要接收所有的频道号码和对应的PMT 的PID,并把这些信息在缓冲区中保存起来。在后部的处理中需要使用PMT的 PID。

2. PMT

PMT在传送流中用于指示组成某一套节目的视频、声频和数据在传送流中的位置,即对应的TS包的PID值,以及每路节目的节目时钟参考(PCR)字段的位置。PMT流结构,代码如下: 



//chapter5/ts.TS_PMT_Stream.txt

typedef struct TS_PMT_Stream    

{    

 unsigned stream_type	: 8; //指示特定PID的节目元素包的类型

 unsigned elementary_PID: 13; //该域指示TS包的PID值,包含相关的节目元素    

 unsigned ES_info_length: 12; //前两位是00,指示跟随其后的相关节目元素的字节数    

 unsigned descriptor;    

}TS_PMT_Stream; 





PMT包含以下信息: 

(1) 当前频道中包含的所有Video数据的PID。

(2) 当前频道中包含的所有Audio数据的PID。

(3) 和当前频道关联在一起的其他数据的PID(如数字广播、数据通信等使用的PID)。

只要处理了PMT,就可以获取频道中所有的PID信息,如当前频道包含多少个Video、共多少个Audio和其他数据,还能知道每种数据对应的PID值。如果要选择其中一个Video和Audio收看,则只需把要收看的节目的Video PID和Audio PID保存起来,在处理Packet的时候进行过滤便可实现。

PMT表结构,代码如下: 



//chapter5/ts.TS_program_map_section.txt

TS_program_map_section() {

table_id                   :8; //固定为0x02 标识PMT表

section_syntax_indicator :1; //固定为0x01

'0'                        :1; //

reserved                    :2; //保留位

section_length             :12 //该字段的头两位必为‘00’,剩余10位指定该分段的字节

//数,紧随section_length 字段开始,并包括CRC。此字段中的值应不超过1021(0x3FD)

program_number            :16 //指出TS流中Program Map Section的版本号

reserved                   :2  //保留位

version_number           :5  //指出TS流中Program Map Section的版本号 

current_next_indicator :1  //当该位被置1时,当前传送的Program Map Section可用;

//当该位被置0时,指示当前传送的Program Map Section不可用,下一个TS流的Program Map 

//Section有效

section_number            :8  //固定为0x00

last_section_number      :8  //固定为0x00

reserved                   :3  //保留

PCR_PID                    :13 //指明TS包的PID值,该TS包含PCR域 

//该PCR值对应于由节目号指定的对应节目  

//如果对于私有数据流的节目定义与PCR无关,这个域的值将为0x1FFF

reserved                  :4  //保留位

program_info_length     :12 //节目信息长度。该字段的头两位必为'00',剩余10位指

//定紧随program_info_length 字段的描述符的字节数 

//(之后的是N个描述符结构,一般可以忽略,这个字段就代表描述符总的长度,单位是Bytes)紧接

//着是频道内部包含的节目类型和对应的PID



for (i = 0; i < N; i++) {

descriptor()

}

for (i = 0; i < N1; i++) {

stream_type           :8//流类型,标志是Video、Audio,还是其他数据 

reserved                 :3 //保留位

elementary_PID          :13 //该节目的声频或视频PID

reserved                 :4 //保留位









ES_info_length         :12 //该字段的头两位必为'00',剩余10位指示紧随ES_info_

//length字段的相关节目元描述符的字节数

for (i = 0; i < N2; i++) {

descriptor()

}

}

CRC_32                       :32 

}





各个字段结构解释如下。 

(1) table_id: 固定为0x02,标志该表是PMT。

(2) section_syntax_indicator: 对于PMT表,设置为1。

(3) section_length: 表示此字节后面有用的字节数,包括CRC32。

(4) program_number: 它指出该节目对应于可应用的Program Map PID。

(5) version_number: 指出PMT 的版本号。

(6) current_next_indicator: 当该位被置1时,当前传送的Program Map Section可用; 当该位被置0时,指示当前传送的Program Map Section不可用,下一个TS流的Program Map Section 有效。

(7) section_number: 总是置为0x00(因为PMT里表示一个Service的信息,一个Section的长度足够)。

(8) last_section_number: 该域的值总是0x00。

(9) PCR_PID: 节目中包含有效PCR字段的传送流中的PID。

(10) program_info_length: 12b,前两位为00。该域指出跟随其后对节目信息的描述符的字节数。

(11) stream_type: 8b,指示特定PID的节目元素包的类型。该处PID由elementary_PID指定。

使用Tsr码流分析工具,可以很方便地解析出对应的PMT结构,如图513所示。



图513PMT结构


PMT的解析函数,代码如下: 



//chapter5/ts_adjust_PMT_table.c

int adjust_PMT_table ( TS_PMT * packet, unsigned char * buffer )  

{   

//读取各个字段

packet->table_id			= buffer[0];  

packet->section_syntax_indicator	= buffer[1] >> 7;  

packet->zero			= buffer[1] >> 6 & 0x01;   

packet->reserved_1			= buffer[1] >> 4 & 0x03;  

packet->section_length 	= (buffer[1] & 0x0F) << 8 | buffer[2];      

packet->program_number	= buffer[3] << 8 | buffer[4];  

packet->reserved_2		= buffer[5] >> 6;  

packet->version_number	= buffer[5] >> 1 & 0x1F;  

packet->current_next_indicator	= (buffer[5] << 7) >> 7;  

packet->section_number		= buffer[6];  

packet->last_section_number	= buffer[7];  

packet->reserved_3		= buffer[8] >> 5;  

packet->PCR_PID	= ((buffer[8] << 8) | buffer[9]) & 0x1FFF;  



PCRID = packet->PCR_PID;  



packet->reserved_4			= buffer[10] >> 4;  

packet->program_info_length= (buffer[10] & 0x0F) << 8 | buffer[11];   

//Get CRC_32  

int len = 0;  

len = packet->section_length + 3;      

packet->CRC_32                = (buffer[len-4] & 0x000000FF) << 24  

| (buffer[len-3] & 0x000000FF) << 16  

| (buffer[len-2] & 0x000000FF) << 8  

| (buffer[len-1] & 0x000000FF);   



int pos = 12;  

//program info descriptor  //节目信息描述符

if ( packet->program_info_length != 0 )  

pos += packet->program_info_length;      

//Get stream type and PID      

for ( ; pos <= (packet->section_length + 2 ) -  4; )  

{  

TS_PMT_Stream pmt_stream;  //流信息

pmt_stream.stream_type =  buffer[pos];  

packet->reserved_5  =   buffer[pos+1] >> 5;  

pmt_stream.elementary_PID =  ((buffer[pos+1] << 8) | buffer[pos+2]) & 0x1FFF;  

packet->reserved_6     =   buffer[pos+3] >> 4;  

pmt_stream.ES_info_length =   (buffer[pos+3] & 0x0F) << 8 | buffer[pos+4];  


pmt_stream.descriptor = 0x00;  //描述符









if (pmt_stream.ES_info_length != 0)  

{  

pmt_stream.descriptor = buffer[pos + 5];  



for( int len = 2; len <= pmt_stream.ES_info_length; len ++ )  

{  

pmt_stream.descriptor = pmt_stream.descriptor<< 8 | buffer[pos + 4 + len];  

}  

pos += pmt_stream.ES_info_length;  

}  

pos += 5;  

packet->PMT_Stream.push_back( pmt_stream );  //存储下来

TS_Stream_type.push_back( pmt_stream );  

}  

return 0;  

}





5.6PS码流详细讲解

PS文件分为3层,包括PS层(Program Stream)、PES层(Packet Elemental Stream)和ES层(Elementary Stream)。ES层是音视频数据层,PES层在音视频数据上加了时间戳等对数据帧的说明信息,PS层在PES层上加入了数据流识别和传输的必要信息。

5.6.1PS码流结构

一个完整的MPEG2文件是一个PS流文件,PS码流结构如图514所示。



图514PS码流结构


可以看出,整个文件分为3层。首先被分为一个个的Program Pack,然后Program Pack里面包含了Program Pack Header和PES包,PES包里又包含了PES Header和声频编码数据或视频编码数据。PS流由很多个PS包组成,PS包的结构,代码如下: 



PS Header + SYS Header(I帧) + PSM Header(I帧) + PES Header + PES Packet





PS流总是以0x000001BA开始,以0x000001B9结束,对于一个PS文件,有且只有一个结束码0x000001B9,但对于网传的PS流,则没有结束码。

PS、PES和ES的关系,如图515所示。



图515PS、PES、ES的关系


PS层主要由Pack Header和数据组成,Pack Header中各个字段,如图516所示。
其中,bslbf(bit string,left bit first)为比特串,左位在先; uimsbf(unsigned integer,most significant bit first)为无符号整数,高位在先。

Pack Header结构体采用了位域来定义,代码如下: 



//chapter/ps.pack_header.txt

struct pack_header //Pack Header结构体

{








图516PS包头结构






unsigned char pack_start_code[4]; //起始码

unsigned char system_clock_reference_base21 : 2;   //参考时钟

unsigned char marker_bit : 1;

unsigned char system_clock_reference_base1 : 3;

unsigned char fix_bit : 2;   

unsigned char system_clock_reference_base22;

unsigned char system_clock_reference_base31 : 2;

unsigned char marker_bit1 : 1;

unsigned char system_clock_reference_base23 : 5;

unsigned char system_clock_reference_base32;

unsigned char system_clock_reference_extension1 : 2;

unsigned char marker_bit2 : 1;

unsigned char system_clock_reference_base33 : 5;

unsigned char marker_bit3 : 1;

unsigned char system_clock_reference_extension2 : 7;

unsigned char program_mux_rate1;

unsigned char program_mux_rate2;

unsigned char marker_bit5 : 1;

unsigned char marker_bit4 : 1;

unsigned char program_mux_rate3 : 6;

unsigned char pack_stuffing_length : 3;









unsigned char reserved : 5;



pack_header()

{

pack_start_code[0] = 0x00;   //起始码固定为0x000001BA

pack_start_code[1] = 0x00;

pack_start_code[2] = 0x01;

pack_start_code[3] = 0xBA;

fix_bit = 0x01;

marker_bit = 0x01;

marker_bit1 = 0x01;

marker_bit2 = 0x01;

marker_bit3 = 0x01;

marker_bit4 = 0x01;

marker_bit5 = 0x01;

reserved = 0x1F;

pack_stuffing_length = 0x00;

system_clock_reference_extension1 = 0;

system_clock_reference_extension2 = 0;

}

//获取参考时钟

void getSystem_clock_reference_base(UINT64 &_ui64SCR)

{

_ui64SCR = (system_clock_reference_base1 << 30) | (system_clock_reference_base21 << 28)

| (system_clock_reference_base22 << 20) | (system_clock_reference_base23 << 15)

| (system_clock_reference_base31 << 13) | (system_clock_reference_base32 << 5)

| (system_clock_reference_base33);

}



void setSystem_clock_reference_base(UINT64 _ui64SCR)

{

system_clock_reference_base1 = (_ui64SCR >> 30) & 0x07;

system_clock_reference_base21 = (_ui64SCR >> 28) & 0x03;

system_clock_reference_base22 = (_ui64SCR >> 20) & 0xFF;

system_clock_reference_base23 = (_ui64SCR >> 15) & 0x1F;

system_clock_reference_base31 = (_ui64SCR >> 13) & 0x03;

system_clock_reference_base32 = (_ui64SCR >> 5) & 0xFF;

system_clock_reference_base33 = _ui64SCR & 0x1F;

}



void getProgram_mux_rate(unsigned int &_uiMux_rate)

{

_uiMux_rate = (program_mux_rate1 << 14) | (program_mux_rate2 << 6) | program_mux_rate3;

}



void setProgram_mux_rate(unsigned int _uiMux_rate)








{

program_mux_rate1 = (_uiMux_rate >> 14) & 0xFF;

program_mux_rate2 = (_uiMux_rate >> 6) & 0xFF;

program_mux_rate3 = _uiMux_rate & 0x3F;

}

};





对于DVD而言,一般开始的Pack里面还有一个System Header,如图517所示。



图517PS的系统头结构


PES层由编码的声频或视频数据(ES)加上PES头组成,PES头主要通过PTS和DTS来提供音视频同步的信息。PES头之后紧跟着的是编码的声频或视频数据,对于DVD而言,一个Program Pack的大小为0x800,所以一帧MPEG2视频被分在多个PES包里,不够一个包的就写在下一帧的第1个Pack里,或在PES Header后面填充0xFF(PES_header_data_length要加上填充的字节数)。

5.6.2PS码流的解析流程

解析PS码流的流程,需要经历几个步骤,包括读取源文件、查找起始码、解析PS头、解析PS系统头、解析PSM、解析PES包等,如图518所示。



图518PS码流解析流程


(1) 开辟缓存空间,从源码文件中读取指定大小的内容,如图519所示。



图519PS码流解析之读取源文件


(2) 可能会存在冗余数据,需要逐字节往后查找,直到下一个起始码,如图520所示。



图520PS码流解析之处理冗余数据


(3) 解析PS Header,如图521所示。

(4) 解析PS System Header(可有可无),如图522所示。

(5) 解析PS System Map(只有I帧才需要),如图523所示。

(6) 解析PES包,如图524所示。

(7) 保存裸数据,如图525所示。



图521PS码流解析之PS头




图522PS码流解析之PS系统头




图523PS码流解析之PSM




图524PS码流解析之PES






图525PS码流解析之裸数据



(8) 保存时间戳,如图526所示。



图526PS码流解析之时间戳


(9) 保存帧类型,如图527所示。



图527PS码流解析之帧类型


PSM提供了对PS流中的原始流和它们之间的相互关系的描述信息; PSM作为一个PES分组出现,当stream_id==0xBC时,说明此PES包是一个PSM; PSM紧跟在系统头部后面; PSM作为PS包的Payload存在。

解析PS包,要先找到PS包的起始码0x000001BA位串,然后解析出系统头部字段,之后进入PS包的负载,判断是否有PSM,根据PSM确定Payload的PES包中所负载的ES流类型,然后根据ES流类型和ID从PES包中解析出具体的ES流; 解包过程则相反,若要从PS流中找出帧类型,必须将PS包解析成ES并组成完整的帧,然后在帧数据的开始根据NAL头进行帧的类型判断。 


PSM只有在关键帧打包时,才会存在; IDR包含了SPS、PPS和I帧; 每个IDR NALU前一般会包含SPS、PPS等NALU,因此将SPS、PPS、IDR的NALU 封装为一个PS包,包括PS头、PS System Header、PSM、PES,所以一个IDR NALU PS 包由外到内的顺序是: PS Header| PS System Header | PSM| PES。

对于其他非关键帧的PS包,直接加上PS头和PES头就可以了。顺序为PS Header | PES Header | H.264 Raw Data。以上是对只有视频Video的情况,如果要把声频Audio也打包进PS封装,只需将数据加上PES Header放到视频PES后。

5.7TS格式与m3u8切片

HLS是由苹果公司提出的基于HTTP的流媒体播出协议。由于它只使用HTTP,因此具有开放、简洁、能穿越防火墙、与CDN系统对接方便等特点。在终端类型上,所有iOS终端(包括Phone、iPod Touch、iPad、Mac)都支持HLS流媒体播放,最新发布的Android系统也开始加入对HLS的支持。

读者可能接触过DVD,DVD节目中的MPEG2格式,确切地说是MPEG2PS,MPEG2PS主要应用于存储具有固定时长的节目,如DVD电影,而MPEGTS则主要应用于实时传送的节目,例如实时广播的电视节目。这两种格式有一些区别,将DVD上的VoB文件的前面截掉(或者干脆使数据损坏),就会导致整个文件无法解码,而电视节目是可以在任何时候都能打开电视机进行解码(收看)的,所以MPEG2TS格式的特点是要求从视频流的任一片段开始都是可以独立解码的。

大多数视频网站采用渐进式下载,将视频下载到播放设备上。视频一般采用流式传输,不只是下载1个文件,而是下载很多小包(本书指的是.ts传输流切片文件)。服务器使用HTTP响应头AcceptRange标识自身支持范围请求(Partial Request)。字段的具体值用于定义范围请求的单位。当浏览器发现AcceptRange头时,可以尝试继续中断了的下载,而不是重新开始。AcceptRange的值可以为Bytes或None。Bytes范围请求的单位是Bytes(字节)。None表示不支持任何范围请求单位,由于其等同于没有返回此头部,因此很少使用。不过一些浏览器,例如IE 9,会依据该头部去禁用或者移除下载管理器的暂停按钮。

HLS的工作原理是把整个流分成一个个小的基于HTTP的文件来下载,每次只下载一些。当媒体流正在播放时,客户端可以选择从许多不同的备用源中以不同的速率下载同样的资源,允许流媒体会话适应不同的数据速率。在开始一个流媒体会话时,客户端会下载一个包含元数据的extended m3u(m3u8)列表文件,用于寻找可用的媒体流。HLS只请求基本的HTTP报文,与实时传输协议(RTP)不同,HLS可以穿过任何允许HTTP数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络CDN来传输媒体流。HLS流由众多TS小文件和m3u8索引文件组成,m3u8切片工具实现TS文件的切片和索引文件生成。

m3u8流切分工具需要支持的功能主要包括将声频或视频内容流化到iPhone、iPod Touch、iPad或者Apple TV上; 不需要任何特殊的媒体服务器支持便可以将现场直播信号通过HLS输出到互联网上,实现具有加密和授权需求的VoD业务。

请求m3u8播放列表的方法,通过m3u8的URI进行请求,则该文件必须以.m3u8或.m3u结尾; 通过HTTP进行请求,则请求头ContentType必须设置为application/vnd.apple.mpegurl 或者 audio/mpegurl。

可以使用ffmpeg命令行实现视频文件的切片,命令如下: 



ffmpeg -i XXX.mp4 -c:v libx264 -c:a copy -f hls XXX.m3u8





其中,XXX.mp4为本地视频文件,XXX.m3u8为最终生成的播放索引列表,与此同时还有多个TS文件。

如果想开发直播流切片工具和文件切片工具,则应分别满足HLS直播流和点播流的切片需求,具体描述如下:

(1) 直播流切片工具(Stream Segmenter),从网络上读取直播数据,通过在线实时切分,将符合HLS规格的直播流输出到互联网上。它一般通过UDP接收由编码器或其他系统输出的TS流,将TS流实时地切分成具有固定播出长度的小文件。这些从连续直播流中分离出来的小文件在播出结构上具有严密的连续性,可以被无缝地重新封装以满足HLS播出要求。该工具必须同时生成m3u8索引文件,直播流m3u8索引文件随着新片段文件的不断生成进行不断更新,以符合HLS直播规范的要求。切分出的小文件以TS文件格式存放,索引文件以具有.m3u8后缀的m3u8文件格式存放。

(2) 文件切片工具,实现将视频或声频文件切分成符合HLS规范要求的片段文件,这些文件能够通过HLS协议对外提供点播服务。文件切片工具与流切片工具的工作内容相似,区别是一个用于切分直播流,另一个用于切分多媒体文件。文件切分工具需要支持MP4、TS、MOV、FLV等多种文件格式。如果要切分的文件满足HLS对文件格式的要求(H.264 + AAC 或者 H.264+MP3),则不需要进行重新编码,可直接进行文件切片。否则需要对声频或视频内容进行重新编码,以满足HLS播出要求。文件切分工具具有“重新编码”和“不重新编码”的工作模式,使用时可以根据需要进行选择。

注意: 有兴趣的读者可以编写代码实现以上功能,或者从网络上搜索一些开源软件。