视频和视频帧:FFMPEG CPU解码API介绍

news2025/1/16 18:50:38

写在前面


本文将介绍的如何用FFMPEG API做视频解码。

视频解码,是将压缩后的视频(压缩格式如H264)通过对应解码算法还原为YUV视频流的过程;在计算机看来,首先输入一段01串(压缩的视频),然后进行大量的浮点运算,最后再输出更长的一段01串(还原的非压缩视频)。计算机内部可以进行浮点数计算的部件是CPU,目前市场上涌现了一批GPU和类GPU芯片,如Nvidia、海思芯片甚至Intel自家的核显。利用前者进行解码一般称为“软解码”,后者被称为“硬解码”,如果没有特殊指定,FFMPEG是用CPU进行解码的,即软解

本文将介绍的是软解,也就是FFMPEG最通用的做法。

如果对视频基础懵懂的同学,建议先阅读本文的前序文章:

本文将介绍以下内容:

  1. FFMPEG3.3以上的新版API介绍;

  1. FFMPEG解码的通用流程以及每个步骤涉及的API详解,这一部分比较硬核,会涉及代码和api底层的解释;

  1. FFMPEG解码的其他注意点。


I. FFMPEG API变化

了解一门语言/工具最好的办法就是阅读他的API文档/开发者手册,然而,计算机领域大多数语言/技术的官方文档都是英文,对国内初学者而言,以此入门,不推荐。笔者推荐的学习路线是:简单的入门博客 -> 专业人士的相关博客(比如业内的雷神的博客) -> 官方文档 -> 参与相关开源项目,大致如此,不必较真。程序员圈内严(gang)谨(jing)人士是真多,所以在一些点上必须陈述清楚。

网上关于FFMPEG入门的资料特别多,但是笔者发现许多博客已经比较陈旧了,新版本(指FFMPEG3.3及以上的版本)的API有较大的变化,本文出现的代码使用的都是新的API。

FFMPEG官方GIT上罗列了API的变化,读者可以看APIchanges;FFMPEG的官方开发者手册见Developer Documentation。不过,在实际开发中,就算真的在新版本上用了旧的API,编译和运行时也不会有问题,只不过在编译时会提示“is deprecated”类似warnning提示。

笔者在后续出现的代码中会注释写明某个API是新版本的,还是沿袭了旧版本的。


II. FFMPEG解码套路

“自古深情留不住,唯有套路得人心”,和很多工具一样,FFMPEG解码也是有套路的,这里先搬出业内大人物雷神(雷霄骅,可以从知乎的帖子-如何看待雷霄骅之死?-知道雷神在流媒体技术上做出的贡献之大,叹息缅怀)当年的解码流程图:

FFMPEG解码流程(雷神版)

上图出自其CSDN博客:最简单的基于FFmpeg的解码器-纯净版。15年的文章,15年,FFMPEG还只有2.x,因此这个解码流程图目前已经不适用了。笔者结合网上最新的资料和自己实际开发使用API的情况,整理了出以下解码流程图(虚线框是可选部分):

FFMPEG解码流程

接下来详细解释FFMPEG解码的通用流程。

解码Step1. 连接和打开视频流

连接和打开视频流必然是后续进行解码的关键,该步骤对应的API调用为:

  • int avformat_network_init(void)官方文档建议加上avformat_network_init(),虽然这个不是必须的。笔者推荐阅读FFMpeg 源码分析(2)avformat_network_init()深入了解该函数的内部源码,说白了,该函数会初始化和启动底层的TLS库,这也就解释了网上很多资料关于如果要打开网络流的话,这个API是必须的的说法了。

  • int avformat_open_input(AVFormatContext** ps, const char* filename, AVInputFormat* fmt, AVDictionary ** options)avformat_open_input()官方说法是“打开并读取视频头信息”,该函数较为复杂,笔者还没有完全吃透他的每一行源码,大致了解其功能为AVFormatContext内存分配。如果是视频文件,会探测其封装格式并将视频源装入内部buffer中;如果是网络流视频,则会创建socket等工作连接视频获取其内容,装入内部buffer中。最后读取视频头信息。源码深入阅读的话,笔者推荐雷神的FFmpeg源代码简单分析:avformat_open_input(),以及简书上的Avformat_open_input函数的分析之--HTTP篇。

