当前位置: 首页 > article >正文

ffmpeg源码分析(九)解协议

本文将聚焦于FFmpeg协议处理模块,以avformat_open_input函数为核心,详细剖析其在最新FFmpeg源码中的实现。

音视频处理流程简介

在这里插入图片描述

avformat_open_input概述

avformat_open_input是FFmpeg用于打开输入多媒体数据的关键函数。它通过统一的接口处理多种协议,使用户无需关心底层实现。该函数的声明如下,位于libavformat/avformat.h

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
  • ps:指向AVFormatContext结构体的指针,用于存储打开的多媒体数据上下文。

  • url:要打开的资源URL。

  • fmt:指定输入格式,如果为NULL,FFmpeg会自动探测。

  • options:用于传递其他选项(字典形式)。

这个函数主要完成的功能就是探测输入的格式,然后配置格式的上下文,

比如输入一个mp4文件的地址,它会判断出这是mp4文件,然后AVFormatContext 就知道该怎么处理这个文件了。

avformat_open_input 打开数据流

/**
 * @brief 打开一个输入媒体文件。
 * 
 * @param ps 用户提供的 AVFormatContext 的指针,可能是一个指向 NULL 的指针来分配一个新的上下文。
 * @param filename 要打开的文件名。
 * @param fmt 如果非 NULL,此参数强制使用特定的输入格式。
 * @param options 填充 AVFormatContext 和解复用器私有选项的字典。返回时,该参数将被销毁并替换为包含未找到选项的字典。
 * 
 * @return 成功返回0,失败返回负的 AVERROR。
 */
int avformat_open_input(AVFormatContext **ps, const char *filename,
                        const AVInputFormat *fmt, AVDictionary **options)
{
    AVFormatContext *s = *ps;  // 获取提供的格式上下文
    FFFormatContext *si;       // 内部格式上下文
    AVDictionary *tmp = NULL;  // 用于选项的临时字典
    int ret = 0;               // 返回值

    // 如果没有提供格式上下文,则分配一个新的,也就是说这个格式上下文是可以由外部分配创建的
    if (!s && !(s = avformat_alloc_context()))
        return AVERROR(ENOMEM); 
  
    // 获取内部格式上下文,这相当于给AVFormatContext补充额外的上下文,这在ffmepg中是比较常见的。注意补充字段的内存是在avformat_alloc_context中分配的,所以不要自己直接malloc AVFormatContext
    si = ffformatcontext(s);   
    if (!s->av_class) {
        av_log(NULL, AV_LOG_ERROR, "输入上下文未通过 avformat_alloc_context() 正确分配\n");
        return AVERROR(EINVAL); 
    }
    if (fmt)
        s->iformat = fmt; // 设置输入格式

    if (options)
        av_dict_copy(&tmp, *options, 0); 

    // 如果设置了IO上下文,则设定定制的解复用标志
    if (s->pb)
        s->flags |= AVFMT_FLAG_CUSTOM_IO;

    if ((ret = av_opt_set_dict(s, &tmp)) < 0)
        goto fail; // 设置格式上下文选项失败

    // 拷贝文件名
    if (!(s->url = av_strdup(filename ? filename : ""))) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    // 【重要】探测输入格式
    if ((ret = init_input(s, filename, &tmp)) < 0)
        goto fail;

    s->probe_score = ret; // 设置探测得分

    // 复制协议白名单
    if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) {
        s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist);
        if (!s->protocol_whitelist) {
            ret = AVERROR(ENOMEM); 
            goto fail;
        }
    }

    // 复制协议黑名单
    if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) {
        s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist);
        if (!s->protocol_blacklist) {
            ret = AVERROR(ENOMEM); 
            goto fail;
        }
    }

    // 检查格式白名单
    if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) {
        av_log(s, AV_LOG_ERROR, "格式不在白名单 '%s' 中\n", s->format_whitelist);
        ret = AVERROR(EINVAL); // 非法参数错误
        goto fail;
    }

    avio_skip(s->pb, s->skip_initial_bytes); // 跳过初始字节

    // 检查文件名是否需要图像编号
    if (s->iformat->flags & AVFMT_NEEDNUMBER) {
        if (!av_filename_number_test(filename)) {
            ret = AVERROR(EINVAL); // 非法参数错误
            goto fail;
        }
    }
    // 初始化时长和开始时间
    s->duration = s->start_time = AV_NOPTS_VALUE; 

    // 分配私有数据
    if (ffifmt(s->iformat)->priv_data_size > 0) {
        if (!(s->priv_data = av_mallocz(ffifmt(s->iformat)->priv_data_size))) {
            ret = AVERROR(ENOMEM); 
            goto fail;
        }
        if (s->iformat->priv_class) {
            *(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;
        }
    }

    // mp3,读取 ID3v2 元数据
    if (s->pb)
        ff_id3v2_read_dict(s->pb, &si->id3v2_meta, ID3v2_DEFAULT_MAGIC, NULL);

    // 【重要】调用iformat格式的 read_header 函数
    if (ffifmt(s->iformat)->read_header)
        if ((ret = ffifmt(s->iformat)->read_header(s)) < 0) {
            if (ffifmt(s->iformat)->flags_internal & FF_INFMT_FLAG_INIT_CLEANUP)
                goto close;
            goto fail;
        }

    // 处理 ID3 标签
    //...

    // 分析 ID3v2 额外数据,如果是特殊格式
    if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac")) {
        // 处理 ID3v2 的 APIC、章节和隐私数据
        // ...
    }

    // 队列附加的图片
    if ((ret = avformat_queue_attached_pictures(s)) < 0)
        goto close;

    if (s->pb && !si->data_offset)
        si->data_offset = avio_tell(s->pb); // 更新数据偏移

    si->raw_packet_buffer_size = 0; // 初始化缓冲区大小

    update_stream_avctx(s); // 更新流的解码上下文

    // 清理选项字典
    if (options) {
        av_dict_free(options);
        *options = tmp;
    }
    *ps = s;
    return 0;

