基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)

系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成

@TOC


前言

前面一章中我们介绍了如何使用 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)是一种电子电路或软件,用于压缩或解压缩数字音频/视频。它将原始(未压缩)的数字音频/视频转换为压缩格式,反之亦然。en.wikipedia.org/wiki/Video_...

但是,如果我们选择将数百万个图像打包到一个文件中,并称之为电影,可能会得到一个巨大的文件。让我们做个计算:

假设我们正在创建一个分辨率为1080 x 1920(高 x 宽)的视频,每个像素的颜色编码需要3个字节(屏幕上的最小点)(或者称为24位颜色,提供了16,777,216种不同的颜色),这个视频以每秒24帧的速度运行,并持续30分钟。

ini 复制代码
toppf = 1080 * 1920 //每帧的像素总数
cpp = 3 //每个像素的成本
tis = 30 * 60 //时间长度(以秒为单位)
fps = 24 //每秒帧数

required_storage = tis * fps * toppf * cpp

这个视频将需要约250.28GB的存储空间或1.19 Gbps的带宽!这就是为什么我们需要使用编解码器。

容器 - 存放音频和视频的地方

容器或封装格式是一种元文件格式,其规范描述了不同数据和元数据在计算机文件中如何共存。en.wikipedia.org/wiki/Digita...

容器是一个包含所有流(通常是音频和视频)的单个文件,并提供同步和通用元数据,如标题、分辨率等。 通常,我们可以通过查看文件的扩展名来推断其格式:例如,video.webm可能是使用 webm 容器的。 当我们谈到视频格式时,常提到的是封装格式,比如 MP4。一个 MP4 文件可以包含一个视频流和一个音频流,其中视频流通常使用 H.264 进行视频压缩,音频流则通常使用 AAC 进行压缩。因此,当我们提及 H.264 和 AAC 时,它们既可以视为视频和音频的编码格式,也可以视为用于视频和音频压缩的算法。然而,通常我们很少单独将一个视频描述为 H.264 格式,因为视频通常以封装格式的形式出现,封装格式包含了视频流和音频流。

在下文中,我们会提及视频文件、封装格式、容器这三个术语,通常它们是指同一个意思。


解封装

回到今天的任务:使用 ffmpeg 将视频解封装。

在大多数情况下,你下载到的视频文件是一个容器,一个视频文件包含多个流(stream),通常包括视频流和音频流。流(stream)是指一系列随时间可用的数据元素。每个流使用不同的编解码器进行编码,编解码器定义了实际数据的编码和解码方式,因此被称为编解码器(CODEC),例如MP3、H.264等。从流中可以读取数据包(packet),数据包包含经过编码器压缩后的数据。将数据包传递给解码器后,我们可以获取到所需的视频帧数据。在FFmpeg中,存放视频帧数据的数据结构被称为帧(frame)。

为了从这个容器中找到视频并将其解码,第一步需要做的是解封装(demux)。前面提到了容器,它就好像一个盒子,你可以往里头装不同的物品,例如视频、音频、字幕等等。解封装就相当于打开这盒子,按需的取出里头的各类物品,以便能够在播放器或者设备上进行播放、编辑或者其他处理。

现在假设我们要从视频文件中获取到视频数据,进行解封装流程为:

  1. 打开 video.mp4 文件
  2. 从 streams 中找到视频流
  3. 从视频流中读取 packet

使用 ffmpeg api 进行解封装

如果站在 ffmpeg api 使用者的角度来看解封装的流程:

  1. 你首先需要将视频文件加载到一个叫 AVFormatContext 的组件中。实际上,它并不完全加载整个文件,通常只读取文件的头部信息。
  2. 加载了视频文件的最小头部信息后,我们就可以访问文件的流 ,在 ffmpeg 中使用 AVStream 来保存这些流的信息。
  3. 假设我们有两个流:一个使用 AAC 编码器进行编码的音频流,另一个是使用 h264 编码器进行编码的视频流。从每个流中,我们可以读取名为 packet 的数据片段,这些数据片段在 ffmepg 使用 AVPacket 来保存。

