前面一章中我们介绍了如何使用 conan 和 cmake 搭建 ffmpeg 运行环境,你做的还顺利吗?如果遇到任何问题,请在进行评论,我看到都会回复的。
从本章开始,将正式开始我们的 ffmpeg 播放器学习之旅。接下去的任务是:使用 ffmpeg 解码视频,并将解码后的视频帧保存在本地(就像对视频截图一样)。其中涉及到两个重要的知识点:解封装和视频解码。今天我们先聊解封装。此外,还会扩展 ffmpeg api 以及编解码相关的知识。
本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 01: Making Screencaps。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。
本文解封装的代码在 ffmpeg_video_player_tutorial-tutorial01。
本章节翻译自 ffmpeg-libav-tutorial Intro 部分。
视频频是由一系列图像组成的,这些图像以一定的频率改变(比如说每秒24帧),从而产生运动的错觉。简而言之,这就是视频的基本原理:以一定的速率运行的一系列图片/帧。
尽管无声视频可以表达各种感情,但添加声音会给体验带来更多的乐趣。声音是作为压力波传播的振动,通过空气或任何其他传输介质(如气体、液体或固体)。在数字音频系统中,麦克风将声音转换为模拟电信号,然后模拟-数字转换器(ADC)(通常使用脉冲编码调制(PCM))将模拟信号转换为数字信号。
编解码器(CODEC)是一种电子电路或软件,用于压缩或解压缩数字音频/视频。它将原始(未压缩)的数字音频/视频转换为压缩格式,反之亦然。https://en.wikipedia.org/wiki/Video_codec
但是,如果我们选择将数百万个图像打包到一个文件中,并称之为电影,可能会得到一个巨大的文件。让我们做个计算:
假设我们正在创建一个分辨率为1080 x 1920(高 x 宽)的视频,每个像素的颜色编码需要3个字节(屏幕上的最小点)(或者称为24位颜色,提供了16,777,216种不同的颜色),这个视频以每秒24帧的速度运行,并持续30分钟。
toppf = 1080 * 1920 //每帧的像素总数
cpp = 3 //每个像素的成本
tis = 30 * 60 //时间长度(以秒为单位)
fps = 24 //每秒帧数
required_storage = tis * fps * toppf * cpp
这个视频将需要约250.28GB的存储空间或1.19 Gbps的带宽!这就是为什么我们需要使用编解码器。
容器或封装格式是一种元文件格式,其规范描述了不同数据和元数据在计算机文件中如何共存。https://en.wikipedia.org/wiki/Digital_container_format
容器是一个包含所有流(通常是音频和视频)的单个文件,并提供同步和通用元数据,如标题、分辨率等。
通常,我们可以通过查看文件的扩展名来推断其格式:例如,video.webm可能是使用 webm 容器的。
当我们谈到视频格式时,常提到的是封装格式,比如 MP4。一个 MP4 文件可以包含一个视频流和一个音频流,其中视频流通常使用 H.264 进行视频压缩,音频流则通常使用 AAC 进行压缩。因此,当我们提及 H.264 和 AAC 时,它们既可以视为视频和音频的编码格式,也可以视为用于视频和音频压缩的算法。然而,通常我们很少单独将一个视频描述为 H.264 格式,因为视频通常以封装格式的形式出现,封装格式包含了视频流和音频流。
在下文中,我们会提及视频文件、封装格式、容器这三个术语,通常它们是指同一个意思。
回到今天的任务:使用 ffmpeg 将视频解封装。
在大多数情况下,你下载到的视频文件是一个容器,一个视频文件包含多个流(stream),通常包括视频流和音频流。流(stream)是指一系列随时间可用的数据元素。每个流使用不同的编解码器进行编码,编解码器定义了实际数据的编码和解码方式,因此被称为编解码器(CODEC),例如MP3、H.264等。从流中可以读取数据包(packet),数据包包含经过编码器压缩后的数据。将数据包传递给解码器后,我们可以获取到所需的视频帧数据。在FFmpeg中,存放视频帧数据的数据结构被称为帧(frame)。
为了从这个容器中找到视频并将其解码,第一步需要做的是解封装(demux)。前面提到了容器,它就好像一个盒子,你可以往里头装不同的物品,例如视频、音频、字幕等等。解封装就相当于打开这盒子,按需的取出里头的各类物品,以便能够在播放器或者设备上进行播放、编辑或者其他处理。
现在假设我们要从视频文件中获取到视频数据,进行解封装流程为:
如果站在 ffmpeg api 使用者的角度来看解封装的流程:
话不多说,让我们上代码。解封装的代码你可以在 ffmpeg_video_player_tutorial-tutorial01 找到。解下来是代码的详细解释,我们会跳过一些细节,但没有关系,你可以在源码中找到你想要的。
首先我们声明一个 AVFormatContext 组件,它里头存放着关于容器的关键信息,主要用于封装(muxing)和解封装(demuxing)媒体文件。
AVFormatContext * pFormatCtx = NULL;
接下来,我们将打开文件并读取其头部信息,并填充AVFormatContext结构体,提供有关格式的最小信息(注意,通常不会打开编解码器)。用于执行此操作的函数是avformat_open_input。它需要一个AVFormatContext、一个文件名和两个可选参数:AVInputFormat(如果传递NULL,FFmpeg将猜测格式)和AVDictionary(这些是解封装器的选项)。
int ret = avformat_open_input(&pFormatCtx, argv[1], NULL, NULL);
如果梳理的话,你可以打印文件格式和视频时长:
printf("Format %s, duration %lld us", pFormatCtx->iformat->long_name, pFormatCtx->duration);
avformat_open_input
只是读取了最小头部信息,接下去我们需要找到文件中流的信息,avformat_find_stream_info 用于执行次操作:
ret = avformat_find_stream_info(pFormatCtx, NULL);
现在,pFormatCtx->nb_streams将保存流的数量,而pFormatCtx->streams[i]将给出第i个流(一个AVStream)。你可以通过循环查看所有流:
for (int i = 0; i < pFormatContext->nb_streams; i++)
{
//
}
由于我们目前只对视频感兴趣,因此在遍历 streams 时我们可以纪录视频流的下标,它在后面是有用的:
for (i = 0; i < pFormatCtx->nb_streams; i++)
{
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStream = i;
break;
}
}
每个 AVStream 里头有一个类型为 AVCodecParameters 成员变量叫 codecpar
,它描述了这个流中编解码相关的信息,例如 codec_id 是啥。关于编解码的信息非常重要,在后面的「视频解码」章节中将使用到这些信息。这里先暂时跳过。
接下来,根据 ffmpeg_video_player_tutorial-tutorial01 中代码,你会看到一系列和编解码相关的操作,例如通过 codec_id 找到编解码。但是先等等,让我们直接跳到读取 packet 的部分中,编解码内容的讲解将放到下一篇博客中。
接下来,我们将从流中读取 packet,首先要做的是先申请一个 AVPacket:
AVPacket * pPacket = av_packet_alloc();
接着使用 av_read_frame
从文件中读取一个 packet:
while (av_read_frame(pFormatContext, pPacket) >= 0) {
//...
}
读取的到的这个 packet,它可能来自视频流,也可能来自其他流。由于我们只对视频数据感兴趣,如果当前的 packet 来自其他流,那么直接忽略处理即可:
if (pPacket->stream_index == videoStream)
{
// do something on video data
}
看!这里对 packet 来源的进行不同的处理,就是所谓的解封装,就这么简单!
本文介绍了视频、音频、编解码器和容器的基本概念,介绍了什么是解封装以及使用 ffmpeg api 进行解封装的基本流程。所有代码可以在 ffmpeg_video_player_tutorial-tutorial01 中找到。