close:
    //【重要】调用iformat格式的 read_close 函数
    if (ffifmt(s->iformat)->read_close)
        ffifmt(s->iformat)->read_close(s);
fail:
    av_dict_free(&tmp);
    if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
        avio_closep(&s->pb);
    avformat_free_context(s);
    *ps = NULL;
    return ret;
}

init_input 初始化媒体输入流

该函数的主要功能是初始化输入媒体文件的处理环境,包括格式探测及文件打开操作。同时,代码设计中考虑了多种情况,包括用户提供的自定义 IO 和文件格式和网络格式,确保了高灵活性和广泛的适用性。


static int init_input(AVFormatContext *s, const char *filename,
                      AVDictionary **options)
{
    int ret;
    AVProbeData pd = { filename, NULL, 0 };
    int score = AVPROBE_SCORE_RETRY;

    //处理自定义io
    if (s->pb) {
        s->flags |= AVFMT_FLAG_CUSTOM_IO;
        // 如果输入格式未指定
        if (!s->iformat)
            // 从自定义 IO 缓冲区中探测输入格式
            return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                          s, 0, s->format_probesize);
        // 如果指定的格式不需要文件(如某些流协议)
        else if (s->iformat->flags & AVFMT_NOFILE)
            av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
                                      "will be ignored with AVFMT_NOFILE format.\n");
        return 0;
    }
    // 如果已知格式且该格式非文件格式,直接返回探测得分
    if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
        (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
        return score;

    // 打开输入文件,这一步是真正打开文件操作
    if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
        return ret;

    if (s->iformat)
        return 0;
    return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                  s, 0, s->format_probesize);
}

av_probe_input_buffer2 探测输入格式

int av_probe_input_buffer2(AVIOContext *pb, const AVInputFormat **fmt,
                           const char *filename, void *logctx,
                           unsigned int offset, unsigned int max_probe_size)
{
    AVProbeData pd = { filename ? filename : "" }; // 初始化探测数据
    uint8_t *buf = NULL; 
    int ret = 0, probe_size, buf_offset = 0; 
    int score = 0; // 探测得分
    int ret2;
    int eof = 0; // 文件结束标志

    // 检查并设置最大探测大小,以及偏移量
    ...


    // 获取 MIME 类型, MIME 类型信息可以被用来指示文件的媒体类型,使ffmpeg快的处理流信息
    if (pb->av_class) {
        uint8_t *mime_type_opt = NULL;
        char *semi;
        av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type_opt);
        pd.mime_type = (const char *)mime_type_opt;
        semi = pd.mime_type ? strchr(pd.mime_type, ';') : NULL; // 截断 MIME 类型
        if (semi) {
            *semi = '\0';
        }
    }

    // 探测循环
    for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt && !eof;
         probe_size = FFMIN(probe_size << 1, FFMAX(max_probe_size, probe_size + 1))) {
        score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0;

        // 分配和读取探测数据
        if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)
            goto fail; // 分配失败
        if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0) {
            // 处理读取错误
            if (ret != AVERROR_EOF)
                goto fail;
            score = 0; // 到达文件末尾
            ret = 0;
            eof = 1;
        }
        buf_offset += ret;
        if (buf_offset < offset)
            continue; // 缓冲区数据不足,继续读取
        pd.buf_size = buf_offset - offset;
        pd.buf = &buf[offset];

        memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE); // 填充缓冲区末尾

        // 猜测文件格式
        *fmt = av_probe_input_format2(&pd, 1, &score);
        if (*fmt) {
            // 检测到格式,记录日志
            if (score <= AVPROBE_SCORE_RETRY) {
                av_log(logctx, AV_LOG_WARNING, "格式 %s 仅以低得分 %d 检测到,可能存在错误识别。\n", (*fmt)->name, score);
            } else {
                av_log(logctx, AV_LOG_DEBUG, "格式 %s 已通过 size=%d 和 score=%d 探测到\n", (*fmt)->name, probe_size, score);
            }
        }
    }

    if (!*fmt)
        ret = AVERROR_INVALIDDATA; // 如果未找到格式,则返回格式无效错误

