流媒体之HLS协议(其三)

在这里插入图片描述

欢迎诸位来阅读在下的博文~
在这里,在下会不定期发表一些浅薄的知识和经验,望诸位能与在下多多交流,共同努力!

江山如画,客心如若,欢迎到访,一展风采

前期博客

流媒体与直播的基础理论(其一)
流媒体协议RTSP(其二)

参考书籍

《FFmpeg入门详解——流媒体直播原理及应用》——梅会东

一、HLS协议简介

HLS与RTMP都是流媒体协议,RTMP由Adobe开发,广泛应用于低延时直播,也是编码器和服务器对接的实际标准协议,在PC(Flash)上有最佳的观感体验;HLS由苹果公司开发,可以支持Live(直播),也可以支持VoD(点播)。HLS是苹果平台的标准流媒体协议,和RTMP在PC上一样支持得非常完善。
HLS全称HTTP Live Streaming,是一种基于HTTP的流媒体网络传输协议。它的基本工作原理是把整个流分成一个一个小的基于HTTP的文件来下载,每次只下载一些。

二、RTMP vs HLS 对比

特性RTMPHLS
延迟1-3 秒,低延迟10-30 秒,延迟较高
实时性适合实时互动场景(如直播、游戏等)主要用于点播,直播场景中实时性较差
兼容性Flash 支持逐渐减少,现代浏览器不支持浏览器、移动端、智能电视等设备广泛支持
传输协议基于 TCP,保持长连接基于 HTTP,使用短连接
自适应码率不支持支持自动码率切换(ABR)
扩展性不适合大规模分发支持 CDN,大规模分发良好
复杂性需要保持持续连接,推流方式复杂基于 HTTP,结构简单,易于实现
主要用途实时直播,互动性较强的场景视频点播、大规模直播流媒体

  • 值得注意的是,苹果公司在WWDC2019发布了新的解决方案来优化延迟性能,使得延迟从8s减低到1~2s,可以预知,HLS是充满活力与可能性的。
  • 不过,RTMP在推流方面依旧十分好用。

三、HLS网络框架结构

在这里插入图片描述

  • 需要留意的是:
    (1)服务器将媒体文件转换为m3u8文件及TS分片;对于直播源,服务器需要实时动态更新。
    (2)客户端请求m3u8文件,根据索引获取TS分片;点播与直播服务器不同的地方是,直播的m3u8文件动态更新,点播只需要请求依次m3u8文件。

四、HLS的索引文件嵌套

在这里插入图片描述

  • 注意:媒体流封装的分片格式只支持MPEG-2传输流(TS)、WebVTT文件 或 Packed Audio文件。

五、HLS协议详细讲解

HLS协议规定了四部分的内容:分别是 视频的封装格式、视频的编码格式、声频的编码格式、m3u8文件
如下:
在这里插入图片描述

m3u8简介

HLS协议中的m3u8,是一个包含TS列表的文本文件,目的是告诉客户端或浏览器可以播放这些TS文件。m3u8的一些主要标签解释如下:
(1)# EXTM3U:每个m3u8文件的第一行必须是这个tag,提供标识作用。
(2)# EXT-X-VERSION:用以标识协议版本。此标签m3u8文本中只能使用一次。例如版本3.
(3)# EXT-X-TARGETDURATION:所有切片的最大时长,如果不设置这个参数,则有些苹果设备就会无法播放。
(4)# EXT-X-MEDIA-SEQUENCE:切片的开始序号。每个切片都有唯一的序号,相邻序号+1。这个编号会持续增长,保证流的连续性。
(5)# EXTINF:TS切片的实际时长。
(6)# EXT-X-PLAYLIST-TYPE:类型,VoD表示点播,Live表示直播。
(7)# EXT-X-ENDLIST:文件结束符号,表示不再向播放列表文件添加媒体文件。

一个典型的例子:

//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.con/low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000
http://example.con/mid.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000
http://example.con/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		//结束标志
  • 注意:BANDWIDTH用于指定视频流的比特率。
  • #EXT-X-STREAM-INF的下一行是二级index文件的路径,可以用相对、绝对路径。二级文件负责给出TS文件的下载地址。

在这里插入图片描述

TS文件

TS文件的结构如下:
在这里插入图片描述

在这里插入图片描述

有关ES、PES、PS、TS的详细解析

  • 包和流的关系是,流含有多个包,比如ES流含有多个ES包。
  • PES包是ES流/包经过PES打包器处理后形成的,包含有分组、打包、加入包头信息等操作。
  • 一个PES包含有一个或多个ES包,一个ES包含有多个存取单元(AU)。
  • 一个AU是编码了的1幅视频图像帧或一个声频帧。
  • PS 是节目流,由PS包组成,和TS属于同一层次但不同类别的流。一个PS包由若干个PES包组成。PS包的包头中包含了同步信息与时钟恢复信息。
  • 一个PS包最多包含具有同一时钟基准的16个视频PES包和32和声频的PES包。PS包不定长。
  • TS 是传输流,由定长的TS包组成(188B)。由于定长,所以某个包即使损坏后,也不影响其余的TS包的正常解码,PS则不然。
  • PS主要用于误码率相对较低的演播室和数字存储(DVD)中。
  • 传输流主要用于传输中,不怕数据包损坏。

PS/TS编码的基本流程

在这里插入图片描述

六、TS码流详细讲解

