FFmpeg5.0源码阅读——mov文件格式解析

news2025/1/10 16:59:04
  • 摘要:之前在Mp4格式详解中详细描述了Mp4文件格式的具体布局方式。为了更加深入理解mp4文件格式,本文记录了ffmpeg中解封装mp4文件的基本实现。
  • 关键字:movFFmpegmp4

1 简介

  mp4文件格式是现如今网络上最常见的视频文件格式,其和mov等格式相同都是IOS Base File Format的实现版本,其文件格式都是基于box。
MP4

2 ff_mov_demuxer

  在FFmpeg中mp4文件解封装的实现在libavformat\mov.c文件中。在FFmpeg中每个封装格式都一个描述当前封装格式的结构体和其选项的AVClass,mp4个是对应的结构体分别为ff_mov_demuxer,mov_class
  mov_class描述了mp4文件的基本选项信息,mov_options是一个FFmpeg中内部定义的key-value列表,其中定义了FFmpeg中的基本选项。
  ff_mov_demuxer描述如何解封装一个mp4文件的,以及一些基本信息。该结构包含文件扩展名,选项列表,解封装的函数指针,标志位等信息。解封装时AVFormatContext都是通过操作函数指针读取文件信息,解封装文件。

static const AVClass mov_class = {
    .class_name = "mov,mp4,m4a,3gp,3g2,mj2",
    .item_name  = av_default_item_name,
    .option     = mov_options,
    .version    = LIBAVUTIL_VERSION_INT,
};

const AVInputFormat ff_mov_demuxer = {
    .name           = "mov,mp4,m4a,3gp,3g2,mj2",
    .long_name      = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),
    .priv_class     = &mov_class,
    .priv_data_size = sizeof(MOVContext),
    .extensions     = "mov,mp4,m4a,3gp,3g2,mj2,psp,m4b,ism,ismv,isma,f4v,avif",
    .flags_internal = FF_FMT_INIT_CLEANUP,
    .read_probe     = mov_probe,
    .read_header    = mov_read_header,
    .read_packet    = mov_read_packet,
    .read_close     = mov_read_close,
    .read_seek      = mov_read_seek,
    .flags          = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS | AVFMT_SHOW_IDS,
};

  ff_mov_class每一项的具体含义:

  • name:由,隔开的格式名称;
  • long_name:全称;
  • priv_class:私有的选项;
  • priv_data_size:私有数据的大小,一般为对应格式的Context,比如mov格式为MOVContext
  • extensions:扩展名,能够看到mov,```mp4````等格式公用同一个解封装器;
  • flag_internal:内部的标志符;
  • const char *mime_type;,隔开的mime_type;
  • read_probe:探测当前文件是那个类型的文件的函数指针,在avformat_find_stream_info用来探测当前文件是否为mp4文件,以及相关流信息;
  • read_header:读取格式header,初始化AVFormatContext的函数指针,avformat_open_input时调用,用来读取文件的基本信息;
  • read_packet:从文件中读取一个packet的函数指针,读取未解码的数据流的函数指针,在av_read_frame时调用;
  • read_close:关闭流,但是不涉及对应流的释放;
  • read_seek:seek到对应的位置,av_seek_frame时调用来seek到对应的位置;
  • flags;:操作文件个标志符,比如是否允许按照bytes seek等。

  解封装的基本流程与用AVFormatContext解封装的基本流程相同:
在这里插入图片描述

3 解封装具体流程

3.1 解封装涉及的结构体

  mp4解封装涉及的结构体比较多,这里挑选几个重点说下。mov.c定义了基础boxMOVAtom的基本结构定义,以及其他接个editlist相关的结构,比如MOVStts,MOVCtts,MOVElst

typedef struct MOVAtom {
    uint32_t type;
    int64_t size; /* total size (excluding the size and type fields) */
} MOVAtom;

  以及一些他描述mp4流,轨道索引等信息的结构体,比如MOVStreamContext,MOVFragmentIndex,MOVContext

3.1 mov_probe

  mov_probe会返回一个分值,该分值表示当前文件为对应文件格式的分值,分值越高该文件为对应格式的概率越高。在probe时,FFmpeg会根据文件的具体格式进行分辨,mov格式就是检测是否存在某个box,如果无法检测到文件符合对应格式,就会退而求其次以扩展名作为依据。所以就会出现有时候检测错误的情况,比如一个随机的mp3文件,其扩展名为mp3,FFmpeg会根据mp3文件进行解封装。解码时并不是每一片都能解码成功,有几率部分片段能够解码正常,但是解码出来的数据是异常的。

#define AVPROBE_SCORE_EXTENSION  50 ///< score for file extension
#define AVPROBE_SCORE_MIME       75 ///< score for file mime type
#define AVPROBE_SCORE_MAX       100 ///< maximum score

  mov_probe探测流文件的伪代码如下,这里输入被简化为指针```p````:

