三篇相关联的文章:
ffmpeg下HLS解析过程-CSDN博客
TS文件格式详解及解封装过程-CSDN博客
FFMPEG解析ts流-CSDN博客
一、简介
关于TS格式解析,可以参考《TS文件格式详解及解封装过程-CSDN博客》,本文主要代码部分解读。建议大家熟读iso13818-1,碰到问题很多情况是因为没有熟悉标准
二、主要结构体
相关结构关系
struct MpegTSContext;
|
V
struct MpegTSFilter;
|
V
+---------------+---------------+
| |
V V
MpegTSPESFilter MpegTSSectionFilter 测
2.1 ts文件过滤器的结构体
struct MpegTSFilter {//ts的过滤器
int pid;
int es_id;
int last_cc; /* last cc code (-1 if first packet) */
int64_t last_pcr;
enum MpegTSFilterType type;//过滤器类型,分辨PES,PCR,SECTION
union {
MpegTSPESFilter pes_filter;
MpegTSSectionFilter section_filter;
}u;
};
2.2 section过滤器结构体
typedef struct MpegTSSectionFilter {
int section_index; //section的索引
int section_h_size; //头大小
int last_ver;
unsigned crc;
unsigned last_crc;
uint8_t *section_buf; //保存section数据
unsigned int check_crc : 1;
unsigned int end_of_section_reached : 1;
SectionCallback *section_cb; //回调函数
void *opaque; //类似于类指针的东西
} MpegTSSectionFilter;
2.3 节目的结构体
struct Program {
unsigned int id; // program id/service id
unsigned int nb_pids;
unsigned int pids[MAX_PIDS_PER_PROGRAM];
int pmt_found; //标识pmt是否已经找到
};
2.4 MpegTSContext
struct MpegTSContext {
const AVClass *class;
/* user data */
AVFormatContext *stream;
/** raw packet size, including FEC if present */
int raw_packet_size;//ts格式长度
int size_stat[3];//get_pcr探测ts三种格式分数用的
int size_stat_count;//get_pcr探测ts三种格式次数
#define SIZE_STAT_THRESHOLD 10
int64_t pos47_full;
/** 如果为真, 所有的pid将会用来去寻找流 */
int auto_guess;
/** 对于每个ts包都进行精确的计算 */
int mpeg2ts_compute_pcr;
/** 修复 dvb teletext pts */
int fix_teletext_pts;AVPacket *pkt;
/** to detect seek */
int64_t last_pos;
int skip_changes;
int skip_clear;
int scan_all_pmts;
int resync_size;
/******************************************/
/* private mpegts data */
/* scan context */
/** structure to keep track of Program->pids mapping */
unsigned int nb_prg;
struct Program *prg;
int8_t crc_validity[NB_PID_MAX];
/** filters for various streams specified by PMT + for the PAT and PMT */
MpegTSFilter *pids[NB_PID_MAX];
int current_pid
};
二、demuxer相关对外接口
2.1 相关接口
AVInputFormat mpegtsraw_demuxer = {
"mpegts",
NULL_IF_CONFIG_SMALL(MPEG-TS (MPEG-2 Transport Stream)),
sizeof(MpegTSContext),
mpegts_probe,
mpegts_read_header,
mpegts_read_close,
read_seek,
mpegts_get_pcr,
.flags = AVFMT_SHOW_IDS|AVFMT_TS_DISCONT,
};
2.2 相关接口解析
2.2.1 mpegts_probe
故名思议,就是探测流是否是mpegts格式
/*
* 函数功能:
* 分析流中是三种TS格式的哪一种
*/
static int mpegts_probe(AVProbeData *p) { #if 1 const int size= p->buf_size; int score, fec_score, dvhs_score; #define CHECK_COUNT 10 if (size < (TS_FEC_PACKET_SIZE * CHECK_COUNT)) return -1; score = analyze(p->buf, TS_PACKET_SIZE * CHECK_COUNT, TS_PACKET_SIZE, NULL); dvhs_score = analyze(p->buf, TS_DVHS_PACKET_SIZE *CHECK_COUNT, TS_DVHS_PACKET_SIZE, NULL); fec_score= analyze(p->buf, TS_FEC_PACKET_SIZE*CHECK_COUNT, TS_FEC_PACKET_SIZE, NULL);
mpegts_probe被av_probe_input_format2调用,根据返回的score来判断那种格式的可能性最大。mpegts_probe调用了analyze函数
/*
* 函数功能:
* 在size大小的buf中,寻找满足特定格式,长度为packet_size的
* packet的个数;
* 显然,返回的值越大越可能是相应的格式(188/192/204)
*/
static int analyze(const uint8_t *buf, int size, int packet_size, int *index){
int stat[TS_MAX_PACKET_SIZE];
int i;
int x=0;
int best_score=0;memset(stat, 0, packet_size*sizeof(int));
for (x=i=0; i < size-3; i++)
{
if ((buf[i] == 0x47) && !(buf[i+1] & 0x80) && (buf[i+3] & 0x30))
{
stat[x]++;
if (stat[x] > best_score)
{
best_score= stat[x];
if (index)
*index= x;
}
}x++;
if (x == packet_size)
x= 0;
}
return best_score;
}
buf[i] == 0x47 && !(buf[i+1] & 0x80) && (buf[i+3] & 0x30)是TS流同步开始的模式,
0x47是TS流同步的标志,记该模式为“TS流同步模式”
buf[i+1] & 0x80是传输错误标志
buf[i+3] & 0x30是adaptation_field_control,为0时表示为ISO/IEC未来使用保留,目前不存在这样的值。
stat数组变量存储的是“TS流同步模式”在某个位置出现的次数。返回的值越大越多是相应的格式(188/192/204)
size-3,这里为什么是-3呢?因为同步标志、传输错误标志和adaptation_field_control占了4个字节,查找的特定格式至少3 个Bytes,所以,至少最后3 个Bytes 不用查找(再找就超界了)
这就是MPEG TS的探测过程
2.2.2 mpegts_read_header
/*
* 函数功能:
*
*/
int mpegts_read_header(AVFormatContext *s, AVFormatParameters *ap)
{
/*
* MpegTSContext , 是为了解码不同容器格式所使用的私有数据,
* 只有在相应的诸如mpegts.c文件才可以使用的.
* 这样,增加了这个库的模块化.
*/
MpegTSContext *ts = s->priv_data;
AVIOContext *pb = s->pb;
uint8_t buf[8*1024];
int len;
int64_t pos;/* read the first 8*1024 bytes to get packet size */
pos = avio_tell(pb); // 获取buf的当前位置,保存流的当前位置,便于检测操作完成后恢复到原来的位置,这样在播放的时候就不会浪费一段流
len = avio_read(pb, buf, sizeof(buf)); // 从pb->opaque中读取8192个字节到buf
if (len != sizeof(buf))
goto fail;/*
* 获得TS包的实际长度,继续探测ts包的长度,这个步骤是不是重复了?*/
ts->raw_packet_size = get_packet_size(buf, sizeof(buf));
if (ts->raw_packet_size <= 0)
{
av_log(s, AV_LOG_WARNING, "Could not detect TS packet size, defaulting to non-FEC/DVHS\n");
ts->raw_packet_size = TS_PACKET_SIZE;
}ts->stream = s;
ts->auto_guess = 0;
//判断是否为MPEGTS解复用器,如果是则进行解复用if (s->iformat == &ff_mpegts_demuxer)
{
/* normal demux */
/* first do a scaning to get all the services */
if (avio_seek(pb, pos, SEEK_SET) < 0)
{
av_log(s, AV_LOG_ERROR, "Unable to seek back to the start\n");
}/*
* 挂载了两个Section类型的过滤器,
* 其实在TS的两种负载中,section是PES的元数据,
* 只有先解析了section,才能进一步解析PES数据,因此先挂上section的过滤器。* 并设置sdt_cb,pat_cb
*/
mpegts_open_section_filter(ts, SDT_PID, sdt_cb, ts, 1);
mpegts_open_section_filter(ts, PAT_PID, pat_cb, ts, 1);/*
*处理packets,处理的packet个数为s->probesize / ts->raw_packet_size
探测一段流,便于检测出SDT,PAT,PMT表
*/
handle_packets(ts, s->probesize / ts->raw_packet_size);/* if could not find service, enable auto_guess */
/*并把auto_guess置为1,auto_guess = 1, 则在handle_packet 的函数中只要发现 个PES 的pid 就 建立该PES 的stream*/
ts->auto_guess = 1;
av_dlog(ts->stream, "tuning done\n");/*将 `AVFMTCTX_NOHEADER` 标志设置到 `s->ctx_flags` 中,表示header已经解析好,不需要再调用 `read_header` 函数了/
s->ctx_flags |= AVFMTCTX_NOHEADER;
}
else
{
...
}avio_seek(pb, pos, SEEK_SET); //seek到pos最开始的位置
return 0;fail:
return -1;
}
关于section的定义,先看结构图
每个业务标都有section,而PMT的section包含了音视频流
一个表里可能有多个section,一个section可能包含多条流
SDT表当中会有节目的名子,提供商名子等等
2.2.2.1mpegts_open_section_filter函数分析
这个函数可以解释mpegts.c代码结构的精妙之处,PSI业务信息表的处理都是通过该函数挂载到MpegTSContext结构的pids字段上的。这样如果你想增加别的业务信息的表处理函数只要通过这个函数来挂载即可,体现了软件设计的著名的“开闭”原则。下面分析一下他的代码。
static MpegTSFilter *mpegts_open_section_filter(MpegTSContext *ts, unsigned int pid,
SectionCallback *section_cb, void *opaque,
int check_crc)
{
MpegTSFilter *filter;
MpegTSSectionFilter *sec;
dprintf(ts->stream, "Filter: pid=0x%x\n", pid);
if (pid >= NB_PID_MAX || ts->pids[pid])
return NULL;
//给filter分配空间,挂载到MpegTSContext的pids上
//就是该实例
filter = av_mallocz(sizeof(MpegTSFilter));
if (!filter)
return NULL;
//挂载filter实例
ts->pids[pid] = filter;
//设置filter相关的参数,因为业务信息表的分析的单位是段,
//所以该filter的类型是MPEGTS_SECTION
filter->type = MPEGTS_SECTION;
//设置pid
filter->pid = pid;
filter->last_cc = -1;
//设置filter回调处理函数
sec = &filter->u.section_filter;
sec->section_cb = section_cb;
sec->opaque = opaque;
//分配段数据处理的缓冲区,调用handle_packet函数后会调用
//write_section_data将ts包中的业务信息表的数据存储在这儿,
//直到一个段收集完成才交付上面注册的回调函数处理。
sec->section_buf = av_malloc(MAX_SECTION_SIZE);
sec->check_crc = check_crc;
if (!sec->section_buf) {
av_free(filter);
return NULL;
}
return filter;
}
2.2.2.2handle_packets函数分析
handle_packets函数在两个地方被调用,一个是mpegts_read_header函数中,另外一个是mpegts_read_packet函数中,被mpegts_read_header函数调用是用来搜索PSI业务信息,nb_packets参数为探测的ts包的个数;在mpegts_read_packet函数中被调用用来搜索补充PSI业务信息和demux PES流,nb_packets为0,0不是表示处理的包的个数为0。
ts->stop_parse当遇到stop_parse大于0时(解析完一个PES时此值为1),退出循环。
接下来分析最重要的地方handler_packets,简单看来
handle_packets()
|
+->read_packet()
|
+->handle_packet()
|
+->write_section_data()
handle_packet是mpegts.c代码的核心,所有的其他代码都是为这个函数准备的。在调用该函数之前先调用read_packet函数获得一个ts包(通常是188bytes),然后传给该函数,packet参数就是TS包。
1、将ts包拼装为section,解析pat section
2、将ts包拼装为pes,依据video pid获取video pes
static int handle_packet(MpegTSContext *ts, const uint8_t *packet)
{
AVFormatContext *s = ts->stream;
MpegTSFilter *tss;
int len, pid, cc, cc_ok, afc, is_start;
const uint8_t *p, *p_end;
int64_t pos;
//从TS包获得包的PID。
pid = AV_RB16(packet + 1) & 0x1fff;
if(pid && discard_pid(ts, pid))
return 0;
##########################################################
是不是PES 或者Section 的开头(payload_unit_start_indicator)
##########################################################
is_start = packet[1] & 0x40;
tss = ts->pids[pid];
//ts->auto_guess在mpegts_read_header函数中被设置为0,
//也就是说在ts检测过程中是不建立pes stream的。
if (ts->auto_guess && tss == NULL && is_start) {
add_pes_stream(ts, pid, -1, 0);
tss = ts->pids[pid];
}
//mpegts_read_header函数调用handle_packet函数只是处理TS流的
//业务信息(PAT,PDT等),因为并没有为对应的PES建立tss,所以tss为空,直接返回。
//如果是pes,tss不为空,则继续
if (!tss)
return 0;
/* continuity check (currently not used) */
cc = (packet[3] & 0xf);
cc_ok = (tss->last_cc < 0) || ((((tss->last_cc + 1) & 0x0f) == cc));
tss->last_cc = cc;
/*
* 解析 adaptation_field_control 语法元素
* =======================================================
* 00 | Reserved for future use by ISO/IEC
* 01 | No adaptation_field, payload only
* 10 | Adaptation_field only, no payload
* 11 | Adaptation_field follwed by payload
* =======================================================
*/
————————————————
/* skip adaptation field */
afc = (packet[3] >> 4) & 3;
p = packet + 4;
if (afc == 0) /* reserved value */
return 0;
if (afc == 2) /* adaptation field only */
return 0;
if (afc == 3) {
/* skip adapation field p[0]对应的语法元素为: adaptation_field_length*/
p += p[0] + 1;
}
##########################################################
p已近 达TS 包中的有效负载的地方
##########################################################
/* if past the end of packet, ignore */
p_end = packet + TS_PACKET_SIZE;
if (p >= p_end)
return 0;
pos = url_ftell(ts->stream->pb);
ts->pos47= pos % ts->raw_packet_size;
if (tss->type == MPEGTS_SECTION) {
/*
*
* 针对Section, 第一个字节对应的语法元素为:pointer_field(见2.4.4.1),
* 它表示在当前TS包中,从pointer_field开始到第一个section的第一个字节间的字节数。
* 当TS包中有至少一个section的起始时,
* payload_unit_start_indicator = 1 且 TS负载的第一个字节为pointer_field;
* pointer_field = 0x00时,表示section的起始就在这个字节之后;
* 当TS包中没有section的起始时,
* payload_unit_start_indicator = 0 且 TS负载中没有pointer_field;
*/
if (is_start) {
//获取pointer field字段,
//新的段从pointer field字段指示的位置开始
len = *p++;
if (p + len > p_end)
return 0;
if (len && cc_ok) {
//这个时候TS的负载有两个部分构成:
//1)从TS负载开始到pointer field字段指示的位置;
//2)从pointer field字段指示的位置到TS包结束
//1)位置代表的是上一个段的末尾部分。
//2)位置代表的新的段开始的部分。
//下面的代码是保存上一个段末尾部分数据,也就是
//1)位置的数据。
########################################################
1).is_start == 1
len > 0
负载部分由A Section 的End 部分和B Section 的Start 组成,把A 的
End 部分写入
########################################################
write_section_data(s, tss,
p, len, 0);
/* check whether filter has been closed */
if (!ts->pids[pid])
return 0;
}
p += len;
//保留新的段数据,也就是2)位置的数据。
if (p < p_end) {
########################################################
2).is_start == 1
len > 0
负载部分由A Section 的End 部分和B Section 的Start 组成,把B 的
Start 部分写入
或者:
3).
is_start == 1
len == 0
负载部分仅是 个Section 的Start 部分,将其写入
########################################################
write_section_data(s, tss,
p, p_end - p, 1);
}
} else {
//保存段中间的数据。
if (cc_ok) {
########################################################
4).is_start == 0
负载部分仅是 个Section 的中间部分部分,将其写入
########################################################
write_section_data(s, tss,
p, p_end - p, 0);
}
}
} else {
int ret;
##########################################################
若是是PES 类型,直接调用其Callback----mpegts_push_data,但显然,只有Section 部分
解析完成后才可能解析PES
##########################################################
// Note: The position here points actually behind the current packet.
if ((ret = tss->u.pes_filter.pes_cb(tss, p, p_end - p, is_start,
pos - ts->raw_packet_size)) < 0)
return ret;
}
return 0;
}
write_section_data()函数则反复收集buffer中的数据,指导完成相关Section的重组过
程,而后调用以前注册的两个section_cb:
static void write_section_data(AVFormatContext *s, MpegTSFilter *tss1,
const uint8_t *buf, int buf_size, int is_start)
{
MpegTSSectionFilter *tss = &tss1->u.section_filter;
int len;
//buf 中是 个段的开始部分。
if (is_start) {
//将内容复制 tss->section_buf 中保存
memcpy(tss->section_buf, buf, buf_size);
//tss->section_index 段索引。
tss->section_index = buf_size;
//段的长度,如今还不知道,设置为-1
tss->section_h_size = -1;
//是否 达段的结尾。
tss->end_of_section_reached = 0;
} else {
//buf 中是段中间的数据。
if (tss->end_of_section_reached)
return;
len = 4096 - tss->section_index;
if (buf_size < len)
len = buf_size;
memcpy(tss->section_buf + tss->section_index, buf, len);
tss->section_index += len;
}
//若是条件知足,计算段的长度
if (tss->section_h_size == -1 && tss->section_index >= 3) {
len = (AV_RB16(tss->section_buf + 1) & 0xfff) + 3;
if (len > 4096)
return;
tss->section_h_size = len;
}
//判断段数据是否收集完毕,若是收集完毕,调用相应的回调函数处理该段。
if (tss->section_h_size != -1 && tss->section_index >= tss->section_h_size) {
tss->end_of_section_reached = 1;
if (!tss->check_crc ||
av_crc(av_crc_get_table(AV_CRC_32_IEEE), -1,
tss->section_buf, tss->section_h_size) == 0)
tss->section_cb(tss1, tss->section_buf, tss->section_h_size);
}
}
Section_cb是一个回调函数,调用的函数有pat_cb,sdt_cb,pmt_cb, Table_id 00代表pat表,02代表pmt表
到了这里还是一头雾水,还没看到解析PES,PTS这些在哪解析的?
处理每一个包,如果是section包,就调用 write_section_data ,这个函数里面如果一个PAT, PMT, SDT表已经构成,则会调用刚刚看到的pat_cb, pmt_cb, sdt_cb,分析到这里,已经不用再管section包了,只看pes包,所以一般会调用 tss->u.pes_filter.pes_cb ,这个函数指针到底是什么呢?在函数 add_pes_stream 里面可以看到, mpegts_open_pes_filter 函数的一个参数 mpegts_push_data 就是这里的 tss->u.pes_filter.pes_cb。
2.2.2.3 mpegts_push_data
一帧视频就是一个PES包,av_read_frame()就是从PES包队列中取出一个PES包。一个PES包是分配在连续的几个TS包中,所以如果我们要获得一帧数据,那么我们需要把连续的几个TS包里的数据全部取出来才能组合成一个PES。那我们怎么知道一个PES的开始和结尾呢?那我们还是一个个遍历每一个TS包,寻找包头里payload_unit_start_indicator为1包,这个标志位代表着是一个PES的开始,那么我从这开始,一直到下一个payload_unit_start_indicator为1,这中间的TS包组成起来就是一个PES。
/*
*函数功能
*解析PES包,得到时间戳、流索引、PES包长度等数据,并将这个PES包压入到PES包队列
*/
static int mpegts_push_data(MpegTSFilter *filter,
const uint8_t *buf, int buf_size, int is_start,
int64_t pos)
{
PESContext *pes = filter->u.pes_filter.opaque;
MpegTSContext *ts = pes->ts;
const uint8_t *p;
int len, code;
if (!ts->pkt)
return 0;
if (is_start) {
if (pes->state == MPEGTS_PAYLOAD && pes->data_index > 0) { //当前包还没处理完,新的包到来。
new_pes_packet(pes, ts->pkt); //把pes的数据传给pkt(提交给上层)
ts->stop_parse = 1; //当前包结束解析
} else {
reset_pes_packet_state(pes); //新的开始
}
pes->state = MPEGTS_HEADER;
pes->ts_packet_pos = pos;
}
p = buf;
//4个字节的TS头,个PES的头,10为填充头
while (buf_size > 0) {
switch (pes->state) {
case MPEGTS_HEADER: //解析pes包头部
if (pes->data_index == PES_START_SIZE) {
/* we got all the PES orsection header. We can now
* decide */
if (pes->header[0] == 0x00&& pes->header[1] == 0x00 &&
pes->header[2] == 0x01){
//前三个为00,00,01。PES起始位
/* it must be an MPEG-2 PESstream */
code = pes->header[3] |0x100;//stream_id
//得到pes长度
pes->total_size =AV_RB16(pes->header + 4);
/* 分配ES的空间 */
pes->buffer = av_malloc(pes->total_size+FF_INPUT_BUFFER_PADDING_SIZE);
/*****************PES解析***********************/
/* PES packing parsing */
case MPEGTS_PESHEADER:
case MPEGTS_PESHEADER_FILL:
if (pes->data_index == pes->pes_header_size) {
const uint8_t *r;
unsigned int flags, pes_ext, skip;
flags = pes->header[7];//SYNTAX: PTS_DTS_flags
r = pes->header + 9;
pes->pts = AV_NOPTS_VALUE;
pes->dts = AV_NOPTS_VALUE;
//pts一共33bit,解析出pts
if ((flags & 0xc0) == 0x80) {
pes->dts = pes->pts = ff_parse_pes_pts(r);
r += 5;
} else if ((flags & 0xc0) == 0xc0) {
pes->pts = ff_parse_pes_pts(r);
r += 5;
pes->dts = ff_parse_pes_pts(r);
r += 5;
}
case MPEGTS_PAYLOAD:
if (pes->buffer) {
if (pes->data_index > 0 &&
pes->data_index + buf_size > pes->total_size) { //加上新的数据超出一个pes包的长度(当前包结束) ???
new_pes_packet(pes, ts->pkt); //把pes的数据传给pkt(提交给上层)
pes->total_size = MAX_PES_PAYLOAD;
pes->buffer = av_buffer_alloc(pes->total_size +
FF_INPUT_BUFFER_PADDING_SIZE);
if (!pes->buffer)
return AVERROR(ENOMEM);
ts->stop_parse = 1; //当前包结束解析
} else if (pes->data_index == 0 &&
buf_size > pes->total_size) {
// pes packet size is < ts size packet and pes data is padded with 0xff
// not sure if this is legal in ts but see issue #2392
buf_size = pes->total_size;
}
/* 取出PES的负载数据组成TS流 */
memcpy(pes->buffer->data + pes->data_index, p, buf_size); //拷贝数据到pes->buffer(可能还没拷完整个pes packet的数据)
pes->data_index += buf_size;
/* emit complete packets with known packet size
* decreases demuxer delay for infrequent packets like subtitles from
* a couple of seconds to milliseconds for properly muxed files.
* total_size is the number of bytes following pes_packet_length
* in the pes header, i.e. not counting the first PES_START_SIZE bytes */
if (!ts->stop_parse && pes->total_size < MAX_PES_PAYLOAD &&
pes->pes_header_size + pes->data_index == pes->total_size + PES_START_SIZE) {
ts->stop_parse = 1; //数据已经拷贝完,停止解析。
new_pes_packet(pes, ts->pkt); //把pes的数据传给pkt(提交给上层)
}
}
buf_size = 0;
break;
ts->stop_parse = 1 意味着一个pes包构成了,所以上面的函数mpegts_read_packet就返回了,这样,一个pes包送上去了,再送到codec去解码,最后送去video或audio输出设置显示了。由些可以看到ts流和avi, mkv这些一样,都是一个容器,真真的数据都是包含在其中的一个一个的串流。
未完待续。。。。。
输入时间戳不边续时的处理机制
目的: 输入时间戳不连续,必须保证输出时间戳的连续。
1. 当视频时间戳连续,而音频时间戳不连续时
不强行修改时间戳,
用插入静音帧来实现重同步
三、问题
1、pat_cb/pmt_cb是在那里调用的?
答:write_section_data判断段数据是否收集完毕,若是收集完毕,调用相应的回调函数处理该段
2、mpegts_push_data是在哪调用的?
handle_packet的u.pes_filter.pes_cb对应mpegts_push_data
3、一个完整的pes包是什么时候完成解析的?
mpegts_push_data->MPEGTS_PAYLOAD->ts->stop_parse = 1