解码流程
关于FFMPEG的解码流程大致步骤可以分为如下几步:
- 打开输入文件
- 查找解码器
- 解码音视频帧
- 处理音视频帧
- 写入设备或文件
代码剖析
下面从代码层面来具体讲解一下解码过程中调用了哪些函数实现的整个解码流程。
#include <stdio.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
int main() {
AVFormatContext *format_ctx = NULL;
AVCodecContext *video_codec_ctx = NULL;
AVCodecContext *audio_codec_ctx = NULL;
AVCodec *video_codec = NULL;
AVCodec *audio_codec = NULL;
AVPacket packet;
AVFrame *video_frame = NULL;
AVFrame *audio_frame = NULL;
SwrContext *swr_ctx = NULL;
const char *input_file = "input.mp4";
int video_stream_index = -1;
int audio_stream_index = -1;
// 初始化 FFmpeg 库
av_register_all();
// 打开输入文件
if (avformat_open_input(&format_ctx, input_file, NULL, NULL) != 0) {
printf("无法打开输入文件\n");
return -1;
}
// 获取媒体信息
if (avformat_find_stream_info(format_ctx, NULL) < 0) {
printf("无法获取媒体信息\n");
avformat_close_input(&format_ctx);
return -1;
}
// 查找视频流索引和音频流索引
for (int i = 0; i < format_ctx->nb_streams; i++) {
if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
} else if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_stream_index = i;
}
}
if (video_stream_index == -1 || audio_stream_index == -1) {
printf("找不到视频或音频流\n");
avformat_close_input(&format_ctx);
return -1;
}
// 获取视频流解码器参数
AVCodecParameters *video_codec_params = format_ctx->streams[video_stream_index]->codecpar;
// 获取音频流解码器参数
AVCodecParameters *audio_codec_params = format_ctx->streams[audio_stream_index]->codecpar;
// 查找视频解码器
video_codec = avcodec_find_decoder(video_codec_params->codec_id);
if (!video_codec) {
printf("找不到视频解码器\n");
avformat_close_input(&format_ctx);
return -1;
}
// 查找音频解码器
audio_codec = avcodec_find_decoder(audio_codec_params->codec_id);
if (!audio_codec) {
printf("找不到音频解码器\n");
avformat_close_input(&format_ctx);
return -1;
}
// 创建视频解码器上下文
video_codec_ctx = avcodec_alloc_context3(video_codec);
if (!video_codec_ctx) {
printf("无法分配视频解码器上下文\n");
avformat_close_input(&format_ctx);
return -1;
}
// 创建音频解码器上下文
audio_codec_ctx = avcodec_alloc_context3(audio_codec);
if (!audio_codec_ctx) {
printf("无法分配音频解码器上下文\n");
avformat_close_input(&format_ctx);
return -1;
}
// 将视频解码器参数复制到视频解码器上下文
if (avcodec_parameters_to_context(video_codec_ctx, video_codec_params) < 0) {
printf("无法设置视频解码器上下文参数\n");
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 将音频解码器参数复制到音频解码器上下文
if (avcodec_parameters_to_context(audio_codec_ctx, audio_codec_params) < 0) {
printf("无法设置音频解码器上下文参数\n");
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 打开视频解码器
if (avcodec_open2(video_codec_ctx, video_codec, NULL) < 0) {
printf("无法打开视频解码器\n");
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 打开音频解码器
if (avcodec_open2(audio_codec_ctx, audio_codec, NULL) < 0) {
printf("无法打开音频解码器\n");
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 分配视频帧内存
video_frame = av_frame_alloc();
if (!video_frame) {
printf("无法分配视频帧内存\n");
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 分配音频帧内存
audio_frame = av_frame_alloc();
if (!audio_frame) {
printf("无法分配音频帧内存\n");
av_frame_free(&video_frame);
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 音频重采样上下文
swr_ctx = swr_alloc();
if (!swr_ctx) {
printf("无法分配音频重采样上下文\n");
av_frame_free(&video_frame);
av_frame_free(&audio_frame);
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 设置音频重采样参数
av_opt_set_int(swr_ctx, "in_channel_layout", audio_codec_ctx->channel_layout, 0);
av_opt_set_int(swr_ctx, "in_sample_rate", audio_codec_ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", audio_codec_ctx->sample_fmt, 0);
av_opt_set_int(swr_ctx, "in_nb_samples", audio_codec_ctx->frame_size, 0);
av_opt_set_int(swr_ctx, "out_channel_layout", audio_codec_ctx->channel_layout, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", audio_codec_ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
// 初始化音频重采样上下文
if (swr_init(swr_ctx) < 0) {
printf("无法初始化音频重采样上下文\n");
swr_free(&swr_ctx);
av_frame_free(&video_frame);
av_frame_free(&audio_frame);
avcodec_free_context(&video_codec_ctx);
avcodec_free_context(&audio_codec_ctx);
avformat_close_input(&format_ctx);
return -1;
}
// 读取数据包并解码
while (av_read_frame(format_ctx, &packet) == 0) {
if (packet.stream_index == video_stream_index) {
// 发送视频数据包到视频解码器
if (avcodec_send_packet(video_codec_ctx, &packet) != 0) {
printf("无法发送视频数据包到解码器\n");
break;
}
// 接收视频解码器输出的帧
while (avcodec_receive_frame(video_codec_ctx, video_frame) == 0) {
// 在此处进行对解码后的视频帧进行处理
// 释放视频帧
av_frame_unref(video_frame);
}
} else if (packet.stream_index == audio_stream_index) {
// 发送音频数据包到音频解码器
if (avcodec_send_packet(audio_codec_ctx, &packet) != 0) {
printf("无法发送音频数据包到解码器\n");
break;
}
// 接收音频解码器输出的帧
while (avcodec_receive_frame(audio_codec_ctx, audio_frame) == 0) {
// 在此处进行对解码后的音频帧进行处理
// 释放音频帧
av_frame_unref(audio_frame);
}
}
// 关闭解码器和文件
avcodec_close(audio_codec_ctx);
avcodec_close(video_codec_ctx);
avformat_close_input(&format_ctx);
代码流程如下:
-
包含头文件:stdio.h 用于标准输入输出;libavcodec/avcodec.h 用于音视频编解码相关函数和结构体的声明;libavformat/avformat.h用于音视频格式处理相关函数和结构体的声明。
-
函数主体:主函数 int main() 作为程序的入口。
-
打开输入文件:avformat_open_input() 函数用于打开输入文件,并将其与相应的格式上下文 (AVFormatContext) 相关联。
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
参数说明:- ps:AVFormatContext指针的指针,用于存储打开的媒体文件的上下文。
- url:要打开的媒体文件的URL地址,可以是本地文件路径、网络流媒体地址等。
- fmt:指定要使用的输入格式,可以传入NULL以自动检测输入格式。
- options:用于设置附加的AVFormatContext选项的字典,可以传入NULL。
返回值:- 返回0表示打开媒体文件成功。
- 返回负数表示打开媒体文件失败,具体的错误代码可通过调用av_strerror获取。
avformat_open_input函数主要进行以下几个操作:- 为AVFormatContext对象分配内存:AVFormatContext是用于存储媒体格式相关信息的结构体。avformat_open_input函数会动态分配内存来存储AVFormatContext对象,并将指针通过参数传出。
- 初始化AVFormatContext对象:avformat_open_input函数会对AVFormatContext对象进行初始化,包括将各个字段和成员变量设置为默认值、清空关联的流和格式等。初始化后的AVFormatContext对象可以被用于打开媒体文件和读取媒体流信息。
- 打开媒体文件:avformat_open_input函数会根据传入的媒体文件URL地址,打开媒体文件。它会调用对应的输入协议处理器(如file、http、rtsp等协议处理器)来打开媒体文件,并建立与之关联的输入上下文。
- 读取媒体文件头信息:在媒体文件成功打开后,avformat_open_input函数会读取媒体文件的头信息,其中包括各个媒体流的基本参数、时长、格式标识等。这些头信息存储在AVFormatContext对象的相关字段中,后续的媒体处理可以通过这些信息获取到媒体流的相关参数。
需要注意的是,avformat_open_input函数并不会立即对媒体文件进行解码或读取媒体流数据,它仅打开和读取媒体文件的头部信息。后续的读取媒体流数据、解码等操作可以在打开媒体文件成功后,对AVFormatContext对象进一步操作来完成
-
获取流信息:avformat_find_stream_info() 函数用于获取音视频流的详细信息,并将其存储在格式上下文中。
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
参数说明:- ic:AVFormatContext指针,表示要读取的媒体文件上下文。
- options:用于设置附加的AVFormatContext选项的字典,可以传入NULL。
返回值:- 返回0表示读取媒体流信息成功。
- 返回负数表示读取媒体流信息失败,具体的错误代码可通过调用av_strerror获取。
avformat_find_stream_info函数是FFmpeg库中用于获取输入文件(音视频文件)的流信息,然后填充AVFormatContext结构的关键函数。它主要进行了以下操作:- 打开输入文件:首先,avformat_find_stream_info函数会尝试打开输入文件,根据输入文件的URL或文件名识别输入格式以及相应的解封装器(demuxer)。
- 解码获取流信息:一旦成功打开输入文件,avformat_find_stream_info函数会通过解封装器逐帧地读取输入文件,并解析出其中的音视频流信息。它会遍历输入文件中的每一个流(音频流或视频流),并从中提取出关键的参数和元数据,如编解码器类型、码率、帧率、分辨率、采样率、通道数等。
- 填充AVFormatContext结构:解码获取流信息后,这些信息会被存储在一个AVFormatContext结构中,用于后续的音视频处理。avformat_find_stream_info函数会将解析出的音视频流信息填充到AVFormatContext结构中的相应字段,如AVStream结构和AVCodecParameters参数等。
- 检查并修正信息:在解析过程中,avformat_find_stream_info函数可能会进行一些修正操作,以确保流信息的正确性。例如,它会检测和修正时间戳、帧率、采样率等与时序相关的参数。
- 返回流数量:最后,avformat_find_stream_info函数会返回成功解析出的音视频流数量,以便应用程序在后续处理中可以获取正确的流索引。
-
查找视频流索引:在格式上下文的 streams 数组中遍历,找到与视频相关的流的索引。
-
打开视频解码器:使用查找到的视频流的编解码参数 (codecParameters) 和解码器 (codec),调用 avcodec_open2() 函数打开视频解码器,返回解码器上下文 (AVCodecContext)。
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
参数说明:- avctx:AVCodecContext指针,表示要打开的编解码器的上下文。
- codec:指向AVCodec结构体的指针,表示要打开的编解码器。
- options:用于设置附加的AVCodecContext选项的字典,可以传入NULL。
返回值:- 返回0表示打开编解码器成功。
- 返回负数表示打开编解码器失败,具体的错误代码可通过调用av_strerror获取。
功能说明: avcodec_open2函数的主要功能是打开指定的编解码器并进行初始化,使其准备好进行实际的编码或解码操作。在调用该函数之前,需要先通过avcodec_alloc_context3函数分配一个AVCodecContext结构体,并根据需要设置其相关参数。 在打开编解码器时,avcodec_open2函数会首先检查传入的AVCodec指针是否为NULL,如果为NULL,则会根据AVCodecContext中指定的codec_id自动选择对应的编解码器。
函数操作详解:- 首先,avcodec_open2函数会调用avcodec_find_decoder或avcodec_find_encoder函数找到对应的编解码器。
- 然后,它会根据传入的options字典设置AVCodecContext的附加选项。这些选项可以用于调整编解码器的一些参数,例如帧率、码率、输出格式等。
- 接下来,avcodec_open2函数会调用编解码器的init函数进行初始化,它会为编解码器分配内存并设置一些默认值,同时也会根据AVCodecContext中的设置来更新编解码器的参数。
- 最后,如果初始化成功,avcodec_open2函数会返回0表示成功打开编解码器;如果失败,会返回负数表示打开编解码器失败。
-
解码视频帧:使用 av_read_frame() 函数读取帧数据,判断数据包的流索引是否为视频流索引,然后调用 avcodec_send_packet() 将数据包发送到解码器,再使用 avcodec_receive_frame() 接收解码后的视频帧。
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
参数说明:- s:AVFormatContext指针,表示要读取的媒体文件的上下文。
- pkt:AVPacket指针,用于存储读取到的媒体数据。
返回值:- 返回0表示成功读取到一帧媒体数据。
- 返回负数表示读取失败,具体的错误代码可通过调用av_strerror获取。
功能说明: av_read_frame函数的主要功能是从媒体文件中读取下一帧数据,并将其保存到AVPacket结构体中。它遵循FFmpeg库中的AVIOContext读写机制,内部会调用底层的输入协议(例如文件、网络等)来读取数据。 在调用该函数之前,需要先通过avformat_alloc_context函数分配一个AVFormatContext结构体,并通过调用avformat_open_input函数打开指定的媒体文件。
函数操作详解:- 首先,av_read_frame函数会从AVIOContext中读取数据,并将其存储到缓冲区中。
- 然后,它会对读取到的数据进行解封装,根据媒体格式的不同,将其转换为对应的AVPacket结构体。
- 在读取到的数据存储到AVPacket结构体之后,可以对AVPacket进行进一步的处理,例如解码、编码或存储等操作。
- 如果成功读取到一帧媒体数据,av_read_frame函数会返回0;如果读取失败,会返回负数表示读取失败。
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
参数说明:- avctx:AVCodecContext指针,表示要发送数据包的编解码器上下文。
- avpkt:AVPacket指针,表示要发送的数据包。
返回值:- 返回0表示成功发送数据包。
- 返回负数表示发送数据包失败,具体的错误代码可通过调用av_strerror获取。
功能说明: avcodec_send_packet函数的主要功能是向编解码器发送数据包,以进行解码或编码操作。它将输入的数据包传递给编解码器,让编解码器进行实际的解码或编码处理。在调用该函数之前,需要先通过avcodec_open2函数打开指定的编解码器。
函数操作详解:- 首先,avcodec_send_packet函数会将传入的AVPacket数据包拷贝到编解码器的内部缓冲区中,并准备进行解码或编码操作。
- 然后,它会通知编解码器有新的数据可用,并开始处理解码或编码操作。
- 在一次解码或编码操作完成之后,可以再次调用avcodec_send_packet函数发送下一个数据包,进一步进行处理。
- 如果成功发送数据包,avcodec_send_packet函数会返回0;如果发送失败,会返回负数表示发送失败。
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
参数说明:- avctx:AVCodecContext指针,表示要接收帧的编解码器上下文。
- frame:AVFrame指针,用于存储接收到的解码后的帧。
返回值:- 返回0表示成功接收到一帧解码后的帧。
- 返回负数表示接收帧失败,具体的错误代码可通过调用av_strerror获取。
功能说明: avcodec_receive_frame函数的主要功能是接收解码后的帧数据,将其存储到AVFrame结构体中。它从编解码器中获取解码后的帧,以便进一步进行处理,例如渲染、存储或后续编码操作。
函数操作详解:- 首先,avcodec_receive_frame函数会尝试从编解码器中获取解码后的帧数据。
- 如果成功接收到一帧解码后的帧,会将其存储到AVFrame结构体中,并将帧数据的相关信息填充到AVFrame中,例如图像宽高、采样率等。
- 接收到的解码后的帧可以进行进一步的处理,例如渲染到屏幕上或存储到文件中等。
- 如果还有剩余的帧需要接收,可以再次调用avcodec_receive_frame函数获取下一帧数据。
-
处理解码后的视频帧数据:在成功接收到一帧解码后的视频帧后,可以通过访问帧数据的指针 (frame->data[0]、frame->data[1]等) 来处理视频帧数据。
-
释放资源:在循环结束后,依次释放数据包 (av_packet_unref())、帧 (av_frame_free())、解码器 (avcodec_close()) 和格式上下文 (avformat_close_input()),确保正确清理和回收内存。