打包TS流

打包TS流,第1张

做这个东西很久了,从去年十二月份开始的,快5个月了。。。期间因为工作一直断断续续,直到最近才有了些进展,也就到此为止吧。

先说下我做的是什么吧,总的来说,就是把H264视频流与AAC音频流封装成TS格式。要完成这么个功能首先要解析H264和AAC,获得视频帧和音频帧以及一些关键信息,比如帧率、采样率什么的为以后的打包做工作。分析到每一帧数据后,再加上PES头,封装成PES数据,在这个过程要注意打PTS、DTS,以及PES类型的设定,一般来说,视频PTS与DTS都要打,而音频只需要打PTS。最后一步就是将PES数据封装成TS了,这一步的坑比较多。做完这些,最难个人感觉就是音视频同步了,至今还比较疑惑,视频播出来总是有瑕疵。

一、解析h264

H264是以0x00 00 01或0x00 00 00 01作为分割NALU单元分割符的,整个H264也是以NALU作为基本单元的,NALU的结构如图一所示:

NALU包含头和负载RBSP,头部的句法结构如图二所示:

这一个字节便包含了整个头部的信息,也就是去除分割符后的头一个字节。 nal_unit_type 指明的NALU的类型,不多说了,贴图吧。。。

NALU的负载RBSP在读取有效数据之前首先要做去除竞争码的 *** 作,遇到0x00 00 03将字节0x03去掉即可。

·0x00 00 00  ----->0x00 00 03 00

·0x00 00 01  ----->0x00 00 03 01

·0x00 00 02  ----->0x00 00 03 02

·0x00 00 03  ----->0x00 00 03 03

负载的类型由前文 nal_unit_type 指定,当值大于等于1,小于等于5的时候指明该负载是片数据( slice ),去除竞争码后的负载采用的是哥伦布编码。在看片数据句法结构的时候可以留意到每个字段后面都有一些符号说明,如 ue(v) 、 se(v) ,给张图看的比较清楚

我贴的是片头句法的一部分,在合成TS的时候只要判断片类型就可以了,也就是说,只要将图4的 slice_type 分析出即可,要想完整的解出片还是有点难度的,不过判断片的类型并不难。

回到前面的哥伦布编码,这是一种变长编码,括号里面带 v 的均是指编码的长度不固定。

