本文的代码下载地址:GitHub,编译环境是 Qt 5.15.2 跟 MSVC2019_64bit 。


解码相关的结构体如下:

1,AVCodecContext,这个结构体可以是 编码器 的上下文,也可以是 解码器 的上下文,两者使用的是同一种数据结构。

2,AVCodec,编解码信息。

3,AVCodecParameters,编解码参数。

4,AVPacket ,数据包(已编码压缩),这里面的数据通常是一帧视频的数据,或者一帧音频的数据。AVPacket 他本身是没有编码数据的,他只是管理编码数据。

5,AVFrame ,解码之后的 YUV 数据。AVFrame 跟 AVPacket 类似,都是一个管理数据的结构体,他们本身是没有数据的,只是引用了数据。


解码相关的API函数如下:

1,avcodec_alloc_context3,通过传递 AVCodec 编解码信息来初始化上下文。

2,avcodec_parameters_to_context,把流的 AVCodecParameters 里面的 编解码参数 复制到 AVCodecContext 。

3,avcodec_open2,打开一个编码器 或者 解码器。

4,avcodec_send_packet,往 AVCodecContext 解码器 发送一个 AVPacket 。

5,avcodec_receive_frame,从 AVCodecContext 解码器 读取一个 AVFrame


我需要着重讲一下 AVCodec 跟 AVCodecParameters 这两个结构体。

1,AVCodec 里面放的是 编解码信息 。

2,AVCodecParameters 里面放的是 编解码参数

怎么理解 编解码信息 跟 编解码参数

通常 AVCodec 是使用 avcodec_find_decoder 函数找出来的,你给这个函数一个 AVCodecID,他就能返回一个解码器指针给你。这是 引入 FFmpeg 库的时候,他初始化了一堆静态的编解码变量给你。

例如 AVCodecID 是 AV_CODEC_ID_H263 ,就会返回 263 相关的 AVCodec 指针, AVCodecID 是 AV_CODEC_ID_H264 ,就会返回 264 相关的 AVCodec 指针。

只要是用 H264 编码 的视频,使用的解码器信息都是一样的,用的是同一个 AVCodec,但是不同的视频文件,宽高,采样这些信息,肯定会有点不一样。

这些不一样的东西放在哪里呢?就是 AVCodecParameters

当 avformat_open_input 函数打开一个 MP4 的时候,编码器参数就会放在 codec_type 字段里,如下:

fmt_ctx->streams[0]->codecpar->codec_type

上面的 codec_type 就是一个 AVCodecParameters,只需要用 avcodec_parameters_to_context 函数把 codec_type 的参数复制给 AVCodecContext 即可,很方便。


请先下载本文代码仔细阅读一遍。

下面就来讲解如何初始化 AVCodecContext 跟 如何打开解码器,如果往解码器发数据,如何往解码器读取数据,请看下图:

这节的代码有非常多的重点。

1,用到了一个新的结构体 AVFrame , AVFrame 跟 AVPacket 类似,都是一个管理数据的结构体,他们本身是没有数据的,只是引用了数据。

2,打开一个解码器的流程是如下:

avcodec_alloc_context3 ➔ avcodec_parameters_to_context ➔ avcodec_find_decoder ➔ avcodec_open2

avcodec_find_decoder 这个函数是根据 解码器ID 来找到一个 AVCodec ,FFmpeg 在 codec_id.h 定义了很多解码器ID,如下:


avcodec_alloc_context3 跟 avcodec_open2 这两个函数都可以接受 AVCodec 参数,选一个函数来接受即可,千万不要往这两个函数传递不一样的 AVCodec 参数。

avcodec_parameters_to_context 这个函数主要用来做什么的呢?不调这个函数,直接打开解码器会有问题吗?这个后续解答。


3,视频解码流程是这样,往一个解码器发一个 AVPacket ,不一定立马就能拿到 一个 AVFrame,因为视频可能有 B 帧,不了解 B 帧的请自行 Google。

所以上面的代码逻辑是,发一个 AVPacket,就 死循环不断的读解码器,直到 返回 EAGAIN,这是因为有可能有多个 AVFrame 需要读取。