int mov_probe(int *p){
    int score = 0, offset = 0, moov_offset = -1;
    while(1){
        int64_t size = AV_RB32(p + offset);              //从当前流的位置读取当前box的大小,伪代码不考虑largesize的情况
        char tag[4] = AV_RL32(p + offset+ 4)            //从接下来的内存中读取tag
        switch(tag){
            case "moov":moov_offset = offset + 4;
            case "mdat":
            case "pnot":
            case "udta":
            case "ftyp":
                if(tag == "ftyp" && tag in ["jp2 " "jpx " "jxl "]){
                    score = std::max(score, 5);
                }else{
                    score = AVPROBE_SCORE_MAX;
                }
                break;
            case "ediw":
            case "wide":
            case "junk":
            case "pict":
                score = std::max(score , AVPROBE_SCORE_MAX - 5);break;
            case "skip":
            case "uuid":
            case "prfl":
                score = std::max(score, AVPROBE_SCORE_EXTENSION);break;
        }
        offset += size
    }

    if(score > AVPROBE_SCORE_MAX - 50 && moov_offset != -1){
        /* moov atom in the header - we should make sure that this is not a
         * MOV-packed MPEG-PS */
        offset = moov_offset;

        while (offset < (len(p) - 16)) { /* Sufficient space */
               /* We found an actual hdlr atom */
            if (AV_RL32(p->buf + offset     ) == MKTAG('h','d','l','r') &&
                AV_RL32(p->buf + offset +  8) == MKTAG('m','h','l','r') &&
                AV_RL32(p->buf + offset + 12) == MKTAG('M','P','E','G')) {
                av_log(NULL, AV_LOG_WARNING, "Found media data tag MPEG indicating this is a MOV-packed MPEG-PS.\n");
                /* We found a media handler reference atom describing an
                 * MPEG-PS-in-MOV, return a
                 * low score to force expanding the probe window until
                 * mpegps_probe finds what it needs */
                return 5;
            } else {
                /* Keep looking */
                offset += 2;
            }
        }
    }
    return score;
}

  

3.2 mov_read_header

  mov_read_header是在avforamt_open_input时调用,解析mp4文件的基本信息。经过此操作基本上从AVForamtContext中和封装格式相关的信息比如iformat,流信息等基本上都已经检测到。
  mov_read_header的实现过程。首先,校验一些参数,不符合要求就会返回错误。然后调用mov_read_default解析流文件中的atom box,从box中读取相关的信息写入到MOVContext中。

    /* check MOV header *///不断嵌套读,直到读到moov box未知
    do {
        if (mov->moov_retry)
            avio_seek(pb, 0, SEEK_SET);
        if ((err = mov_read_default(mov, pb, atom)) < 0) {
            av_log(s, AV_LOG_ERROR, "error reading header\n");
            return err;
        }
    } while ((pb->seekable & AVIO_SEEKABLE_NORMAL) && !mov->found_moov && !mov->moov_retry++);

  mov_read_default中就是不断嵌套读当前atom box内部的所有box然后解析,根据type判断是否为符合要求的box,符合的话就会调用对应的解析函数去解析。结合上面的do...while可以理解这里采用的是深度优先的解析方式。具体的解析函数是下面的一个静态函数表格,函数内会通过for循环去寻找是否为符合要求的box然后解析。说实话这样效率感人,索引表感觉更合理。