以上,就完成了文件流的初步初始化工作。

补充说明:这一个步骤结束后,就可以调用APIav_dump_format()打印文件的基本信息了,如文件时长、比特率、fps、编码格式等,信息大概如下:

Input #0, avi, from '${input_video_file_name}': Metadata: encoder : Lavf57.83.100 Duration: 00:10:00.00, start: 0.000000, bitrate: 4196 kb/s Stream #0:0: Video: h264 (High) (H264 / 0x34363248), yuvj420p(pc, bt709, progressive), 1920x1080, 4194 kb/s, 12 fps, 12 tbr, 12 tbn, 24 tbc

有读者可能会问:“网上看到的大部分博客,第一句调用的是av_register_all(),该函数的作用是注册所有的编解码器、复用/解复用组件等,你为什么不提呢?” 原因很简单,函数av_register_all()在FFMPEG4.0及以上版本中被弃用了,见av_register_all() has been deprecated in ffmpeg 4.0。(这都9102年了,赶快使用4.0以上的FFMPEG吧!)

解码Step2. 定位视频流数据

无论是离线的还是在线的视频文件,相对正确的称呼应该是“多媒体”文件。要知道,这些文件一般不止有一路视频流数据,可能同时包括多路音频数据、视频数据甚至字幕数据等。因此我们在做解码之前,需要首先找到我们需要的视频流数据。

  • int avformat_find_stream_info(AVFormatContext** ic, AVDictionary ** options)avformat_find_stream_info()进一步解析该视频文件信息,主要是指AVFormatContext结构体的AVStream。从雷神的FFmpeg源代码简单分析:avformat_find_stream_info()文章可以了解到,该函数内部已经做了一套完整的解码流程,获取了多媒体流的信息。请注意,一个视频文件中可能会同时包括视频文件和音频文件等多个媒体流,这也就解释了为什么后续还要遍历AVFormatContext的streams成员(类型是AVStream)做对应的解码。

注意,笔者认为视频流的基本信息,如fps、码率、视频长度等信息是在第一步的avformat_open_input打开视频头文件步骤中获取到的。但是如果视频文件是h264/mpeg裸流数据,可能没有头信息,无法获取。笔者在这里记一笔,后面继续深入研究。

解码Step3. 准备解码器codec

codec是FFMPEG的灵魂,顾名思义,解码必须由解码器完成。准备解码器的步骤包括:寻找合适的解码器 -> 拷贝解码器(optiona)-> 打开解码器。

  • 寻找合适的解码器 - AVCodec\* avcodec_find_decoder(enum AVCodecID id)avcodec_find_decoder是从codec库内返回与id匹配到的解码器。另外还有一个与其对应的寻找解码器的API-AVCodec* avcodec_find_decoder_by_name(const char* name),这个函数是从codec库内返回名字为name的解码器,一般在硬解码时,会通过应解码器名字指定应解码器(硬解码的流程会更复杂些,往往还需要打开相关硬件的底层库驱动等,本文不会涉及)。问题来了,id要怎么获取呢?上一步找到的AVStream中的成员变量codecpar->codec_id就是了,codecpar类型为AVCodecParameters。网上很多资料上是codec->codec_id,codec类型为AVCodecContext,在FFMPEG3.4及以上版本中已经被弃用了,官方推荐使用codecpar。两者的区别,读者自行到FFMPEG官方doc上了解。

  • 拷贝解码器 - AVCodecContext\* avcodec_alloc_context3(const AVCodec\* codec)和int avcodec_parameters_to_context(const AVCodec\* codec, const AVCodecParameters\* par)avcodec_alloc_context3()创建了AVCodecContext,而avcodec_parameters_to_context()才真正执行了内容拷贝。avcodec_parameters_to_context()是新的API,替换了旧版本的avcodec_copy_context()。

  • 打开解码器 - avcodec_open2(AVCodecContext* avctx, const AVCodec* codec, AVDictionary ** options)源码阅读还是推荐雷神的FFmpeg源代码简单分析:avcodec_open2(),该函数主要服务于解码器,包括为其分配相关变量内存、检查解码器状态等。

以上,1-3的步骤,笔者统称为“解码初始化阶段”。至此,如果每一步的API返回值都OK的话,可以开始真正的解码工作了!

