FFmpeg 框架简读 —— Demux 部分

本文作者:肖杨

来自公众号:流媒体技术

av_read_frame内部实现详情

本次的 FFmpeg 5.0 升级该函数没有改动。不过追溯历史,该接口也经历过迭代,在古早的 release/2.5 版本之前曾存在一个类似的函数 av_read_packet。按照某些博客的说法,旧接口 av_read_packet 获取的数据可能是不完整的,而 av_read_frame 保证了视频数据一帧的完整性,所以在过渡了几个版本后,2.5 版本起便完全见不到它的踪影。

想要快速了解 av_read_frame 的内部实现,最好先了解一下 demux 的一些概念和该函数内的大概层级:在 ffmpeg 的部分的 av_read_frame 函数内部大概可以分成3层,每层的分工明确、职责单一。同时与之相关的,在AVFormatInternal 内部还有三个存放 AVPacket 的缓存队列,分别是:

    packet_buffer
    raw_packet_buffer
    parse_queue

在AVStreamInternal内含有一个标记成员名为request_probe。

FFmpeg 框架简读 —— Demux 部分

av_read_frame          ->  read_frame_internal          ->   ff_read_frame / parse_packet
   提供给用户              取帧内部实现                           与IO层接触       与Parser打交道
packet_buffer      raw_packet_buffer + parse_queue          raw_packet_buffer  parse_queue

三个缓存队列:

packet_buffer: 暂存所有直接可以拿给用户的包

av_read_frame 这一层是直接面向用户的取帧接口,他使用的 packet_buffer 队列,除了在 av_find_stream_info 时会存放一些用于 probe 的帧外,其余主要都是从 read_frame_internal 取到的帧。所以 packet_buffer 队列内存放的帧,都是可以直接返回上层用户的(顶多会经过一个生成补缺 pts/dts 的处理)

raw_packet_buffer:暂存所有确认codec前解复用的原始数据包

read_frame_internal 这层像是一个承上启下的桥梁,内部是从 IO 层拿到帧后的处理工作,主要接触两个队列:raw_packet_buffer 和 parse_queue。

read_frame_internal 主要存储找到该协议适配的预期 codec 前解复用出来的原始数据包,这个时间段也对应着 AVStreamInternal->request_probe 标记,表明该 Stream 正在探测 codec,此阶段的数据包由于不确定后续是否需要 parse 或其他处理,所以将暂存在该队列中,在 probe_codec 成功后,将优先返回该队列数据。

AVFormatInternal 中的 raw_packet_buffer 队列有一个最大容量:2500000 字节,由 ffmpeg 通过宏来定义,剩余容量不足时会主动 probe_codec;关于 AVStreamInternal 的 request_probe 状态也有一个相关字段控制最大区间:AVFormatContext->max_probe_packets,它代表在 probe_codec 阶段中,确定编码格式时最多可参考的数据包数量,用户可以 AVOption 手动指定该值,默认是 2500。

parse_queue:暂存所有已经parse过的包

在初始化阶段完成之后,该流的所需信息也都获取完备了。这时再从 IO 层取出的数据包将不会存在上述的 raw_packet_buffer 中了。大部分的流在 demux 后就可以直接进行下一步解码,但有些协议下取出的原始数据包需要 AVCodecParser 的处理,这时便出现了 parse_queue。

从 IO 层拿出的一个原始数据包,经过 AVCodecParser 解析之后可能会被分为多个AVPacket,所以在static int parse_packet(AVFormatContext *s, AVPacket *pkt, int stream_index, int flush) 这一层提交一个原始数据包,可能会得到 n 个解析后的数据包,ffmpeg 会将他们全部存入 parse_queue 队列中,再由上层(static int read_frame_internal(AVFormatContext *s, AVPacket *pkt) 层)逐一处理和返回。


struct AVFormatInternal {
     /**
     * This buffer is only needed when packets were already buffered but
     * not decoded, for example to get the codec parameters in MPEG
     * streams.
     */
    struct PacketList *packet_buffer;
    struct PacketList *packet_buffer_end;


 
    /* av_seek_frame() support */
    int64_t data_offset; /**< offset of the first packet */


 
    /**
     * Raw packets from the demuxer, prior to parsing and decoding.
     * This buffer is used for buffering packets until the codec can
     * be identified, as parsing cannot be done without knowing the
     * codec.
     */
    struct PacketList *raw_packet_buffer;
    struct PacketList *raw_packet_buffer_end;
  
  
    /**
     * Packets split by the parser get queued here.
     */
    AVPacket *parse_pkt;
    struct PacketList *parse_queue;
    struct PacketList *parse_queue_end;


