音视频入门基础:MPEG2-TS专题(9)——FFmpeg源码中,解码TS Header的实现

一、引言

FFmpeg源码对MPEG2-TS传输流/TS文件解复用时,在通过read_packet函数读取出一个transport packet后,会调用handle_packet函数来处理该transport packet:

cpp 复制代码
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中:

cpp 复制代码
/* 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 PID
            is_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的上下文信息:

cpp 复制代码
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宏定义分析》:

cpp 复制代码
    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:

cpp 复制代码
    is_start = packet[1] & 0x40;

得到上述解析出来的PID属性对应的用于PAT和PMT表指定的各种流的过滤器,赋值给变量tss:

cpp 复制代码
    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是否被丢弃:

cpp 复制代码
    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进行处理:

cpp 复制代码
    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表示不存在:

cpp 复制代码
    has_adaptation   = afc & 2;

adaptation_field_control属性的的值为'01'时表示该transport packet无适配域仅有载荷,为'11'时表示适配域和载荷都存在。将"载荷是否存在"赋值给变量has_payload,has_payload为1表示载荷存在,has_payload为0表示不存在:

cpp 复制代码
    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)处于不连续状态:

cpp 复制代码
    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属性:

cpp 复制代码
    /* 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属性的值:

cpp 复制代码
    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:

cpp 复制代码
    cc_ok = pid == 0x1FFF || // null packet PID
            is_discontinuity ||
            tss->last_cc < 0 ||
            expected_cc == cc;

如果continuity_counter属性不合法,打印错误日志:"Continuity check failed for pid %d expected %d got %d\n":

cpp 复制代码
    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":

cpp 复制代码
    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:

cpp 复制代码
    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函数定义如下:

cpp 复制代码
/* 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:

cpp 复制代码
    afc = (packet[3] >> 4) & 3;
    if (afc <= 1)
        return AVERROR_INVALIDDATA;

将TS Header适配域中的adaptation_field_length属性读取出来,赋值给变量len,这样变量len就会存贮适配域长度。如果适配域长度为0(len == 0),返回AVERROR_INVALIDDATA:

cpp 复制代码
    p   = packet + 4;
    len = p[0];
    p++;
    if (len == 0)
        return AVERROR_INVALIDDATA;

判断适配域中PCR_flag的是否为0,如果为0(!(flags & 0x10)为真),表示适配域中没有PCR域 ,返回AVERROR_INVALIDDATA:

cpp 复制代码
    flags = *p++;
    len--;
    if (!(flags & 0x10))
        return AVERROR_INVALIDDATA;

得到PCR域中的program_clock_reference_base属性,赋值给*ppcr_high,得到PCR域中的program_clock_reference_extension属性,赋值给*ppcr_low:

cpp 复制代码
    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 是一样的:

cpp 复制代码
    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;
    }
相关推荐
Everbrilliant892 小时前
GL C++显示相机YUV视频数据使用帧缓冲FBO后期处理,实现滤镜功能。
音视频·opengl图片水印·opengl文字水印·opengl帧缓冲·opengl离屏渲染(osr)·opengl fbo·opengl图像合成
yangshuo12815 小时前
如何将手机的画面和音频全部传输到电脑显示和使用电脑外放输出
智能手机·音视频
陈皮话梅糖@8 小时前
iOS 集成ffmpeg
ios·ffmpeg
芥末的无奈8 小时前
GStreamer 简明教程(九):插件开发,以一个音频特效插件为例
音视频·gstreamer
winxp-pic1 天前
视频行为分析系统,可做安全行为检测,比如周界入侵,打架
安全·音视频
姓学名生1 天前
李沐vscode配置+github管理+FFmpeg视频搬运+百度API添加翻译字幕
vscode·python·深度学习·ffmpeg·github·视频
学习嵌入式的小羊~1 天前
RV1126+FFMPEG推流项目(11)编码音视频数据 + FFMPEG时间戳处理
ffmpeg·音视频
刘大猫.1 天前
vue3使用音频audio标签
音视频·audio·preload·加载音频文件·vue3使用audio·vue3使用音频·audio标签
优联前端2 天前
Web 音视频(二)在浏览器中解析视频
前端·javascript·音视频·优联前端·webav
我真不会起名字啊2 天前
“深入浅出”系列之音视频开发:(3)音视频开发的学习路线和必备知识
音视频