fail:
    // 回退并重用探测缓冲区
    ret2 = ffio_rewind_with_probe_data(pb, &buf, buf_offset);
    if (ret >= 0)
        ret = ret2;

    av_freep(&pd.mime_type); // 释放 MIME 类型内存
    return ret < 0 ? ret : score; // 返回探测得分或错误
}

av_probe_input_format2

av_probe_input_format2实际的探测av_probe_input_format3里

const AVInputFormat *av_probe_input_format3(const AVProbeData *pd,
                                            int is_opened, int *score_ret)
{
    AVProbeData lpd = *pd;
    const AVInputFormat *fmt1 = NULL;
    const AVInputFormat *fmt = NULL;
    int score, score_max = 0;
    void *i = 0;
    //用于缓冲区填充
    const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE];
    enum nodat {
        NO_ID3,
        ID3_ALMOST_GREATER_PROBE,
        ID3_GREATER_PROBE,
        ID3_GREATER_MAX_PROBE,
    } nodat = NO_ID3;

    if (!lpd.buf)
       // 如果缓冲区为空,则填充
        lpd.buf = (unsigned char *) zerobuffer;

    if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
         ...
        // 检查并处理 ID3v2 标签
    }

    // 迭代所有支持输入格式
    while ((fmt1 = av_demuxer_iterate(&i))) {
        if (fmt1->flags & AVFMT_EXPERIMENTAL)
            continue;// 跳过实验性格式
        // 特别处理 image2文件
        if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
            continue;
        score = 0;
        // 如果格式具有探测函数,则调用该函数进行文件格式检测
        if (ffifmt(fmt1)->read_probe) {
            
            score = ffifmt(fmt1)->read_probe(&lpd);
            if (score)
                av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);
            if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {
                switch (nodat) {
                case NO_ID3:
                    score = FFMAX(score, 1);
                    break;
                case ID3_GREATER_PROBE:
                case ID3_ALMOST_GREATER_PROBE:
                    score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
                    break;
                case ID3_GREATER_MAX_PROBE:
                    score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
                    break;
                }
            }
        } else if (fmt1->extensions) {
            if (av_match_ext(lpd.filename, fmt1->extensions))
                score = AVPROBE_SCORE_EXTENSION;
        }
        if (av_match_name(lpd.mime_type, fmt1->mime_type)) {
            if (AVPROBE_SCORE_MIME > score) {
                av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME);
                score = AVPROBE_SCORE_MIME;
            }
        }
        if (score > score_max) {
            score_max = score;
            fmt       = fmt1;
        } else if (score == score_max)
            fmt = NULL;
    }
    if (nodat == ID3_GREATER_PROBE)
        score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max);
    *score_ret = score_max;

    return fmt;
}

以HLS格式为例

m3u8列表打开流程

avformat_open_input→init_input-> 格式打开入口

av_probe_input_format2→av_probe_input_format3 :探测出是hls格式

io_open→io_open_default→io_open_default→ffio_open_whitelist→ffurl_open_whitelist→ffurl_connect→http_open->http_open_cnx->http_open_cnx_internal->http_connect :打开hls,开始下载


http://www.kler.cn/a/451264.html

相关文章:

  • 数字工厂管理系统就是ERP系统吗
  • 构建一个rust生产应用读书笔记7-确认邮件2
  • 精准提升:从94.5%到99.4%——目标检测调优全纪录
  • C#(事件)2
  • 高强度螺栓等级划分
  • 链路聚合与GVRP的混合构建(eNSP)
  • 记录 io.springfox 3.0.0 整合 spring boot 2.6.x 由于 springfox bug 引发问题
  • HTML制作一个普通的背景换肤案例2024版
  • udp tcp协议
  • 重温设计模式--备忘录模式
  • Sui 基金会任命 Christian Thompson 为新任负责人
  • 快速建站(网站如何在自己的电脑里跑起来) 详细步骤 一
  • 时钟抖动定义和测量方法
  • macos自动制作dmg安装包脚本
  • 深入理解数据结构:数组、链表与列表
  • 【魅力golang】之-通道
  • Unity中如何实现绘制Sin函数图像
  • whisper.cpp: Android端测试 -- Android端手机部署音频大模型
  • 独一无二,万字详谈——Linux之文件管理
  • 虚幻引擎结构之UWorld
  • 16.1、网络安全风险评估过程
  • 基于Spring Boot的九州美食城商户一体化系统
  • HTML+CSS+JS制作外贸网站(内附源码,含5个页面)
  • 3.学习webpack配置 尝试打包ts文件
  • 【Git】-- 版本说明
  • 每天40分玩转Django:Django国际化