在网上下载了一些pdf版的ffmpeg的tutorial中文版,在学习过程中发现有很多错误,这些错误,或者是文章中的代码中的变量作者没有定义,或者是由于ffmpeg一直在更新,”以时俱进“,而这些资料早已年久失修,一些函数早已更名,或被别的函数替代,学习过程中发现很多问题,故决定做下笔记,做出总结:
FFMPEG 是一个很好的库,可以用来创建视频应用或者生成特定的工具。FFMPEG
几乎为你把所有的繁重工作都做了,比 如解码、编码、复用和解复用。这使得
多媒体应用程序变得容易编写。它是一个简单的,用C 编写的,快速的并且能够
解码 几乎所有你能用到的格式,当然也包括编码多种格式。
唯一的问题是它的文档基本上是没有的。有一个单独的指导讲了它的基本原理另
外还有一个使用doxygen 生成的文档。这就是为什么当我决定研究 FFMPEG 来弄
清楚音视频应用程序是如何工作的过程中,我决定把这个过程用文档的形式记录
并且发布出来作为初学指导的原因。
在FFMPEG 工程中有一个示例的程序叫作ffplay。它是一个用C 编写的利用
ffmpeg 来实现完整视频播放的简单播放器。这个指导将从原来Martin Bohme 写
的一个更新版本的指导开始(我借鉴了一些),基于Fabrice Bellard 的ffplay,
我将从那里开发一个可以使用的视频播放器。在每一个指导中,我将介 绍一个
或者两个新的思想并且讲解我们如何来实现它。每一个指导都会有一个C 源文
件,你可以下载,编译并沿着这条思路来自己做。源文件将向你展示一个真正 的
程序是如何运行,我们如何来调用所有的部件,也将告诉你在这个指导中技术实
现的细节并不重要。当我们结束这个指导的时候,我 们将有一个少于1000 行代
码的可以工作的视频播放器。
在写播放器的过程中,我们将使用SDL 来输出音频和视频。SDL 是一个优秀的跨
平台的多媒体库,被用在MPEG 播放、模拟器和很多视频游戏中。你将需要下载
并安装SDL 开发库到你的系统中,以便于编译这个指导中的程序。
这篇指导适用于具有相当编程背景的人。至少至少应该懂得C 并且有队列和互斥
量等概念。你应当了解基本的多媒体中的像波形一类的概念,但是你不必知道的
太 多,因为我将在这篇指导中介绍很多这样的概念。
更新:我修正了在指导7 和8 中的一些代码错误,也添加-lavutil 参数。
指 导1:制作屏幕录像
源代码:tutorial01.c
概要
电影文件有很多基本的组成部分。首先,文件本身被称为容 器Container,容
器的类型决定了信息被存放在文件中的位置。AVI 和 Quicktime 就是容器的例
子。接着,你有一组流, 例如,你经常有的是一个音频流和一个视频流。(一
个流只是一种想像出来的词语,用来表示一连 串的通过时间来串连的数据元
素)。在流中的数据元素被称为帧Frame。 每个流是由不同的编码器来编码生
成的。编解码器描述了实际的数据是如何被编码Coded 和解码DECoded 的,因此
它的名字叫做CODEC。Divx 和 MP3 就是编解码器的例子。接着从流中被读出来
的叫做包 Packets。包是一段数据,它包含了一段可以被解码成方便我们最后在
应用程序中操作的原始帧的数据。根据我们的目的,每个包包含 了完整的帧或
者对于音频来说是许多格式的完整帧。
基本上来说,处理视频和音频流是很容易的:
10 从video.avi 文件中打开视频流video_stream
20 从视频流中读取包到帧中
30 如果这个帧还不完整,跳到20
40 对这个帧进行一些操作
50 跳回到20
虽然很多程序可能在对帧进行操作的时候非常的复杂,但是在这个程序中若使用ffmpeg 来处理多种媒体是相当容易的
。因此在这篇指导中,我们将打开一个文件,读
取里面的视频流,而且我们对帧的操作将是把这个帧写到一个PPM 文件中。
打开文件
首先来看一下我们如何打开一个文件。通过ffmpeg,你必需先初始化这个库:
(注意在某些系统中必需 用<ffmpeg/avcodec.h>和<ffmpeg/avformat.h>来替
换)
这里注册了所有的文件格式和编解码器的库,所以它们将被自动的使用在被打
开的合适格式的文件上。注意你只需要调用av_register_all()一次,因此我们
在主函数main()中来调用它。如果你喜欢, 也可以只注册特定的格式和编解码
器,但是通常你没有必要这样做。
现在我们可以真正的打开文件:
我们通过第一个参数来获得文件名。这个av_open_input_file函数读取文件的头部并且把信息保存到
我们给的AVFormatContext 结构体中。最后三个参数用来指定特殊的文件格式,
缓冲大小和格式参数,但如果把它们设置为空NULL 或者0,libavformat 将自动
检测这些参数。(av_open_input_file函数现在已被avformat_open_input函数取代)
关于avformat_open_input:
这个函数
只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
这个av_find_stream_info函数(已被avformat_find_stream_info函数取代)为pFormatCtx->streams 填充上正确的信息。我们引进一个手工调试的
函数来看一下里面有什么:
现在pFormatCtx->streams 仅仅是一组大小为pFormatCtx->nb_streams 的指针,
所以让我们先跳过它直到 我们找到一个视频流。
源代码中的pFormatCtx->streams[i]->codec->codec_type类型为:
流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文)
的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一
个指向它的指针。但是我们必需要找到真正的 编解码器并且打开它:
有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加
CODEC_FLAG_TRUNCATED 到pCodecCtx->flags 和添 加一个hack 来粗糙的修正帧
率。这两个修正已经不再存在于ffplay.c 中。因此我必须假设它们不再必要。
我们移除了那些代码后还有一个需要指出的不同点:pCodecCtx->time_base 现
在已经保存了帧率的信息。time_base 是一个结构体,它里面有一个分子和分母
(AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数
的帧率(例如NTSC 使用29.97fps)。
保存数据
现在我们需要找到一个地方来保存帧:
因为我们准备输出保存24 位RGB 色的PPM 文件,我们必需把帧的格式从原来的
转换为RGB。FFMPEG 将为我们做这些转换。在大多数项目中(包括我们的这个)
我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内
存。
即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始
的数据。我们使用avpicture_get_size 来获得我们需要的大小, 然后手工申请
内存空间:
av_malloc 是ffmpeg 的malloc,用来实现一个简单的malloc 的包装,这样来保
证内存地址是对齐的(4 字节对齐或者2 字节对齐)。它并不能保证你不被内
存泄漏,重复释放或者其它malloc 的问题所困扰。
现在我们使用avpicture_fill函数把帧和我们新申请到的内存结合。关于
AVPicture 的构成:AVPicture 结构体是AVFrame 结构体的子集――AVFrame 结
构体的开始部分与AVPicture 结构体是一样的。
其中的avpicture_fill()函数含义:
最后,我们已经准备好来从流中读取数据了。
读取数据
我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最后转换
格式并保存。
这个循环过程是比较简单的:av_read_frame()读取一个包并且把它保存到
AVPacket 结构体中。注意我们仅仅申请了一个包的结构体――ffmpeg 为我们申
请了内部的数据的内存并通过packet.data 指针来指向它。这些数据可以在后面
通过av_free_packet()函数来释放。函avcodec_decode_video()把包转换为帧。
然而当解码一个包的时候,我们可能没有得到我们需要的关于一帧的完整信息。因此,
当我们得 到一个帧的时候,avcodec_decode_video()为我们设置了帧结束标志
frameFinished。然后我们使用 img_convert()函数来把帧从原始格式
(pCodecCtx->pix_fmt)转换成为RGB 格式(img_convert()函数已被sws_scale()函数取代)。要记住,你可以把一个 AVFrame
结构体的指针转换为AVPicture 结构体的指针。最后我们把帧以及高度,宽度信息
传递给我们的SaveFrame 函数。
SwsContext:视频分辩率、色彩空间变换时所需要的上下文句柄。
关于sws_scale()函数:
关于包Packets的注释
从技术上讲一个包可以包含部分或者其它的数据,但是 ffmpeg 的解释器保证了
我们得到的包Packets 包含的要么是完整的要么是多种完整的帧。
现在我们需要做的是让SaveFrame 函数能把RGB 信息定稿到一个PPM 格式的文件
中。我们将生成一个简单的PPM 格式文件,请相信,它是可以工作 的。
我们做了一些标准的文件打开动作,然后写入RGB 数据。我们一次向文件写入一
行数据。PPM 格式文件的是一种包含一长串的RGB 数据的文件。如果你了解 HTML
色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,就像
#ff0000#ff0000....就表示了了个红色的屏幕。(它被保存成 二进制方式并且
没有分隔符,但是你自己是知道如何分隔的)。文件的头部表示了图像的宽度和
高度以及最大的RGB 值的大小
。
关于PPM文件:
ppm是一种简单的图像格式,仅包含格式、图像宽高、bit数等信息和图像数据。
图像数据的保存格式可以用ASCII码,也可用二进制,下面只说说一种ppm格式中比较简单的一种:24位彩色、二进制保存的图像。
文件头+rgb数据:
P6\n
width height\n
255\n
rgbrgb...
其中P6表示ppm的这种格式;\n表示换行符;width和height表示图像的宽高,用空格隔开;255表示每个颜色分量的最大值;rgb数据从上到下,从左到右排放
图像数据的保存格式可以用ASCII码,也可用二进制,下面只说说一种ppm格式中比较简单的一种:24位彩色、二进制保存的图像。
文件头+rgb数据:
P6\n
width height\n
255\n
rgbrgb...
其中P6表示ppm的这种格式;\n表示换行符;width和height表示图像的宽高,用空格隔开;255表示每个颜色分量的最大值;rgb数据从上到下,从左到右排放
文件头由3行文本组成,可由fgets读出
1)第一行为“P6",表示文件类型
2)第二行为图像的宽度和高度
3)第三行为最大的象素值
接下来是图像数据块。按行顺序存储。每个象素占3个字节,依次为红绿蓝通道,每个通道为1字节整
数。左上角为坐标原点。
1)第一行为“P6",表示文件类型
2)第二行为图像的宽度和高度
3)第三行为最大的象素值
接下来是图像数据块。按行顺序存储。每个象素占3个字节,依次为红绿蓝通道,每个通道为1字节整
数。左上角为坐标原点。
现在,回顾我们的main()函数。一旦我们开始读取完视频流,我们必需清理一
切:
你会注意到我们使用av_free 来释放我们使用avcode_alloc_fram 和av_malloc
来分配的内存。
在我Linux系统下编译的命令:
在我Linux系统下编译的命令:
gcc ./tutorial01.c
-o ./tutorial01
-lavutil -lavformat -lavcodec -lswscale -lz
-lm -I /home/Jiakun/ffmpeg_build/include -L /home/Jiakun/ffmpeg_build/lib/