音视频处理流程中讲解了音视频处理的流程(解复用、解码、渲染等),针对于解复用部分在音视频的那几个关键参数中已经提过了,本文不再赘述,本文将重点介绍解码的过程。
注意:本文以文件/流url的形式解码,对于文件缓存buffer将是后续章节的讨论重点。
1. 结构体和接口
在 FFmpeg 的解码流程中,解码器的配置和数据处理是两个主要部分。
1.1 解码器配置相关
解码器配置的核心任务是找到合适的解码器,并为其配置解码上下文。以下是涉及的主要结构体和函数:
结构体
接口
//查找解码器
//param codec_id: AVCodecID 类型的枚举值,表示要查找的解码器类型。
//return: 返回找到的解码器,如果未找到则返回 NULL。
const AVCodec *codec = avcodec_find_decoder(codec_id);
//初始化解码器上下文
//param codec: 要使用的解码器,可以是nullptr,也可以是特定的解码器。
//return: 返回分配的解码器上下文,如果分配失败则返回 NULL。
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
//将参数从流传递给上下文
//param codec_ctx: 解码器上下文。
//param codecpar: 媒体流的编解码器参数。
//return: 返回 0 表示成功,非零表示失败。
int avcodec_parameters_to_context(AVCodecContext *codec_ctx, const AVCodecParameters *codecpar);
//打开解码器
//param codec_ctx: 解码器上下文。
//param codec: 要使用的解码器。
//param options: 附加选项,通常为 NULL。
//return: 返回 0 表示成功,非零表示失败。
int avcodec_open2(AVCodecContext *codec_ctx, const AVCodec *codec, AVDictionary options);
//释放解码器上下文
//param codec_ctx: 解码器上下文。
//return: 无返回值。
void avcodec_free_context(AVCodecContext codec_ctx);1.2 解码数据相关
解码数据的核心任务是将输入的压缩数据解码为未压缩的帧。以下是涉及的主要结构体和函数:
结构体
接口
//创建一个空的数据包
//return: 返回一个空的数据包。
AVPacket *av_packet_alloc();
//释放数据包
//param pkt: 要释放的数据包。
//return: 无返回值。
void av_packet_free(AVPacket pkt);
//创建一个空的帧
//return: 返回一个空的帧。
AVFrame *av_frame_alloc();
//释放帧
//param frame: 要释放的帧。
//return: 无返回值。
void av_frame_free(AVFrame frame);
//读取一个数据包
//param fmt_ctx: 输入的格式上下文。
//param pkt: 输出的数据包。
//return: 返回 0 表示成功,非零表示失败。
int av_read_frame(AVFormatContext *fmt_ctx, AVPacket *pkt);
//发送数据包到解码器
//param codec_ctx: 解码器上下文。
//param pkt: 输入的数据包。
//return: 返回 0 表示成功,非零表示失败。
int avcodec_send_packet(AVCodecContext *codec_ctx, const AVPacket *pkt);
//接收解码帧
//param codec_ctx: 解码器上下文。
//param frame: 输出的帧数据。
//return: 返回 0 表示成功,非零表示失败。
int avcodec_receive_frame(AVCodecContext *codec_ctx, AVFrame *frame);注意:
2. 实例代码
以下是一个完整的解码流程代码示例。代码包括了初始化解码器、读取压缩数据、解码帧,以及如何正确释放资源。
为了确保代码可以运行并验证其正确性,代码中保留了保存解码数据的代码。值得注意的是,音频文件以交错格式保存的,播放/查看时需要注意。
所谓交错格式为:
[sample1_ch1][sample1_ch2][sample2_ch1][sample2_ch2]...与之对应的是非交错格式:
[sample1_ch1][sample2_ch1]...[sample1_ch2][sample2_ch2]...示例代码如下
#include <iostream>
#include<string>
extern "C"
{
#include"libavformat/avformat.h"
#include <libavcodec/avcodec.h>
#include <libavutil/frame.h>
#include <libavutil/mem.h>
}
static void decode(AVCodecContext* dec_ctx, AVPacket* pkt, AVFrame* frame, FILE* outfile,AVMediaType type)
{
int ret, data_size;
// 将压缩的数据包发送到解码器
ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
fprintf(stderr, "Error sending packet for decoding\n");
exit(1);
}
// 从解码器接收解码后的帧
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
fprintf(stderr, "Error during decoding\n");
exit(1);
}
// 获取每个样本的字节大小
data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);
if (data_size < 0) {
fprintf(stderr, "Failed to calculate data size\n");
exit(1);
}
if (AVMEDIA_TYPE_AUDIO == type)
{
// 检查采样格式是否为平面格式
if (av_sample_fmt_is_planar(dec_ctx->sample_fmt)) {
// 平面格式 -> 转换为交错格式
for (int i = 0; i < frame->nb_samples; i++) {
for (int ch = 0; ch < dec_ctx->ch_layout.nb_channels; ch++) {
fwrite(frame->data[ch] + data_size * i, 1, data_size, outfile);
}
}
}
else {
// 交错格式 -> 直接写入
fwrite(frame->data[0], 1, data_size * frame->nb_samples * dec_ctx->ch_layout.nb_channels, outfile);
}
}
else if (AVMEDIA_TYPE_VIDEO == type)
{
// Write YUV420 data to output file
for (int i = 0; i < frame->height; i++)
fwrite(frame->data[0] + i * frame->linesize[0], 1, frame->width, outfile); // Y plane
for (int i = 0; i < frame->height / 2; i++)
fwrite(frame->data[1] + i * frame->linesize[1], 1, frame->width / 2, outfile); // U plane
for (int i = 0; i < frame->height / 2; i++)
fwrite(frame->data[2] + i * frame->linesize[2], 1, frame->width / 2, outfile); // V plane
}
}
}
void DecodeData(const char *input_file, const char *output_file,AVMediaType type)
{
auto fmt_ctx = avformat_alloc_context();
// 打开输入文件
if (avformat_open_input(&fmt_ctx, input_file, NULL, NULL) < 0) {
fprintf(stderr, "Could not open input file\n");
exit(1);
}
// 查找流信息
int stream_index = -1;
stream_index = av_find_best_stream(fmt_ctx, type, -1, -1, nullptr, 0);
if (stream_index == -1) {
fprintf(stderr, "Could not find audio stream\n");
exit(1);
}
// 获取音频流参数并找到解码器
AVCodecParameters *codecpar = fmt_ctx->streams[stream_index]->codecpar;
auto codec = avcodec_find_decoder(codecpar->codec_id);
if (!codec) {
fprintf(stderr, "Unsupported codec\n");
exit(1);
}
// 初始化解码器上下文
auto dec_ctx = avcodec_alloc_context3(codec);
if (!dec_ctx) {
fprintf(stderr, "Could not allocate audio codec context\n");
exit(1);
}
if (avcodec_parameters_to_context(dec_ctx, codecpar) < 0) {
fprintf(stderr, "Failed to copy codec parameters to codec context\n");
exit(1);
}
if (avcodec_open2(dec_ctx, codec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
exit(1);
}
// 打开输出文件
FILE* outfile = fopen(output_file, "wb");
if (!outfile) {
fprintf(stderr, "Could not open output file\n");
exit(1);
}
auto pkt = av_packet_alloc();
auto frame = av_frame_alloc();
// 解码数据
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == stream_index)
decode(dec_ctx, pkt, frame, outfile,type);
av_packet_unref(pkt);
}
// 刷新解码器
decode(dec_ctx, NULL, frame, outfile,type);
fclose(outfile);
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&dec_ctx);
avformat_close_input(&fmt_ctx);
}
int main(int argc, char argv) {
DecodeData("d://11.mp4", "d://1.pcm", AVMEDIA_TYPE_AUDIO);
DecodeData("d://11.mp4", "d://12.yuv", AVMEDIA_TYPE_VIDEO);
return 0;
}3. 总结
音频解码和视频解码遵循相同的运行规律,都是通过读取压缩数据、解码、输出数据的过程。所以本文的示例代码依据AVMEDIA_TYPE_AUDIO和AVMEDIA_TYPE_VIDEO来区分音频解码和视频解码。解码文件过程固定,熟悉这个过程即可。