解码Step4. 解码

解码的核心是重复进行取包、拆包解帧的工作,这里说的包是FFMPEG非常重要的数据结构之一:AVPacket,帧是其中同样重要的数据结构:AVFrame。

  • AVPacket该数据结构的介绍和分析网上资料很多,推荐阅读FFMPEG结构体:AVPacket解析,简言之,该结构保存了解码,或者说解压缩之前的多媒体数据,包括流数据本身和附加信息。AVPacket是由函数int av_read_frame(AVFormatContext* s, AVPacket* pkt)获取得到的,该函数的具体实现在新版本中做了改良,确保每次取出的一定是完整的帧数据,推荐深入阅读雷神的ffmpeg 源代码简单分析 : av_read_frame()和FFMPEG 源码分析:av_read_frame。修正:AVPacket和GOP没有任何关系,仅仅是FFMPEG用以存储一段解码/解压缩之前的数据结构而已。笔者一度怀疑AVPacket其实是个GOP,但是一直没有找到官方或者“民间”有类似的说话;而且在做视频解码时,发现一个AVPacket解码出来一般只有一个frame,不符合H264中对GOP的定义。笔者在这里mark下,后续深度研究下。

  • AVFrame该数据结构的介绍和分析网上资料也不少,推荐阅读FFMPEG结构体分析:AVFrame,简言之,该结构保存了解码后,即解压缩后的帧本身的数据和附加信息。AVFrame在新版本中由函数int avcodec_send_packet(AVCodecContext* avctx, AVPacket* pkt)和int avcodec_receive_frame(AVCodecContext\* avctx, AVFrame\* frame)产生,前者真正地执行了解码操作,后者则是从缓存或者解码器内存中取出解压出来地帧数据。老版本中用的是avcodec_decode_video2(),目前已经被弃用。此外需要注意的是,一般而言,一次avcodec_send_packet()对应一次avcodec_receive_frame(),但是也会有一次对应多次的情况。这个关系参考一个AVPacket对应一个或多个AVFrame。


III. FFMPEG解码的其他注意点

FFMPEG的处理套路直接简单,可是想要应用到实际中,仅仅了解套路,还不足够!那么,还需要注意哪些才可以搭建一套可实际使用的解码呢?

1. 帧转码

软解得到的帧格式是YUV格式的,具体格式可以存放在AVFrame的format(类型为int)成员中,打印出数值后,再到AVPixelFormat中查找具体是哪个格式。一般而言,大多是实际使用场景中,最常用的是RGB格式,因此接下来就以RGB举例说明如何做帧转码。注意,其他格式的做法也是一样的。

核心是调用int sws_scale(struct SwsContext* c, ...),该函数接受的参数有一大堆,具体参数和对应的含义建议查询官网,该函数主要做了尺寸缩放(scale)和转码(transcode)工作,源码阅读推荐雷神的FFmpeg源代码简单分析:libswscale的sws_scale()。

第一个参数struct SwsContext* c,需要调用struct SwsContext* sws_getContext(..., enum AVPixelFormat dstFormat, ...)创建,该函数也是一堆参数,请自行官网查询,其中参数enum AVPixelFormat dstFormat,指定了目标格式,随后调用sws_scale()后得到的目标帧就是dstFormat格式的。

因此,如果你的目标格式是RGB,只需要指定dstFormat为需要的RGB类型即可,FFMPEG中的RGB系列的有AV_PIX_FMT_RGB24、AV_PIX_FMT_ARGB等。

2. 帧输出

除了考虑输出帧的格式,另一个实际的问题是:解出来的帧放在哪儿,怎么放?

放在哪儿的问题看个人需求,有些可能直接dump到磁盘,保存成本地视频文件或者一帧一帧的图片;在有些应用场景,解码可能只是系统最前端模块,此时可能需要存放到共享内存或者系统内存。

随之而来的是怎么放的问题,前者如保存成视频,可以通过fopen()创建视频文件,接着再解码的循环内部调用fwrite()将帧数据保存到文件,最后用fclose()关闭即可;后者一定涉及到需要把AVFrame的帧数据转化成uint8_t*/unsigned char*的操作,可以调用API函数int av_image_copy_to_buffer()达到这个目的。

