avformat_open_input() 代码分析

----------------------------------------
avformat_open_input() 代码分析
author:hjjdebug
date: 2022年 10月 05日 星期三
----------------------------------------
我不想把代码抄下来一行行分析,有人已经这么做了.
而是想就关键问题进行分析.
文件被成功打开后,会返回一个 AVFormatContext 结构.
这是一个复杂的结构,构建也需要分步进行.

说明: 我调试的时候是用ffmpeg 库打开了一个ts 流, 测试代码见后.
就从ts流格式入手吧.

下面从4个方面来分析
----------------------------------------
甲. 输入文件格式是如何确定的? avf->iformat
----------------------------------------
    init_input->av_probe_input_buffer2->av_probe_input_format2 来决定的.
    其输入参数是pd, 输出参数是score得分和AVInputFormat *
    score 满分100, 得分越高可信度越高.

1.    先解释一下输入参数 pd 是什么吧,
    它是探测数据结构.
typedef struct AVProbeData {
    const char *filename;
    unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. */
    int buf_size;       /**< Size of buf except extra allocated bytes */
    const char *mime_type; /**< mime_type, when known. */
} AVProbeData;

意义很明确,能看得懂,就是检测缓冲区中的一段数据,下面给个pd 实例
(gdb) p pd
  $3 = {
    filename = 0x7fffffffe22f "/opt/test/test1.ts",
    buf = 0x555555562360 "G@\021\020",
    buf_size = 2048,
    mime_type = 0x0
  }

2. av_probe_input_format2 是如何探测的? 它返回score及s->iformat.

    while ((fmt1 = av_demuxer_iterate(&i))) { //这下会枚举几百种格式
            score = fmt1->read_probe(&pd);
    }
    原来它是枚举每一种输入格式,调用输入格式的探测函数,返回得分,得分高者即为探测的格式.

----------------------------------------
乙: avf->nb_streams 是如何确定的? 答:在read_header中确定
----------------------------------------
ts 流中,找到pmt 表时,就能确定streams 的个数.
此时的调用栈为:
  #0  0x00007ffff6654284 in avformat_new_stream (s=0x555555559040, c=0x0) at libavformat/utils.c:4596
  #1  0x00007ffff657d285 in pmt_cb (filter=0x55555555ee80, section=0x55555555de00 "\002\260\035", section_len=32) at libavformat/mpegts.c:2418
  #2  0x00007ffff6576fcc in write_section_data (ts=0x55555556a3c0, tss1=0x55555555ee80, buf=0x5555555624dd "\002\260\035", buf_size=183, is_start=1) at libavformat/mpegts.c:466
  #3  0x00007ffff657e816 in handle_packet (ts=0x55555556a3c0, packet=0x5555555624d8 "GP", pos=564) at libavformat/mpegts.c:2810
  #4  0x00007ffff657f020 in handle_packets (ts=0x55555556a3c0, nb_packets=26595) at libavformat/mpegts.c:2975
  #5  0x00007ffff657f694 in mpegts_read_header (s=0x555555559040) at libavformat/mpegts.c:3093
  #6  0x00007ffff6644e51 in avformat_open_input (ps=0x555555558018 <avf>, filename=0x7fffffffe22f "/opt/test/test1.ts", fmt=0x0, options=0x0) at libavformat/utils.c:609
  大致意思为,
  1. 调用iformat->read_header(s) 此例就调到了mpegts_read_header(s), 因为ts流的read_header 指针就指向这里
      此处曾把AVFormatContext s 放到 ts->stream
      ts->stream     = s;
    此处 ts 是私有类 MpegTSContext, 后面avformat_new_stream 时会用到它

  2. 调用 handle_packets(ts, probesize / ts->raw_packet_size);
    此处 probesize=5000,000 tw->raw_packet_size 是188字节,传进的是包的个数26595,  ts 是个什么变量呢?

    MpegTSContext *ts = s->priv_data;  // ts 是AVFormatContext 的私有数据,在init_input()时,获得了inputformat.,然后分配私有数据.并依据options初始化了这个私有类.

    /* Allocate private data. */
    if (s->iformat->priv_data_size > 0) {
        if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
        if (s->iformat->priv_class) {
//请正确理解变量,指针,指针的指针这3层关系,一个变量,可以存数据(data),也可以存数据地址(指针),也可以存指针的地址(指针的指针)
//例如数据时int,则后面时int*, int **这样的声明关系
            *(const AVClass **) s->priv_data = s->iformat->priv_class;
            av_opt_set_defaults(s->priv_data);
            if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)
                goto fail;
        }
    }
    此处:
    s 是AVFormatContext,
    ts 是私有类 MpegTSContext
    ts 结构有一个8K 个 mpeg ts 过滤器 MpegTSFilter *pids[8192];     
    这些过滤器的指针用到的会被填充. 例如pat 过滤器, pmt 过滤器,及其它过滤器.
        mpegts_open_section_filter(ts, PAT_PID, pat_cb, ts, 1);
        mpegts_open_section_filter(ts, pmt_pid, pmt_cb, ts, 1);
        ...
    
    该函数的原型如下,ts 是上下文,pid,cb 必须, 后面ts 是作为opaque 传入的,是cb 的参数, 1是是否检查crc。
    MpegTSFilter *mpegts_open_section_filter(MpegTSContext *ts,
                                                unsigned int pid,
                                                SectionCallback *section_cb,
                                                void *opaque,
                                                int check_crc)

  struct MpegTSFilter {
      int pid;
      int es_id;
      ...
      enum MpegTSFilterType type;
      union {
          MpegTSPESFilter pes_filter;
          MpegTSSectionFilter section_filter;
      } u;
  }

  struct MpegTSSectionFilter {
      int section_index;    //灵活使用
      int section_h_size;  // 由数据流中读取填充
      ...
      uint8_t *section_buf;    // 数据流中的地址
      SectionCallback *section_cb; //回调函数
      void *opaque; //回调参数
  }

    后面就是一个包一个包的扫描了,读一个包read_packet(s, packet, ts->raw_packet_size, &data); 这里就不分析了,