    ...
}


struct AVStreamInternal {
    /**
     * The codec context used by avformat_find_stream_info, the parser, etc.
     */
    AVCodecContext *avctx;
  
    /**
     * stream probing state
     * -1   -> probing finished
     *  0   -> no probing requested
     * rest -> perform probing with request_probe being the minimum score to accept.
     */
    int request_probe;
}


struct AVFormatContext 
    /**
     * Maximum number of packets that can be probed
     * - encoding: unused
     * - decoding: set by user
     */
    int max_probe_packets;
};

介绍过大概的层级分工和用到的缓存队列之后,下面来简单剖析一下各层的实现细节。

第一层 av_read_frame 层

av_read_frame 内部首先会判断 AVFormatContext->flags 标记中是否含有 AVFMT_FLAG_GENPTS,这个标记代表:在解复用时,是否需要为丢失 pts 的 AVPacket 生成递增 pts,需要用户手动指定。生成 pts 的细节暂时跳过。

通常情况并为指定此 flag,则优先到 packet_buffer 缓存中取包,否则调用 read_frame_internal

第二层 read_frame_internal 层

如果用户没有设置 GENPTS 字段,上层则会直接跳到 read_frame_internal 开始取帧。

像 “单一职责” 原则一样,read_frame_internal 封装了整个解复用流程可能涉及到的流程和细节。对上层来说,只要调用到该函数便能拿到一帧;从它的内部角度看,他承载着 IO 层与 Parser 层,是 demux 过程中的主要处理函数。那么我们进一步看内部的具体细节:

  • 优先拿取解析过的缓存包:read_frame_internal首先会判断 parse_queue 缓存中,是否有已经经由 AVParser 处理后的帧。存在于 parse_queue 缓存的帧是已经完全处理好,可以直接作为结果返回给上层的,如果不存在缓存,继续前进取帧。
  • 取帧过程中会先从 ff_read_packet 中进行取帧,如果遇到了某些错误,则按情况处理:若ff_read_packet 层返回了 EAGAIN,透传给上层直至用户即可;若遇到了其他错误,则退出取帧 并 Reset 所有流的 parser。
  • 如果上一步没有发生错误,从 IO 层取帧之后一切皆正常,即开始我们 parse 前的准备了。如果该 AVStream 标记了 need_parsing 字段,会确保 AVStream->AVCodecParserContext 存在,并已经由 av_parser_init 初始化过了。若这个流需要更新 AVStream->AVSteramInternal->AVCodecContext, ffmpeg 会关闭该 Stream 已经存在的 parser,同时也要关闭 codec,因为 Parser 的选用是取决于 Stream 与 Codec 的类型的。这种需要更新 AVCodec 状态会在 avformat_new_stream 后被设置,还可能在某些协议的内部处理是被设置。
  • 在把数据包交给 parse_packet 层之前,需要考虑两种状况:

        如果这个流 demux 后的数据不需要 parse,直接返回给上层即可,这通常取决于各个协议层的实现,某些协议会设给 AVtream->need_parsing 一个 AVStreamParseType 的枚举类型以示 parse 需求。


enum AVStreamParseType {
    AVSTREAM_PARSE_NONE,
    AVSTREAM_PARSE_FULL,       /**< full parsing and repack */
    AVSTREAM_PARSE_HEADERS,    /**< Only parse headers, do not repack. */
    AVSTREAM_PARSE_TIMESTAMPS, /**< full parsing and interpolation of timestamps for frames not starting on a packet boundary */
    AVSTREAM_PARSE_FULL_ONCE,  /**< full parsing and repack of the first frame only, only implemented for H.264 currently */
    AVSTREAM_PARSE_FULL_RAW,   /**< full parsing and repack with timestamp and position generation by parser for raw
                                    this assumes that each packet in the file contains no demuxer level headers and
                                    just codec level data, otherwise position generation would fail */
};
  • 无需 parse  或通过 parse_packet 层 parse 成功后会进行一些 packet 的处理工作:将 AVStream 附带的 discard_padding、skip_samples 信息以及一些其他的 global side data 通过 av_packet_new_side_data 接口注入给这一帧 AVPacket,至此这一层便处理完毕。

此外在 parse 前,还需参考一个字段 AVStream->discard,这个字段代表在 demux 时需要以何程度的择帧和丢包,如果标记了 AVDISCARD_ALL,会绕过 parser 直接 av_packet_unref 丢包。开发者可以在 demux 前直接设置此字段,用户也可以在 ffmpeg 命令行内通过参数来指定,如  ffmpeg   -skip_frame  nointra  -i  $input  $output。


