前言
书接上回,我们介绍了ffmpeg的一些基础知识,使用方法,接下来介绍如何使用ffmpeg进行开发,所谓使用ffmpeg进行开发,就是依赖它的基础库,调用它的API来实现我们的功能。
当然要看懂文章需要一些C++的基础知识,能看懂基本语法,了解指针(一级指针/二级指针)的基本知识。
我们在上篇文章中也介绍了ffmpeg对于音视频操作的主要流程:
其实差不多每个阶段都通过对应的关键函数以及关键结构体来对应,因此,接下来先重点介绍一下这些重要的结构体,以及它的用法。
核心结构体
AVFormatContext
媒体信息存储结构,同时管理了IO音视频流对文件进行读写,相当于保存了音视频信息的上下文。
它在ffmpeg中的作用是非常重要的,在封装/解封装/编解码过程中都需要用到它。在程序中关于某个音视频的所有信息归根结底都来自于AVFormatContext。
结构体信息
1 | objectivec复制代码 |
以上只是一个简略的结构体成员信息展示,但是已经能体现它管理IO,数据流,保存媒体信息的的功能了,对于初学者而言,只需要关注nb_streams和streams这两个成员,表示流的数量以及流数组。后面我们需要通过流来获取对应的信息。
使用的基本方法
输入过程
- 申请内存创建结构体
1 | ini复制代码// 可以,但一般没必要 |
但是一般在开发中并不需要开发者手动创建结构体,而是在读取文件的接口中通过库自动创建即可
1 | c复制代码AVFormatContext *formatContext = NULL; |
注意我们需要在avformat_open_input函数中传入正确的媒体文件路径,这样才能正确读取到文件头的信息。
- 为防止获取不到文件头信息,可以尝试进一步获取音视频流的信息
1 | c复制代码if (avformat_find_stream_info(formatContext, NULL) < 0) { |
avformat_open_input即使成功调用,也不一定能获取到文件头信息,因为可能有的媒体格式没有文件头?哈哈,所以一般继续调用avformat_find_stream_info可以获取正确的信息
- 读取数据包:确认有音视频数据之后,可以通过av_read_frame把数据流读取到AVPacket中
1 | scss复制代码// 此处主要涉及解码过程,可以略过,知道解码过程也需要传递formatContext信息即可 |
- 关闭输入
1 | scss复制代码avformat_close_input(&formatContext); |
avformat_close_input会关闭输入流,同时释放AVFormatContext结构体
输出过程
上面介绍的主要输入过程中使用AVFormatContext的基本方式,那么输出过程是否一致呢?函数调用上略有区别。
- 创建输出音视频格式的AVFormatContext
1 | objectivec复制代码// 通常在你需要进行音视频编码并生成一个新的音视频文件时使用 |
- 写入数据
1 | scss复制代码//先写入头文件 |
- 释放avformatcontext结构体
1 | scss复制代码 avformat_free_context(output_format_context); |
AVStream
AVStream是AVFormatContext结构体中的一个成员(数组结构),它表示媒体文件中某一种数据的流以及对应的媒体信息,比如该流表示视频流,则同时也会含有视频相关的宽高,帧率等信息,以及time_base等基础信息。
1 | objectivec复制代码 |
对于初学者而言,可以先重点关注time_base和codecpar这两个成员,time_base不用讲,是ffmpeg中的时间基本单位,codecpar则表示了当前流的解码信息。
我们可以看一下AVCodecParameters这个结构体的成员情况
1 | objectivec复制代码typedef struct AVCodecParameters { |
可以看到AVCodecParameters是把音频和视频这两种信息混合在一起了,在流属于不同类型时使用不同的字段,或者同一个字段表达不同的含义。比如format,如果是视频,则表示AVPixelFormat枚举类型,如果是音频,则表示AVSampleFormat枚举类型。
- 如何获取AVStream
1 | ini复制代码// formatContext->nb_streams表示流的个数 |
AVCodec
1 | objectivec复制代码//编解码器 |
AVCodec可以表示一个编解码器,里面包含了编解码的一些基本信息。
一般而言,媒体文件中的音频流,视频流中都保存有解码器ID等信息,通过这个ID可以获取对应AVCodec,从而获取该解码器的比较全面的信息。
- 获取AVCodec
1 | rust复制代码// formatContext即 AVFormatContext的结构体对象,此时应该已经创建并读取了信息 |
获取到AVCodec之后,需要通过它构建一个可用编解码器上下文(提供编解码过程中待解码数据的背景和配置)
AVCodecContext
1 | objectivec复制代码 |
AVCodecContext就是我们前面说的编解码上下文,主要包含待解码数据的一些特性,便于在解码过程中解码器正确解析数据。比如等待解码的是视频数据,那么解码器需要知道time_base(关于time_base的概念不懂可以看前一篇文章)统一时间单位;每帧图片的宽高;视频帧的像素格式(关于像素格式见(移动开发中关于视频的一些基本概念),了解像素排列方式….
有了以上信息,解码器才可以正确的对数据进行解码。
AVCodecContext中音频的的frame_size,在解码器中可能不存在,因此在解码过程避免使用这个字段,可以找decoded_frame中的nb_samples来替代
- 创建(解码为例)
1 | scss复制代码// id 是从前文中通过AVStream中获取的 |
利用AVCodec构建AVCodecContext,然后把AVStream中已知的一些信息复制到AVCodecContext中,接着初始化并开启编解码器。
- 销毁
1 | scss复制代码avcodec_free_context(&pCodecCtx) |
AVCodecContext在编解码过程中都会被用到。
AVPacket
读取文件获取AVFormatContext结构体,并且获取了解码器上下文
AVPacket是存储压缩编码数据相关信息的结构体
1 | arduino复制代码 |
AVPacket的成员主要包括time_base,pts,dts等一些在解码时可能被用到的参数以及编码数据data。
创建过程
- 在正常的编解码过程中,AVPacket手动申请内存,则需要手动释放内存,如果自动申请内存则不需要。
- 从编解码器中接收数据放到packet中,使用完之后,需要释放引用av_packet_unref 即可
1 | scss复制代码AVPacket pkt; // 自动申请出内存 |
使用方式
1 | scss复制代码// 从AVFormatContext中读取数据到avpacket中 |
AVFrame
AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),除此之外就是数据对应的一些属性:时长,格式等
1 | arduino复制代码视频和音频共用一个结构体,因此有的属性是双方公用,有的可能主要用于一方 |
data与linesize
关于data和linesize这两个字段,分别表示原始数据存储数组和每一行的大小。但是数据是如何排列的我们并不清楚。
之前讲视频的基础知识时,我们讲到YUV的数据排列有多种方式,因此想要知道data中的YUV数据排列,我们还需要知道AVFrame的format,这个format来自于AVCodecContext->pix_fmt,这个解码器的参数设置成什么,最终解码出来的杨素格式就是什么。假如不指定的话,默认会解码为YUV420p。
我们假设视频数据解码出来的AVFrame,format是YUV420P,那么data和linesize的数据在ffmpeg中的内存示意图可能是这样的:
类似的音频数据解码出来的AVFrame,format是AV_SAMPLE_FMT_FLTP,双声道,那么对应的数据在ffmpeg中的内存示意图可能是这样的:
以上都是planar的存储模式,如果是packed(关于planar/packed的解释见文章)存储模式呢?
YUV422 packed存储格式的视频,ffmpeg中的内存示意图大概是这样的:
当然,其实对于初学者而言,一般不需要直接操作data和linesize,但是能够把ffmpeg中的数据结构和所学的音视频知识做一个对应理解会更深刻。
对于linsize,音频类型。一般只有linesize[0]会被设置;视频则需要看存储方式的不同,常用的planar模式下,linsize数组一般会用到前三个。
对于data指针数组而言,音频数据占用数组几个的指针要看声道数和存储格式(palnar/packed);视频则只看存储格式,planar一般占3个,packed占
使用方式
- 创建,申请内存空间
1 | ini复制代码 // 申请内存空间 |
- 解码
解码就是AVPakcet=>AVFrame的过程。
1 | scss复制代码/********一次循环************/ |
- 编码
编码则是AVFrame=>AVPacket的过程(解码的逆过程)。
1 | scss复制代码// av_encode_ctx 编码器的上下文 |
- 销毁
1 | scss复制代码// 使用完之后 |
手动填充AVFrame->data
ffmpeg中,我们是通过av_frame_alloc函数来获得AVFrame,但是这个函数只是开辟了AVFrame结构体空间,而avframe->data是一个成员为指针的数组,这些成员指针和它们指向的内存空间并未被开辟出来。我们从源码实现中也能看到:
1 | objectivec复制代码AVFrame *av_frame_alloc(void) |
是因为在编解码过程中编解码器会帮助我们开辟这块空间,所以我们不必管。
但是假如我们在编解码之外使用AVFrame,比如把YUV类型的AVFrame转换为RGB类型的AVFrame,那么AVFrame->data的空间就需要我们自己开辟了,也需要我们进行释放。
- 填充AVFrame->data
以视频帧为例
1 | scss复制代码// Allocate an AVFrame structure |
总结
本文主要详细介绍了ffmpeg中比较重要的几个结构体,他们都伴随着音视频处理的某个阶段而存在的,因此了解他们有助于我们理解音视频的处理流程。
我们把前面的音视频解码播放流程图添加关键API和搭配关键结构体就会发现ffmpeg的处理流程还是比较简洁的。接下来我们尝试用一个较完整的demo来熟悉ffmpeg的使用方式。
本文转载自: 掘金