返回 EAGAIN 代表解码器需要更多的 AVPacket。如果已经读到 文件末尾,没有 AVPacket 能读出来了,怎么知道 解码器还有没有数据可以读?

这时候就需要一个 新的标记 AVERROR_EOF,当没有 AVPacket 能读出来的时候,就需要往 解码器发一个 size 跟 data 都是 NUL 的 AVPacket。这样解码器内部就知道已经没有更多的输入了,只要把剩下的 AVFrame 全部刷出去,刷完就会返回 AVERROR_EOF

音频解码器也是一样的流程。


上面的代码,运行之后会打印出以下信息。

上面打印的都是 AVFrame 这个结构体的字段,

1,width 跟 height 是图片的宽高,很明显 这是一个 1920 x 1080 的图片。

2,pts,此帧视频的显示时间,第一帧通常是 0 ,也有一些文件把第一帧视频的pts改大,例如改成1000,但第一帧音频的pts还是保留为0,这种视频开始只有声音,界面是黑屏的。

不过这种情况也看 播放器怎么处理,我用迅雷看的时候,迅雷播放器不管。还是一开始就显示视频。

所以这就是 音视频的复杂之处,各种格式的字段特别多,而且没有标准,就说这个 pts,是不是第一帧一定要是 0,也没规定。不同的播放器处理还不一样。

3,format,打印出来是 0 ,这个其实是 AVPixelFormat 枚举的类型,0 代表 AV_PIX_FMT_YUV420P ,如下:

上图这种写法比较省笔墨,第一个是 -1 ,后面的就会自己递增。


4,key_frame ,这个字段代表当前帧是不是关键帧,第一帧通常都是关键帧,

5,pict_type,这个是 AVPictureType 枚举的类型,1 代表 AV_PICTURE_TYPE_I 。

AV_PICTURE_TYPE_I 跟 key_frame 应该是一样的。


前面抛出了一个问题,avcodec_parameters_to_context 这个函数主要用来做什么的呢?不调这个函数,直接打开解码器会有问题吗?

我们来看一下这个函数的实现,如下:

从上图可以看出,就是把一些 宽高,像素格式 复制给 AVCodecContext,因此这个 avcodec_parameters_to_context 函数必须调。

前面的 avcodec_find_decoder 只是根据 codec_id 找到解码器,例如找到 H264 这个解码器了。但是要解码数据,还是需要知道码率,宽高之类的信息。

我实验了一下,如果不调 avcodec_parameters_to_context,会报以下错误。


从前面打印出来的 format 等于 0 ,已经知道解码出来的数据是 yuv420p 的格式,背景知识请看《YUV数据分析》跟《YUV数据分析》。

后面的 p 代表 AVFrame 里面的像数据的内存布局是 planar 格式,如果没有 p 就是 packed 格式,这是 FFmpeg 的命名习惯。

实际上像素是如何存储的是通过 AVPixFmtDescriptor 来确定的,推荐阅读《AVPixFmtDescriptor结构》

这里简单讲解一下 planar 跟 packed 的区别,AVFrame 里面有一个 data 字段,它指向的就是 YUV 的数据,他是一个指针数组,如下:

如果是 planar 格式,data[0] 会指向 Y 数据,data[1] 指向 U 数据,data[2] 指向 V 数据。那这些数据的大小是多少呢?还有一个 linesize 数组来管理大小。

我们来验证一下是不是这样样子。yuv420 的格式,U 或者 V 的大小应该是 Y的 4 分之一,如下:

printf(" Y size : %d \n",frame->linesize[0]);
printf(" U size : %d \n",frame->linesize[1]);
printf(" V size : %d \n",frame->linesize[2]);

上图是 2分之一,明显不对,为什么会这样呢?因为 linesize 里面是 stride 值。

stride 值 = 图像宽度 * 分量数 * 单位样本宽度 / 水平子采样因子 / 8

分量数就是通道数,因为是 planer ,所以分量数都是1,如果是 packed ,分量数是 3。单位样本宽度 是指 一个 U 占多少位,目前我们是 8 位,就是一个字节。