static const MOVParseTableEntry mov_default_parse_table[] = {
{ MKTAG('A','C','L','R'), mov_read_aclr },
{ MKTAG('A','P','R','G'), mov_read_avid },
{ MKTAG('A','A','L','P'), mov_read_avid },
{ MKTAG('A','R','E','S'), mov_read_ares },
{ MKTAG('a','v','s','s'), mov_read_avss },
...
{ MKTAG('m','d','c','v'), mov_read_mdcv },
{ MKTAG('c','l','l','i'), mov_read_clli },
{ MKTAG('d','v','c','C'), mov_read_dvcc_dvvc },
{ MKTAG('d','v','v','C'), mov_read_dvcc_dvvc },
{ 0, NULL }
};}

  经过上面的步骤,流的基本信息已经存储在MOVContextMOVStreamContext中,之后就是将解析出来的信息进行处理或者写到AVFormatContext中。比如从side_data中读取转换矩阵,然后解析当前视频的旋转角,读取chatper,timebase等等。

3.3 mov_read_packet

  mov_read_packet会在avformat_find_stream_infoav_read_frame内被调用。前者只会调用几次用来确认流数据的详细信息,而后是是从流中读取未解码的数据。

  首先会调用mov_find_next_sample根据当前读取的sample,以及其他时间戳相关的信息解析出下一帧要读取的时间戳。并进行一些size相关的检查,校正要读取的sample的大小以及改变全局的索引(FFMpeg内部的iformat有保存全部的pos索引来表示当前读取到的位置)。

    sample = mov_find_next_sample(s, &st);
    if (!sample || (mov->next_root_atom && sample->pos > mov->next_root_atom)) {
        if (!mov->next_root_atom)
            return AVERROR_EOF;
        if ((ret = mov_switch_root(s, mov->next_root_atom, -1)) < 0)
            return ret;
        goto retry;
    }

  然后就是根据标志位来判断当前packet是否要丢弃,调用av_get_packet读取数据,在进行一些size上的校正后,调用avio_read直接读文件。而具体的读取当然不是一次性读完,因此mov中的数据是按照box存储的,因此会一直读取到满足预期的大小或者报错为止。

if (st->codecpar->codec_id == AV_CODEC_ID_EIA_608 && sample->size > 8)
    ret = get_eia608_packet(sc->pb, pkt, sample->size);
else
    ret = av_get_packet(sc->pb, pkt, sample->size);

  最后就是填充packet的sidedata,以及更新ctts,stsc等相关的索引,以及一些善后的工作。

3.4 mov_read_seek

  seek的实现比较简单,大部分为计算时间戳,更新索引,调整ctts,stsc索引等内容。

3.5 mov_read_close

  mov_read_close是在avformat_close_input时调用,其实现比较简单就是关闭流释放各种context。

static int mov_read_close(AVFormatContext *s)
{
    MOVContext *mov = s->priv_data;
    int i, j;

    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];
        MOVStreamContext *sc = st->priv_data;

        if (!sc)
            continue;

        av_freep(&sc->ctts_data);
        for (j = 0; j < sc->drefs_count; j++) {
            av_freep(&sc->drefs[j].path);
            av_freep(&sc->drefs[j].dir);
        }
        av_freep(&sc->drefs);

        sc->drefs_count = 0;

        if (!sc->pb_is_copied)
            ff_format_io_close(s, &sc->pb);         //内部就是调用io_close

        sc->pb = NULL;
        av_freep(&sc->chunk_offsets);
        av_freep(&sc->stsc_data);
        av_freep(&sc->sample_sizes);
        av_freep(&sc->keyframes);
        av_freep(&sc->stts_data);
        av_freep(&sc->sdtp_data);
        av_freep(&sc->stps_data);
        av_freep(&sc->elst_data);
        av_freep(&sc->rap_group);
        av_freep(&sc->display_matrix);
        av_freep(&sc->index_ranges);

        if (sc->extradata)
            for (j = 0; j < sc->stsd_count; j++)
                av_free(sc->extradata[j]);
        av_freep(&sc->extradata);
        av_freep(&sc->extradata_size);

        mov_free_encryption_index(&sc->cenc.encryption_index);
        av_encryption_info_free(sc->cenc.default_encrypted_sample);
        av_aes_ctr_free(sc->cenc.aes_ctr);

        av_freep(&sc->stereo3d);
        av_freep(&sc->spherical);
        av_freep(&sc->mastering);
        av_freep(&sc->coll);
    }

    av_freep(&mov->dv_demux);
    avformat_free_context(mov->dv_fctx);
    mov->dv_fctx = NULL;

    if (mov->meta_keys) {
        for (i = 1; i < mov->meta_keys_count; i++) {
            av_freep(&mov->meta_keys[i]);
        }
        av_freep(&mov->meta_keys);
    }

    av_freep(&mov->trex_data);
    av_freep(&mov->bitrates);

    for (i = 0; i < mov->frag_index.nb_items; i++) {
        MOVFragmentStreamInfo *frag = mov->frag_index.item[i].stream_info;
        for (j = 0; j < mov->frag_index.item[i].nb_stream_info; j++) {
            mov_free_encryption_index(&frag[j].encryption_index);
        }
        av_freep(&mov->frag_index.item[i].stream_info);
    }
    av_freep(&mov->frag_index.item);

    av_freep(&mov->aes_decrypt);
    av_freep(&mov->chapter_tracks);

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/580799.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