3. 调用handle_packet
    处理一个包. ret = handle_packet(ts, data, avio_tell(s->pb)); //从调用可见data 正指向一个包的开头,pos 是文件的数据偏移
    再调用 write_section_data, 其参数 tss1 是MpegTSFilter指针,这个指针来于包的pid, tss1 = ts->pids[pid];
     此时的包ID肯定是pmt的包ID(pmt的包id 是解析pat包得到的), 因而会调用pmt_cb
    tss->section_cb(tss1, cur_section_buf, tss->section_h_size);
    MpegTSSectionFilter *tss = &tss1->u.section_filter; // tss1是tsfilter, tss是sectionFilter, tss1更大一些.
    uint 8 *cur_section_buf = tss->section_buf;
    此处的tss 是 MpegTSSectionFilter
    前面注册的回调就是pmt_cb, 因而 tss->section_cb(函数指针) == pmt_cb

4. 调用 pmt_cb
    它传入的第一个参数是tss1,     MpegTSFilter *, 第二个参数是section数据指针,第三个是大小
    pmt 分析会读取pmt包头, 然后进行读取pmt包体找到所有stream. 当找到新的stream 后,要创建一个stream
      pes = add_pes_stream(ts, pid, pcr_pid);  //添加一个pes 流 ,    其中, pes->stream  = ts->stream;
        if (pes && !pes->st) {
            st = avformat_new_stream(pes->stream, NULL);  // 传进去的参数就是 AVFormatContext * ,见前述
            st->id = pes->pid;
        }
5. 调用avformat_new_stream , 然后avf->nb_streams 就会加1, 即扫描pmt表,发现一个流就会创建一个流,然后流个数加1

----------------------------------------
丙  AVFormatContext 中av_class 是何时赋值的 ?
----------------------------------------
    这个简单,在内存分配时就已经赋值了.
    avformat_alloc_context() -> avformat_get_context_defaults(AVFormatContext *s)
    s->av_class = &av_format_context_class; //把av_format_context_class 地址付给av_class
    av_opt_set_defaults(s);
    把avformat对象中数据依据options中定义的默认值来赋值.

    AVClass : 代码中定义了很多个AVClass 变量 , 有400多个,
    每个class 有名称,有选项指针, 有的还有child_iterate
    它们或作为私有类放入demux对象(属于AVInputFormat 对象)
    或mux对象(属于AVOutputFormat 对象中)

    这些muxer,demuxer会按数组方式组织在一起供程序使用. 程序往往是通过遍历这个列表,找到输入文件是
    哪种输出格式. 并通过文件内容分析来填充AVFormatContext 结构内容.

