本文主要将FFmpeg的编码流程,网上关于ffmpeg编码文章参差不齐,我在实现编码相关基础功能的时候,遇到了各种各样的问题,因此也就萌生了自己写一些FFmpeg编码的念头,在帮助自己回顾知识的基础上,希望也能给其他同学一些帮助。
FFmpeg有两套规范api,本文主要围绕编码新接口来讨论的,旧编码接口也基本被废弃了。
之前我基于FFmpeg的github仓库,使用release/5.0
分支编译出了FFmpeg库文件和相关产物,想直接使用的同学可以点击这里传送。
一、编码流程图
首先我们先给出编码的流程图,每个流程的解析我们放在后面的环节。
接下来我们详细讲解下整个流程中的关键点。
二、编码流程简述
根据我浅薄的理解,编码的过程其实就是解码过程中的反向流程,在之前的音视频 ijkplayer源码解析系列2--如何解码图像中,我们分析过了解码的流程,解码最核心的流程其实就是:解复用和解码,那么编码的流程其实就是复用和解码。
2.1 基础概念
复用(mux)是一种在单一通信信道上同时传输多信号或数据流的过程。在多媒体处理中,复用通常指的是将音频流和视频流等多种媒体数据集流合并到一个文件中。
例如,一个MP4文件就是一个复用的例子,它可能包含了H.264编码的视频流和AAC编码的音频流。
复用的主要目的是为了方便存储和传输。通过服用,可以将多种媒体数据流打包成一个文件,方便存储和传输。同时,复用还可以保证多种媒体数据流的同步,例如在播放视频时,可以保证音频和视频的同步。
编码(Encode)是一种将信息从一种形式或格式转换为另一种形式的过程。在多媒体处理中,编码通常指的是将原始的音频或视频数据转换为固定格式,以减小数据的大小,方便存储和传输。
例如,H.264编码器可以将原始的视频帧(如YUV格式)转换为H.264格式的视频流。
编码的主要目的是为了减小数据的大小,提高存储和传输的效率。同时,编码还可以保护数据的安全。
接下来我们就顺着复用+编码的思路解析下整个编码的流程。
2.2 关键类
在解析整个复杂流程之前,我们可以简单的描述下编码的过程,减轻下大家的理解成本。
首先我们拿到采集到的二进制文件,写入待编码对象(AVFrame)中,借助经过编码器拿到编码后的对象(AVPacket),然后就是复用的流程,会将AVPacket写入最后的输出文件中。在上述流程中编码器还是流(AVStream)有着莫大的关系,在后面我们也会逐步讲解。
在上述流程中,出现了一些关键词,这些关键词由引申了下述的类。
- AVPacket:主要用于存储压缩编码(即尚未解码)的数据。
- AVSream:代表媒体文件中的一个媒体流,比如一个视频文件中可能包含一个视频流和多个音频流。每一个AVStream中包含了该媒体流的所有信息,如编码格式、时间基等。
- AVFrame:用于存储解码后的数据。当AVPacket中的数据经过解码器解码后,就会被转换为AVFrame。
- AVFormatContext:FFmpeg中重要的结构体,它包含了媒体文件的所有信息,比如流的数量,每个流的类型(音频流、视频流等),每个流对应的编解码器等信息。
- AVCodec:AVCodec代表一个编解码器,它包含了编解码器的所有信息,如编解码器的名称、类型(音频编解码器、视频编解码器)、编解码器支持的数据格式等。FFmpeg库中包含了大量的AVCodec,支持多种多样的音频、视频格式的编解码。
- AVCodecContext:编解码器的上下文,它包含了编解码器的所有状态信息,如当前的编解码参数、编解码器内部的缓冲区等。当你使用一个编解码器进行编解码操作时,你需要首先创建一个AVCodecContext,然后设置各种参数,然后使用AVCodecContext进行编解码操作。
三、编码流程解析
3.1 创建AVFormatContext
不管在编码还是在解码,我们都离不开AVFormatContext
,也正如我们刚才所有的,它包含了媒体文件中的所有信息,所以第一步也就是根据输出文件创建对应的AVFormatContext
。创建的方法也十分简单:
cpp
AVFormatContext *av_audio_format_context;
encodeResult = avformat_alloc_output_context2(&av_audio_format_context, NULL, NULL, output_cstr);
output_cstr
就是输出文件的路径,avformat_alloc_output_context2会根据文件类型创建对应的AVFormatContext
上下文
3.2 打开输出文件并初始化AVIOContext
在写入输出文件之前,我们也必须打开文件,通过指定输出文件路径output_cstr
去打开文件,同时也就初始化了AVIOContext *av_audio_format_context->pb
上下文:
cpp
encodeResult = avio_open(&av_audio_format_context->pb, output_cstr, AVIO_FLAG_READ_WRITE);
3.3 确认编码器AVCodec
接着需要通过输出文件的格式找到我们需要的编码器,在经过刚刚的流程后,我们就可以通过AVFormatContext
的成员变量const struct AVOutputFormat *oformat
,借助avcodec_find_encoder
找到对应的编码器。
首先我们先了解下AVOutputFormat
,AVOutputFormat
包含了输出媒体文件的所有信息,如文件格式的名称、文件格式的描述、文件格式支持的编码器等。当你需要将数据编码并写入到文件中时,你需要首先创建一个AVOutputFormat
,然后设置AVOutputFormat
的各种参数,最后使用AVOutputFormat
将编码后的数据写入到文件中。
AVOutputFormat
中包含了
cpp
enum AVCodecID audio_codec; /**< default audio codec */
enum AVCodecID video_codec; /**< default video codec */
enum AVCodecID subtitle_codec; /**< default subtitle codec */
audio_codec
、video_codec
字段分别表示默认的音频和视频编码器。在你创建AVOutputFormat
时,这两个字段通常会被自动设置为该输出格式默认支持的音频和视频编码器。
然而,这并不是因为这你必须使用这两个默认的编码器。在实际使用中,你可以根据需要自行设置这两个字段,选择你需要的音频和视频编码器。例如。如果你需要将数据写入到一个MP4文件中,但是你希望使用H.265编码器而不是默认的H.264编码器,你就可以讲video_codec设置为H.265编码器。
需要注意的是,不是所有的编码器都可以用于所有的输出格式。在设置audio_codec
和video_codec
时,你需要确保你选择的编码器是被你的输出格式所支持。否则,你可能会遇到错误或者无法正确的写入数据。
那么如何找到编码器呢,以音频编码器为例:
cpp
AVOutputFormat *audiOutputFormat = av_audio_format_context->oformat;
AVCodec *audioCodec = avcodec_find_encoder(audiOutputFormat->audio_codec);
3.4 初始化编码器上下文
初始化编码器参数十分简单,可以直接参考下面代码:
cpp
AVCodecContext *audioCodecContext = avcodec_alloc_context3(audioCodec);
audioCodecContext->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL;
audioCodecContext->codec_id = AV_CODEC_ID_AAC;
audioCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
audioCodecContext->sample_fmt = pCodecCtx->sample_fmt;
audioCodecContext->sample_rate = pCodecCtx->sample_rate;
audioCodecContext->channel_layout = pCodecCtx->channel_layout;
audioCodecContext->channels = pCodecCtx->channels;
audioCodecContext->bit_rate = pCodecCtx->bit_rate;
利用avcodec_alloc_context3
创建AVCodecContext
编码器上下文,编码器上下文内设置基础的编码器参数,这里我们以AAC音频为例,给出了上述代码,大家按实际情况进行设置即可。
3.5 创建AVStream流
在3.1我们创建了AVFormatContext实例
,在3.3创建了AVCodec实例
,借助这两个实例就可以创建AVStream流
:
cpp
AVStream *audioStream = avformat_new_stream(av_audio_format_context, audioCodec)
这样我们就创建了对应媒体流,同时这个媒体流也建立了和AVFormatContext上下文
和AVCodec编解码器
的联系了。
3.6 初始化AVStream流参数
我们直接借助avcodec_parameters_from_context
用刚刚初始化的编码器参数上下文AVCodecContext实例
初始化AVStream
流里的AVCodecParameters *codecpar
,即编码器的参数。
cpp
avcodec_parameters_from_context(audioStream->codecpar, audioCodecContext);
3.7 打开编码器
在之前的流程,已经初始化了编码器(AVCodec)和流(AVStream)相关的部分,紧接着就是打开编码器了:
cpp
avcodec_open2(audioCodecContext, audioCodec, NULL);
在打开编码器以后,我们就可以开始写入媒体文件了
3.8 写入文件头
写入文件头的方式是相对固定的,直接调用avformat_write_header
即可:
cpp
avformat_write_header(av_audio_format_context, NULL);
3.9 编码输入媒体数据
编码输入数据的流程也是相对固定的,直接借助avcodec_send_frame
将输入AVFramez帧
送入待编码队列,然后使用avcodec_receive_packet
取出编码成功的AVPacket
3.10 将编码成功的数据写入输出文件
写入输出文件一般来说有两种方式,a_write_frame和av_interleaved_write_frame。 av_write_frame和av_interleaved_write_frame都是FFmpeg库中用于输出媒体数据的函数,但它们之间存在一些差异。
-
av_write_frame直接将提供的AVPacket写入媒体文件,不会改变包的顺序,也不会进行额外的处理。因此,使用此函数时,你必须确保提供的AVPackets已经是正确的时间顺序。
-
av_interleaved_write_frame在写入媒体数据之前,会根据DTS(解码时间戳)对提供的AVPackets进行重新排序,以确保它们是按照正确 的时间顺序存储的。这对于确保音频和视频帧正确同步特别重要。比如,在某些容器格式中,必须交错存储音频和视频帧,以便在播放时正确同步。
简而言之,两者的主要区别在于av_interleaved_write_frame会对输出数据进行额外的时间排序处理,以确保输出流是交错和同步 的,而av_write_frame不会。通常建议使用av_interleaved_write_frame,除非你确认你的媒体数据已经是按照正确的时间和顺序排列的。
3.11 写入文件尾
写入文件尾也是十分简单,直接调用av_write_trailer
写入文件尾即可
3.12 最后就是释放对应的资源
- av_packet_unref释放无需使用的AVPacket
- av_frame_free释放无需使用的AVFrame
- avcodec_close释放编码器上下文
- avformat_close_input释放文件上下文
本章也就到此为止了,后续会给出简单的实现demo给大家参考