音视频入门基础:MPEG2-TS专题(17)——FFmpeg源码中,解析TS program map section的实现

一、引言

由《音视频入门基础: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++;
        }
相关推荐
handsomestWei2 小时前
java实现多图合成mp4和视频附件下载
java·开发语言·音视频·wutool·图片合成视频·视频附件下载
G佳伟10 小时前
【亲测有效】百度Ueditor富文本编辑器添加插入视频、视频不显示、和插入视频后二次编辑视频标签不显示,显示成img标签,二次保存视频被替换问题,解决方案
chrome·百度·音视频
灵感素材坊19 小时前
解锁音乐创作新技能:AI音乐网站的正确使用方式
人工智能·经验分享·音视频
modest —YBW19 小时前
视频大小怎么计算?视频码率是什么,构成视频清晰度的核心要素!
音视频
cuijiecheng201819 小时前
音视频入门基础:RTP专题(10)——FFmpeg源码中,解析RTP header的实现
ffmpeg·音视频
AI服务老曹20 小时前
运用先进的智能算法和优化模型,进行科学合理调度的智慧园区开源了
运维·人工智能·安全·开源·音视频
Macdo_cn1 天前
My Metronome for Mac v1.4.2 我的节拍器 支持M、Intel芯片
macos·音视频
kiramario1 天前
【结束】JS如何不通过input的onInputFileChange使用本地mp4文件并播放,nextjs下放入public文件的视频用video标签无法打开
开发语言·javascript·音视频
余~~185381628001 天前
矩阵碰一碰发视频的后端源码技术,支持OEM
线性代数·矩阵·音视频
划水哥~1 天前
高清下载油管视频到本地
音视频