或根据哥伦布编码动态确定,或由前文指定,符号 ue(v) 与 se(v) 说明解码的长度可根据哥伦布编码的规则动态指定,而 u(v) 这里的解码长度得由上文指定,有几个字段都是如此,如何由上文确定长度的资料不太好找,只找到一个英文的网站,它说明了几个字段(http://m.blog.csdn.net/article/details?id=40302581)

解码哥伦布编码的字段可以选用网上的库,看起来还行,但因为是在研究学习这个东西,

我选择了自己实现这个算法,也不是特别复杂。首先说下无符号的哥伦布编码,按网上的定义,哥伦布的编码方式有0阶、1阶、2阶...每种好像都不一样,也没有都去研究,因为H264用到的只是0阶的哥伦布编码,弄懂这个就够用了。

1.1 无符号0阶指数哥伦布编码 (u e)

按照算法的描述,应先将数据以二进制方式排列,接着连续读取M个0比特位,碰到1比特位结束读取,这时解码的数据长度就已经确定了,为2*M+1个比特,包含前面已经读取的M个0比特。再将最后的M位比特转化成十进制数值W,即可获得解码结果为W-1,到此为止,一次完整的无符号解码就完成了。

·无符号哥伦布编码测试例子:

                                                  (解码后)

0x4C  0x85  0x31  0xC4  0x09 =====>1  2  3  4  5  6  7  8

1.2 有符号0阶指数哥伦布编码 ( se)

有符号的哥伦布编码建立在无符号的基础上,按照无符号的解码步骤得到十进制数n后,再实行以下的动作即可。

·获取n被2除后的商w(注意这个商是去除了小数的整数部分)

·n不能被2整除,w加1为解码结果

·n可以被2整除,w的相反数即为解码结果

                                                (解码后)

0x4C  0x85  0x31  0xC4  0x09 =====>1  -1  2  -2  3  -3  4  -4

1.3 帧类型

前面在解析片数据的时候有提到过片的类型,实际上这里指明的是片所装载的视频帧的类型,按定义分为I,P,B,SP,SI,图5给出 slice_type 与帧类型的对应关系

我在解码的时候只关心了I,P,B帧,另外的两种帧的意义感觉和I,P帧类似,所以没做过多的研究。I,P,B帧的差别在于编码方式的差异,I帧数据不依赖其他的帧数据,可独立解码,而P帧使用前向预测编码,它的解码依靠前面解码的I帧和P帧,B帧用了双向预测编码,解码B帧图像,需要同时参考前后的帧数据。它们压缩数据的能力也依次增强,一般来说,B帧比较多的网络视频流同等比特量所传输的视频质量会更高,但是相应的,播放器的解码压力也更大。由于P、B帧的解码需要参考其他帧,所以在解码的时候可能会产生误码扩散的情况,但由于播放器在遇到I帧的时候会丢弃之前解码的视频数据,再以当前的I帧作为以后P、B帧的参考帧,因此这一举措及时的阻止了错误的扩散,说到这里,我得额外提下HLS协议,这个协议在制作切片的时候总是以I帧开头,不仅是为了在播放视频的时候立马刷出图像,我想更多的是因为H264这种编码格式的限制而不得不以I帧开头,因为只有I帧是可以独立解码的,如果开头的是其他需要参考的帧,他们的解码毫无意义。

二、 打包 PES

PES是对裸流的一层封装,H264和AAC就是两种裸流,他们就像还没装订的书,随意的被堆放在角落里,PES的封装就是将散乱的纸打上页码装成书的过程。这里的页码指的就是PTS与 DTS ,显示时间戳和解码时间戳,这个时间可以指示播放器何时显示一帧图像或者音频,一般来说视频帧PTS、DTS都需要打,尤其是在有B帧的情况下,因为B帧需要参考前后两帧,所以它要等到前后帧都被解码出后才能解此B帧,因此B帧的显示时间和解码时间是不一致的,而音频帧没有这种情况,解一帧放一帧即可,它是不需要DTS的。PES头的句法结构如图6

PES的整结构没有完整的贴上来,只贴到PTS、DTS这部分句法,其实在打包TS的时候,这部分已经够用了,其他的可以不去关心。接下来说明一下几个重要的字段。

·packet_start_code_prefix

这3个字节是PES的起始码,将PES包一一隔开,起始是固定的0x00 0x00 0x01

· stream_id

指明PES负载的类型,视频为0xE0,音频为0xC0

· PES_packe_length

说明在此字段最后一个字节之后PES分组的字节数。‘0’值表明PES分组的长度既没有说明也没有限制,这种情况只有在PES分组的有效负载是传送流分组中的视频原始流时才允许。

· PTS_DTS_flag

2位标志,若为‘10’,则PES分组首部有PTS字段若为‘11’,则PES分组首部有PTS和DTS字段若为’00’则PTS和DTS都不在PES分组首部出现‘01’值被禁止。

· PES_header_data_length

说明在此字段最后一个字节还剩余的PES头部分组数据量,一般可能指的是PTS与DTS部分编码的长度。

PES分组中比较重要的字段就上面这些的,对于PTS与DTS的编解码,按照PTS_DTS_flag以及相应的句法指示去做即可。

三、 合成TS

TS流每个包的大小都介于188-204字节之间,一般就是188字节,每个PES包基本都大于188字节,所以在把PES封装成TS时,需要将PES分割成多个TS包,每个包都有唯一的ID标识它的类型,音视频PES打包成TS包的ID由TS中的PMT表指定,PMT表被单独打成一个TS包,它也有ID标识,这个标识由PAT表指定,与PMT类似,PAT也被打成独立的包,它的ID是固定的0,所以,不管是解码还是编码TS流,都应从PAT表入手。

3.1 TS 包格式

TS包由头与负载组成,头部占四个字节,如果还有自适应区,那么自适应区的长度由第五个字节指定。这里先简要的分析下TS头的前四个字节。

· sync_byte

此处是同步字节,固定为0x47

·transport_error_indicator

这个比特位我没有用到过,一直都是0

· payload_unit_start_indicator

此比特位为1,标志着PES包负载的开始,不仅如此,如果是PAT、PMT包,这里也被置为1

· transport_priority

我暂没用到,默认是0

· PID

每个包种类的标识

· transport_scrambling_control

我暂未使用

· adaptation_field_control

此字段标识是否有自适应区

00:是保留值。

01:负载中只有有效载荷。

10:负载中只有自适应字段。

11:先有自适应字段,再有有效载荷。

如果含有自适应区,那么头四个字节后就是自适应区的码流结构,最后才是TS包的负载部分

· continuity_counter

这个字段的值从00 - 15反复连续变化

3.2 自适应区格式

· adaptation_field_length

这个字节指明了自适应区的长度,不包含此字节

· PCR_flag

如果此位被置为1,按照句法格式,接下来存在PCR值,这个值按照特定的公式转化成了 program_reference_base 和 program_clock_reference_extensio n 两个字段的值

PCR是音视频同步的关键参数,它决定播放器什么时候播放一帧音频或视频,它本质上是一个类似于时间戳的东西,它可以用PES包的PTS或DTS来赋值,假设这个值是 timestamp ,那么就有

program_clock_reference_base =  timestamp       (mod)  2^33

program_clock_reference_extension  = (300 * timestamp)  (mod)  300

PCR  = program_clock_reference_base * 300  +program_clock_reference_extension

3.3 PAT 表

PAT表指明了PMT表的PID,每个PMT表又可以指出音视频包的PID值,从这里可以看出一个PMT表就代表一个音视频流,或者说一个节目,当有多种PMT表时,说明在这个TS流中存在多种音视频流,多个节目,从而实现了节目复用的效果,实际上PAT表是可以索引多种PMT表的,但我在打流的时候只上了一种PMT,没关系,东西在精不在多,把一种研究透了,其他的也就是复制粘贴的效果。

图10贴的是PAT表负载的section部分,简单的划分下PAT表四个区域,第一个是头四个字节,第二是自适应区,不过这个自适应区没内容,因此标志自适应长度的那个字节为0,第三个就是这个section部分,在里面指明了PMT表的PID值,第四个是填充区域,严格来说,它和section共同组成了ts包负载的部分,一般填充区域的比特位都置为1。

·table_id

PAT表此值为0

· section_syntax_indicator

此位置为1

· reserved

像这种标志比特位都是置为1的

· section_length

这12位指出了从该字段之后到CRC_32(包含它)为止的字节数

· CRC_32

4字节的循环校验码,它根据section区域动态计算得出

3.4 PMT 表

PMT表的作用是定位音视频包的PID值,以方便播放器找到并解码,接下来说下几个重要的字段。

· table_id

此处的值固定为0x02

· PCR_PID

这个字段指明了PCR值所在TS包的PID

· program_info_length

指定了接下来描述子 decriptor() 的大小,没什么需要描述的就置为0吧

·s tream_type

流类型,如下图所示

H264的视频,流类型为0x1b,AAC音频一般为0x0f,随后的PID值就是对应的TS包标识了

· ES_info_length

说明ES流的附加信息,可有可无,如果没有附加信息,这个字段值为0

·CRC_32

与PAT的校验码类似

3.5 打包 H264 视频帧

一般来说,一个负载为I、P、B片的PES包可看做一帧,也许说的不太准确,一帧视频从概念上说是连环画的完整的一页,而一片呢可能就只是一页的局部画面了,但我打包的时候是根据I、B、P片为单位的,为了好理解也叫一帧吧。

梳理下从H264到TS的过程,首先H264的单元叫NALU,一个I、B、P片就对应一个NALU,NALU加上PES头打上PTS或DTS后得到一个PES包,我们现在要做的就是将这个包含视频数据的PES包分割成一个又一个的TS包,也就是打包一帧视频。

PES包数据在打包TS时,均作为TS包的负载部分。装载PES的第一个TS包,前面说过, payload_unit_start_indicator 字段要为1,这标志着一帧PES数据的开始。一般打包视频帧的时候也会打上PCR的值,不是每帧都要打,不过对于I帧最好还是打上。还有一点要注意的是,在从NALU到PES的时候,在每帧数据前都要加上 0x00 00 00 01 09 xx 这六个字节,然后再打PES头,xx的值好像可以任意选定,如此打出的流苹果之类的播放器才能支持。

最后再说下I帧,在打包I帧的时候比打包其他两帧有更多需要注意的问题,首先,在I帧的前面最好能打上PAT与PMT表,如此一来,在任意一段的TS流中,只要包含了I帧,那么有很大几率可以被解码播放,这也是TS流的一个特点。H264除开包含I、B、P的NALU外,还有两种包含视频信息的NALU,SPS与PPS,分别是序列参数集与图像参数集,他们对于解码I、B、P片有重要作用,没了这两种NALU播放器是解码不出视频的,所以为了保证视频能正常解码,在打包TS流时,这两种NALU被放到了I帧数据的前面,它们是与I帧数据一起被封装成PES包再分割成TS的,这与其他帧一个NALU对应一个PES包有所不同。

在分割视频的一个PES包时,TS头的 continuity_counter 值要保持连续,从0-15反复变化。它接着上一次打包视频帧时最后那个TS包的 coutinuity_counter 值连续变化。

3.6 打包 AAC 音频帧

AAC的格式比较简单,它的单元以0xFFF做为起始码,我们打包的就是一个以0xFFF开头的AAC单元。我的做法是一个AAC单元对应一个PES包,再分割成一个个TS包,这样做可能有些浪费数据量,因为一个AAC单元相比一帧视频要小得多,在打包成TS时,为了保持每个TS包都是188字节,在装载PES数据的最后一个TS包时往往要加上填充字节,填充字节是加在自适应区的,由于一个AAC单元所装载的数据量很小,所以相比于视频帧来说,AAC单元的数量多,那么最后打成的TS包就含有较多无用的填充字节,浪费传输的带宽,我看FFMPEG在打音频帧的时候是10多个AAC单元被同时包成一个PES包的,我不知道它这样打的规则,所以也没有用,而且一个AAC单元对应一个PES包的做法实现起来比较简单并且播放器也能正常解码,所以就采用了这种打法。

打音频帧比视频帧简单多了,除了开头包的 payload_unit_start_indicator 要为1,以及 coutinuity_counter 值连续变化之外,没有别的要求,不过还是要提醒一点的是 coutinuity_counter 值是接着上一次打包音频数据的 coutinuity_counter 来的。

3.7 音视频同步

这个问题我也在探究中,所以我这里不做说明,只贴出几个参考的链接,这些链接中包含了PTS与DTS的打法,但是没有PCR的打法,我目前就是视频帧PES包的PTS直接就作为TS包的PCR值了。

[TS流打包总结]

http://blog.csdn.net/yuan1125/article/details/51540918

[ ffmpeg转码MPEG2-TS的音视频同步机制分析 ]

http://blog.chinaunix.net/uid-26000296-id-3483782.html

四、 后记

这次学习打TS流的过程我很满意,虽然花费的时间比较长,但这也算得上我第一次探究学习的经历吧,以前要学一样东西,资料总是齐备的,看几眼就能知道个大概,因为是在学校,有教材与老师领着,但这仅限于教材上的东西,在进入社会工作后,很多知识与技能都是没有完备的教程的,即使有,可能也不一定能找得到。学习这些资料缺失的知识是一次探究学习的过程,因为资料稀缺,为了了解其中一个知识面,不得不借助搜索工具大量查阅,在翻阅了几十个网站终于得到一句话甚至是一个字的线索时,那时不得不说,我是惊喜和满足的,到最后我能打成一段可以播放的TS流时,我觉得这五个月再长也值了。

1、首先将要合成的ts文件放在同一个文件夹下,ts文件的排序要有一定的规则,最简单的就是:1.ts、2.ts、3.ts等。

2、在电脑桌面点按Win + R打开运行窗口。

3、输入“cmd”,点击“确定”,打开下面窗口。

4、输入命令行:“copy /b  F:\f\*.ts  E:\f\new.ts”回车。

5、然后回到文件夹就会发现“new.ts”文件合成完毕。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/7961856.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-12
下一篇 2023-04-12

发表评论

登录后才能评论

评论列表(0条)

保存