最重要的是水平采样因子,水平子采样因子指在水平方向上每多少个像素采样出一个色度样本。yuv420 水平方向其实是每两个像素采样一个色度样本,所以是水平采样因子是2。

有些朋友可能不太理解,如果是 2,UV 应该是 Y 的2分之一,不是 4分之一。那是因为还有一个垂直采样因子,yuv420的 垂直采样因子 也是 2 。但是 垂直采样因子 不会影响 stride 值。你就这样理解,UV 的水平采样因子是2,相对于 Y 他们的 height 也少了一半。水平跟垂直都少了一半,就是 4分之一。

所以 frame->linesize 并不是 UV 分量的真实数据大小,而是一个 stride 值。这个值可能还会内存对齐。

关注 stride 的知识,请看《图像步幅》


如果是 packed 格式 ,YUV 是交替存储,例如 YUVYUVYUV,这样子,这时候 data[0] 就指向所有的数据,而 linesize[0] 代表这帧图片的大小。

在 packed 格式 里 data[1] 跟 data[2] 都是没有用。

这里讲个扩展知识,即使是 planar 的视频格式,也只用到 3 个下标,为什么 AV_NUM_DATA_POINTERS 是 8 ,为什么要定义一个 8 大小的数组。不是浪费了其他 5 个指针吗?

这是因为 音频帧 跟 视频帧 共用 AVFrame 的结构,音频帧有些是 8 声道交响曲,有前有后,有左有右。

那是不是音频帧只能支持 8 声道?9声道 ffmpeg 就不支持了?也不是,他还有一个 extended_data 来指向高于 8 声道的数据。

这里不要误会,extended_data 并不指向第9声道数据,extended_data 跟 data 是一样的,只是你可以用 extended_data[8] 获取到第9声道的数据。

对于视频帧,最多就用了 3个下标,所以用 data 还是 extended_data 来获取数据都是一样的,但是 ffmpeg.c 使用的是 extended_data


下面就来加一些小功能,来融会贯通一下前面的知识。修改一下解码之后的 yuv 数据,然后保存成 yuv 图片文件。

从前面得知,data[0] 就是 Y 数据,那我们把前面 5 个像素的 Y 改成 FF,他就会变成白色。在 《YUV数据分析》一本中做过类似的实验,当时是手动修改。

由于 YUV 图片是裸数据,没有什么头部之类的格式,我们直接把 data 存储到文件即可。代码如下:

运行之后,用 7yuv 打开查看,可以看到顶部的 5 个像素被改了,如下:


这里讲一个扩展知识,上面的代码 fopen 打开文件的使用 使用了 wb,必须用二进制方式打开文件。要不会导致一些诡异的问题,例如图片拉伸,如下:

从上图可以看到,字体倾斜了,具体原因请看《fopen等:文本方式和二进制方式打开文件的区别》


FFmpeg 也有类似的命令,可以把视频转成图片,下面是提取第一帧转成图片,命令如下:

ffmpeg -i juren-30s.mp4 -vframes 1 -pix_fmt yuv420p -f rawvideo yuv420p-888.yuv

上面的命令是使用 yuv 复用器实现的。

ffmpeg 本身封装了一个 yuv 的复用器,不用自己打开文件写入文件。这样会容易出问题,例如不小心把 wb 写成 w

从函数使用角度来看 yuv 其实也是一种封装格式,跟 mp4 同一个级别的。yuv 的封装格式叫 rawvideo,代码文件是 libavforamt/rawenc.c

如下:

从上图可以看出,yuv 的复用器 rawenc.c 是跟 mp4 的· mov.c 同一个目录的。所以我把 yuv 复用器看成是一个伪复用器。

虽然 yuv 跟 mp4 在标准定义上是完全不同的东西,但是 FFmpeg 这么封装,使用上确实比较好用,可以让逻辑更加通用跟清晰。

FFmpeg 还有一些其他功能也是定义一个 伪工具 来实现的。


参考文章:

1,《色彩空间与像素格式》 - 叶余


推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:

Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