还有一个API函数int av_image_fill_arrays(),它是把AVFrame的data成员关联到某个地址空间,如果有了av_image_copy_to_buffer,那么该函数是否还必须,这个问题笔者目前还没有了解,这里记一笔吧。

3. 刷新缓冲区

在实际做解码工作时一定要注意刷新缓冲区!!!如果不这么做的话,最后解码出来的帧数目和实际视频帧数是对不齐的,会发现总是少了一些尾帧。原因就是FFMPEG内部有一个buffer,需要再把buffer的帧刷出来。其实做法也很简单,在解码的最后,将packet的data和size成员分别赋值为nullptr和0,这个时候缓冲区所有的帧数据都会被放进一个packet中,因此最后再进行一次解码就可以拿出所有的帧数据了。

4. 资源释放

FFMPEG非常重要的一点,有些申请的变量一定要在结束前显示释放。具体哪些API的调用需要显示释放,在官方文档上都有详细的说明。这里补充本样例代码的变量释放部分:

了解了以上几点,整个解码流程是真正搭建起来了。最后提AVDictionary,一个名称为可选项,但是实际上非常有用的结构。

5. options - AVDictionary

在第二章的时候频繁出现了AVDictionary** options参数,尽管这个参数可以被置为nullptr,但实际上这个参数的用处还是挺大的,比如设置FFMPEG缓存区大小、探测码流格式的时间、最大延时、超时时间、以及支持的协议的白名单等。关于该方法的源码,笔者推荐阅读FFmpeg接口-AVDictionary。

原文 https://zhuanlan.zhihu.com/p/64739970

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

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

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

相关文章

用 @types 前缀的包是什么?有什么用?

前言 解决过 TypeScript 的项目大概都是从两个方向,Vue3 方向和 React Native 方向,而在 React Native 方向上我经常会遇到一个烦人的错误 Could not find a declaration file for module ‘juejin-type-study’. ‘d:/fe-project/nodejs/types-study/n…

看懂这篇文章-你就懂了信息安全的密码学

一、前言一个信息系统缺少不了信息安全模块,今天就带着大家全面了解并学习一下信息安全中的密码学知识,本文将会通过案例展示让你了解抽象的密码学知识,阅读本文你将会有如下收获: 熟悉现代密码学体系包含的主流密码技术 掌握Base…

SignalR 实时通讯

SignalR 实时通讯1.SignalR1.1.SignalR 简介1.2.SignalR 功能1.3.传输1.4.中心2.服务器2.1.配置中心2.2.上下文对象2.3.客户端对象2.4.创建2.5.中心功能实现4.客户端6.案例演示(DotNet 客户端)1.SignalR 1.1.SignalR 简介 SignalR 是一个开放源代码库&a…

内容感知、AI融合:让实景三维看山是山,看水是水

实景三维具备还原客观物理世界的优势性,但也正由于部分真实性的欠缺备受争议。这是因为传统的三维建模软件大多基于像元的匹配与计算的逻辑,对地物进行无差别的重建处理,最终生成的模型看起来扭曲怪异、残缺变形。常见的模型缺陷有&#xff1…

2022 OpenCV Spatial AI大赛前三名项目分享,开源、上手即用,优化了OAK智能双目相机的深度效果。

编辑:OAK中国 首发:oakchina.cn 喜欢的话,请多多👍⭐️✍ 内容可能会不定期更新,官网内容都是最新的,请查看首发地址链接。 ▌前言 Hello,大家好,这里是OAK中国,我是助手…

深圳居住证申领指南

打开广东政务服务网,在首页搜索【深圳经济特区居住证申领】在搜索结果中可以发现有如下链接,点击在线办理 会转到登陆界面,直接使用个人登录并用微信扫描登录 根据提示进行手机登录验证。 完成登录认证之后会自转到深圳经济特区居住证申领界…

二分查找由浅入深--算法--java

二分查找写在开头算法前提:算法逻辑算法实现简单实现leftright可能超过int表示的最大限度代码分析和变换更多需求:求索引最小的值java二分API应用基础题思考难度方法写在开头 二分查找应该是算比较简单的这种算法了,我本以为还可以。但有时候…

Word处理控件Aspose.Words功能演示:使用 Java 比较 MS Word 文档

