基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码

系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
  2. 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)

@TOC


前言

在前面章节 基于 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 api 进行视频解码的步骤

概括来说,视频解码的步骤包括:

  1. 创建解码器
  2. 解封装,从视频流中读取一个 packet
  3. 将 packet 送给解码器,解码器进行解码
  4. 从解码器中,取回解码后的数据

创建解码器

在 ffmpeg 中与解码器相关的结构体有两个:AVCodec 和 AVCodecContext。

AVCodec结构体包含了编解码器的特定信息,如编解码器的类型、名称、支持的像素格式或音频样本格式等。你可以使用 avcodec_find_decoder 从 ffmpeg 支持的编解码器中找到你需要的那个。

c 复制代码
AVCodec *avcodec_find_decoder(enum AVCodecID id);

avcodec_find_decoder 函数的主要目的是根据给定编解码器ID(AVCodecID)找到合适的解码器。在实现逻辑中,它对FFmpeg支持的所有编解码器进行迭代,并比较它们的AVCodecID与所需的AVCodecID。

如果发现有无法找到某个 id,有可能是因为你使用的 ffmpeg 做了裁剪,不支持这种类型的 codec,这时候你可以在代码中打印一下当前 ffmpeg 支持的 codec 信息:

cpp 复制代码
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 包含以下主要信息:

  1. 编解码器类型(音频、视频或字幕)
  2. 编解码器的 ID(用于标识特定的编解码器,例如 H.264,MP3 等)
  3. 时间基(用于计算时间戳)
  4. 帧率或采样率(视频或音频播放的速度)
  5. 比特率(编解码后的数据流的速率)
  6. 编码或解码期间使用的各种配置选项(如像素格式,音频通道数量,视频分辨率等)

要使用特定的 AVCodec 对象进行编解码,需要为其配置一个相应的 AVCodecContext,并设置相应的参数。然后使用 FFmpeg 提供的函数(如 avcodec_open2,avcodec_send_packet 等)对数据进行编解码。

因此,AVCodecContext 是连接原始数据、编解码器(AVCodec)和输出数据之间的桥梁。它帮助用户在输入和输出之间传递数据,并提供编解码过程所需的参数。

在代码中,使用 avcodec_alloc_context3 创建一个 AVCodecContext

cpp 复制代码
pCodecCtx = avcodec_alloc_context3(pCodec); 

接着,需要填充 AVCodecContext 中各种信息,一种简便的方式是使用 avcodec_parameters_to_context

cpp 复制代码
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoStream]->codecpar);

最后一步,使用 avcodec_open2 打开编解码器并与 AVCodecContext 相关联。

cpp 复制代码
avcodec_open2(pCodecCtx, pCodec, NULL);

解封装,读取 packet

关于解封装我们在 基于 FFMPEG 的跨平台视频播放器简明教程(二):基础知识和解封装(demux) 已经做了详细的介绍。

从文件中读取一个 packet 非常简单,代码如下:

cpp 复制代码
AVPacket * pPacket = av_packet_alloc(); 
av_read_frame(pFormatCtx, pPacket); // 从 AVFormatContext 中读取一个 packet
if(pPacket->stream_index == videoStream) // 只处理视频流
{
	// do something
} 
  1. av_packet_alloc 用于申请一个 AVPacket
  2. av_read_frame 从 AVFormatContext 中读取一个 packet
  3. 判断当前 packet 是否是视频数据(或者其他你想要的数据),接着进行处理

将 packet 送给解码器,解码器进行解码

这一步非常简单,调用 avcodec_send_packet 即可。avcodec_send_packet函数的主要作用如下:

  1. 将输入的压缩数据包传递给解码器进行解码。
  2. 在数据发送完毕时(例如,文件结束或流结束),传递NULL数据包以通知解码器将剩余数据刷新。

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 函数的主要作用如下:

  1. 尝试从解码器获得已解码的帧(例如,解码后的视频或音频帧)。
  2. 提供对解码器内部缓冲区和状态管理的抽象,使得调用者不需要直接处理内部缓冲区和状态。
  3. 在解码器已经处理完所有输入数据包且内部缓冲区已空时,返回AVERROR_EOF,从而告知调用者解码过程已完成。
  4. 如果解码器需要更多的输入数据包才能生成解码帧,则返回AVERROR(EAGAIN),告知调用者继续发送数据包。

avcodec_send_packetavcodec_receive_frame 一般是成配对使用的,但是你看代码通常这部分代码会夹杂了一些 while/for 循环,这是为啥?这是因为 packet 与 frame 的生成速度不一定是一对一的:avcodec_send_packet 发送了一个 packet 之后,avcodec_receive_frame 可能没有产生,也可能产出多帧。因此你需要用一个 for/while 循环来处理。

cpp 复制代码
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 进行视频解码的流程,步骤顺序为:

  1. 创建解码器
  2. 解封装,从视频流中读取一个 packet
  3. 将 packet 送给解码器,解码器进行解码
  4. 从解码器中,取回解码后的数据

整个过程中,最为关键的部分是使用 avcodec_send_packet 和 avcodec_receive_frame 进行解码操作。理解这两个 api 是理解视频解码的关键。

参考

相关推荐
l***775218 小时前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg
ZouZou老师1 天前
FFmpeg性能优化经典案例
性能优化·ffmpeg
aqi001 天前
FFmpeg开发笔记(九十)采用FFmpeg套壳的音视频转码百宝箱FFBox
ffmpeg·音视频·直播·流媒体
齐齐大魔王1 天前
FFmpeg
ffmpeg
你好音视频1 天前
FFmpeg RTSP拉流流程深度解析
ffmpeg
IFTICing2 天前
【环境配置】ffmpeg下载、安装、配置(Windows环境)
windows·ffmpeg
haiy20112 天前
FFmpeg 编译
ffmpeg
aqi002 天前
FFmpeg开发笔记(八十九)基于FFmpeg的直播视频录制工具StreamCap
ffmpeg·音视频·直播·流媒体
八月的雨季 最後的冰吻2 天前
FFmepg--28- 滤镜处理 YUV 视频帧:实现上下镜像效果
ffmpeg·音视频
ganqiuye2 天前
向ffmpeg官方源码仓库提交patch
大数据·ffmpeg·video-codec