TS流将具有共同时间基准或独立时间基准的一个或多个PES组合(复合)而成单一数据流。
在这里插入图片描述

  • 注意:TS是位流格式,TS流是可以按位读取的。

TS包格式

TS包是基于Packet的位流格式,每个包是188B(或204B,后面有16B的CRC校验数据)。如图:
在这里插入图片描述

TS流包头字段结构:

字段名称字节位置描述
同步字节(Sync Byte)第1个字节固定为0x47,用于标识TS包的开始。
传输错误指示(TEI)第2个字节,第1位当设置为1时,表示当前包中至少有一个不可纠正的错误位。
有效负载单元起始指示(PUSI)第2个字节,第2位当设置为1时,表示当前包的有效负载部分以一个PES包或PSI/SI表的第一个字节开始。
传输优先级(Priority)第2个字节,第3位用于传输机制,但在解码时通常不被使用。
包标识符(PID)占13位用于标识TS包属于哪个特定的流或服务。
传输加扰控制(Scrambling)第4个字节,第1位和第2位用于指示TS包是否被加扰以及加扰的方式。
自适应字段控制(Adaptation)第4个字节,第3位和第4位用于指示TS包是否包含自适应字段,以及自适应字段的位置和长度。
连续计数器(Counter)第4个字节,第5位到第8位用于检测包的丢失或重复,范围从0到15,每发送一个TS包,计数器加1。
  • 一些TS表的PID值是固定的,如下:
PID值
PAT0x0000
CAT0x0001
TSDT0x0002
EIT/ST0x0012
RST/ST0x0013
TDT/TOT/ST0x0014

在TS流中,TS包有可能是音视频数据,也有可能是表格(PAT/PMT/…)。举例说明,TS流的包顺序如下:

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

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

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

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

TS包的长度是188B,分为TS Header和TS Body。其中TS Header里买你会有个PID字段,标识当前DATA的类型。其实,DATA其实是PES包,而PES包是对ES流的封装,等价“ PES Header + ES ”。这里的ES是原始流,是指经过压缩后的H.264、AAC等格式的音视频数据。

帧数据、PES包、TS包的对应关系

一帧数据封装成一个PES包,如果 PES包 <= 184字节,则一个TS包就可以放下,如果还有空余的地方,就填充无实际用处的字节,使其满足184字节。如果 PES包 > 184字节,则需要多个TS包封装,这里需要注意的是,PES Header只需要首个TS包封装即可。伪代码如下:

第一个TS包 : TS Header + PES头 + 部分ES
第二个TS包 : TS Header + 部分ES
第三个TS包 : TS Header + 部分ES
....
第n个TS包 : TS Header + 填充字节 + 部分ES

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的字段,代码如下:

typedef struct TS_PAT_Program  
{  
    unsigned program_number :	16;	//节目号  
    unsigned program_map_PID:	13;	// 节目映射表(PMT)的PID,每个节目对应一个   
}TS_PAT_Program;
program_association_section()
{  
    unsigned table_id 		: 8; 	//固定为0x00 ,标志是该表是PAT表  
    unsigned section_syntax_indicator    : 1; //段语法标志位,固定为1  
    unsigned0: 1; //0  
    unsigned reserved_1	: 2; // 保留位  
    unsigned section_length	: 12; //段长度字节  

    
    unsigned transport_stream_id: 16;   //该传输流的ID,区别于一个网络中其它多路复用的流
    unsigned reserved_2	: 2;// 保留位
    unsigned version_number	: 5; //范围0-31,表示PAT的版本号  
	unsigned current_next_indicator: 1; //发送的当前PAT是否有效,还是下一个有效

//PAT可能分为多段传输,第一段为00,以后每个分段加1,最多可能有256个分段
// 给出section号,在sub_table中,第一个section其section_number为"0x00"
//每增加一个section,section_number加1
    unsigned section_number	: 8; //分段的号码。
    							//PAT可能分多段传输,第一段为0,以后每个分段加1,最多可能有256个分段。
    
    //最后一个分段的号码 ,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; //校验码     
}
  • 这两个结构体的关系是,program_association_section 结构体包含了多个 TS_PAT_Program 结构体,每个 TS_PAT_Program 结构体代表PAT中的一个节目条目。在解析PAT时,首先解析 program_association_section 结构体以获取PAT的基本信息,然后根据其中的循环部分,解析每个 TS_PAT_Program 结构体以获取每个节目的具体信息。

PAT的解析函数代码如下:

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流结构,代码如下:

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;
TS_program_map_section() {
    table_id                    :8; //固定为0x02 标识PMT表
    section_syntax_indicator :1; //固定为0x01
    '0'                          :1; //
    reserved                    :2; // 保留位
    section_length             :12 //该字段的头两bit必为‘00’,剩余10bit指定该分段的字节数,紧随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 
}

两个结构体的关系是,TS_program_map_section 结构体包含了多个 TS_PMT_Stream 结构体。在解析PMT时,首先解析 TS_program_map_section 结构体以获取PMT的基本信息,然后根据循环部分,解析每个 TS_PMT_Stream 结构体以获取关于每个流的具体信息。每个 TS_PMT_Stream 结构体代表PMT中的一个流条目,提供了流类型、PID和描述符等信息。

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

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;  
}

至此,结束~
在这里插入图片描述
望诸位不忘三连支持一下~

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值