Aspose.Words 是一种高级Word文档处理API,用于执行各种文档管理和操作任务。API支持生成,修改,转换,呈现和打印文档,而无需在跨平台应用程序中直接使用Microsoft Word。此外, Aspose API支持流行文件格式处…

动态规划初阶-爬楼梯问题

示例1: 输入:cost [10,15,20] 输出:15 解释:你将从下标为 1 的台阶开始。 - 支付 15 ,向上爬两个台阶,到达楼梯顶部。 总花费为 15 。示例2: 输入:cost [1,100,1,1,1,100,1,1,10…

使用Docker安装MongoDB,整合SpringBoot

使用Docker安装MongoDB MongoDB 和 MySQL 都是常用的数据库管理系统,但它们的设计目标不同,因此在某些方面的性能表现也有所不同。 MongoDB 是一个文档型数据库,它采用了面向文档的数据模型,支持动态查询和索引,适合…

Docker部署实战

文章目录Docker部署应用准备制作容器镜像启动容器上传镜像docker exec数据卷(Volume)声明原理实践Docker部署 应用准备 这一次,我们来用 Docker 部署一个用 Python 编写的 Web 应用。这个应用的代码部分(app.py)非常…

【同步、共享和内容协作软件】上海道宁与​ownCloud让您的团队随时随地在任何设备上轻松处理数据

ownCloud是 一款开源文件同步、共享和 内容协作软件 可让团队随时随地 在任何设备上轻松处理数据 ownCloud开发并提供 用于内容协作的开源软件 使团队能够轻松地无缝 共享和处理文件 而无需考虑设备或位置 开发商介绍 ownCloud成立于2010年,是一个托管和同…

设计模式-笔记

文章目录七大原则单例模式桥模式 bridge观察者模式 observer责任链模式 Chain of Responsibility命令模式 Command迭代器模式 Iterator中介者模式 Mediator享元模式 Flyweight Pattern组合模式 composite装饰模式 Decorator外观模式 Facade简单工厂模式工厂方法模式工厂抽象模式…

Postgresql中的unlogged table

在PG中,有一种表的类型为unlogged table,名如其字,该种类型的表不会写入wal日志中,所以在写入的速度上比普通的堆表快很多,但是该表在数据库崩溃的时候,会被truncate,数据会丢失,而且该表也不支…

Leetcode21. 合并两个有序链表

一、题目描述: 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1: 输入:l1 [1,2,4], l2 [1,3,4]输出:[1,1,2,3,4,4] 示例 2: 输入:l1 [], l2…

Java程序开发中如何使用lntelliJ IDEA?

完成了IDEA的安装与启动,下面使用IDEA创建一个Java程序,实现在控制台上打印HelloWorld!的功能,具体步骤如下。 1.创建Java项目 进入New Project界面后,单击New Project选项按钮创建新项目,弹出New Project对话框&…

【k8s】Kubernetes的学习(1.k8s概念和架构)

目录 1.首先要知道,Kubernetes为什么简称为k8s? 2.Kubernetes概述 2.1 kubernetes基本介绍 2.2 kubernetes的特性 2.3 kubernetes集群架构组件 2.3.1 Master (主控节点) 2.3.2 node (工作节点) 2.4 k8s核心概念 2.4.1 Pod 2.4.2 controller 2.4.3 Se…

操作系统权限提升(十九)之Linux提权-SUID提权

系列文章 操作系统权限提升(十八)之Linux提权-内核提权 SUID提权 SUID介绍 SUID是一种特殊权限,设置了suid的程序文件,在用户执行该程序时,用户的权限是该程序文件属主的权限,例如程序文件的属主是root,那么执行该…

redux-saga

redux-saga 官网:About | Redux-Saga 中文网:自述 Redux-Saga redux-saga 是一个用于管理 异步获取数据(副作用) 的redux中间件;它的目标是让副作用管理更容易,执行更高效,测试更简单,处理故障时更容易… …

C#:Krypton控件使用方法详解(第十讲) ——kryptonColorButton

今天介绍的Krypton控件中的kryptonColorButton,下面介绍这个控件的外观属性:Cursor属性:表示鼠标移动过该控件的时候,鼠标显示的形状。属性值如下图所示:EmptyBorderColor属性:表示当所选颜色为空时&#x…