音视频入门基础:MPEG2-TS专题(9)——FFmpeg源码中,解码TS Header的实现
一、引言
FFmpeg源码对MPEG2-TS传输流/TS文件解复用时,在通过read_packet函数读取出一个transport packet后,会调用handle_packet函数来处理该transport packet:
static int handle_packets(MpegTSContext *ts, int64_t nb_packets)
{
//...for (;;) {
//...ret = read_packet(s, packet, ts->raw_packet_size, &data);if (ret != 0)break;ret = handle_packet(ts, data, avio_tell(s->pb));
//...}
//...
}
二、handle_packet函数
(一)handle_packet函数的定义
FFmpeg源码中使用handle_packet函数来处理一个transport packet,该函数的前半部分实现解码一个transport packet的TS Header。该函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的源文件libavformat/mpegts.c中:
/* handle one TS packet */
static int handle_packet(MpegTSContext *ts, const uint8_t *packet, int64_t pos)
{MpegTSFilter *tss;int len, pid, cc, expected_cc, cc_ok, afc, is_start, is_discontinuity,has_adaptation, has_payload;const uint8_t *p, *p_end;pid = AV_RB16(packet + 1) & 0x1fff;is_start = packet[1] & 0x40;tss = ts->pids[pid];if (ts->auto_guess && !tss && is_start) {add_pes_stream(ts, pid, -1);tss = ts->pids[pid];}if (!tss)return 0;if (is_start)tss->discard = discard_pid(ts, pid);if (tss->discard)return 0;ts->current_pid = pid;afc = (packet[3] >> 4) & 3;if (afc == 0) /* reserved value */return 0;has_adaptation = afc & 2;has_payload = afc & 1;is_discontinuity = has_adaptation &&packet[4] != 0 && /* with length > 0 */(packet[5] & 0x80); /* and discontinuity indicated *//* continuity check (currently not used) */cc = (packet[3] & 0xf);expected_cc = has_payload ? (tss->last_cc + 1) & 0x0f : tss->last_cc;cc_ok = pid == 0x1FFF || // null packet PIDis_discontinuity ||tss->last_cc < 0 ||expected_cc == cc;tss->last_cc = cc;if (!cc_ok) {av_log(ts->stream, AV_LOG_DEBUG,"Continuity check failed for pid %d expected %d got %d\n",pid, expected_cc, cc);if (tss->type == MPEGTS_PES) {PESContext *pc = tss->u.pes_filter.opaque;pc->flags |= AV_PKT_FLAG_CORRUPT;}}if (packet[1] & 0x80) {av_log(ts->stream, AV_LOG_DEBUG, "Packet had TEI flag set; marking as corrupt\n");if (tss->type == MPEGTS_PES) {PESContext *pc = tss->u.pes_filter.opaque;pc->flags |= AV_PKT_FLAG_CORRUPT;}}p = packet + 4;if (has_adaptation) {int64_t pcr_h;int pcr_l;if (parse_pcr(&pcr_h, &pcr_l, packet) == 0)tss->last_pcr = pcr_h * 300 + pcr_l;/* skip adaptation field */p += p[0] + 1;}//...return 0;
}
形参ts:既是输入型参数也是输出型参数,指向一个MpegTSContext类型变量。MpegTSContext结构体声明如下,存贮MPEG2-TS的上下文信息:
typedef struct MpegTSContext MpegTSContext;struct MpegTSContext {const AVClass *class;/* user data */AVFormatContext *stream;/** raw packet size, including FEC if present */int raw_packet_size;int64_t pos47_full;/** if true, all pids are analyzed to find streams */int auto_guess;/** compute exact PCR for each transport stream packet */int mpeg2ts_compute_pcr;/** fix dvb teletext pts */int fix_teletext_pts;int64_t cur_pcr; /**< used to estimate the exact PCR */int64_t pcr_incr; /**< used to estimate the exact PCR *//* data needed to handle file based ts *//** stop parsing loop */int stop_parse;/** packet containing Audio/Video data */AVPacket *pkt;/** to detect seek */int64_t last_pos;int skip_changes;int skip_clear;int skip_unknown_pmt;int scan_all_pmts;int resync_size;int merge_pmt_versions;int max_packet_size;int id;/******************************************//* 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;AVStream *epg_stream;AVBufferPool* pools[32];
};
形参packet:输入型参数,存贮该transport packet的数据。
形参pos:输入型参数,文件位置指针当前位置相对于TS文件的文件首的偏移字节数。
返回值:返回0表示成功,返回一个负数表示出错。
(二)handle_packet函数中,解码TS Header的固定长度部分的实现
handle_packet函数中,首先通过下面语句将TS Header中的PID属性读取出来,赋值给变量pid。关于AV_RB16函数的用法可以参考:《FFmpeg源码:AV_RB32、AV_RB16、AV_RB8宏定义分析》:
pid = AV_RB16(packet + 1) & 0x1fff;
将TS Header中的payload_unit_start_indicator属性读取出来,经过运算赋值给变量is_start。所以is_start的值为true时表示payload_unit_start_indicator属性为1,为false时表示payload_unit_start_indicator属性为0:
is_start = packet[1] & 0x40;
得到上述解析出来的PID属性对应的用于PAT和PMT表指定的各种流的过滤器,赋值给变量tss:
tss = ts->pids[pid];if (ts->auto_guess && !tss && is_start) {add_pes_stream(ts, pid, -1);tss = ts->pids[pid];}if (!tss)return 0;
根据调用者的programs selection,决定该pid是否被丢弃:
if (is_start)tss->discard = discard_pid(ts, pid);if (tss->discard)return 0;
将TS Header中的adaptation_field_control属性读取出来,赋值给变量afc。如果afc的值为0,表示adaptation_field_control属性的值为0,表示保留 (供未来使用),此时handle_packet函数直接返回,解码器不对该transport packet进行处理:
afc = (packet[3] >> 4) & 3;if (afc == 0) /* reserved value */return 0;
adaptation_field_control属性的的值为'10'时表示该transport packet仅有适配域,为'11'时表示适配域和载荷都存在。将“适配域是否存在”赋值给变量has_adaptation,has_adaptation为1表示适配域存在,has_adaptation为0表示不存在:
has_adaptation = afc & 2;
adaptation_field_control属性的的值为'01'时表示该transport packet无适配域仅有载荷,为'11'时表示适配域和载荷都存在。将“载荷是否存在”赋值给变量has_payload,has_payload为1表示载荷存在,has_payload为0表示不存在:
has_payload = afc & 1;
如果该transport packet适配域存在(has_adaptation为真),并且适配域长度adaptation_field_length不为0(packet[4] != 0),并且不连续指示位discontinuity_indicator为1(packet[5] & 0x80为真),变量is_discontinuity的值为1,表示当前分组(当前transport packet)处于不连续状态:
is_discontinuity = has_adaptation &&packet[4] != 0 && /* with length > 0 */(packet[5] & 0x80); /* and discontinuity indicated */
将TS Header中的continuity_counter属性读取出来,赋值给变量cc。 tss->last_cc保存同一个PID的上一个transport packet的continuity_counter属性:
/* continuity check (currently not used) */cc = (packet[3] & 0xf);
//...tss->last_cc = cc;
如果该transport packet的载荷存在(变量has_payload为真),让变量expected_cc赋值为同一个PID的上一个transport packet的continuity_counter属性的值加1;如果载荷不存在,让变量expected_cc赋值为同一个PID的上一个transport packet的continuity_counter属性的值:
expected_cc = has_payload ? (tss->last_cc + 1) & 0x0f : tss->last_cc;
从《音视频入门基础:MPEG2-TS专题(3)——TS Header简介》可以知道,PID为0x1FFF(pid == 0x1FFF)时,该transport packet为null packet(空包);continuity_counter属性用于检查同一个PID的transport packet的连续性,每当一个transport packet中包含载荷时,该计数器加1。所以下面语句的意思是:检测continuity_counter属性的合法性,如果合法,变量cc_ok的值为1,不合法,值为0:
cc_ok = pid == 0x1FFF || // null packet PIDis_discontinuity ||tss->last_cc < 0 ||expected_cc == cc;
如果continuity_counter属性不合法,打印错误日志:"Continuity check failed for pid %d expected %d got %d\n":
if (!cc_ok) {av_log(ts->stream, AV_LOG_DEBUG,"Continuity check failed for pid %d expected %d got %d\n",pid, expected_cc, cc);if (tss->type == MPEGTS_PES) {PESContext *pc = tss->u.pes_filter.opaque;pc->flags |= AV_PKT_FLAG_CORRUPT;}}
如果TS header的transport_error_indicator属性的值为1(packet[1] & 0x80为真),表示该transport packet损坏,打印错误日志:"Packet had TEI flag set; marking as corrupt\n":
if (packet[1] & 0x80) {av_log(ts->stream, AV_LOG_DEBUG, "Packet had TEI flag set; marking as corrupt\n");if (tss->type == MPEGTS_PES) {PESContext *pc = tss->u.pes_filter.opaque;pc->flags |= AV_PKT_FLAG_CORRUPT;}}
(三)handle_packet函数中,解码TS Header的适配域的实现
解析完TS Header的固定长度部分,handle_packet函数接下来会解析适配域。handle_packet函数中通过调用parse_pcr函数来解析适配域中的PCR:
p = packet + 4;if (has_adaptation) {int64_t pcr_h;int pcr_l;if (parse_pcr(&pcr_h, &pcr_l, packet) == 0)tss->last_pcr = pcr_h * 300 + pcr_l;/* skip adaptation field */p += p[0] + 1;}
parse_pcr函数定义如下:
/* return the 90kHz PCR and the extension for the 27MHz PCR. return* (-1) if not available */
static int parse_pcr(int64_t *ppcr_high, int *ppcr_low, const uint8_t *packet)
{int afc, len, flags;const uint8_t *p;unsigned int v;afc = (packet[3] >> 4) & 3;if (afc <= 1)return AVERROR_INVALIDDATA;p = packet + 4;len = p[0];p++;if (len == 0)return AVERROR_INVALIDDATA;flags = *p++;len--;if (!(flags & 0x10))return AVERROR_INVALIDDATA;if (len < 6)return AVERROR_INVALIDDATA;v = AV_RB32(p);*ppcr_high = ((int64_t) v << 1) | (p[4] >> 7);*ppcr_low = ((p[4] & 1) << 8) | p[5];return 0;
}
形参ppcr_high:输出型参数,执行parse_pcr函数后,*ppcr_high会得到PCR域中的program_clock_reference_base属性。
形参ppcr_low:输出型参数,执行parse_pcr函数后,*ppcr_low会得到PCR域中的program_clock_reference_extension属性。
形参packet:输入型参数,存贮该transport packet的数据。
返回值:返回0表示解析成功,返回一个负数表示解析失败。
parse_pcr函数中,首先将TS Header中的adaptation_field_control属性读取出来,赋值给变量afc。如果afc不大于1,表示adaptation_field_control属性的值为'00'或'01',此时TS Header中没有适配域,parse_pcr函数返回AVERROR_INVALIDDATA:
afc = (packet[3] >> 4) & 3;if (afc <= 1)return AVERROR_INVALIDDATA;
将TS Header适配域中的adaptation_field_length属性读取出来,赋值给变量len,这样变量len就会存贮适配域长度。如果适配域长度为0(len == 0),返回AVERROR_INVALIDDATA:
p = packet + 4;len = p[0];p++;if (len == 0)return AVERROR_INVALIDDATA;
判断适配域中PCR_flag的是否为0,如果为0(!(flags & 0x10)为真),表示适配域中没有PCR域 ,返回AVERROR_INVALIDDATA:
flags = *p++;len--;if (!(flags & 0x10))return AVERROR_INVALIDDATA;
得到PCR域中的program_clock_reference_base属性,赋值给*ppcr_high,得到PCR域中的program_clock_reference_extension属性,赋值给*ppcr_low:
v = AV_RB32(p);*ppcr_high = ((int64_t) v << 1) | (p[4] >> 7);*ppcr_low = ((p[4] & 1) << 8) | p[5];
————————————————分隔符————————————————
回到handle_packet函数,当解析适配域成功(parse_pcr(&pcr_h, &pcr_l, packet) == 0为真)时,通过语句:tss->last_pcr = pcr_h * 300 + pcr_l 计算出PCR。可以看到FFmpeg源码中计算PCR的方法和《音视频入门基础:MPEG2-TS专题(8)——TS Header中的适配域》中介绍的公式:PCR = program_clock_reference_base × 300 + program_clock_reference_extension 是一样的。p[0]为适配域中的adaptation_field_length属性,p[0] + 1为实际的适配域长度。所以通过语句:p += p[0] + 1可以跳过适配域,让指针p指向该transport packet适配域的下一个字节:
if (has_adaptation) {int64_t pcr_h;int pcr_l;if (parse_pcr(&pcr_h, &pcr_l, packet) == 0)tss->last_pcr = pcr_h * 300 + pcr_l;/* skip adaptation field */p += p[0] + 1;}