enum AVDiscard{
    /* We leave some space between them for extensions (drop some
     * keyframes for intra-only or drop just some bidir frames). */
    AVDISCARD_NONE    =-16, ///< discard nothing
    AVDISCARD_DEFAULT =  0, ///< discard useless packets like 0 size packets in avi
    AVDISCARD_NONREF  =  8, ///< discard all non reference
    AVDISCARD_BIDIR   = 16, ///< discard all bidirectional frames
    AVDISCARD_NONINTRA= 24, ///< discard all non intra frames
    AVDISCARD_NONKEY  = 32, ///< discard all frames except keyframes
    AVDISCARD_ALL     = 48, ///< discard all
};


//位于libavcodec/options_table.h的AVDiscard系列声明,命令行通过AVOption作为媒介输入对应字符串,指定skip_frame等级
{"skip_frame"      , "skip decoding for the selected frames",               OFFSET(skip_frame),       AV_OPT_TYPE_INT, {.i64 = AVDISCARD_DEFAULT }, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"none"            , "discard no frame",                    0, AV_OPT_TYPE_CONST, {.i64 = AVDISCARD_NONE    }, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"default"         , "discard useless frames",              0, AV_OPT_TYPE_CONST, {.i64 = AVDISCARD_DEFAULT }, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"noref"           , "discard all non-reference frames",    0, AV_OPT_TYPE_CONST, {.i64 = AVDISCARD_NONREF  }, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"bidir"           , "discard all bidirectional frames",    0, AV_OPT_TYPE_CONST, {.i64 = AVDISCARD_BIDIR   }, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"nokey"           , "discard all frames except keyframes", 0, AV_OPT_TYPE_CONST, {.i64 = AVDISCARD_NONKEY  }, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"nointra"         , "discard all frames except I frames",  0, AV_OPT_TYPE_CONST, {.i64 = AVDISCARD_NONINTRA}, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"all"             , "discard all frames",                  0, AV_OPT_TYPE_CONST, {.i64 = AVDISCARD_ALL     }, INT_MIN, INT_MAX, V|D, "avdiscard"},
{"bidir_refine", "refine the two motion vectors used in bidirectional macroblocks", OFFSET(bidir_refine), AV_OPT_TYPE_INT, {.i64 = 1 }, 0, 4, V|E},

read_frame_internal 在 ffmpeg 中实现了将 format 格式的 packet,最终转换成一帧帧的 ES 流 packet,并解析填充了 packet 的 pts,dts 等信息,为最终解码提供了重要的数据,read_frame_internal 调用 ff_read_packet,每次只读取一个包,然后直到 parser 完这个包的所有数据,才开始读取下一个包,parser 完的数据被保存在 parser 结构的数据缓冲中,这样即使 ff_read_packet 读取的下一包和前一包的流不一样,由于 parser 也不一样,所以实现了 read_frame_internal 这个函数调用,可以解析出不同流的 ES 流, 而 read_frame_internal 函数除非出错否则必须解析出一帧数据才能返回。

第三层 ff_read_packet 层

上文提到,ff_read_packet 层主要的工作职责就是与 IO 层打交道。同时作为 raw_packet_buffer 缓存队列的输入者,他的主要工作内容是:

  •  根据 request_probe 标记确认是否还在 probe 状态:如果已经 probe 完毕,且 raw_packet_buffer 有缓存,优先返回队列内的数据,直到这层队列完全,直接从 IO 层去取,这层缓存失效。如果还处在 probe 状态,确认 raw_packet_buffer 剩余空间是否还够用,不够则立即主动 probe_codec,不然后续从 IO 层读出来的原始数据包将无处安放。
  • 如果还在 probe,直接调用 AVFormatContext->AVInputFormat->read_frame,向 IO 层取帧。收到了非 EAGAIN 的错误码则 probe_codec        
  • av_packet_make_refcounted 增加这一帧的引用计数,并根据 stream 的 offset 加工整个数据包的 pts/dts

    以 mp4 文件为例,read_frame 指针指向了函数 mov_read_packet 的地址。根据 mp4 的函数协议和其特点,在 mp4 格式的 read_header 阶段,会解析名为 ‘stsc’ 的 box,该 box 记录了所有数据片的数据偏移量,也就是在读取元信息的阶段就将所有数据帧的位置记录在协议私有上下文 MOVContext中,所以在 demux 时,mov 直接通过 avio_seek 读取目标数据帧。

FFmpeg 框架简读 —— Demux 部分

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(1)

相关推荐

发表回复

登录后才能评论