欢迎诸位来阅读在下的博文~
在这里,在下会不定期发表一些浅薄的知识和经验,望诸位能与在下多多交流,共同努力!
文章目录
前期博客
参考书籍
《FFmpeg入门详解——流媒体直播原理及应用》——梅会东
一、HLS协议简介
HLS与RTMP都是流媒体协议,RTMP由Adobe开发,广泛应用于低延时直播,也是编码器和服务器对接的实际标准协议,在PC(Flash)上有最佳的观感体验;HLS由苹果公司开发,可以支持Live(直播),也可以支持VoD(点播)。HLS是苹果平台的标准流媒体协议,和RTMP在PC上一样支持得非常完善。
HLS全称HTTP Live Streaming,是一种基于HTTP的流媒体网络传输协议。它的基本工作原理是把整个流分成一个一个小的基于HTTP的文件来下载,每次只下载一些。
二、RTMP vs HLS 对比
特性 | RTMP | HLS |
---|---|---|
延迟 | 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值 |
---|---|
PAT | 0x0000 |
CAT | 0x0001 |
TSDT | 0x0002 |
EIT/ST | 0x0012 |
RST/ST | 0x0013 |
TDT/TOT/ST | 0x0014 |
在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
unsigned ‘0’ : 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;
}
至此,结束~
望诸位不忘三连支持一下~