----------------------------------------
丁: stream->codecpar->code_id 是怎样赋值的? 在read_header中赋值.
----------------------------------------
ts 流中,找到pmt 表时,就能确定stream的ID
streamID 会对应到codec的类型和codec的ID
这需要在合适的位置设置断点,并且要会设置.
1.内存断点观察avf->streams, 断在了avformat_new_stream() 中,跟踪,退出到
    ff_raw_video_read_header 中,发现: 其id 来自于 iformat
    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
    st->codecpar->codec_id = s->iformat->raw_codec_id;  这是h264视频格式的

  对于ts流,则是在pmt表中存储有stream_type, 根据stream_type 可找到AVCODEC_ID 并进而找到codec, 如下调用
  #0  mpegts_find_stream_type (st=0x555555560680, stream_type=27, types=0x7ffff6683c08 <ISO_types+72>) at libavformat/mpegts.c:884
  #1  0x00007ffff6555240 in mpegts_set_stream_info (st=0x555555560680, pes=0x555555560380, stream_type=27, prog_reg_desc=0) at libavformat/mpegts.c:917
  #2  0x00007ffff655a5d2 in pmt_cb (filter=0x55555555fcc0, section=0x55555555ec40 "\002\260\035", section_len=32) at libavformat/mpegts.c:2448
  #3  0x00007ffff655402c in write_section_data (ts=0x55555556b0c0, tss1=0x55555555fcc0, buf=0x5555555631dd "\002\260\035", buf_size=183, is_start=1) at libavformat/mpegts.c:466
  #4  0x00007ffff655b992 in handle_packet (ts=0x55555556b0c0, packet=0x5555555631d8 "GP", pos=564) at libavformat/mpegts.c:2810
  #5  0x00007ffff655c1b4 in handle_packets (ts=0x55555556b0c0, nb_packets=26595) at libavformat/mpegts.c:2975
  #6  0x00007ffff655c844 in mpegts_read_header (s=0x555555559fc0) at libavformat/mpegts.c:3093

 存储codec_id:
       st->codecpar->codec_id   = types->codec_id;
(gdb) p *types
  $13 = {
    stream_type = 27,
    codec_type = AVMEDIA_TYPE_VIDEO,
    codec_id = AV_CODEC_ID_H264
  }
  上面数据怎么来的?

  pmt表中存储了类型27(0x1b), 从下面表格中就可以找到AV_CODEC_ID_H264

static const StreamType ISO_types[] = {
    { 0x01, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_MPEG2VIDEO },
    { 0x02, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_MPEG2VIDEO },
    { 0x03, AVMEDIA_TYPE_AUDIO, AV_CODEC_ID_MP3        },
    { 0x04, AVMEDIA_TYPE_AUDIO, AV_CODEC_ID_MP3        },
    { 0x0f, AVMEDIA_TYPE_AUDIO, AV_CODEC_ID_AAC        },
    { 0x10, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_MPEG4      },
    { 0x1b, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_H264       },
    { 0x1c, AVMEDIA_TYPE_AUDIO, AV_CODEC_ID_AAC        },
    { 0x20, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_H264       },
    { 0x21, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_JPEG2000   },
    { 0x24, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_HEVC       },
    { 0x42, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_CAVS       },
    { 0xd1, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_DIRAC      },
    { 0xd2, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_AVS2       },
    { 0xea, AVMEDIA_TYPE_VIDEO, AV_CODEC_ID_VC1        },
    { 0 },
};

如此程序也就分析完毕了. 是的,这是对庞杂代码的简要概括. 保留了骨干,抛弃一些简易的东西.

测试代码:

#include "libavformat/avformat.h"
#include "libavutil/timestamp.h"

AVFormatContext *avf = NULL;
int main(int argc, char **argv)
{
	if(argc==1)
	{
		printf("usage:%s <file>\n",argv[0]);
		return 1;
	}
	const char *filename=argv[1];
	avf = avformat_alloc_context();
	int ret;
	
	if ((ret = avformat_open_input(&avf, filename, NULL, NULL)) < 0) {
		fprintf(stderr, "%s: %s\n", filename, av_err2str(ret));
		return 1;
	}
	if ((ret = avformat_find_stream_info(avf, NULL)) < 0) {
		fprintf(stderr, "%s: could not find codec parameters: %s\n", filename,
				av_err2str(ret));
		return 1;
	}
	//read packet
	AVPacket packet;
	ret = av_read_frame(avf, &packet);
	if (ret < 0) {
		printf("read: %d (%s)\n", ret, av_err2str(ret));
	} else {
		AVRational *tb = &avf->streams[packet.stream_index]->time_base;
		printf("read: size=%d stream=%d dts=%s(%s) pts=%s(%s)\n",
				packet.size, packet.stream_index,
				av_ts2str(packet.dts), av_ts2timestr(packet.dts, tb),
				av_ts2str(packet.pts), av_ts2timestr(packet.pts, tb));
		av_packet_unref(&packet);
	}

	avformat_close_input(&avf);

	return 0;
}

运行结果:

./test_open /opt/test/test1.ts
read: size=25709 stream=0 dts=126000(1.4) pts=133200(1.48)

参考1: ffmpeg4.4 源代码

参考2: pmt 表

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值