复合查询.

基本查询 查询工资高于500或岗位为MANAGER的雇员&#xff0c;同时还要满足他们的姓名首字母为大写的J select * from EMP where (sal>500 or jobMANAGER) and ename like J%;按照部门号升序而雇员的工资降序排序 select * from EMP order by deptno, sal desc;使用年薪进…

为建筑物的供暖系统实施MPC控制器的小型项目(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【网络】- TCP/IP四层(五层)协议 - 网际层(网络层) - 路由控制

目录 一、概述二、路由表(Routing table)三、最长匹配、默认路由、特定主机路由四 、IP数据报路由过程五、路由聚合 一、概述 网际协议 IP 大致分为三大作用模块&#xff0c; ①IP寻址、 ②路由&#xff08;最终节点为止的转发&#xff09; 、③IP分包与组包。前面两篇文章讨论…

dpdk ip分片报文重组处理

dpdk ip报文重组及分片API及处理逻辑介绍 DPDK的分片和重组实现零拷贝&#xff0c;详细介绍可以参阅DPDK分片与重组用户手则 相关数据结构 /** Fragmented packet to reassemble.* First two entries in the frags[] array are for the last and first fragments.*/ struct …

【测试平台开发】

【测试平台开发】 一、 后端开发 1、常见的技术架构与组件 语言&#xff1a; 项目注重高并发&#xff1a;选用go 注重区块链&#xff1a;选用go、rust(主打高性能) 大型浏览网站&#xff08;如电商&#xff09;&#xff1a;Java 技术架构与组件&#xff1a; 前端技术架构&a…

多元回归预测 | Matlab白鲸算法(BWO)优化BP神经网络回归预测,BWO-BP回归预测,多变量输入模型

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元回归预测 | Matlab白鲸算法(BWO)优化BP神经网络回归预测,BWO-BP回归预测,多变量输入模型 评价指标包括:MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分源码 %--------------…

供收藏:国内各种免费可用ChatGPT实测(兼验伪) 版本不断更新补充 更新日期:2023/05/28

文章目录 供收藏&#xff1a;国内各种免费可用ChatGPT实测(兼验伪) 版本不断更新补充 更新日期&#xff1a;2023/05/28国内大厂的人工智能语言模型国内可访问的ChatGPT资源&#xff08;创业公司&#xff09;ZelinAI&#xff08;国内可直接访问的ChatGPT&#xff09;注册邀请码网…

2023全国大学生信息安全竞赛(ciscn)初赛题解

战队信息 安全知识 甚至不用看视频&#xff0c;百度就有答案。除了那个最新的美国时政&#xff0c;其它的ChatGPT就能回答。 Misc 签到卡 关注公众号&#xff0c;根据提示&#xff0c;直接print(open(‘/flag’).read())&#xff1a; 国粹 脑洞题&#xff0c;给的题目原图…

【LeetCode热题100】打卡第6天:正则表达式匹配

文章目录 正则表达式匹配⛅前言&#x1f512;题目&#x1f511;题解 正则表达式匹配 ⛅前言 大家好&#xff0c;我是知识汲取者&#xff0c;欢迎来到我的LeetCode热题100刷题专栏&#xff01; 精选 100 道力扣&#xff08;LeetCode&#xff09;上最热门的题目&#xff0c;适合…

从原理总结chatGPT的Prompt的方法

一 什么是chatGPT chatGPT全称是Generative Pre-trained Transformer&#xff0c;它是一种专注于对话生成的语言模型&#xff0c;可以根据用户的文本输入&#xff0c;做出相应的智能回答。chatGPT是由OpenAI于2018年研发的语言模型&#xff0c;其中OpenAI是于2015年由特斯拉的…

Postman新手教程

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE初阶 目录 文章目录 一、Postman背景介绍 二、Postman下载地址 三、Postman简单使用 一、Postman背景介绍 Postman是Chrome插件类产品中的代表产品之一&#xff0c;这款网页调试工具不仅可以调…

位图布隆过滤器

位图 概念&#xff1a;就是用每一位来存放某种状态&#xff0c;适用于海量数据&#xff0c;数据无重复的场景。通常是用来判断某个数据存不存在的。 比如&#xff0c;需要在40亿个整数中&#xff0c;查看某个数是否存在&#xff1f; 1G1024M*1024KB*1024B~10亿字节~80亿比特。…

k8s实战篇1-用minikube发布服务hello-minikube

1 安装minikube curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 sudo install minikube-darwin-amd64 /usr/local/bin/minikube 2 Install kubectl binary with curl on macOS 1 Download the latest release: curl -LO "h…

Eclipse Ⅶ

哈喽各位&#xff0c;今天继续分享第七部分的内容&#xff0c;喜欢可以点赞和收藏&#xff0c;这是我的动力来源hahahhah&#xff01; 今天谈谈Eclipse 生成 jar 包、Eclipse 关闭项目以及Eclipse 编译项目。 废话不多说&#xff0c;开始咯&#xff01; Eclipse 生成 jar 包…

Linux常见指令-2

我们本期继续学习Linux基本指令&#xff0c;没有看过第一期的小伙伴建议先看第一期 (4条消息) Linux常见指令-1_KLZUQ的博客-CSDN博客 目录 15.时间相关指令 16.cal指令 17.find指令 18.grep指令 19.zip/unzip指令 20.tar指令 21.bc指令 22.uname –r指令 22.重要的几…

PMP考试总结-2023-05-27

目录 前言 为什么会参加PMP考试&#xff1f; 那么什么是PMP&#xff1f; Plan 目标&#xff1a; 方式方法&#xff1a; 达标标准&#xff1a; Do 执行内容&#xff1a; Check 执行效果 计划的复盘 一、考试前及当天的计划&#xff1a; 二、整个备考计划&#xff…

如何正确地使用ES6提高我们的代码质量

前言 相信每个前端工程师&#xff0c;或者了解前端的人都知道ES6。它是js的一次巨变&#xff0c;它为我们开发js前端项目的时候带来了许多更好的去书写代码的方式。但是很多时候我们可能都没有过度地去关注优化代码这一块内容&#xff0c;哪怕有也只是注意到了一些比较大众化&…

Linux进程概念引入

文章目录 冯诺依曼体系操作系统概念设计目的定位系统调用和库函数的概念 进程概念描述进程PCBtask_struct内容分类 组织进程查看进程通过系统调用获取进程标识符通过系统调用创建进程 冯诺依曼体系 目前我们的计算机基本都是遵守冯诺依曼体系的&#xff0c;在冯诺依曼体系中&am…

[Kubernetes] - RabbitMQ学习

1.消息队列 消息&#xff1a; 在应用间传送的数据队列&#xff0c;先进先出 1.2. 作用 好处&#xff1a;解耦&#xff0c; 容错&#xff0c;削峰坏处&#xff1a;降低系统可用性&#xff0c;系统复杂度提高&#xff0c;一致性问题&#xff1b; RabbitMQ组成部分&#xff1a…

云上高校导航 导入 与 配置教程

开通 云开发 功能&#xff08;首月免费&#xff0c;次月19.9&#xff09;&#xff0c;激活 云数据库、云存储和云函数 功能。 将 项目 文件夹下 最新版本的 文件夹下的 Cloud-based_University_Navigation 整个文件夹 复制到项目路径下&#xff08;比如 D:\WeChatProjects&…