话不多说,让我们上代码。解封装的代码你可以在 ffmpeg_video_player_tutorial-tutorial01 找到。解下来是代码的详细解释,我们会跳过一些细节,但没有关系,你可以在源码中找到你想要的。

首先我们声明一个 AVFormatContext 组件,它里头存放着关于容器的关键信息,主要用于封装(muxing)和解封装(demuxing)媒体文件。

cpp 复制代码
AVFormatContext * pFormatCtx = NULL;

接下来,我们将打开文件并读取其头部信息,并填充AVFormatContext结构体,提供有关格式的最小信息(注意,通常不会打开编解码器)。用于执行此操作的函数是avformat_open_input。它需要一个AVFormatContext、一个文件名和两个可选参数:AVInputFormat(如果传递NULL,FFmpeg将猜测格式)和AVDictionary(这些是解封装器的选项)。

cpp 复制代码
int ret = avformat_open_input(&pFormatCtx, argv[1], NULL, NULL);

如果梳理的话,你可以打印文件格式和视频时长:

cpp 复制代码
printf("Format %s, duration %lld us", pFormatCtx->iformat->long_name, pFormatCtx->duration);

avformat_open_input 只是读取了最小头部信息,接下去我们需要找到文件中流的信息,avformat_find_stream_info 用于执行次操作:

cpp 复制代码
ret = avformat_find_stream_info(pFormatCtx, NULL);

现在,pFormatCtx->nb_streams将保存流的数量,而pFormatCtx->streams[i]将给出第i个流(一个AVStream)。你可以通过循环查看所有流:

cpp 复制代码
for (int i = 0; i < pFormatContext->nb_streams; i++)
{
  //
}

由于我们目前只对视频感兴趣,因此在遍历 streams 时我们可以纪录视频流的下标,它在后面是有用的:

cpp 复制代码
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:

cpp 复制代码
AVPacket * pPacket = av_packet_alloc();

接着使用 av_read_frame 从文件中读取一个 packet:

arduino 复制代码
while (av_read_frame(pFormatContext, pPacket) >= 0) {
  //...
}

读取的到的这个 packet,它可能来自视频流,也可能来自其他流。由于我们只对视频数据感兴趣,如果当前的 packet 来自其他流,那么直接忽略处理即可:

cpp 复制代码
if (pPacket->stream_index == videoStream)
{
	// do something on video data
}

看!这里对 packet 来源的进行不同的处理,就是所谓的解封装,就这么简单!


总结

本文介绍了视频、音频、编解码器和容器的基本概念,介绍了什么是解封装以及使用 ffmpeg api 进行解封装的基本流程。所有代码可以在 ffmpeg_video_player_tutorial-tutorial01 中找到。

参考

相关推荐
问道飞鱼4 小时前
【工具介绍】Ffmpeg工具介绍与简单使用
ffmpeg·视频工具
l***77528 小时前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg
ZouZou老师14 小时前
FFmpeg性能优化经典案例
性能优化·ffmpeg
aqi0017 小时前
FFmpeg开发笔记(九十)采用FFmpeg套壳的音视频转码百宝箱FFBox
ffmpeg·音视频·直播·流媒体
齐齐大魔王19 小时前
FFmpeg
ffmpeg
你好音视频20 小时前
FFmpeg RTSP拉流流程深度解析
ffmpeg
IFTICing1 天前
【环境配置】ffmpeg下载、安装、配置(Windows环境)
windows·ffmpeg
haiy20111 天前
FFmpeg 编译
ffmpeg
aqi001 天前
FFmpeg开发笔记(八十九)基于FFmpeg的直播视频录制工具StreamCap
ffmpeg·音视频·直播·流媒体
八月的雨季 最後的冰吻2 天前
FFmepg--28- 滤镜处理 YUV 视频帧:实现上下镜像效果
ffmpeg·音视频