在前面章节 基于 FFMPEG 的跨平台视频播放器简明教程(二):基础知识和解封装(demux) 中我们引入了视频编解码的基础知识以及解封装的概念。
请记住我们的任务:使用 ffmpeg 解码视频,并将解码后的视频帧保存在本地(就像对视频截图一样)。今天,围绕这个任务让我们继续下一个知识点:视频解码。
本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 01: Making Screencaps。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。
本文的代码在 ffmpeg_video_player_tutorial-tutorial01。
概括来说,视频解码的步骤包括:
在 ffmpeg 中与解码器相关的结构体有两个:AVCodec 和 AVCodecContext。
AVCodec结构体包含了编解码器的特定信息,如编解码器的类型、名称、支持的像素格式或音频样本格式等。你可以使用 avcodec_find_decoder
从 ffmpeg 支持的编解码器中找到你需要的那个。
AVCodec *avcodec_find_decoder(enum AVCodecID id);
avcodec_find_decoder 函数的主要目的是根据给定编解码器ID(AVCodecID)找到合适的解码器。在实现逻辑中,它对FFmpeg支持的所有编解码器进行迭代,并比较它们的AVCodecID与所需的AVCodecID。
如果发现有无法找到某个 id,有可能是因为你使用的 ffmpeg 做了裁剪,不支持这种类型的 codec,这时候你可以在代码中打印一下当前 ffmpeg 支持的 codec 信息:
const AVCodec *codec = NULL;
void *i = 0;
printf("List of supported codecs:\n");
// Iterate over all codecs using av_codec_iterate
// Note: use av_codec_next(codec) instead for older versions of FFmpeg
while ((codec = av_codec_iterate(&i))) {
printf("Codec name: %s, codec type: %s\n", codec->name,
codec->type == AVMEDIA_TYPE_AUDIO ? "Audio"
: codec->type == AVMEDIA_TYPE_VIDEO ? "Video"
: codec->type == AVMEDIA_TYPE_SUBTITLE ? "Subtitle"
: "Other/Unknown");
}
AVCodec 结构体仅仅是对某个编解码器的描述,要进行编解码还需要 AVCodecContext 参与。
在 FFmpeg 中,AVCodecContext 是一个结构体,它表示编解码器的上下文,主要负责存储与编解码器相关的配置信息和状态。AVCodecContext 的作用在于为音频、视频或字幕数据的编码和解码过程提供所需要的各种参数和数据。AVCodecContext 包含以下主要信息:
要使用特定的 AVCodec 对象进行编解码,需要为其配置一个相应的 AVCodecContext,并设置相应的参数。然后使用 FFmpeg 提供的函数(如 avcodec_open2,avcodec_send_packet 等)对数据进行编解码。
因此,AVCodecContext 是连接原始数据、编解码器(AVCodec)和输出数据之间的桥梁。它帮助用户在输入和输出之间传递数据,并提供编解码过程所需的参数。
在代码中,使用 avcodec_alloc_context3
创建一个 AVCodecContext
pCodecCtx = avcodec_alloc_context3(pCodec);
接着,需要填充 AVCodecContext 中各种信息,一种简便的方式是使用 avcodec_parameters_to_context
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoStream]->codecpar);
最后一步,使用 avcodec_open2
打开编解码器并与 AVCodecContext 相关联。
avcodec_open2(pCodecCtx, pCodec, NULL);
关于解封装我们在 基于 FFMPEG 的跨平台视频播放器简明教程(二):基础知识和解封装(demux) 已经做了详细的介绍。
从文件中读取一个 packet 非常简单,代码如下:
AVPacket * pPacket = av_packet_alloc();
av_read_frame(pFormatCtx, pPacket); // 从 AVFormatContext 中读取一个 packet
if(pPacket->stream_index == videoStream) // 只处理视频流
{
// do something
}
av_packet_alloc
用于申请一个 AVPacketav_read_frame
从 AVFormatContext 中读取一个 packet这一步非常简单,调用 avcodec_send_packet
即可。avcodec_send_packet函数的主要作用如下:
avcodec_send_packet 函数的返回值是值得注意的,用于表示操作的结果。以下是可能的返回值及其含义:
0:操作成功。这意味着输入的压缩数据包已成功传递给解码器。
AVERROR(EAGAIN):当前解码器的状态不允许接收更多的数据包。这通常意味着解码器内部缓冲区已满,需要先调用avcodec_receive_frame()函数接收解码帧才能继续发送数据包。
AVERROR_EOF:解码器已经被刷新并且不再接受数据包。这意味着文件或流已结束,并且解码器已经清空。
AVERROR(EINVAL):提供的AVCodecContext或AVPacket无效,例如AVCodecContext为NULL。也可能意味着解码器没有被正确打开,或者在编码器AVCodecContext上调用了avcodec_send_packet。
AVERROR(ENOMEM):解码器内部缓冲区分配失败,内存不足。
其他负数:其他库错误或解码器实现特定的错误代码,具体的错误代码可以通过 av_err2str 函数将错误码转为字符串进行输出。
这一步也非常简单,使用 avcodec_receive_frame
从 codec 中取回解码后的数据。avcodec_receive_frame 函数的主要作用如下:
avcodec_send_packet
和 avcodec_receive_frame
一般是成配对使用的,但是你看代码通常这部分代码会夹杂了一些 while/for 循环,这是为啥?这是因为 packet 与 frame 的生成速度不一定是一对一的:avcodec_send_packet
发送了一个 packet 之后,avcodec_receive_frame
可能没有产生,也可能产出多帧。因此你需要用一个 for/while 循环来处理。
while (ret >= 0) {
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
// EOF exit loop
break;
} else if (ret < 0) {
// could not decode packet
printf("Error while decoding.\n");
// exit with error
return -1;
}
}
本文说明了使用 ffmpeg api 进行视频解码的流程,步骤顺序为:
整个过程中,最为关键的部分是使用 avcodec_send_packet 和 avcodec_receive_frame 进行解码操作。理解这两个 api 是理解视频解码的关键。