H.264 打包 MPEG-TS 流
H.264 打包 MPEG-TS 流
– 作者 Amour Wang
1.简要说明
本文主要介绍了H264打包成MPEG-TS 流的关键部分,及中间碰到的一些问题。至于H264 和TS 流的相关标准这边不再做详细介绍。
2.H264 打包TS 流过程
TS 流组成说明(这边针对本文例子中的情况,其他情况参照TS 标准):
TS 流以包为单位,每个包的大小为188,主要包含了几种不同类型的包
1. PAT 表:这个表定义了当前TS流中所有的节目,其PID为0x0000,它是PSI的根节点,要查寻找节目必须从PAT表开始查找。主要包含了TS 流 ID,节目频道号,PMT 的ID
2. PMT 表 : 节目映射表,包含频道中所有的PID信息,根据PID 就可以从包中过滤出对应的视频或音频数据
3. PES:简单说PES 就是对H264 的描述的一些信息的标准
打包过程:
1. TS header: 所有包都是以4 个字节的包头开始的,针对比较难理解的部分说明一下
负载单元开始:这个文档上写的很官方,其实就是当一段数据超过一个包的大小必须分成几个包,第一个包的这个标志为1,其他包为0.
PID: 用来表示这个包的类型,这个好理解,但是是重要的标志位,所以强调一下
自适应区域控制:目前只用到0x3 和 0x1,分别表示有和没有自适应区域
连续计数器:这个地方要注意一下,各个类型的包的计数是各自计数的
2. PAT构造,这个部分大部分数据都是比较固定的,自己定义一下PMT 的ID ,其他基本就按照标准进行填写就行,这边PAT 比较简单所以一个包负载就够了.
int ts_pat_header(char *buf) {
BITS_BUFFER_S bits;
if (!buf) {
return 0;
}
bits_initwrite(&bits, 32, (unsigned char *) buf);
bits_write(&bits, 8, 0x00); // table id, 固定为0x00
bits_write(&bits, 1, 1); // section syntax indicator, 固定为1
bits_write(&bits, 1, 0); // zero, 0
bits_write(&bits, 2, 0x03); // reserved1, 固定为0x03
bits_write(&bits, 12, 0x0D); // section length, 表示这个字节后面有用的字节数, 包括CRC32
bits_write(&bits, 16, 0x0001); // transport stream id, 用来区别其他的TS流
bits_write(&bits, 2, 0x03); // reserved2, 固定为0x03
bits_write(&bits, 5, 0x00); // version number, 范围0-31
bits_write(&bits, 1, 1); // current next indicator, 0 下一个表有效, 1当前传送的PAT表可以使用
bits_write(&bits, 8, 0x00); // section number, PAT可能分为多段传输,第一段为00
bits_write(&bits, 8, 0x00); // last section number
bits_write(&bits, 16, 0x0001); // program number
bits_write(&bits, 3, 0x07); // reserved3和pmt_pid是一组,共有几个频道由program number指示
bits_write(&bits, 13, TS_PID_PMT); // pmt of pid in ts head 这边可以自己定义PMT 的ID
char * crc = CRC32(buf); //计算CRC
bits_write(&bits, 8, crc[0]);
bits_write(&bits, 8, crc[1]);
bits_write(&bits, 8, crc[2]);
bits_write(&bits, 8, crc[3]);
bits_align(&bits);
return bits.i_data;
}
3.PMT 构造:这部分也是没有多大问题,大部分按照标准填写都没有问题,自己定义一下 video 的PID 即可.
这边还有一个问题,并不是每一帧H264 开头都必须有PAT 和PMT ,因为每个H264 的帧数据差别非常多,可能有的才1包数据就够了,有的需要几百包,而PAT 和PMT 是隔段时间就出现在ts 流中的.
所以我这边参照FFMPEG 中的方式,每隔40个TS 包插入一个PAT 和PMT.
int ts_pmt_header(char *buf) {
BITS_BUFFER_S bits;
if (!buf) {
return 0;
}
bits_initwrite(&bits, 32, (unsigned char *) buf);
bits_write(&bits, 8, 0x02); // table id, 固定为0x02
bits_write(&bits, 1, 1); // section syntax indicator, 固定为1
bits_write(&bits, 1, 0); // zero, 0
bits_write(&bits, 2, 0x03); // reserved1, 固定为0x03
bits_write(&bits, 12, 0x12); // section length, 表示这个字节后面有用的字节数, 包括CRC32
bits_write(&bits, 16, 0x0001); // program number, 表示当前的PMT关联到的频道号码
bits_write(&bits, 2, 0x03); // reserved2, 固定为0x03
bits_write(&bits, 5, 0x00); // version number, 范围0-31
bits_write(&bits, 1, 1); // current next indicator, 0 下一个表有效, 1当前传送的PAT表可以使用
bits_write(&bits, 8, 0x00); // section number, PAT可能分为多段传输,第一段为00
bits_write(&bits, 8, 0x00); // last section number
bits_write(&bits, 3, 0x07); // reserved3, 固定为0x07
bits_write(&bits, 13,
TS_PID_VIDEO); // pcr of pid in ts head, 如果对于私有数据流的节目定义与PCR无关,这个域的值将为0x1FFF
bits_write(&bits, 4, 0x0F); // reserved4, 固定为0x0F
bits_write(&bits, 12, 0x00); // program info length, 前两位bit为00
bits_write(&bits, 8, TS_PMT_STREAMTYPE_H264_VIDEO); // stream type, 标志是Video还是Audio还是其他数据
bits_write(&bits, 3, 0x07); // reserved, 固定为0x07
bits_write(&bits, 13, TS_PID_VIDEO); // elementary of pid in ts head,这边可以定义
bits_write(&bits, 4, 0x0F); // reserved, 固定为0x0F
bits_write(&bits, 12, 0x00); // elementary stream info length, 前两位bit为00
char * crc = CRC32(buf); //计算CRC
bits_write(&bits, 8, crc[0]);
bits_write(&bits, 8, crc[1]);
bits_write(&bits, 8, crc[2]);
bits_write(&bits, 8, crc[3]);
bits_align(&bits);
return bits.i_data;
}
4.PES 构造:这部分是最麻烦的,也是坑最多的地方.
<1> PES 数据并不是自己一个包,后面不要填FF,而是PES所在的包,后面直接跟H264数据
<2> 当H264 数据不足一个包,或者末端数据不足一个包的时候,不是先填数据再补0xFF,而是先补0xFF到剩下刚好足够填剩下数据的空位.
<3>PCR /PTS 换算的问题: 由于PCR和PTS 有33个位组成,如果直接使用int 类型只有4个字节不够,而long 有8个字节, 需要注意大小端的问题, 在arm 下测试是小端模式,所以需要进行转换
<4>PTS 计算方法:
static uint32_t video_frame_rate = 30;
static uint32_t video_pts_increment = 90000 / video_frame_rate; //用一秒钟除以帧率,得到每一帧应该耗时是多少,单位是 timescale单位
static uint64_t video_pts = 0;
<5>
int mk_pes_packet(char *buf, int bVideo, int length, int bDtsEn, unsigned int pts,
unsigned int dts) {
PES_HEAD_S pesHead; //pes 头
PES_OPTION_S pesOption; //标志位
PES_PTS_S pesPts; //pts
PES_PTS_S pesDts; //dts
if (!buf) {
return 0;
}
memset(&pesHead, 0, sizeof(pesHead));
memset(&pesOption, 0, sizeof(pesOption));
memset(&pesPts, 0, sizeof(pesPts));
memset(&pesDts, 0, sizeof(pesDts));
pesHead.startcode = htonl(0x000001) >> 8; //大小端问题
pesHead.stream_id = bVideo ? 0xE0 : 0xC0; //如果是video 则为E0
//if (PES_MAX_SIZE < length) {
pesHead.pack_len = 0; //这边本来应该需要填写PES 的长度,超过的话可以填0,但是参照FFMPEG 是直接填的0,具体测试播放器也都可以兼容
/* } else {
pesHead.pack_len = htons(
length + sizeof(pesOption) + sizeof(pesPts) + (bDtsEn ? sizeof(pesDts) : 0));
}*/
pesOption.fixed = 0x02;
pesOption.pts_dts = bDtsEn ? 0x03 : 0x02;
pesOption.head_len = sizeof(pesPts) + (bDtsEn ? sizeof(pesDts) : 0);
//PTS 和 DTS 的填充,注意大小端的问题
pesPts.fixed2 = pesPts.fixed3 = pesPts.fixed4 = 0x01;
pesPts.fixed1 = bDtsEn ? 0x03 : 0x02;
pesPts.ts1 = (pts >> 30) & 0x07;
pesPts.ts2 = (pts >> 22) & 0xFF;
pesPts.ts3 = (pts >> 15) & 0x7F;
pesPts.ts4 = (pts >> 7) & 0xFF;
pesPts.ts5 = pts & 0x7F;
pesDts.fixed1 = pesDts.fixed2 = pesDts.fixed3 = pesDts.fixed4 = 0x01;
pesDts.ts1 = (dts >> 30) & 0x07;
pesDts.ts2 = (dts >> 22) & 0xFF;
pesDts.ts3 = (dts >> 15) & 0x7F;
pesDts.ts4 = (dts >> 7) & 0xFF;
pesDts.ts5 = dts & 0x7F;
char *head = buf;
memcpy(head, &pesHead, sizeof(pesHead));
head += sizeof(pesHead);
memcpy(head, &pesOption, sizeof(pesOption));
head += sizeof(pesOption);
memcpy(head, &pesPts, sizeof(pesPts));
head += sizeof(pesPts);
if (bDtsEn) {
memcpy(head, &pesDts, sizeof(pesDts));
head += sizeof(pesPts);
}
return (head - buf);
}
参考文献:
http://www.cnblogs.com/lifan3a/articles/6214419.html
http://blog.csdn.net/u013354805/article/details/51591229
http://blog.csdn.net/u013354805/article/details/51578457
http://blog.csdn.net/u013354805/article/details/51586086