如何使用FFmpeg的解码器相关推荐

  1. FFmpeg视频解码器

    本博客是摘自雷霄骅大神的课程<基于 FFmpeg + SDL 的视频播放器的制作>课程 里的内容,非常适合音视频小白入门,在这里感谢雷神的指导! 目录 视频解码知识 VC下FFmpeg开发 ...

  2. 最简单的基于FFmpeg的解码器

    参考链接:<基于 FFmpeg + SDL 的视频播放器的制作>课程的视频_雷霄骅(leixiaohua1020)的专栏-CSDN博客_雷霄骅ffmpeg视频教程 如有不详细之处可以观看雷 ...

  3. 最简单的基于FFmpeg的解码器-纯净版(不包含libavformat)

    ===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...

  4. ffmpeg h264解码器分析sps(二)

    ffmpeg h264解码器解析sps下半部分 sps->mb_width = get_ue_golomb(gb) + 1; /*以宏块为单位的宽度*/ sps->mb_height = ...

  5. FFMPEG - 视频解码器

    视频解码知识 纯净的视频解码流程 压缩编码数据->像素数据. 例如解码H.264,就是"H.264码流->YUV". 一般的视频解码流程 视频码流一般存储在一定的封装格 ...

  6. ffmpeg硬件解码器的使用

    什么是硬件解码? 普通解码是利用cpu去解码也就是软件解码 硬件解码就是利用gpu去解码 为什么要使用硬件解码? 首先最大的好处 快 硬解播放出来的视频较为流畅,并且能够延长移动设备播放视频的时间: ...

  7. ffmpeg h264解码器提取

    ffmpeg包含了很多的音视频解码器,本文试图通过对ffmpeg的简单分析提取h264解码器. 使用ffmpeg解码可以参考ffmpeg源码下的doc/examples/decoding_encodi ...

  8. 【FFmpeg_SDL_MFC】1、FFMPEG视频解码器

    使用ffmpeg对封装格式mp4.ts等数据进行解码,介绍解码相关流程,函数接口.数据接口等. 一.前言 1.1目录 • 视频解码知识 • VC下FFmpeg开发环境的搭建 • 示例程序运行 • FF ...

  9. 最简单的基于FFMPEG+SDL的视频播放器:拆分-解码器和播放器

    ===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...

最新文章

  1. 团队博客(第四周)-“名字好难想”
  2. 将多个csv文件导入到pandas中并串联到一个DataFrame中
  3. puppet完全攻略(一)puppet应用原理及安装部署
  4. MongoDB分片实战(三):性能和优化
  5. 把UltraEdit改造成VC
  6. sunplus8202v 无线游戏手柄——续
  7. mongodb自定义字段_MongoDB哈希分片
  8. 苹果Mac专业级照片编辑器:RAW Power
  9. java中如何查看代码运行时间?
  10. unity全栈开发是什么意思_unity3D用什么语言开发好?
  11. 四、文件信息 五、进程环境
  12. LINKERD 2.11 中文实战手册
  13. 绿色智慧档案馆构想之智慧档案馆环境综合管控一体化平台
  14. Excel使用攻略(1)
  15. 【文献阅读】ChangeNet——变化检测网络(A. Varghese等人,ECCV,2018)
  16. 【转】Hive导入10G数据的测试
  17. 初阶指针(纯干货!!!)
  18. 一款GIF录屏小软件
  19. ppt_旋转抽奖_制作步骤
  20. Python兼职有哪些?

热门文章

  1. 常见的NoSQL数据库有哪些
  2. EMV规范(四)——读应用数据
  3. mysqladmin
  4. 老笔记本机械硬盘换固态装系统,再战10年!
  5. 你只会用 split?试试 StringTokenizer,性能可以快 4 倍!!
  6. 多表联查时的sql删除语句的写法,即级联删除,将相关联的数据级联删除
  7. 亚马逊测评自养号环境系统的介绍和用法
  8. Cased by: java.lang.ClassNotFoundException: com.google.common.util.concurrent.SettableFuture
  9. Mysql 循环更新
  10. linux do_irq 报错 代码,linux-2.6.38中断机制分析—asm_do_IRQ