一、引言
由《音视频入门基础:MPEG2-TS专题(16)------PMT简介》可以知道,PMT表(Program map table)由一个或多个段(Transport stream program map section,简称TS program map section,即组成PMT表的段)组成。当某个PMT表非常大时,只要接收到一个TS program map section的完整数据就可以进行解析,而不需要接收到完整的表时才开始解析工作,解析完各个TS program map section后再把这些信息汇总起来。FFmpeg源码中,通过pmt_cb函数解析TS program map section。
二、pmt_cb函数定义
pmt_cb函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的源文件libavformat/mpegts.c中:
cpp
static void pmt_cb(MpegTSFilter *filter, const uint8_t *section, int section_len)
{
MpegTSContext *ts = filter->u.section_filter.opaque;
MpegTSSectionFilter *tssf = &filter->u.section_filter;
struct Program old_program;
SectionHeader h1, *h = &h1;
PESContext *pes;
AVStream *st;
const uint8_t *p, *p_end, *desc_list_end;
int program_info_length, pcr_pid, pid, stream_type;
int desc_list_len;
uint32_t prog_reg_desc = 0; /* registration descriptor */
int stream_identifier = -1;
struct Program *prg;
int mp4_descr_count = 0;
Mp4Descr mp4_descr[MAX_MP4_DESCR_COUNT] = { { 0 } };
int i;
av_log(ts->stream, AV_LOG_TRACE, "PMT: len %i\n", section_len);
hex_dump_debug(ts->stream, section, section_len);
p_end = section + section_len - 4;
p = section;
if (parse_section_header(h, &p, p_end) < 0)
return;
if (h->tid != PMT_TID)
return;
if (!h->current_next)
return;
if (skip_identical(h, tssf))
return;
av_log(ts->stream, AV_LOG_TRACE, "sid=0x%x sec_num=%d/%d version=%d tid=%d\n",
h->id, h->sec_num, h->last_sec_num, h->version, h->tid);
if (!ts->scan_all_pmts && ts->skip_changes)
return;
prg = get_program(ts, h->id);
if (prg)
old_program = *prg;
else
clear_program(&old_program);
if (ts->skip_unknown_pmt && !prg)
return;
if (prg && prg->nb_pids && prg->pids[0] != ts->current_pid)
return;
if (!ts->skip_clear)
clear_avprogram(ts, h->id);
clear_program(prg);
add_pid_to_program(prg, ts->current_pid);
pcr_pid = get16(&p, p_end);
if (pcr_pid < 0)
return;
pcr_pid &= 0x1fff;
add_pid_to_program(prg, pcr_pid);
update_av_program_info(ts->stream, h->id, pcr_pid, h->version);
av_log(ts->stream, AV_LOG_TRACE, "pcr_pid=0x%x\n", pcr_pid);
program_info_length = get16(&p, p_end);
if (program_info_length < 0)
return;
program_info_length &= 0xfff;
while (program_info_length >= 2) {
uint8_t tag, len;
tag = get8(&p, p_end);
len = get8(&p, p_end);
av_log(ts->stream, AV_LOG_TRACE, "program tag: 0x%02x len=%d\n", tag, len);
program_info_length -= 2;
if (len > program_info_length)
// something else is broken, exit the program_descriptors_loop
break;
program_info_length -= len;
if (tag == IOD_DESCRIPTOR) {
get8(&p, p_end); // scope
get8(&p, p_end); // label
len -= 2;
mp4_read_iods(ts->stream, p, len, mp4_descr + mp4_descr_count,
&mp4_descr_count, MAX_MP4_DESCR_COUNT);
} else if (tag == REGISTRATION_DESCRIPTOR && len >= 4) {
prog_reg_desc = bytestream_get_le32(&p);
len -= 4;
}
p += len;
}
p += program_info_length;
if (p >= p_end)
goto out;
// stop parsing after pmt, we found header
if (!ts->pkt)
ts->stop_parse = 2;
if (prg)
prg->pmt_found = 1;
for (i = 0; i < MAX_STREAMS_PER_PROGRAM; i++) {
st = 0;
pes = NULL;
stream_type = get8(&p, p_end);
if (stream_type < 0)
break;
pid = get16(&p, p_end);
if (pid < 0)
goto out;
pid &= 0x1fff;
if (pid == ts->current_pid)
goto out;
stream_identifier = parse_stream_identifier_desc(p, p_end) + 1;
/* now create stream */
if (ts->pids[pid] && ts->pids[pid]->type == MPEGTS_PES) {
pes = ts->pids[pid]->u.pes_filter.opaque;
if (ts->merge_pmt_versions && !pes->st) {
st = find_matching_stream(ts, pid, h->id, stream_identifier, i, &old_program);
if (st) {
pes->st = st;
pes->stream_type = stream_type;
pes->merged_st = 1;
}
}
if (!pes->st) {
pes->st = avformat_new_stream(pes->stream, NULL);
if (!pes->st)
goto out;
pes->st->id = pes->pid;
}
st = pes->st;
} else if (is_pes_stream(stream_type, prog_reg_desc)) {
if (ts->pids[pid])
mpegts_close_filter(ts, ts->pids[pid]); // wrongly added sdt filter probably
pes = add_pes_stream(ts, pid, pcr_pid);
if (ts->merge_pmt_versions && pes && !pes->st) {
st = find_matching_stream(ts, pid, h->id, stream_identifier, i, &old_program);
if (st) {
pes->st = st;
pes->stream_type = stream_type;
pes->merged_st = 1;
}
}
if (pes && !pes->st) {
st = avformat_new_stream(pes->stream, NULL);
if (!st)
goto out;
st->id = pes->pid;
}
} else {
int idx = ff_find_stream_index(ts->stream, pid);
if (idx >= 0) {
st = ts->stream->streams[idx];
}
if (ts->merge_pmt_versions && !st) {
st = find_matching_stream(ts, pid, h->id, stream_identifier, i, &old_program);
}
if (!st) {
st = avformat_new_stream(ts->stream, NULL);
if (!st)
goto out;
st->id = pid;
st->codecpar->codec_type = AVMEDIA_TYPE_DATA;
if (stream_type == 0x86 && prog_reg_desc == AV_RL32("CUEI")) {
mpegts_find_stream_type(st, stream_type, SCTE_types);
mpegts_open_section_filter(ts, pid, scte_data_cb, ts, 1);
}
}
}
if (!st)
goto out;
if (pes && !pes->stream_type)
mpegts_set_stream_info(st, pes, stream_type, prog_reg_desc);
add_pid_to_program(prg, pid);
if (prg) {
prg->streams[i].idx = st->index;
prg->streams[i].stream_identifier = stream_identifier;
prg->nb_streams++;
}
av_program_add_stream_index(ts->stream, h->id, st->index);
desc_list_len = get16(&p, p_end);
if (desc_list_len < 0)
goto out;
desc_list_len &= 0xfff;
desc_list_end = p + desc_list_len;
if (desc_list_end > p_end)
goto out;
for (;;) {
if (ff_parse_mpeg2_descriptor(ts->stream, st, stream_type, &p,
desc_list_end, mp4_descr,
mp4_descr_count, pid, ts) < 0)
break;
if (pes && prog_reg_desc == AV_RL32("HDMV") &&
stream_type == 0x83 && pes->sub_st) {
av_program_add_stream_index(ts->stream, h->id,
pes->sub_st->index);
pes->sub_st->codecpar->codec_tag = st->codecpar->codec_tag;
}
}
p = desc_list_end;
}
if (!ts->pids[pcr_pid])
mpegts_open_pcr_filter(ts, pcr_pid);
out:
for (i = 0; i < mp4_descr_count; i++)
av_free(mp4_descr[i].dec_config_descr);
}
该函数的作用是:解析TS流中的TS program map section,提取出里面的属性。
形参filter:输出型参数,指向一个MpegTSFilter类型变量。执行pmt_cb函数后,(PESContext *)(((MpegTSContext *)(filter->u.section_filter.opaque))->pids[pid]->u.pes_filter.opaque)会得到从TS program map section中解析出来的属性。pid为该节目音频或视频流的PID。
形参section:输入型参数。存放一个TS program map section的数据,即"将一个或多个包含PMT表信息的transport packet(TS包)去掉它们TS Header和pointer_field后的有效数据组合起来后"的数据。
section_len:输入型参数。该TS program map section的长度,单位为字节。
返回值:无
三、pmt_cb函数的内部实现分析
pmt_cb函数中首先通过下面语句让指针p_end指向该TS program map section中有效数据的末尾,即CRC_32属性的开头;让指针p指向该TS program map section的开头:
cpp
const uint8_t *p, *p_end;
//...
p_end = section + section_len - 4;
p = section;
通过parse_section_header函数解析Section Header,这样指针h就会得到从Section Header中解析出来的属性。关于parse_section_header函数的用法可以参考:《音视频入门基础:MPEG2-TS专题(13)------FFmpeg源码中,解析Section Header的实现》:
cpp
if (parse_section_header(h, &p, p_end) < 0)
return;
宏PMT_TID定义如下:
cpp
#define PMT_TID 0x02 /* Program Map section */
判断SectionHeader中的table_id属性是否为PMT_TID(0x02),PMT表的table_id固定为0x02,如果不是,表示这不是PMT表,pmt_cb函数直接返回:
cpp
if (h->tid != PMT_TID)
return;
判断SectionHeader中的current_next_indicator属性的值,如果值为1,表示发送的TS program map section为当前有效的,pmt_cb函数继续往下执行;值为0表示发送的该Section尚未有效并且下一个Section将生效,pmt_cb函数直接返回:
cpp
if (!h->current_next)
return;
h->id是该TS program map section的program_number属性。从《音视频入门基础:MPEG2-TS专题(15)------FFmpeg源码中,解析Program association section的实现》可以知道,FFmpeg源码解析完PAT表的Section后,ts->stream->programs会得到从PAT表的Section中解析出来的属性。所以在pmt_cb函数中通过语句:prg = get_program(ts, h->id),让变量prg拿到之前保存在ts->stream->programs中的该节目对应的program_number属性和program_map_PID属性的信息:
cpp
prg = get_program(ts, h->id);
if (prg)
old_program = *prg;
else
clear_program(&old_program);
if (ts->skip_unknown_pmt && !prg)
return;
if (prg && prg->nb_pids && prg->pids[0] != ts->current_pid)
return;
if (!ts->skip_clear)
clear_avprogram(ts, h->id);
clear_program(prg);
add_pid_to_program(prg, ts->current_pid);
读取TS program map section中的PCR_PID属性(PCR所在transport packet的PID),赋值给变量pcr_pid:
cpp
pcr_pid = get16(&p, p_end);
if (pcr_pid < 0)
return;
pcr_pid &= 0x1fff;
将上述得到的PCR_PID属性赋值给ts->stream->programs[i]->pcr_pid,将version_number属性赋值给ts->stream->programs[i]->pmt_version,i为该节目是TS流中的第几个节目:
cpp
add_pid_to_program(prg, pcr_pid);
update_av_program_info(ts->stream, h->id, pcr_pid, h->version);
读取TS program map section中的program_info_length属性,赋值给变量program_info_length:
cpp
program_info_length = get16(&p, p_end);
if (program_info_length < 0)
return;
program_info_length &= 0xfff;
program_info_length属性不小于2,表示program_info_length属性之后存在节目描述信息,读取节目描述信息:
cpp
while (program_info_length >= 2) {
uint8_t tag, len;
tag = get8(&p, p_end);
len = get8(&p, p_end);
av_log(ts->stream, AV_LOG_TRACE, "program tag: 0x%02x len=%d\n", tag, len);
program_info_length -= 2;
if (len > program_info_length)
// something else is broken, exit the program_descriptors_loop
break;
program_info_length -= len;
if (tag == IOD_DESCRIPTOR) {
get8(&p, p_end); // scope
get8(&p, p_end); // label
len -= 2;
mp4_read_iods(ts->stream, p, len, mp4_descr + mp4_descr_count,
&mp4_descr_count, MAX_MP4_DESCR_COUNT);
} else if (tag == REGISTRATION_DESCRIPTOR && len >= 4) {
prog_reg_desc = bytestream_get_le32(&p);
len -= 4;
}
p += len;
}
循环读取TS program map section中的stream_type属性(媒体流的类型),赋值给变量stream_type:
cpp
for (i = 0; i < MAX_STREAMS_PER_PROGRAM; i++) {
st = 0;
pes = NULL;
stream_type = get8(&p, p_end);
if (stream_type < 0)
break;
//...
}
循环读取TS program map section中的elementary_PID属性(节目的音频或视频PID),赋值给变量pid:
cpp
pid = get16(&p, p_end);
if (pid < 0)
goto out;
pid &= 0x1fff;
将上述得到的elementary_PID属性和PCR_PID属性保存到指针pes指向的PESContext结构中:
cpp
else if (is_pes_stream(stream_type, prog_reg_desc)) {
if (ts->pids[pid])
mpegts_close_filter(ts, ts->pids[pid]); // wrongly added sdt filter probably
pes = add_pes_stream(ts, pid, pcr_pid);
if (ts->merge_pmt_versions && pes && !pes->st) {
st = find_matching_stream(ts, pid, h->id, stream_identifier, i, &old_program);
if (st) {
pes->st = st;
pes->stream_type = stream_type;
pes->merged_st = 1;
}
}
if (pes && !pes->st) {
st = avformat_new_stream(pes->stream, NULL);
if (!st)
goto out;
st->id = pes->pid;
}
}
将上述得到的stream_type属性保存到指针pes指向的PESContext结构中:
cpp
if (pes && !pes->stream_type)
mpegts_set_stream_info(st, pes, stream_type, prog_reg_desc);
将上述得到的elementary_PID属性保存到指针prg指向的内存中。由于prg等于((MpegTSContext *)(filter->u.section_filter.opaque))->prg,而上述代码中又存在:MpegTSContext *ts = filter->u.section_filter.opaque 以及 pes = ts->pids[pid]->u.pes_filter.opaque。所以执行pmt_cb函数后,(PESContext *)(((MpegTSContext *)(filter->u.section_filter.opaque))->pids[pid]->u.pes_filter.opaque)会得到从TS program map section中解析出来的属性,pid为该节目音频或视频流的PID:
cpp
add_pid_to_program(prg, pid);
if (prg) {
prg->streams[i].idx = st->index;
prg->streams[i].stream_identifier = stream_identifier;
prg->nb_streams++;
}