一、TS 格式标准介绍
TS是一种音视频封装格式,全称为MPEG2-TS。其中TS即"Transport Stream"的缩写。
先简要介绍一下什么是MPEG2-TS:
DVD的音视频格式为MPEG2-PS,全称是Program Stream。而TS的全称则是Transport Stream。MPEG2-PS主要应用于存储的具有固定时长的节目,如DVD电影,而MPEG-TS则主要应用于实时传送的节目,比如实时广播的电视节目。这两种格式的主要区别是什么呢?简单地打个比喻说,你将DVD上的VOB文件的前面一截cut掉(或者干脆就是数据损坏),那么就会导致整个文件无法解码了,而电视节目是你任何时候打开电视机都能解码(收看)的。
所以,MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。
我们可以看出,TS格式是主要用于直播的码流结构,具有很好的容错能力。通常TS流的后缀是.ts、.mpg或者.mpeg,多数播放器直接支持这种格式的播放。TS流中不包含快速seek的机制,只能通过协议层实现seek。HLS协议基于TS流实现的。
二、TS 格式详解
2. TS码流整体结构
MPEG-2中规定TS传输包的长度是固定的,长度为188字节。标准规定每个TS包只能包含一个基本流的数据,不存在跨基本流的情况。
所有的TS包都分为包头和净荷部分。TS包中可以填入很多东西(填入的东西都是填入到净荷部分),有:视频、音频、数据(包括PSI、SI以及其它任何形式的数据)。TS只是传输层的协议,所以比较多的面向错误处理的误码纠正。
用c语言描述下MPEG-TS码流,如下:
MPEG_transport_stream() {
do {
transport_packet()
} while (nextbits() == sync_byte)
}
下图是对TS码流的一个分层结构:
TS包头
TS包的包头提供关于传输方面的信息:同步、有无差错、有无加扰、PCR(节目参考时钟)等标志。TS包的包头长度不固定,前32比特(4个字节)固定,后面可能跟有自适应字段(适配域)。32个比特(4个字节)是最小包头。包头的结构固定如下:
各字段含义如下:
- sync_byte(同步字节):固定为0x47;该字节由解码器识别,使包头和有效负载可相互分离。
- transport_error_indicator(传输错误标志):‘1’表示在相关的传输包中至少有一个不可纠正的错误位。当被置1后,在错误被纠正之前不能重置为0。
- payload_unit_start_indicator(负载起始标志):为1时,表示当前TS包的有效载荷中包含PES或者PSI的起始位置;在前4个字节之后会有一个调整字节,其的数值为后面调整字段的长度length。因此有效载荷开始的位置应再偏移1+[length]个字节。
- transport_priority(传输优先级标志):‘1’表明当前TS包的优先级比其他具有相同PID, 但此位没有被置‘1’的TS包高。
- PID:指示存储与分组有效负载中数据的类型。PID值0x0000—0x000F保留。其中0x0000为PAT保留;0x0001为CAT保留;0x1fff为分组保留,即空包。标准中定义的PID分配见下表:
PID值 |
描述 |
0 |
PAT(Program Association Table) |
1 |
CAT(Conditional Access Table) |
3-0xF |
Reserved |
0x10-0x1FFE |
自定义PID,可用于PMT的pid、network的pid或者其他目标 |
0x1FFF |
空包 |
- |
注意PCR的PID可以选择0、1或者0x10-0x1FFE的任意值。 |
- transport_scrambling_control(加扰控制标志):表示TS流分组有效负载的加密模式。空包为‘00’,如果传输包包头中包括调整字段,不应被加密。其他取值含义是用户自定义的。
- adaptation_field_control(适配域控制标志):表示包头是否有调整字段或有效负载。‘00’为ISO/IEC未来使用保留;‘01’仅含有效载荷,无调整字段;‘10’ 无有效载荷,仅含调整字段;‘11’ 调整字段后为有效载荷,调整字段中的前一个字节表示调整字段的长度length,有效载荷开始的位置应再偏移[length]个字节。空包应为‘10’。
- continuity_counter(连续性计数器):随着每一个具有相同PID的TS流分组而增加,当它达到最大值后又回复到0。范围为0~15。
关于adaption_filed字段建议参考标准文档的ch2.4.3.4 Adaptation field一节。
TS包负载部分
TS包中净荷所传输的信息包括两种类型:
- 视频、音频的PES包以及辅助数据;
- 节目专用信息PSI。
当然,TS包也可以是空包。空包用来填充TS流,可能在重新进行多路复用时被插入或删除。
在系统复用时,视频、音频的ES流需进行打包形成视频、音频的 PES流,辅助数据(如图文电视信息)不需要打成PES包。
3. 节目专用信息PSI(Program Specific Information)
在TS流中传输的主要有四类表格,其中包含了解复用和显示节目相关的信息。
节目信息的结构性的描述如下;
- 节目关联表Program Association Table (PAT) 0x0000
- 节目映射表Program Map Tables (PMT)
- 条件接收表Conditional Access Table (CAT) 0x0001
- 网络信息表Network Information Table(NIT) 0x0010
- 传输流描述表Transport Stream Description Table(TSDT) 0x02
其中PMT中定义了与特定节目相关的PID信息,比如音频包pid、视频包pid以及pcr的pid;CAT表格用于流加扰情况下配置参数;NIT是可选的,标准中未详细定义;TSDT也是可选的。
这些表格信息保存到TS中,需要先切分成section,然后放到TS包中。
这里仅详细说明PAT和PMT表的构成,其他表格建议参考标准文档。
PAT表
TS流中会定期出现PAT表。PAT表提供了节目号和对应PMT表格的PID的对应关系。
其具体结构如下图:
第一个字段table_id,8位,用于标识PSI section负载数据的类型。其取值含义如下:
Value |
description |
0x00 |
program_association_section |
0x01 |
conditional_access_section (CA_section) |
0x02 |
TS_program_map_section |
0x03 |
TS_description_section |
0x04 |
ISO_IEC_14496_scene_description_section |
0x05 |
ISO_IEC_14496_object_descriptor_section |
0x06-0x37 |
ITU-T Rec. H.222.0 / ISO/IEC 13818-1 reserved |
0x38-0x3F |
Defined in ISO/IEC 13818-6 |
0x40-0xFE |
User private |
0xFF |
forbidden |
PAT中定义的节目号(program_number)与PMT_PID的映射。当节目号为0时,存储的是network_PID。
详细定义建议参考2.4.4.3 Program association Table一节。
PMT表
PMT在传送流中用于指示组成某一套节目的视频、音频和数据在传送流中的位置,即对应的TS包的PID值,以及每路节目的节目时钟参考(PCR)字段的位置。
其结构定义如下:
其中的stream_type标识了对应pid的类型,比如音频、视频或者其他类型(具体建议参考2.4.4.9 Semantic definition of fields in Transport Stream program map section一节)。
4. PES包
PES包使用固定的24位起始码0x000001和一个8为的stream-id,用于说明当前包的类型。PES包中可以包含DTS/PTS等时间戳信息。整体结构如下图:
PES包非定长,音频的PES包小于等于64K,视频的一般为一帧一个PES包。一帧图象的PES包通常要由许多个TS包来传输。MPEG-2中规定,一个PES包必须由整数个TS包来传输。如果承载一个PES包的最后一个TS包没能装满,则用填充字节来填满;当下一个新的PES包形成时,需用新的TS包来开始传输。
PES包的结构如下:
PES_packet() {
packet_start_code_prefix : 24
stream_id : 8
PES_packet_length: 16
optional_pes_header
pes_packet_data
}
- packet_start_code_prefix:24位起始码,固定必须是'0000 0000 0000 0000 0000 0001' (0x000001)。用于标识包的开始。
- stream_id:在PS流中该字段标识其存储的基本流的类型和索引号,在TS流中该字段仅标识其存储的基本流的类型。
- PES_packet_length:16位,用于存储PES包的长度。
- optional_pes_header需要视stream_id类型而定,其长度不固定(这里包含DTS/PTS时间戳信息)。
- pes_packet_data其长度是PES_packet_length定义的长度值。
最后两个字段的解析,建议参考标准文件的2.4.3.7 Semantic definition of fields in PES packet一节。
5.ffmpeg 实现封装:
AVOutputFormat ff_mpegts_muxer = {
.name = "mpegts",
.long_name = NULL_IF_CONFIG_SMALL("MPEG-TS (MPEG-2 Transport Stream)"),
.mime_type = "video/MP2T",
.extensions = "ts,m2t,m2ts,mts",
.priv_data_size = sizeof(MpegTSWrite),
.audio_codec = AV_CODEC_ID_MP2,
.video_codec = AV_CODEC_ID_MPEG2VIDEO,
.init = mpegts_init,
.write_packet = mpegts_write_packet,
.write_trailer = mpegts_write_end,
.deinit = mpegts_deinit,
.check_bitstream = mpegts_check_bitstream,
.flags = AVFMT_ALLOW_FLUSH | AVFMT_VARIABLE_FPS | AVFMT_NODIMENSIONS,
.priv_class = &mpegts_muxer_class,
};
mpegts_write_packet 函数:
static int mpegts_write_packet(AVFormatContext *s, AVPacket *pkt)
{
if (!pkt) {
mpegts_write_flush(s);
return 1;
} else {
return mpegts_write_packet_internal(s, pkt);
}
}
mpegts_write_packet_internal
static int mpegts_write_packet_internal(AVFormatContext *s, AVPacket *pkt)
{
AVStream *st = s->streams[pkt->stream_index];
int size = pkt->size;
uint8_t *buf = pkt->data;
uint8_t *data = NULL;
MpegTSWrite *ts = s->priv_data;
MpegTSWriteStream *ts_st = st->priv_data;
const int64_t delay = av_rescale(s->max_delay, 90000, AV_TIME_BASE) * 2;
const int64_t max_audio_delay = av_rescale(s->max_delay, 90000, AV_TIME_BASE) / 2;
int64_t dts = pkt->dts, pts = pkt->pts;
int opus_samples = 0;
int side_data_size;
uint8_t *side_data = NULL;
int stream_id = -1;
side_data = av_packet_get_side_data(pkt,
AV_PKT_DATA_MPEGTS_STREAM_ID,
&side_data_size);
if (side_data)
stream_id = side_data[0];
if (ts->copyts < 1) {
if (pts != AV_NOPTS_VALUE)
pts += delay;
if (dts != AV_NOPTS_VALUE)
dts += delay;
}
if (ts_st->first_pts_check && pts == AV_NOPTS_VALUE) {
av_log(s, AV_LOG_ERROR, "first pts value must be set\n");
return AVERROR_INVALIDDATA;
}
ts_st->first_pts_check = 0;
//H264 封装
if (st->codecpar->codec_id == AV_CODEC_ID_H264) {
const uint8_t *p = buf, *buf_end = p + size;
uint32_t state = -1;
int extradd = (pkt->flags & AV_PKT_FLAG_KEY) ? st->codecpar->extradata_size : 0;
int ret = ff_check_h264_startcode(s, st, pkt);
if (ret < 0)
return ret;
if (extradd && AV_RB24(st->codecpar->extradata) > 1)
extradd = 0;
do {
p = avpriv_find_start_code(p, buf_end, &state);
av_log(s, AV_LOG_TRACE, "nal %"PRId32"\n", state & 0x1f);
if ((state & 0x1f) == 7)
extradd = 0;
} while (p < buf_end && (state & 0x1f) != 9 &&
(state & 0x1f) != 5 && (state & 0x1f) != 1);
if ((state & 0x1f) != 5)
extradd = 0;
if ((state & 0x1f) != 9) { // AUD NAL
data = av_malloc(pkt->size + 6 + extradd);
if (!data)
return AVERROR(ENOMEM);
memcpy(data + 6, st->codecpar->extradata, extradd);
memcpy(data + 6 + extradd, pkt->data, pkt->size);
AV_WB32(data, 0x00000001);
data[4] = 0x09;
data[5] = 0xf0; // any slice type (0xe) + rbsp stop one bit
buf = data;
size = pkt->size + 6 + extradd;
}
//AAC 封装
} else if (st->codecpar->codec_id == AV_CODEC_ID_AAC) {
if (pkt->size < 2) {
av_log(s, AV_LOG_ERROR, "AAC packet too short\n");
return AVERROR_INVALIDDATA;
}
if ((AV_RB16(pkt->data) & 0xfff0) != 0xfff0) {
int ret;
AVPacket pkt2;
if (!ts_st->amux) {
av_log(s, AV_LOG_ERROR, "AAC bitstream not in ADTS format "
"and extradata missing\n");
} else {
av_init_packet(&pkt2);
pkt2.data = pkt->data;
pkt2.size = pkt->size;
av_assert0(pkt->dts != AV_NOPTS_VALUE);
pkt2.dts = av_rescale_q(pkt->dts, st->time_base, ts_st->amux->streams[0]->time_base);
ret = avio_open_dyn_buf(&ts_st->amux->pb);
if (ret < 0)
return ret;
ret = av_write_frame(ts_st->amux, &pkt2);
if (ret < 0) {
ffio_free_dyn_buf(&ts_st->amux->pb);
return ret;
}
size = avio_close_dyn_buf(ts_st->amux->pb, &data);
ts_st->amux->pb = NULL;
buf = data;
}
}
// h265 封装
} else if (st->codecpar->codec_id == AV_CODEC_ID_HEVC) {
const uint8_t *p = buf, *buf_end = p + size;
uint32_t state = -1;
int extradd = (pkt->flags & AV_PKT_FLAG_KEY) ? st->codecpar->extradata_size : 0;
int ret = check_hevc_startcode(s, st, pkt);
if (ret < 0)
return ret;
if (extradd && AV_RB24(st->codecpar->extradata) > 1)
extradd = 0;
do {
p = avpriv_find_start_code(p, buf_end, &state);
av_log(s, AV_LOG_TRACE, "nal %"PRId32"\n", (state & 0x7e)>>1);
if ((state & 0x7e) == 2*32)
extradd = 0;
} while (p < buf_end && (state & 0x7e) != 2*35 &&
(state & 0x7e) >= 2*32);
if ((state & 0x7e) < 2*16 || (state & 0x7e) >= 2*24)
extradd = 0;
if ((state & 0x7e) != 2*35) { // AUD NAL
data = av_malloc(pkt->size + 7 + extradd);
if (!data)
return AVERROR(ENOMEM);
memcpy(data + 7, st->codecpar->extradata, extradd);
memcpy(data + 7 + extradd, pkt->data, pkt->size);
AV_WB32(data, 0x00000001);
data[4] = 2*35;
data[5] = 1;
data[6] = 0x50; // any slice type (0x4) + rbsp stop one bit
buf = data;
size = pkt->size + 7 + extradd;
}
// opus封装
} else if (st->codecpar->codec_id == AV_CODEC_ID_OPUS) {
if (pkt->size < 2) {
av_log(s, AV_LOG_ERROR, "Opus packet too short\n");
return AVERROR_INVALIDDATA;
}
/* Add Opus control header */
if ((AV_RB16(pkt->data) >> 5) != 0x3ff) {
uint8_t *side_data;
int side_data_size;
int i, n;
int ctrl_header_size;
int trim_start = 0, trim_end = 0;
opus_samples = opus_get_packet_samples(s, pkt);
side_data = av_packet_get_side_data(pkt,
AV_PKT_DATA_SKIP_SAMPLES,
&side_data_size);
if (side_data && side_data_size >= 10) {
trim_end = AV_RL32(side_data + 4) * 48000 / st->codecpar->sample_rate;
}
ctrl_header_size = pkt->size + 2 + pkt->size / 255 + 1;
if (ts_st->opus_pending_trim_start)
ctrl_header_size += 2;
if (trim_end)
ctrl_header_size += 2;
data = av_malloc(ctrl_header_size);
if (!data)
return AVERROR(ENOMEM);
data[0] = 0x7f;
data[1] = 0xe0;
if (ts_st->opus_pending_trim_start)
data[1] |= 0x10;
if (trim_end)
data[1] |= 0x08;
n = pkt->size;
i = 2;
do {
data[i] = FFMIN(n, 255);
n -= 255;
i++;
} while (n >= 0);
av_assert0(2 + pkt->size / 255 + 1 == i);
if (ts_st->opus_pending_trim_start) {
trim_start = FFMIN(ts_st->opus_pending_trim_start, opus_samples);
AV_WB16(data + i, trim_start);
i += 2;
ts_st->opus_pending_trim_start -= trim_start;
}
if (trim_end) {
trim_end = FFMIN(trim_end, opus_samples - trim_start);
AV_WB16(data + i, trim_end);
i += 2;
}
memcpy(data + i, pkt->data, pkt->size);
buf = data;
size = ctrl_header_size;
} else {
/* TODO: Can we get TS formatted data here? If so we will
* need to count the samples of that too! */
av_log(s, AV_LOG_WARNING, "Got MPEG-TS formatted Opus data, unhandled");
}
}
if (ts_st->payload_size && (ts_st->payload_size + size > ts->pes_payload_size ||
(dts != AV_NOPTS_VALUE && ts_st->payload_dts != AV_NOPTS_VALUE &&
dts - ts_st->payload_dts >= max_audio_delay) ||
ts_st->opus_queued_samples + opus_samples >= 5760 /* 120ms */)) {
mpegts_write_pes(s, st, ts_st->payload, ts_st->payload_size,
ts_st->payload_pts, ts_st->payload_dts,
ts_st->payload_flags & AV_PKT_FLAG_KEY, stream_id);
ts_st->payload_size = 0;
ts_st->opus_queued_samples = 0;
}
if (st->codecpar->codec_type != AVMEDIA_TYPE_AUDIO || size > ts->pes_payload_size) {
av_assert0(!ts_st->payload_size);
// for video and subtitle, write a single pes packet
mpegts_write_pes(s, st, buf, size, pts, dts,
pkt->flags & AV_PKT_FLAG_KEY, stream_id);
ts_st->opus_queued_samples = 0;
av_free(data);
return 0;
}
if (!ts_st->payload_size) {
ts_st->payload_pts = pts;
ts_st->payload_dts = dts;
ts_st->payload_flags = pkt->flags;
}
memcpy(ts_st->payload + ts_st->payload_size, buf, size);
ts_st->payload_size += size;
ts_st->opus_queued_samples += opus_samples;
av_free(data);
return 0;
}