FFMEG sdk1
概要 概要
电影文件有很多基本的组成部分。首先,文件本身被称为容 容容 容 器 器器 器 Container Container Container,容
器的类型决定了信息被存放在文件中的位置。AVI 和 Quicktime 就是容器的例
子。接着,你有一组流 流流 流, 例如,你经常有的是一个音频流和一个视频流。(一
个流只是一种想像出来的词语,用来表示一连 串的通过时间来串连的数据元
素)。在流中的数据元素被称为帧 帧帧 帧 Frame Frame 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>来替
换)
#include <avcodec.h>
#include <avformat.h>
...
int main(int argc, charg *argv[]) {
av_register_all();
这里注册了所有的文件格式和编解码器的库,所以它们将被自 动的使用在被打
开的合适格式的文件上。注意你只需要调用av_register_all()一次,因此我们
在主函数main()中来调用它。如果你喜欢, 也可以只注册特定的格式和编解码
器,但是通常你没有必要这样做。
现在我们可以真正的打开文件:
AVFormatContext *pFormatCtx;
// Open video file
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
return -1; // Couldn't open file
我们通过第一个参数来获得文件名。这个函数读取文件的头部并且把信息保存到
我们给的AVFormatContext 结构体中。最后三个参数用来指定特殊的 文件格式,
缓冲大小和格式参数,但如果把它们设置为空NULL 或者0,libavformat 将自动
检测这些参数。
这个函数只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
// Retrieve stream information
if(av_find_stream_info(pFormatCtx)<0)
return -1; // Couldn't find stream information
这个函数为pFormatCtx->streams 填充上正确的信息。我们引进一个手工调试的
函数来看一下里面有什么:
// Dump information about file onto standard error
dump_format(pFormatCtx, 0, argv[1], 0);
现在pFormatCtx->streams仅仅是一组大小为pFormatCtx->nb_streams的指针,
所以让我们先跳过它直到 我们找到一个视频流。
int i;
AVCodecContext *pCodecCtx;
// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1)
return -1; // Didn't find a video stream
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文)
的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一
个指向他的指针。但是我们必需要找到真正的 编解码器并且打开它:
AVCodec *pCodec;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec)<0)
return -1; // Could not open codec
有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加
CODEC_FLAG_TRUNCATED 到 pCodecCtx->flags 和添 加一个hack 来粗糙的修正帧
率。这两个修正已经不在存在于ffplay.c 中。因此,我必需假设它们不再必要。
我们移除了那些代码后还有一个需要指出的 不同点:pCodecCtx->time_base 现
在已经保存了帧率的信息。time_base 是一个结构体,它里面有一个分子和分母
(AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数
的帧率(例如NTSC 使用29.97fps)。
保存数据
现在我们需要找到一个地方来保存帧:
AVFrame *pFrame;
// Allocate video frame
pFrame=avcodec_alloc_frame();
因为我们准备输出保存24 位 RGB 色的PPM 文件,我们必需把帧的格式从原来的
转换为RGB。FFMPEG 将为我们做这些转换。在大多数项目中(包括我们 的这个)
我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内
存。
// Allocate an AVFrame structure
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
return -1;
即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始
的数据。我们使用avpicture_get_size 来获得我们需要的大小, 然后手工申请
内存空间:
uint8_t *buffer;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_malloc 是 ffmpeg 的 malloc,用来实现一个简单的malloc 的包装,这样来保
证内存地址是对齐的(4 字节对齐或者2 字节对齐)。它并不能保 护你不被内
存泄漏,重复释放或者其它malloc 的问题所困扰。
现在我们使用avpicture_fill 来把帧和我们新申请的内存来结合。关于
AVPicture 的结成:AVPicture 结构体是AVFrame 结 构体的子集――AVFrame 结
构体的开始部分与AVPicture 结构体是一样的。
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);
最后,我们已经准备好来从流中读取数据了。
读取数据
我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最好后转换
格式并且保存。
int frameFinished;
AVPacket packet;
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video(pCodecCtx, pFrame, &frameFinished,
packet.data, packet.size);
// Did we get a video frame?
if(frameFinished) {
// Convert the image from its native format to RGB
img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24,
(AVPicture*)pFrame, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height);
// Save the frame to disk
if(++i<=5)
SaveFrame(pFrameRGB, pCodecCtx->width,
pCodecCtx->height, i);
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
这个循环过程是比较简单的:av_read_frame()读取一个包并且把它保存到
AVPacket 结构体中。注意我们仅仅申请了一个包的结构体 ――ffmpeg 为我们申
请了内部的数据的内存并通过packet.data 指针来指向它。这些数据可以在后面
通过av_free_packet()来释 放。函数avcodec_decode_video()把包转换为帧。
然而当解码一个包的时候,我们可能没有得到我们需要的关于帧的信息。因此,
当我们得 到下一帧的时候,avcodec_decode_video()为我们设置了帧结束标志
frameFinished。最后,我们使用 img_convert()函数来把帧从原始格式
(pCodecCtx->pix_fmt)转换成为RGB 格式。要记住,你可以把一个 AVFrame
结构体的指针转换为AVPicture 结构体的指针。最后,我们把帧和高度宽度信息
传递给我们的SaveFrame 函数。
关于包Packets Packets 的注释 的注释
从技术上讲一个包可以包含部分或者其它的数据,但是 ffmpeg 的解释器保证了
我们得到的包Packets 包含的要么是完整的要么是多种完整的帧。
现在我们需要做的是让SaveFrame 函数能把RGB 信息定稿到一个PPM 格式的文件
中。我们将生成一个简单的PPM 格式文件,请相信,它是可以工作 的。
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// Open file
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile=fopen(szFilename, "wb");
if(pFile==NULL)
return;
// Write header
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data
for(y=0; y<height; y++)
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
// Close file
fclose(pFile);
}
我们做了一些标准的文件打开动作,然后写入RGB 数据。我们一次向文件写入一
行数据。PPM 格式文件的是一种包含一长串的RGB 数据的文件。如果你了解 HTML
色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,就像
#ff0000#ff0000....就表示了了个红色的屏幕。(它被保存成 二进制方式并且
没有分隔符,但是你自己是知道如何分隔的)。文件的头部表示了图像的宽度和
高度以及最大的RGB 值的大小。
现在,回顾我们的main()函数。一旦我们开始读取完视频流,我们必需清理一
切:
// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);
// Free the YUV frame
av_free(pFrame);
// Close the codec
avcodec_close(pCodecCtx);
// Close the video file
av_close_input_file(pFormatCtx);
return 0;
你会注意到我们使用av_free 来释放我们使用avcode_alloc_fram 和 av_malloc
来分配的内存。
上面的就是代码!下面,我们将使用 Linux 或者其它类似的平台,你将运行:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil
-lm
如果你使用的是老版本的ffmpeg,你可以去掉-lavutil 参数:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lm
大多数的图像处理函数可以打开PPM 文件。可以使用一些电影文件来进行测试
如何用FFmpeg FFmpeg 编写一个简单播放器详 编写一个简单播放器详
细步骤介绍(3) (3)
FFmpeg, 播放器, 编写
上来得到这个库的源代码或者如果有可能的话你可以直接下载开发包到你的操
作系统中。按照这个指导,你将需要编译这个库。(剩下的几个指导中也是一样)
SDL 库中有许多种方式来在屏幕上绘制图形,而且它有一个特殊的方式来在屏幕
上显示图像――这种方式叫做YUV 覆盖。YUV(从技术上来讲并不叫YUV 而是叫
做 YCbCr)是一种类似 于 RGB 方式的存储原始图像的格式。粗略的讲,Y 是亮度
分量,U 和 V 是色度分量。(这种格式比RGB 复杂的多,因为很多的颜色信息被
丢弃了,而且你可以每 2 个 Y 有 1 个 U 和 1 个 V)。SDL 的 YUV 覆盖使用一组原
始的YUV 数据并且在屏幕上显示出他们。它可以允许4 种不同的 YUV 格式,但
是其中的YV12 是最快的一种。还有一个叫做YUV420P 的 YUV 格式,它和YV12
是一样的,除了U 和 V 分量的位置被调换了以外。 420 意味着它以4:2:0 的
比例进行了二次抽样,基本上就意味着1 个颜色分量对应着4 个亮度分量。所以
它的色度信息只有原来的1/4。这是一种节省带宽 的好方式,因为人眼感觉不
到这种变化。在名称中的P 表示这种格式是平面的――简单的说就是Y,U 和 V
分量分别在不同的数组中。FFMPEG 可以把图像格式 转换为YUV420P,但是现在
很多视频流的格式已经是YUV420P 的了或者可以被很容易 的转换成YUV420P 格
式。
于是,我们现在计划把指导1 中的SaveFrame()函数替换掉,让它直接输出我们
的帧到屏幕上去。但一开始我们必需要先看一下如何使用SDL 库。首先 我们必
需先包含SDL 库的头文件并且初始化它。
#include <SDL.h>#include <SDL_thread.h> if(SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO | SDL_INIT_TIMER)) { fprintf(stderr, "Could not
initialize SDL - %s\n", SDL_GetError()); exit(1);}
SDL_Init()函数告诉了SDL 库,哪些特性我们将要用到。当然SDL_GetError()
是一个用来手工除错的函数。
创建一个显示
现在我们需要在屏幕上的一个地方放上一些东西。在 SDL 中显示图像的基本区域
叫做面 面面 面 surface surface surface。
SDL_Surface *screen; screen = SDL_SetVideoMode(pCodecCtx->width,
pCodecCtx->height, 0, 0);if(!screen) { fprintf(stderr, "SDL: could not
set video mode - exiting\n"); exit(1);}
这就创建了一个给定高度和宽度的屏幕。下一个选项是屏幕的颜色深度――0 表
示使用和当前一样的深度。(这个在OS X 系统上不能正常工作,原因请看源代
码)
现在我们在屏幕上来创建一个YUV 覆盖以便于我们输入视频上去:
SDL_Overlay *bmp; bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
pCodecCtx->height, SDL_YV12_OVERLAY,
screen);
正如前面我们所说的,我们使用YV12 来显示图像。
显示图像
前面那些都是很简单的。现在我们需要来显示图像。让我们看一下是如何来处理
完成后的帧的。我们将原来对RGB 处理的方式,并且替换SaveFrame() 为显示
到屏幕上的代码。为了显示到屏幕上,我们将先建立一个AVPicture 结构体并且
设置其数据指针和行尺寸来为我们的YUV 覆盖服务:
if(frameFinished) { SDL_LockYUVOverlay(bmp); AVPicture
pict; pict.data[0] = bmp->pixels[0]; pict.data[1] =
bmp->pixels[2]; pict.data[2] =
bmp->pixels[1]; pict.linesize[0] =
bmp->pitches[0]; pict.linesize[1] =
bmp->pitches[2]; pict.linesize[2] = bmp->pitches[1]; // Convert
the image into YUV format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); }
首先,我们锁定这个覆盖,因为我们将要去改写它。这是一个避免以后发生问题
的好习惯。正如前面所示的,这个AVPicture 结构体有一个数据指针指向一 个
有 4 个元素的指针数据。由于我们处理的是YUV420P,所以我们只需要3 个通道
即只要三组数据。其它的格式可能需要第四个指针来表示alpha 通道或 者其它
参数。行尺寸正如它的名字表示的意义一样。在 YUV 覆盖中相同功能的结构体是
像素pixel 和程度pitch。(程度pitch 是在SDL 里用来表 示指定行数据宽度
的值)。所以我们现在做的是让我们的覆盖中的pict.data 中的三个指针有一个
指向必要的空间的地址。类似的,我们可以直接从覆盖中得到行尺寸信息。像 前
面一样我们使用img_convert 来把格式转换成PIX_FMT_YUV420P。
绘制图像
但我们仍然需要告诉SDL 如何来实际显示我们给的数据。我们也会传递一个表明
电影位置、宽度、高度和缩放大小的矩形参数给SDL 的函数。这样,SDL 为我 们
做缩放并且它可以通过显卡的帮忙来进行快速缩放。
SDL_Rect rect; if(frameFinished) { // Convert the image into YUV
format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); rect.x =
0; rect.y = 0; rect.w = pCodecCtx->width; rect.h =
pCodecCtx->height; SDL_DisplayYUVOverlay(bmp, &rect); }
现在我们的视频显示出来了!
让我们再花一点时间来看一下SDL 的特性:它的事件驱动系统。SDL 被 设置成
当你在SDL 中点击或者移动鼠标或者向它发送一个信号它都将产生一个事件的
驱动方式。如果你的程序想要处理用户输入的话,它就会检测这些事件。你的程
序也可以产生事件并且传递给SDL事件系统。当使用SDL进行多线程编程的时候,
这相当有用,这方面代码我们可以在指导4 中看到。在这个程序中,我们将在 处
理完包以后就立即轮询事件。现在而言,我们将处理SDL_QUIT 事件以便于我们
退出:
SDL_Event event; av_free_packet(&packet); SDL_PollEvent
(&event); switch(event.type) { case
SDL_QUIT: SDL_Quit(); exit(0); break; default:
break; }
让我们去掉旧的冗余代码,开始编译。如果你使用的是Linux 或者其变体,使用
SDL 库进行编译的最好方式 为:
gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lz -lm
\`sdl-config --cflags --libs`
这里的sdl-config 命令会打印出用于gcc 编译的包含正确SDL 库的适当参数。
为了进行编译,在你自己的平台你可能需要做的有点不同:请查阅一下 SDL 文
档中关于你的系统的那部分。一旦可以编译,就马上运行它。
当运行这个程序的时候会发生什么呢?电影简直跑疯了!实际上,我们只是以我
们能从文件中解码帧的最快速度显示了所有的电影的帧。现在我们没有任何代码
来计 算出我们什么时候需要显示电影的帧。最后(在指导5),我们将花足够
的时间来探讨同步问题。但一开始我们会先忽略这个,因为我们有更加重要的事
情要处理: 音频!
指 指指 指 导 导导 导 333 3: :: :播放声音 播放声音
现在我们要来播放声音。SDL 也为我们准备了输出声音的方法。函数
SDL_OpenAudio()本身就是用来打开声音设备的。它使用一个叫做
SDL_AudioSpec 结构体作为参数,这个结构体中包含了我们将要输出的音频的所
有信息。
在我们展示如何建立之前,让我们先解释一下电脑是如何处理音频的。数字音频
是由一长串的样本流组成的。每个样本表示声音波形中的一个值。声音按照一个
特定 的采样率来进行录制,采样率表示以多快的速度来播放这段样本流,它的
表示方式为每秒多少次采样。例如22050 和 44100 的采样率就是电台和CD 常用
的 采样率。此外,大多音频有不只一个通道来表示立体声或者环绕。例如,如
果采样是立体声,那么每次的采样数就为2 个。当我们从一个电影文件中等到数
据的时候,我们不知道我们将得到多少个样 本,但是ffmpeg 将不会给我们部分
的样本――这意味着它将不 会把立体声分割开来。
SDL 播放声音的方式是这样的:你先设置声音的选项:采样率(在 SDL 的结构体
中被叫做freq 的表示频率frequency),声音通道数和其它的参 数,然后我们
设置一个回调函数和一些用户数据userdata。当开始播放音频的时候,SDL 将不
断地调用这个回调函数并且要求它来向声音缓冲填入一个特 定的数量的字节。
当我们把这些信息放到SDL_AudioSpec 结构体中后,我们调用函数
SDL_OpenAudio()就会打开声音设备并且给我们送 回另外一个AudioSpec 结构
体。这个结构体是我们实际上用到的--因为我们不能保证得到我们所要求的。
设置音频
目前先把讲的记住,因为我们实际上还没有任何关于声音流的信息。让我们回过
头来看一下我们的代码,看我们是如何找到视频流的,同样我们也可以找到声音
流。
// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO
&&
videoStream < 0) {
videoStream=i;
}
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_AUDIO &&
audioStream < 0) {
audioStream=i;
}
}
if(videoStream==-1)
return -1; // Didn't find a video stream
if(audioStream==-1)
return -1;
从这里我们可以从描述流的AVCodecContext 中得到我们想要的信息,就像我们
得到视频流的信息一样。
AVCodecContext *aCodecCtx;
aCodecCtx=pFormatCtx->streams[audioStream]->codec;
包含在编解码上下文中的所有信息正是我们所需要的用来建立音频的信息:
wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
让我们浏览一下这些:
·freq 前面所讲的采样率
·format 告诉SDL 我们将要给的格式。在“S16SYS”中的S 表示有符号的signed,
16 表示每个样本是16 位长的,SYS 表示大小头的顺序是与使用的系统相同的。
这些格式是由 avcodec_decode_audio2 为我们给出来的输入音频的格式。
·channels 声音的通道数
·silence 这是用来表示静音的值。因为声音采样是有符号的,所以0 当然就
是这个值。
·samples 这是当我们想要更多声音的时候,我们想让SDL 给出来的声音缓冲
区的尺寸。一个比较合适的值在512 到 8192 之间;ffplay 使用1024。
·callback 这个是我们的回调函数。我们后面将会详细讨论。
·userdata 这个是SDL 供给回调函数运行的参数。我们将让回调函数得到整个
编解码的上下文;你将在后面知道原因。
最后,我们使用SDL_OpenAudio 函数来打开声音。
如果你还记得前面的指导,我们仍然需要打开声音编解码器本身。这是很显然的。
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
avcodec_open(aCodecCtx, aCodec);
队列
嗯!现在我们已经准备好从流中取出声音信息。但是我们如何来处理这些信息
呢?我们将会不断地从文件中得到这些包,但同时SDL 也将调用回调函数。解决
方法 为创建一个全局的结构体变量以便于我们从文件中得到的声音包有地方存
放同时也保证SDL中的声音回调函数audio_callback能从这个地方得到声 音数
据。所以我们要做的是创建一个包的队列queue。在 ffmpeg 中有一个叫
AVPacketList 的结构体可以帮助我们,这个结构体实际是一串包 的链表。下面
就是我们的队列结构体:
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
首先,我们应当指出nb_packets 是与size 不一样的--size 表示我们从
packet->size 中得到的字节数。你会注意到我们有一 个互斥量mutex 和一个条
件变量cond 在结构体里面。这是因为SDL 是在一个独立的线程中来进行音频处
理的。如果我们没有正确的锁定这个队列,我们有 可能把数据搞乱。我们将来
看一个这个队列是如何来运行的。每一个程序员应当知道如何来生成的一个队
列,但是我们将把这 部分也来讨论从而可以学习到SDL 的函数。
一开始我们先创建一个函数来初始化队列:
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
接着我们再做一个函数来给队列中填入东西:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
SDL_LockMutex(q->mutex);
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
函数SDL_LockMutex()锁定队列的互斥量以便于我们向队列中添加东西,然后函
数 SDL_CondSignal()通过我们的条件变量为一个接 收函数(如果它在等待)发
出一个信号来告诉它现在已经有数据了,接着就会解锁互斥量并让队列可以自由
访问。
下面是相应的接收函数。注意函数SDL_CondWait()是如何按照我们的要求让函
数阻塞block 的(例如一直等到队列中有数据)。
int quit = 0;
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for(;;) {
if(quit) {
ret = -1;
break;
}
pkt1 = q->first_pkt;
if (pkt1) {
q->first_pkt = pkt1->next;
if (!q->first_pkt)
q->last_pkt = NULL;
q->nb_packets--;
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
正如你所看到的,我们已经用一个无限循环包装了这个函数以便于我们想用阻塞
的方式来得到数据。我们通过使用SDL 中的函数SDL_CondWait()来 避免无限循
环。基本上,所有的CondWait 只等待从SDL_CondSignal()函数(或者
SDL_CondBroadcast()函数)中发出 的信号,然后再继续执行。然而,虽然看起
来我们陷入了我们的互斥体中--如果我们一直保持着这个锁,我们的函数将永
远无法把数据放入到队列中去!但 是,SDL_CondWait()函数也为我们做了解锁
互斥量的动作然后才尝试着在得到信号后去重新锁定它。
意外情况
你们将会注意到我们有一个全局变量quit,我们用它来保证还没有设置程序退
出的信号(SDL 会自动处理TERM 类似的信 号)。否则,这个线程将不停地运行
直到我们使用kill -9 来结束程序。FFMPEG 同样也提供了一个函数来进行回调
并检查我们是否需要退出一些被阻塞的函数:这个函数就是
url_set_interrupt_cb。
int decode_interrupt_cb(void) {
return quit;
}
...
main() {
...
url_set_interrupt_cb(decode_interrupt_cb);
...
SDL_PollEvent(&event);
switch(event.type) {
case SDL_QUIT:
quit = 1;
...
当然,这仅仅是用来给ffmpeg 中的阻塞情况使用的,而不是SDL 中的。我们还
必需要设置quit 标志为1。
为队列提供包
剩下的我们唯一需要为队列所做的事就是提供包了:
PacketQueue audioq;
main() {
...
avcodec_open(aCodecCtx, aCodec);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
函数SDL_PauseAudio()让音频设备最终开始工作。如果没有立即供给足够的数
据,它会播放静音。
我们已经建立好我们的队列,现在我们准备为它提供包。先看一下我们的读取包
的循环:
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
....
}
} else if(packet.stream_index==audioStream) {
packet_queue_put(&audioq, &packet);
} else {
av_free_packet(&packet);
}
注意:我们没有在把包放到队列里的时候释放它,我们将在解码后来释放它。
取出包
现在,让我们最后让声音回调函数audio_callback 来从队列中取出包。回调函
数的格式必需为void callback(void *userdata, Uint8 *stream, int len),
这里的userdata 就是我们给到SDL 的指针,stream 是我们要把声音数据写入的
缓冲区指针,len 是缓冲区的大小。下面就是代码:
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
int len1, audio_size;
static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
static unsigned int audio_buf_size = 0;
static unsigned int audio_buf_index = 0;
while(len > 0) {
if(audio_buf_index >= audio_buf_size) {
audio_size = audio_decode_frame(aCodecCtx, audio_buf,
sizeof(audio_buf));
if(audio_size < 0) {
audio_buf_size = 1024;
memset(audio_buf, 0, audio_buf_size);
} else {
audio_buf_size = audio_size;
}
audio_buf_index = 0;
}
len1 = audio_buf_size - audio_buf_index;
if(len1 > len)
len1 = len;
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream += len1;
audio_buf_index += len1;
}
}
这基本上是一个简单的从另外一个我们将要写的audio_decode_frame()函数中概要 概要
电影文件有很多基本的组成部分。首先,文件本身被称为容 容容 容 器 器器 器 Container Container Container,容
器的类型决定了信息被存放在文件中的位置。AVI 和 Quicktime 就是容器的例
子。接着,你有一组流 流流 流, 例如,你经常有的是一个音频流和一个视频流。(一
个流只是一种想像出来的词语,用来表示一连 串的通过时间来串连的数据元
素)。在流中的数据元素被称为帧 帧帧 帧 Frame Frame 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>来替
换)
#include <avcodec.h>
#include <avformat.h>
...
int main(int argc, charg *argv[]) {
av_register_all();
这里注册了所有的文件格式和编解码器的库,所以它们将被自 动的使用在被打
开的合适格式的文件上。注意你只需要调用av_register_all()一次,因此我们
在主函数main()中来调用它。如果你喜欢, 也可以只注册特定的格式和编解码
器,但是通常你没有必要这样做。
现在我们可以真正的打开文件:
AVFormatContext *pFormatCtx;
// Open video file
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
return -1; // Couldn't open file
我们通过第一个参数来获得文件名。这个函数读取文件的头部并且把信息保存到
我们给的AVFormatContext 结构体中。最后三个参数用来指定特殊的 文件格式,
缓冲大小和格式参数,但如果把它们设置为空NULL 或者0,libavformat 将自动
检测这些参数。
这个函数只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
// Retrieve stream information
if(av_find_stream_info(pFormatCtx)<0)
return -1; // Couldn't find stream information
这个函数为pFormatCtx->streams 填充上正确的信息。我们引进一个手工调试的
函数来看一下里面有什么:
// Dump information about file onto standard error
dump_format(pFormatCtx, 0, argv[1], 0);
现在pFormatCtx->streams仅仅是一组大小为pFormatCtx->nb_streams的指针,
所以让我们先跳过它直到 我们找到一个视频流。
int i;
AVCodecContext *pCodecCtx;
// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1)
return -1; // Didn't find a video stream
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文)
的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一
个指向他的指针。但是我们必需要找到真正的 编解码器并且打开它:
AVCodec *pCodec;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec)<0)
return -1; // Could not open codec
有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加
CODEC_FLAG_TRUNCATED 到 pCodecCtx->flags 和添 加一个hack 来粗糙的修正帧
率。这两个修正已经不在存在于ffplay.c 中。因此,我必需假设它们不再必要。
我们移除了那些代码后还有一个需要指出的 不同点:pCodecCtx->time_base 现
在已经保存了帧率的信息。time_base 是一个结构体,它里面有一个分子和分母
(AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数
的帧率(例如NTSC 使用29.97fps)。
保存数据
现在我们需要找到一个地方来保存帧:
AVFrame *pFrame;
// Allocate video frame
pFrame=avcodec_alloc_frame();
因为我们准备输出保存24 位 RGB 色的PPM 文件,我们必需把帧的格式从原来的
转换为RGB。FFMPEG 将为我们做这些转换。在大多数项目中(包括我们 的这个)
我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内
存。
// Allocate an AVFrame structure
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
return -1;
即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始
的数据。我们使用avpicture_get_size 来获得我们需要的大小, 然后手工申请
内存空间:
uint8_t *buffer;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_malloc 是 ffmpeg 的 malloc,用来实现一个简单的malloc 的包装,这样来保
证内存地址是对齐的(4 字节对齐或者2 字节对齐)。它并不能保 护你不被内
存泄漏,重复释放或者其它malloc 的问题所困扰。
现在我们使用avpicture_fill 来把帧和我们新申请的内存来结合。关于
AVPicture 的结成:AVPicture 结构体是AVFrame 结 构体的子集――AVFrame 结
构体的开始部分与AVPicture 结构体是一样的。
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);
最后,我们已经准备好来从流中读取数据了。
读取数据
我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最好后转换
格式并且保存。
int frameFinished;
AVPacket packet;
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video(pCodecCtx, pFrame, &frameFinished,
packet.data, packet.size);
// Did we get a video frame?
if(frameFinished) {
// Convert the image from its native format to RGB
img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24,
(AVPicture*)pFrame, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height);
// Save the frame to disk
if(++i<=5)
SaveFrame(pFrameRGB, pCodecCtx->width,
pCodecCtx->height, i);
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
这个循环过程是比较简单的:av_read_frame()读取一个包并且把它保存到
AVPacket 结构体中。注意我们仅仅申请了一个包的结构体 ――ffmpeg 为我们申
请了内部的数据的内存并通过packet.data 指针来指向它。这些数据可以在后面
通过av_free_packet()来释 放。函数avcodec_decode_video()把包转换为帧。
然而当解码一个包的时候,我们可能没有得到我们需要的关于帧的信息。因此,
当我们得 到下一帧的时候,avcodec_decode_video()为我们设置了帧结束标志
frameFinished。最后,我们使用 img_convert()函数来把帧从原始格式
(pCodecCtx->pix_fmt)转换成为RGB 格式。要记住,你可以把一个 AVFrame
结构体的指针转换为AVPicture 结构体的指针。最后,我们把帧和高度宽度信息
传递给我们的SaveFrame 函数。
关于包Packets Packets 的注释 的注释
从技术上讲一个包可以包含部分或者其它的数据,但是 ffmpeg 的解释器保证了
我们得到的包Packets 包含的要么是完整的要么是多种完整的帧。
现在我们需要做的是让SaveFrame 函数能把RGB 信息定稿到一个PPM 格式的文件
中。我们将生成一个简单的PPM 格式文件,请相信,它是可以工作 的。
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// Open file
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile=fopen(szFilename, "wb");
if(pFile==NULL)
return;
// Write header
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data
for(y=0; y<height; y++)
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
// Close file
fclose(pFile);
}
我们做了一些标准的文件打开动作,然后写入RGB 数据。我们一次向文件写入一
行数据。PPM 格式文件的是一种包含一长串的RGB 数据的文件。如果你了解 HTML
色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,就像
#ff0000#ff0000....就表示了了个红色的屏幕。(它被保存成 二进制方式并且
没有分隔符,但是你自己是知道如何分隔的)。文件的头部表示了图像的宽度和
高度以及最大的RGB 值的大小。
现在,回顾我们的main()函数。一旦我们开始读取完视频流,我们必需清理一
切:
// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);
// Free the YUV frame
av_free(pFrame);
// Close the codec
avcodec_close(pCodecCtx);
// Close the video file
av_close_input_file(pFormatCtx);
return 0;
你会注意到我们使用av_free 来释放我们使用avcode_alloc_fram 和 av_malloc
来分配的内存。
上面的就是代码!下面,我们将使用 Linux 或者其它类似的平台,你将运行:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil
-lm
如果你使用的是老版本的ffmpeg,你可以去掉-lavutil 参数:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lm
大多数的图像处理函数可以打开PPM 文件。可以使用一些电影文件来进行测试
如何用FFmpeg FFmpeg 编写一个简单播放器详 编写一个简单播放器详
细步骤介绍(3) (3)
FFmpeg, 播放器, 编写
上来得到这个库的源代码或者如果有可能的话你可以直接下载开发包到你的操
作系统中。按照这个指导,你将需要编译这个库。(剩下的几个指导中也是一样)
SDL 库中有许多种方式来在屏幕上绘制图形,而且它有一个特殊的方式来在屏幕
上显示图像――这种方式叫做YUV 覆盖。YUV(从技术上来讲并不叫YUV 而是叫
做 YCbCr)是一种类似 于 RGB 方式的存储原始图像的格式。粗略的讲,Y 是亮度
分量,U 和 V 是色度分量。(这种格式比RGB 复杂的多,因为很多的颜色信息被
丢弃了,而且你可以每 2 个 Y 有 1 个 U 和 1 个 V)。SDL 的 YUV 覆盖使用一组原
始的YUV 数据并且在屏幕上显示出他们。它可以允许4 种不同的 YUV 格式,但
是其中的YV12 是最快的一种。还有一个叫做YUV420P 的 YUV 格式,它和YV12
是一样的,除了U 和 V 分量的位置被调换了以外。 420 意味着它以4:2:0 的
比例进行了二次抽样,基本上就意味着1 个颜色分量对应着4 个亮度分量。所以
它的色度信息只有原来的1/4。这是一种节省带宽 的好方式,因为人眼感觉不
到这种变化。在名称中的P 表示这种格式是平面的――简单的说就是Y,U 和 V
分量分别在不同的数组中。FFMPEG 可以把图像格式 转换为YUV420P,但是现在
很多视频流的格式已经是YUV420P 的了或者可以被很容易 的转换成YUV420P 格
式。
于是,我们现在计划把指导1 中的SaveFrame()函数替换掉,让它直接输出我们
的帧到屏幕上去。但一开始我们必需要先看一下如何使用SDL 库。首先 我们必
需先包含SDL 库的头文件并且初始化它。
#include <SDL.h>#include <SDL_thread.h> if(SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO | SDL_INIT_TIMER)) { fprintf(stderr, "Could not
initialize SDL - %s\n", SDL_GetError()); exit(1);}
SDL_Init()函数告诉了SDL 库,哪些特性我们将要用到。当然SDL_GetError()
是一个用来手工除错的函数。
创建一个显示
现在我们需要在屏幕上的一个地方放上一些东西。在 SDL 中显示图像的基本区域
叫做面 面面 面 surface surface surface。
SDL_Surface *screen; screen = SDL_SetVideoMode(pCodecCtx->width,
pCodecCtx->height, 0, 0);if(!screen) { fprintf(stderr, "SDL: could not
set video mode - exiting\n"); exit(1);}
这就创建了一个给定高度和宽度的屏幕。下一个选项是屏幕的颜色深度――0 表
示使用和当前一样的深度。(这个在OS X 系统上不能正常工作,原因请看源代
码)
现在我们在屏幕上来创建一个YUV 覆盖以便于我们输入视频上去:
SDL_Overlay *bmp; bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
pCodecCtx->height, SDL_YV12_OVERLAY,
screen);
正如前面我们所说的,我们使用YV12 来显示图像。
显示图像
前面那些都是很简单的。现在我们需要来显示图像。让我们看一下是如何来处理
完成后的帧的。我们将原来对RGB 处理的方式,并且替换SaveFrame() 为显示
到屏幕上的代码。为了显示到屏幕上,我们将先建立一个AVPicture 结构体并且
设置其数据指针和行尺寸来为我们的YUV 覆盖服务:
if(frameFinished) { SDL_LockYUVOverlay(bmp); AVPicture
pict; pict.data[0] = bmp->pixels[0]; pict.data[1] =
bmp->pixels[2]; pict.data[2] =
bmp->pixels[1]; pict.linesize[0] =
bmp->pitches[0]; pict.linesize[1] =
bmp->pitches[2]; pict.linesize[2] = bmp->pitches[1]; // Convert
the image into YUV format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); }
首先,我们锁定这个覆盖,因为我们将要去改写它。这是一个避免以后发生问题
的好习惯。正如前面所示的,这个AVPicture 结构体有一个数据指针指向一 个
有 4 个元素的指针数据。由于我们处理的是YUV420P,所以我们只需要3 个通道
即只要三组数据。其它的格式可能需要第四个指针来表示alpha 通道或 者其它
参数。行尺寸正如它的名字表示的意义一样。在 YUV 覆盖中相同功能的结构体是
像素pixel 和程度pitch。(程度pitch 是在SDL 里用来表 示指定行数据宽度
的值)。所以我们现在做的是让我们的覆盖中的pict.data 中的三个指针有一个
指向必要的空间的地址。类似的,我们可以直接从覆盖中得到行尺寸信息。像 前
面一样我们使用img_convert 来把格式转换成PIX_FMT_YUV420P。
绘制图像
但我们仍然需要告诉SDL 如何来实际显示我们给的数据。我们也会传递一个表明
电影位置、宽度、高度和缩放大小的矩形参数给SDL 的函数。这样,SDL 为我 们
做缩放并且它可以通过显卡的帮忙来进行快速缩放。
SDL_Rect rect; if(frameFinished) { // Convert the image into YUV
format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); rect.x =
0; rect.y = 0; rect.w = pCodecCtx->width; rect.h =
pCodecCtx->height; SDL_DisplayYUVOverlay(bmp, &rect); }
现在我们的视频显示出来了!
让我们再花一点时间来看一下SDL 的特性:它的事件驱动系统。SDL 被 设置成
当你在SDL 中点击或者移动鼠标或者向它发送一个信号它都将产生一个事件的
驱动方式。如果你的程序想要处理用户输入的话,它就会检测这些事件。你的程
序也可以产生事件并且传递给SDL事件系统。当使用SDL进行多线程编程的时候,
这相当有用,这方面代码我们可以在指导4 中看到。在这个程序中,我们将在 处
理完包以后就立即轮询事件。现在而言,我们将处理SDL_QUIT 事件以便于我们
退出:
SDL_Event event; av_free_packet(&packet); SDL_PollEvent
(&event); switch(event.type) { case
SDL_QUIT: SDL_Quit(); exit(0); break; default:
break; }
让我们去掉旧的冗余代码,开始编译。如果你使用的是Linux 或者其变体,使用
SDL 库进行编译的最好方式 为:
gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lz -lm
\`sdl-config --cflags --libs`
这里的sdl-config 命令会打印出用于gcc 编译的包含正确SDL 库的适当参数。
为了进行编译,在你自己的平台你可能需要做的有点不同:请查阅一下 SDL 文
档中关于你的系统的那部分。一旦可以编译,就马上运行它。
当运行这个程序的时候会发生什么呢?电影简直跑疯了!实际上,我们只是以我
们能从文件中解码帧的最快速度显示了所有的电影的帧。现在我们没有任何代码
来计 算出我们什么时候需要显示电影的帧。最后(在指导5),我们将花足够
的时间来探讨同步问题。但一开始我们会先忽略这个,因为我们有更加重要的事
情要处理: 音频!
指 指指 指 导 导导 导 333 3: :: :播放声音 播放声音
现在我们要来播放声音。SDL 也为我们准备了输出声音的方法。函数
SDL_OpenAudio()本身就是用来打开声音设备的。它使用一个叫做
SDL_AudioSpec 结构体作为参数,这个结构体中包含了我们将要输出的音频的所
有信息。
在我们展示如何建立之前,让我们先解释一下电脑是如何处理音频的。数字音频
是由一长串的样本流组成的。每个样本表示声音波形中的一个值。声音按照一个
特定 的采样率来进行录制,采样率表示以多快的速度来播放这段样本流,它的
表示方式为每秒多少次采样。例如22050 和 44100 的采样率就是电台和CD 常用
的 采样率。此外,大多音频有不只一个通道来表示立体声或者环绕。例如,如
果采样是立体声,那么每次的采样数就为2 个。当我们从一个电影文件中等到数
据的时候,我们不知道我们将得到多少个样 本,但是ffmpeg 将不会给我们部分
的样本――这意味着它将不 会把立体声分割开来。
SDL 播放声音的方式是这样的:你先设置声音的选项:采样率(在 SDL 的结构体
中被叫做freq 的表示频率frequency),声音通道数和其它的参 数,然后我们
设置一个回调函数和一些用户数据userdata。当开始播放音频的时候,SDL 将不
断地调用这个回调函数并且要求它来向声音缓冲填入一个特 定的数量的字节。
当我们把这些信息放到SDL_AudioSpec 结构体中后,我们调用函数
SDL_OpenAudio()就会打开声音设备并且给我们送 回另外一个AudioSpec 结构
体。这个结构体是我们实际上用到的--因为我们不能保证得到我们所要求的。
设置音频
目前先把讲的记住,因为我们实际上还没有任何关于声音流的信息。让我们回过
头来看一下我们的代码,看我们是如何找到视频流的,同样我们也可以找到声音
流。
// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO
&&
videoStream < 0) {
videoStream=i;
}
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_AUDIO &&
audioStream < 0) {
audioStream=i;
}
}
if(videoStream==-1)
return -1; // Didn't find a video stream
if(audioStream==-1)
return -1;
从这里我们可以从描述流的AVCodecContext 中得到我们想要的信息,就像我们
得到视频流的信息一样。
AVCodecContext *aCodecCtx;
aCodecCtx=pFormatCtx->streams[audioStream]->codec;
包含在编解码上下文中的所有信息正是我们所需要的用来建立音频的信息:
wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
让我们浏览一下这些:
·freq 前面所讲的采样率
·format 告诉SDL 我们将要给的格式。在“S16SYS”中的S 表示有符号的signed,
16 表示每个样本是16 位长的,SYS 表示大小头的顺序是与使用的系统相同的。
这些格式是由 avcodec_decode_audio2 为我们给出来的输入音频的格式。
·channels 声音的通道数
·silence 这是用来表示静音的值。因为声音采样是有符号的,所以0 当然就
是这个值。
·samples 这是当我们想要更多声音的时候,我们想让SDL 给出来的声音缓冲
区的尺寸。一个比较合适的值在512 到 8192 之间;ffplay 使用1024。
·callback 这个是我们的回调函数。我们后面将会详细讨论。
·userdata 这个是SDL 供给回调函数运行的参数。我们将让回调函数得到整个
编解码的上下文;你将在后面知道原因。
最后,我们使用SDL_OpenAudio 函数来打开声音。
如果你还记得前面的指导,我们仍然需要打开声音编解码器本身。这是很显然的。
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
avcodec_open(aCodecCtx, aCodec);
队列
嗯!现在我们已经准备好从流中取出声音信息。但是我们如何来处理这些信息
呢?我们将会不断地从文件中得到这些包,但同时SDL 也将调用回调函数。解决
方法 为创建一个全局的结构体变量以便于我们从文件中得到的声音包有地方存
放同时也保证SDL中的声音回调函数audio_callback能从这个地方得到声 音数
据。所以我们要做的是创建一个包的队列queue。在 ffmpeg 中有一个叫
AVPacketList 的结构体可以帮助我们,这个结构体实际是一串包 的链表。下面
就是我们的队列结构体:
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
首先,我们应当指出nb_packets 是与size 不一样的--size 表示我们从
packet->size 中得到的字节数。你会注意到我们有一 个互斥量mutex 和一个条
件变量cond 在结构体里面。这是因为SDL 是在一个独立的线程中来进行音频处
理的。如果我们没有正确的锁定这个队列,我们有 可能把数据搞乱。我们将来
看一个这个队列是如何来运行的。每一个程序员应当知道如何来生成的一个队
列,但是我们将把这 部分也来讨论从而可以学习到SDL 的函数。
一开始我们先创建一个函数来初始化队列:
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
接着我们再做一个函数来给队列中填入东西:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
SDL_LockMutex(q->mutex);
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
函数SDL_LockMutex()锁定队列的互斥量以便于我们向队列中添加东西,然后函
数 SDL_CondSignal()通过我们的条件变量为一个接 收函数(如果它在等待)发
出一个信号来告诉它现在已经有数据了,接着就会解锁互斥量并让队列可以自由
访问。
下面是相应的接收函数。注意函数SDL_CondWait()是如何按照我们的要求让函
数阻塞block 的(例如一直等到队列中有数据)。
int quit = 0;
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for(;;) {
if(quit) {
ret = -1;
break;
}
pkt1 = q->first_pkt;
if (pkt1) {
q->first_pkt = pkt1->next;
if (!q->first_pkt)
q->last_pkt = NULL;
q->nb_packets--;
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
正如你所看到的,我们已经用一个无限循环包装了这个函数以便于我们想用阻塞
的方式来得到数据。我们通过使用SDL 中的函数SDL_CondWait()来 避免无限循
环。基本上,所有的CondWait 只等待从SDL_CondSignal()函数(或者
SDL_CondBroadcast()函数)中发出 的信号,然后再继续执行。然而,虽然看起
来我们陷入了我们的互斥体中--如果我们一直保持着这个锁,我们的函数将永
远无法把数据放入到队列中去!但 是,SDL_CondWait()函数也为我们做了解锁
互斥量的动作然后才尝试着在得到信号后去重新锁定它。
意外情况
你们将会注意到我们有一个全局变量quit,我们用它来保证还没有设置程序退
出的信号(SDL 会自动处理TERM 类似的信 号)。否则,这个线程将不停地运行
直到我们使用kill -9 来结束程序。FFMPEG 同样也提供了一个函数来进行回调
并检查我们是否需要退出一些被阻塞的函数:这个函数就是
url_set_interrupt_cb。
int decode_interrupt_cb(void) {
return quit;
}
...
main() {
...
url_set_interrupt_cb(decode_interrupt_cb);
...
SDL_PollEvent(&event);
switch(event.type) {
case SDL_QUIT:
quit = 1;
...
当然,这仅仅是用来给ffmpeg 中的阻塞情况使用的,而不是SDL 中的。我们还
必需要设置quit 标志为1。
为队列提供包
剩下的我们唯一需要为队列所做的事就是提供包了:
PacketQueue audioq;
main() {
...
avcodec_open(aCodecCtx, aCodec);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
函数SDL_PauseAudio()让音频设备最终开始工作。如果没有立即供给足够的数
据,它会播放静音。
我们已经建立好我们的队列,现在我们准备为它提供包。先看一下我们的读取包
的循环:
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
....
}
} else if(packet.stream_index==audioStream) {
packet_queue_put(&audioq, &packet);
} else {
av_free_packet(&packet);
}
注意:我们没有在把包放到队列里的时候释放它,我们将在解码后来释放它。
取出包
现在,让我们最后让声音回调函数audio_callback 来从队列中取出包。回调函
数的格式必需为void callback(void *userdata, Uint8 *stream, int len),
这里的userdata 就是我们给到SDL 的指针,stream 是我们要把声音数据写入的
缓冲区指针,len 是缓冲区的大小。下面就是代码:
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
int len1, audio_size;
static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
static unsigned int audio_buf_size = 0;
static unsigned int audio_buf_index = 0;
while(len > 0) {
if(audio_buf_index >= audio_buf_size) {
audio_size = audio_decode_frame(aCodecCtx, audio_buf,
sizeof(audio_buf));
if(audio_size < 0) {
audio_buf_size = 1024;
memset(audio_buf, 0, audio_buf_size);
} else {
audio_buf_size = audio_size;
}
audio_buf_index = 0;
}
len1 = audio_buf_size - audio_buf_index;
if(len1 > len)
len1 = len;
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream += len1;
audio_buf_index += len1;
}
}
这基本上是一个简单的从另外一个我们将要写的audio_decode_frame()函数中
获取数据的循环,这个循环把结果写入到中间缓冲区,尝试着向 流中写入len
字节并且在我们没有足够的数据的时候会获取更多的数据或者当我们有多余数
据的时候保存下来为后面使用。这个audio_buf 的大小为 1.5 倍的声音帧的大
小以便于有一个比较好的缓冲,这个声音帧的大小是ffmpeg 给出的。
最后解码音频
让我们看一下解码器的真正部分:audio_decode_frame
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf,
int buf_size) {
static AVPacket pkt;
static uint8_t *audio_pkt_data = NULL;
static int audio_pkt_size = 0;
int len1, data_size;
for(;;) {
while(audio_pkt_size > 0) {
data_size = buf_size;
len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf,
&data_size,
audio_pkt_data, audio_pkt_size);
if(len1 < 0) {
audio_pkt_size = 0;
break;
}
audio_pkt_data += len1;
audio_pkt_size -= len1;
if(data_size <= 0) {
continue;
}
return data_size;
}
if(pkt.data)
av_free_packet(&pkt);
if(quit) {
return -1;
}
if(packet_queue_get(&audioq, &pkt, 1) < 0) {
return -1;
}
audio_pkt_data = pkt.data;
audio_pkt_size = pkt.size;
}
}
整个过程实际上从函数的尾部开始,在这里我们调用了packet_queue_get()函
数。我们从队列中取出包,并且保存它的信息。然后,一旦我们有 了可以使用
的包,我们就调用函数avcodec_decode_audio2(),它的功能就像它的姐妹函数
avcodec_decode_video()一样,唯一的区别是它的一个包里可能有不止一个声音
帧,所以你可能要调用很多次来解码出包中所有的数据。同 时也要记住进行指
针 audio_buf 的强制转换,因为SDL 给出的是8 位整型缓冲指针而ffmpeg 给出
的数据是16 位的整型指针。你应该也会注意到 len1 和 data_size 的不同,len1
表示解码使用的数据的在包中的大小,data_size 表示实际返回的原始声音数据
的大小。
当我们得到一些数据的时候,我们立刻返回来看一下是否仍然需要从队列中得到
更加多的数据或者我们已经完成了。如果我们仍然有更加多的数据要处理,我们
把它 保存到下一次。如果我们完成了一个包的处理,我们最后要释放它。
就是这样。我们利用主的读取队列循环从文件得到音频并送到队列中,然后被
audio_callback 函数从队列中读取并处理,最后把数据送给SDL,于 是 SDL 就
相当于我们的声卡。让我们继续并且编译:
gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm \
`sdl-config --cflags --libs`
啊哈!视频虽然还是像原来那样快,但是声音可以正常播放了。这是为什么呢?
因为声音信息中的采样率--虽然我们把声音数据尽可能快的填充到声卡缓冲
中,但 是声音设备却会按照原来指定的采样率来进行播放。
我们几乎已经准备好来开始同步音频和视频了,但是首先我们需要的是一点程序
的组织。用队列的方式来组织和播放音频在一个独立的线程中工作的很好:它使
得程 序更加更加易于控制和模块化。在我们开始同步音视频之前,我们需要让
我们的代码更加容易处理。所以下次要讲的是:创建一个线程。
如何用FFmpeg FFmpeg 编写一个简单播放器详 编写一个简单播放器详
细步骤介绍(5) (5)
FFmpeg, 播放器, 编写
} else {
break;
}
}
// Is this a packet from the video stream?
if(packet->stream_index == is->videoStream) {
packet_queue_put(&is->videoq, packet);
} else if(packet->stream_index == is->audioStream) {
packet_queue_put(&is->audioq, packet);
} else {
av_free_packet(packet);
}
}
这里没有什么新东西,除了我们给音频和视频队列限定了一个最大值并且我们添
加一个检测读错误的 函数。格式上下文里面有一个叫做pb 的 ByteIOContext
类型结构体。这个结构体是用来保存一些低级的文件信息。函数url_ferror 用
来检测结构体并发 现是否有些读取文件错误。
在循环以后,我们的代码是用等待其余的程序结束和提示我们已经结束的。这些
代码是有益的,因为 它指示出了如何驱动事件--后面我们将显示影像。
while(!is->quit) {
SDL_Delay(100);
}
fail:
if(1){
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
}
return 0;
我们使用SDL 常量SDL_USEREVENT 来从用户事件中得到值。第一个用户事件的值
应当是SDL_USEREVENT,下一个是 SDL_USEREVENT+1 并且依此类推。在我们的
程序中FF_QUIT_EVENT 被定义成SDL_USEREVENT+2。如果喜欢,我们也可以 传
递用户数据,在这里我们传递的是大结构体的指针。最后我们调用
SDL_PushEvent()函数。在我们的事件分支中,我们只是像以前放入
SDL_QUIT_EVENT 部分一样。我们将在自己的事件队列中详细讨论,现在只是确
保我们正确放入了FF_QUIT_EVENT 事件,我们将在后面捕 捉到它并且设置我们
的退出标志quit。
得到帧: :: :video_thread video_thread video_thread
当我们准备好解码器后,我们开始视频线程。这个线程从视频队列中读取包,把
它解码成视频帧,然后调用queue_picture 函数把处理好的帧放入到图 片队列
中:
int video_thread(void *arg) {
VideoState *is = (VideoState *)arg;
AVPacket pkt1, *packet = &pkt1;
int len1, frameFinished;
AVFrame *pFrame;
pFrame = avcodec_alloc_frame();
for(;;) {
if(packet_queue_get(&is->videoq, packet, 1) < 0) {
// means we quit getting packets
break;
}
// Decode video frame
len1 = avcodec_decode_video(is->video_st->codec, pFrame,
&frameFinished,
packet->data, packet->size);
// Did we get a video frame?
if(frameFinished) {
if(queue_picture(is, pFrame) < 0) {
break;
}
}
av_free_packet(packet);
}
av_free(pFrame);
return 0;
}
在这里的很多函数应该很熟悉吧。我们把avcodec_decode_video 函数移到了这
里,替换了一些参数,例如:我们把AVStream 保存在我 们自己的大结构体中,
所以我们可以从那里得到编解码器的信息。我们仅仅是不断的从视频队列中取包
一直到有人告诉我们要停止或者出错为止。
把帧队列化
让我们看一下保存解码后的帧pFrame 到图像队列中去的函数。因为我们的图像
队列是SDL 的覆盖的集合(基本上不用让视频显示函数再做计算了),我们需 要
把帧转换成相应的格式。我们保存到图像队列中的数据是我们自己做的一个结构
体。
typedef struct VideoPicture {
SDL_Overlay *bmp;
int width, height;
int allocated;
} VideoPicture;
我们的大结构体有一个可以保存这些缓冲区。然而,我们需要自己来申请
SDL_Overlay(注意:allocated 标志会指明我们是否已经做了这个申 请的动作
与否)。
为了使用这个队列,我们有两个指针--写入指针和读取指针。我们也要保证一
定数量的实际数据在缓冲中。要写入到队列中,我们先要等待缓冲清空以便于有
位置 来保存我们的VideoPicture。然后我们检查看我们是否已经申请到了一个
可以写入覆盖的索引号。如果没有,我们要申请一段空间。我们也要重新申请 缓
冲如果窗口的大小已经改变。然而,为了避免被锁定,尽是避免在这里申请(我
现在还不太清楚原因;我相信是为了避免在其它线程中调用SDL 覆盖函数的原
因)。
int queue_picture(VideoState *is, AVFrame *pFrame) {
VideoPicture *vp;
int dst_pix_fmt;
AVPicture pict;
SDL_LockMutex(is->pictq_mutex);
while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
!is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if(is->quit)
return -1;
// windex is set to 0 initially
vp = &is->pictq[is->pictq_windex];
if(!vp->bmp ||
vp->width != is->video_st->codec->width ||
vp->height != is->video_st->codec->height) {
SDL_Event event;
vp->allocated = 0;
event.type = FF_ALLOC_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
SDL_LockMutex(is->pictq_mutex);
while(!vp->allocated && !is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if(is->quit) {
return -1;
}
}
这里的事件机制与前面我们想要退出的时候看到的一样。我们已经定义了事件
FF_ALLOC_EVENT 作为SDL_USEREVENT。我们把事件发到事 件队列中然后等待申
请内存的函数设置好条件变量。
让我们来看一看如何来修改事件循环:
for(;;) {
SDL_WaitEvent(&event);
switch(event.type) {
case FF_ALLOC_EVENT:
alloc_picture(event.user.data1);
break;
记住event.user.data1 是我们的大结构体。就这么简单。让我们看一下
alloc_picture()函数:
void alloc_picture(void *userdata) {
VideoState *is = (VideoState *)userdata;
VideoPicture *vp;
vp = &is->pictq[is->pictq_windex];
if(vp->bmp) {
// we already have one make another, bigger/smaller
SDL_FreeYUVOverlay(vp->bmp);
}
// Allocate a place to put our YUV image on that screen
vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
is->video_st->codec->height,
SDL_YV12_OVERLAY,
screen);
vp->width = is->video_st->codec->width;
vp->height = is->video_st->codec->height;
SDL_LockMutex(is->pictq_mutex);
vp->allocated = 1;
SDL_CondSignal(is->pictq_cond);
SDL_UnlockMutex(is->pictq_mutex);
}
你可以看到我们把SDL_CreateYUVOverlay 函数从主循环中移到了这里。这段代
码应该完全可以自我注释。记住我们把高度和宽度保存到 VideoPicture 结构体
中因为我们需要保存我们的视频的大小没有因为某些原因而改变。
好,我们几乎已经全部解决并且可以申请到YUV 覆盖和准备好接收图像。让我们
回顾一下queue_picture 并看一个拷贝帧到覆盖的代码。你应该能认 出其中的
一部分:
int queue_picture(VideoState *is, AVFrame *pFrame) {
if(vp->bmp) {
SDL_LockYUVOverlay(vp->bmp);
dst_pix_fmt = PIX_FMT_YUV420P;
pict.data[0] = vp->bmp->pixels[0];
pict.data[1] = vp->bmp->pixels[2];
pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0];
pict.linesize[1] = vp->bmp->pitches[2];
pict.linesize[2] = vp->bmp->pitches[1];
// Convert the image into YUV format that SDL uses
img_convert(&pict, dst_pix_fmt,
(AVPicture *)pFrame, is->video_st->codec->pix_fmt,
is->video_st->codec->width, is->video_st->codec->height);
SDL_UnlockYUVOverlay(vp->bmp);
if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_windex = 0;
}
SDL_LockMutex(is->pictq_mutex);
is->pictq_size++;
SDL_UnlockMutex(is->pictq_mutex);
}
return 0;
}
这部分代码和前面用到的一样,主要是简单的用我们的帧来填充YUV 覆盖。最后
一点只是简单的给队列加1。这个队列在写的时候会一直写入到满为止,在读的
时 候会一直读空为止。因此所有的都依赖于is->pictq_size 值,这要求我们必
需要锁定它。这里我们做的是增加写指针(在必要的时候采用轮转 的方式),
然后锁定队列并且增加尺寸。现在我们的读者函数将会知道队列中有了更多的信
息,当队列满的时候,我们的写入函数也会知道。
显示视频
这就是我们的视频线程。现在我们看过了几乎所有的线程除了一个--记得我们
调用schedule_refresh()函数吗?让我们看一下实际中是如何做 的:
static void schedule_refresh(VideoState *is, int delay) {
SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}
函数SDL_AddTimer()是 SDL 中的一个定时(特定的毫秒)执行用户定义的回调
函数(可以带一些参数user data)的简单函数。我们将用这个函数来定时刷新
视频--每次我们调用这个函数的时候,它将设置一个定时器来触发定时事件来
把一帧从图像队列中显示到屏 幕上。
但是,让我们先触发那个事件。
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
SDL_Event event;
event.type = FF_REFRESH_EVENT;
event.user.data1 = opaque;
SDL_PushEvent(&event);
电影文件有很多基本的组成部分。首先,文件本身被称为容 容容 容 器 器器 器 Container Container Container,容
器的类型决定了信息被存放在文件中的位置。AVI 和 Quicktime 就是容器的例
子。接着,你有一组流 流流 流, 例如,你经常有的是一个音频流和一个视频流。(一
个流只是一种想像出来的词语,用来表示一连 串的通过时间来串连的数据元
素)。在流中的数据元素被称为帧 帧帧 帧 Frame Frame 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>来替
换)
#include <avcodec.h>
#include <avformat.h>
...
int main(int argc, charg *argv[]) {
av_register_all();
这里注册了所有的文件格式和编解码器的库,所以它们将被自 动的使用在被打
开的合适格式的文件上。注意你只需要调用av_register_all()一次,因此我们
在主函数main()中来调用它。如果你喜欢, 也可以只注册特定的格式和编解码
器,但是通常你没有必要这样做。
现在我们可以真正的打开文件:
AVFormatContext *pFormatCtx;
// Open video file
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
return -1; // Couldn't open file
我们通过第一个参数来获得文件名。这个函数读取文件的头部并且把信息保存到
我们给的AVFormatContext 结构体中。最后三个参数用来指定特殊的 文件格式,
缓冲大小和格式参数,但如果把它们设置为空NULL 或者0,libavformat 将自动
检测这些参数。
这个函数只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
// Retrieve stream information
if(av_find_stream_info(pFormatCtx)<0)
return -1; // Couldn't find stream information
这个函数为pFormatCtx->streams 填充上正确的信息。我们引进一个手工调试的
函数来看一下里面有什么:
// Dump information about file onto standard error
dump_format(pFormatCtx, 0, argv[1], 0);
现在pFormatCtx->streams仅仅是一组大小为pFormatCtx->nb_streams的指针,
所以让我们先跳过它直到 我们找到一个视频流。
int i;
AVCodecContext *pCodecCtx;
// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1)
return -1; // Didn't find a video stream
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文)
的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一
个指向他的指针。但是我们必需要找到真正的 编解码器并且打开它:
AVCodec *pCodec;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec)<0)
return -1; // Could not open codec
有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加
CODEC_FLAG_TRUNCATED 到 pCodecCtx->flags 和添 加一个hack 来粗糙的修正帧
率。这两个修正已经不在存在于ffplay.c 中。因此,我必需假设它们不再必要。
我们移除了那些代码后还有一个需要指出的 不同点:pCodecCtx->time_base 现
在已经保存了帧率的信息。time_base 是一个结构体,它里面有一个分子和分母
(AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数
的帧率(例如NTSC 使用29.97fps)。
保存数据
现在我们需要找到一个地方来保存帧:
AVFrame *pFrame;
// Allocate video frame
pFrame=avcodec_alloc_frame();
因为我们准备输出保存24 位 RGB 色的PPM 文件,我们必需把帧的格式从原来的
转换为RGB。FFMPEG 将为我们做这些转换。在大多数项目中(包括我们 的这个)
我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内
存。
// Allocate an AVFrame structure
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
return -1;
即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始
的数据。我们使用avpicture_get_size 来获得我们需要的大小, 然后手工申请
内存空间:
uint8_t *buffer;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_malloc 是 ffmpeg 的 malloc,用来实现一个简单的malloc 的包装,这样来保
证内存地址是对齐的(4 字节对齐或者2 字节对齐)。它并不能保 护你不被内
存泄漏,重复释放或者其它malloc 的问题所困扰。
现在我们使用avpicture_fill 来把帧和我们新申请的内存来结合。关于
AVPicture 的结成:AVPicture 结构体是AVFrame 结 构体的子集――AVFrame 结
构体的开始部分与AVPicture 结构体是一样的。
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);
最后,我们已经准备好来从流中读取数据了。
读取数据
我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最好后转换
格式并且保存。
int frameFinished;
AVPacket packet;
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video(pCodecCtx, pFrame, &frameFinished,
packet.data, packet.size);
// Did we get a video frame?
if(frameFinished) {
// Convert the image from its native format to RGB
img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24,
(AVPicture*)pFrame, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height);
// Save the frame to disk
if(++i<=5)
SaveFrame(pFrameRGB, pCodecCtx->width,
pCodecCtx->height, i);
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
这个循环过程是比较简单的:av_read_frame()读取一个包并且把它保存到
AVPacket 结构体中。注意我们仅仅申请了一个包的结构体 ――ffmpeg 为我们申
请了内部的数据的内存并通过packet.data 指针来指向它。这些数据可以在后面
通过av_free_packet()来释 放。函数avcodec_decode_video()把包转换为帧。
然而当解码一个包的时候,我们可能没有得到我们需要的关于帧的信息。因此,
当我们得 到下一帧的时候,avcodec_decode_video()为我们设置了帧结束标志
frameFinished。最后,我们使用 img_convert()函数来把帧从原始格式
(pCodecCtx->pix_fmt)转换成为RGB 格式。要记住,你可以把一个 AVFrame
结构体的指针转换为AVPicture 结构体的指针。最后,我们把帧和高度宽度信息
传递给我们的SaveFrame 函数。
关于包Packets Packets 的注释 的注释
从技术上讲一个包可以包含部分或者其它的数据,但是 ffmpeg 的解释器保证了
我们得到的包Packets 包含的要么是完整的要么是多种完整的帧。
现在我们需要做的是让SaveFrame 函数能把RGB 信息定稿到一个PPM 格式的文件
中。我们将生成一个简单的PPM 格式文件,请相信,它是可以工作 的。
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// Open file
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile=fopen(szFilename, "wb");
if(pFile==NULL)
return;
// Write header
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data
for(y=0; y<height; y++)
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
// Close file
fclose(pFile);
}
我们做了一些标准的文件打开动作,然后写入RGB 数据。我们一次向文件写入一
行数据。PPM 格式文件的是一种包含一长串的RGB 数据的文件。如果你了解 HTML
色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,就像
#ff0000#ff0000....就表示了了个红色的屏幕。(它被保存成 二进制方式并且
没有分隔符,但是你自己是知道如何分隔的)。文件的头部表示了图像的宽度和
高度以及最大的RGB 值的大小。
现在,回顾我们的main()函数。一旦我们开始读取完视频流,我们必需清理一
切:
// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);
// Free the YUV frame
av_free(pFrame);
// Close the codec
avcodec_close(pCodecCtx);
// Close the video file
av_close_input_file(pFormatCtx);
return 0;
你会注意到我们使用av_free 来释放我们使用avcode_alloc_fram 和 av_malloc
来分配的内存。
上面的就是代码!下面,我们将使用 Linux 或者其它类似的平台,你将运行:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil
-lm
如果你使用的是老版本的ffmpeg,你可以去掉-lavutil 参数:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lm
大多数的图像处理函数可以打开PPM 文件。可以使用一些电影文件来进行测试
如何用FFmpeg FFmpeg 编写一个简单播放器详 编写一个简单播放器详
细步骤介绍(3) (3)
FFmpeg, 播放器, 编写
上来得到这个库的源代码或者如果有可能的话你可以直接下载开发包到你的操
作系统中。按照这个指导,你将需要编译这个库。(剩下的几个指导中也是一样)
SDL 库中有许多种方式来在屏幕上绘制图形,而且它有一个特殊的方式来在屏幕
上显示图像――这种方式叫做YUV 覆盖。YUV(从技术上来讲并不叫YUV 而是叫
做 YCbCr)是一种类似 于 RGB 方式的存储原始图像的格式。粗略的讲,Y 是亮度
分量,U 和 V 是色度分量。(这种格式比RGB 复杂的多,因为很多的颜色信息被
丢弃了,而且你可以每 2 个 Y 有 1 个 U 和 1 个 V)。SDL 的 YUV 覆盖使用一组原
始的YUV 数据并且在屏幕上显示出他们。它可以允许4 种不同的 YUV 格式,但
是其中的YV12 是最快的一种。还有一个叫做YUV420P 的 YUV 格式,它和YV12
是一样的,除了U 和 V 分量的位置被调换了以外。 420 意味着它以4:2:0 的
比例进行了二次抽样,基本上就意味着1 个颜色分量对应着4 个亮度分量。所以
它的色度信息只有原来的1/4。这是一种节省带宽 的好方式,因为人眼感觉不
到这种变化。在名称中的P 表示这种格式是平面的――简单的说就是Y,U 和 V
分量分别在不同的数组中。FFMPEG 可以把图像格式 转换为YUV420P,但是现在
很多视频流的格式已经是YUV420P 的了或者可以被很容易 的转换成YUV420P 格
式。
于是,我们现在计划把指导1 中的SaveFrame()函数替换掉,让它直接输出我们
的帧到屏幕上去。但一开始我们必需要先看一下如何使用SDL 库。首先 我们必
需先包含SDL 库的头文件并且初始化它。
#include <SDL.h>#include <SDL_thread.h> if(SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO | SDL_INIT_TIMER)) { fprintf(stderr, "Could not
initialize SDL - %s\n", SDL_GetError()); exit(1);}
SDL_Init()函数告诉了SDL 库,哪些特性我们将要用到。当然SDL_GetError()
是一个用来手工除错的函数。
创建一个显示
现在我们需要在屏幕上的一个地方放上一些东西。在 SDL 中显示图像的基本区域
叫做面 面面 面 surface surface surface。
SDL_Surface *screen; screen = SDL_SetVideoMode(pCodecCtx->width,
pCodecCtx->height, 0, 0);if(!screen) { fprintf(stderr, "SDL: could not
set video mode - exiting\n"); exit(1);}
这就创建了一个给定高度和宽度的屏幕。下一个选项是屏幕的颜色深度――0 表
示使用和当前一样的深度。(这个在OS X 系统上不能正常工作,原因请看源代
码)
现在我们在屏幕上来创建一个YUV 覆盖以便于我们输入视频上去:
SDL_Overlay *bmp; bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
pCodecCtx->height, SDL_YV12_OVERLAY,
screen);
正如前面我们所说的,我们使用YV12 来显示图像。
显示图像
前面那些都是很简单的。现在我们需要来显示图像。让我们看一下是如何来处理
完成后的帧的。我们将原来对RGB 处理的方式,并且替换SaveFrame() 为显示
到屏幕上的代码。为了显示到屏幕上,我们将先建立一个AVPicture 结构体并且
设置其数据指针和行尺寸来为我们的YUV 覆盖服务:
if(frameFinished) { SDL_LockYUVOverlay(bmp); AVPicture
pict; pict.data[0] = bmp->pixels[0]; pict.data[1] =
bmp->pixels[2]; pict.data[2] =
bmp->pixels[1]; pict.linesize[0] =
bmp->pitches[0]; pict.linesize[1] =
bmp->pitches[2]; pict.linesize[2] = bmp->pitches[1]; // Convert
the image into YUV format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); }
首先,我们锁定这个覆盖,因为我们将要去改写它。这是一个避免以后发生问题
的好习惯。正如前面所示的,这个AVPicture 结构体有一个数据指针指向一 个
有 4 个元素的指针数据。由于我们处理的是YUV420P,所以我们只需要3 个通道
即只要三组数据。其它的格式可能需要第四个指针来表示alpha 通道或 者其它
参数。行尺寸正如它的名字表示的意义一样。在 YUV 覆盖中相同功能的结构体是
像素pixel 和程度pitch。(程度pitch 是在SDL 里用来表 示指定行数据宽度
的值)。所以我们现在做的是让我们的覆盖中的pict.data 中的三个指针有一个
指向必要的空间的地址。类似的,我们可以直接从覆盖中得到行尺寸信息。像 前
面一样我们使用img_convert 来把格式转换成PIX_FMT_YUV420P。
绘制图像
但我们仍然需要告诉SDL 如何来实际显示我们给的数据。我们也会传递一个表明
电影位置、宽度、高度和缩放大小的矩形参数给SDL 的函数。这样,SDL 为我 们
做缩放并且它可以通过显卡的帮忙来进行快速缩放。
SDL_Rect rect; if(frameFinished) { // Convert the image into YUV
format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); rect.x =
0; rect.y = 0; rect.w = pCodecCtx->width; rect.h =
pCodecCtx->height; SDL_DisplayYUVOverlay(bmp, &rect); }
现在我们的视频显示出来了!
让我们再花一点时间来看一下SDL 的特性:它的事件驱动系统。SDL 被 设置成
当你在SDL 中点击或者移动鼠标或者向它发送一个信号它都将产生一个事件的
驱动方式。如果你的程序想要处理用户输入的话,它就会检测这些事件。你的程
序也可以产生事件并且传递给SDL事件系统。当使用SDL进行多线程编程的时候,
这相当有用,这方面代码我们可以在指导4 中看到。在这个程序中,我们将在 处
理完包以后就立即轮询事件。现在而言,我们将处理SDL_QUIT 事件以便于我们
退出:
SDL_Event event; av_free_packet(&packet); SDL_PollEvent
(&event); switch(event.type) { case
SDL_QUIT: SDL_Quit(); exit(0); break; default:
break; }
让我们去掉旧的冗余代码,开始编译。如果你使用的是Linux 或者其变体,使用
SDL 库进行编译的最好方式 为:
gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lz -lm
\`sdl-config --cflags --libs`
这里的sdl-config 命令会打印出用于gcc 编译的包含正确SDL 库的适当参数。
为了进行编译,在你自己的平台你可能需要做的有点不同:请查阅一下 SDL 文
档中关于你的系统的那部分。一旦可以编译,就马上运行它。
当运行这个程序的时候会发生什么呢?电影简直跑疯了!实际上,我们只是以我
们能从文件中解码帧的最快速度显示了所有的电影的帧。现在我们没有任何代码
来计 算出我们什么时候需要显示电影的帧。最后(在指导5),我们将花足够
的时间来探讨同步问题。但一开始我们会先忽略这个,因为我们有更加重要的事
情要处理: 音频!
指 指指 指 导 导导 导 333 3: :: :播放声音 播放声音
现在我们要来播放声音。SDL 也为我们准备了输出声音的方法。函数
SDL_OpenAudio()本身就是用来打开声音设备的。它使用一个叫做
SDL_AudioSpec 结构体作为参数,这个结构体中包含了我们将要输出的音频的所
有信息。
在我们展示如何建立之前,让我们先解释一下电脑是如何处理音频的。数字音频
是由一长串的样本流组成的。每个样本表示声音波形中的一个值。声音按照一个
特定 的采样率来进行录制,采样率表示以多快的速度来播放这段样本流,它的
表示方式为每秒多少次采样。例如22050 和 44100 的采样率就是电台和CD 常用
的 采样率。此外,大多音频有不只一个通道来表示立体声或者环绕。例如,如
果采样是立体声,那么每次的采样数就为2 个。当我们从一个电影文件中等到数
据的时候,我们不知道我们将得到多少个样 本,但是ffmpeg 将不会给我们部分
的样本――这意味着它将不 会把立体声分割开来。
SDL 播放声音的方式是这样的:你先设置声音的选项:采样率(在 SDL 的结构体
中被叫做freq 的表示频率frequency),声音通道数和其它的参 数,然后我们
设置一个回调函数和一些用户数据userdata。当开始播放音频的时候,SDL 将不
断地调用这个回调函数并且要求它来向声音缓冲填入一个特 定的数量的字节。
当我们把这些信息放到SDL_AudioSpec 结构体中后,我们调用函数
SDL_OpenAudio()就会打开声音设备并且给我们送 回另外一个AudioSpec 结构
体。这个结构体是我们实际上用到的--因为我们不能保证得到我们所要求的。
设置音频
目前先把讲的记住,因为我们实际上还没有任何关于声音流的信息。让我们回过
头来看一下我们的代码,看我们是如何找到视频流的,同样我们也可以找到声音
流。
// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO
&&
videoStream < 0) {
videoStream=i;
}
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_AUDIO &&
audioStream < 0) {
audioStream=i;
}
}
if(videoStream==-1)
return -1; // Didn't find a video stream
if(audioStream==-1)
return -1;
从这里我们可以从描述流的AVCodecContext 中得到我们想要的信息,就像我们
得到视频流的信息一样。
AVCodecContext *aCodecCtx;
aCodecCtx=pFormatCtx->streams[audioStream]->codec;
包含在编解码上下文中的所有信息正是我们所需要的用来建立音频的信息:
wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
让我们浏览一下这些:
·freq 前面所讲的采样率
·format 告诉SDL 我们将要给的格式。在“S16SYS”中的S 表示有符号的signed,
16 表示每个样本是16 位长的,SYS 表示大小头的顺序是与使用的系统相同的。
这些格式是由 avcodec_decode_audio2 为我们给出来的输入音频的格式。
·channels 声音的通道数
·silence 这是用来表示静音的值。因为声音采样是有符号的,所以0 当然就
是这个值。
·samples 这是当我们想要更多声音的时候,我们想让SDL 给出来的声音缓冲
区的尺寸。一个比较合适的值在512 到 8192 之间;ffplay 使用1024。
·callback 这个是我们的回调函数。我们后面将会详细讨论。
·userdata 这个是SDL 供给回调函数运行的参数。我们将让回调函数得到整个
编解码的上下文;你将在后面知道原因。
最后,我们使用SDL_OpenAudio 函数来打开声音。
如果你还记得前面的指导,我们仍然需要打开声音编解码器本身。这是很显然的。
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
avcodec_open(aCodecCtx, aCodec);
队列
嗯!现在我们已经准备好从流中取出声音信息。但是我们如何来处理这些信息
呢?我们将会不断地从文件中得到这些包,但同时SDL 也将调用回调函数。解决
方法 为创建一个全局的结构体变量以便于我们从文件中得到的声音包有地方存
放同时也保证SDL中的声音回调函数audio_callback能从这个地方得到声 音数
据。所以我们要做的是创建一个包的队列queue。在 ffmpeg 中有一个叫
AVPacketList 的结构体可以帮助我们,这个结构体实际是一串包 的链表。下面
就是我们的队列结构体:
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
首先,我们应当指出nb_packets 是与size 不一样的--size 表示我们从
packet->size 中得到的字节数。你会注意到我们有一 个互斥量mutex 和一个条
件变量cond 在结构体里面。这是因为SDL 是在一个独立的线程中来进行音频处
理的。如果我们没有正确的锁定这个队列,我们有 可能把数据搞乱。我们将来
看一个这个队列是如何来运行的。每一个程序员应当知道如何来生成的一个队
列,但是我们将把这 部分也来讨论从而可以学习到SDL 的函数。
一开始我们先创建一个函数来初始化队列:
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
接着我们再做一个函数来给队列中填入东西:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
SDL_LockMutex(q->mutex);
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
函数SDL_LockMutex()锁定队列的互斥量以便于我们向队列中添加东西,然后函
数 SDL_CondSignal()通过我们的条件变量为一个接 收函数(如果它在等待)发
出一个信号来告诉它现在已经有数据了,接着就会解锁互斥量并让队列可以自由
访问。
下面是相应的接收函数。注意函数SDL_CondWait()是如何按照我们的要求让函
数阻塞block 的(例如一直等到队列中有数据)。
int quit = 0;
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for(;;) {
if(quit) {
ret = -1;
break;
}
pkt1 = q->first_pkt;
if (pkt1) {
q->first_pkt = pkt1->next;
if (!q->first_pkt)
q->last_pkt = NULL;
q->nb_packets--;
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
正如你所看到的,我们已经用一个无限循环包装了这个函数以便于我们想用阻塞
的方式来得到数据。我们通过使用SDL 中的函数SDL_CondWait()来 避免无限循
环。基本上,所有的CondWait 只等待从SDL_CondSignal()函数(或者
SDL_CondBroadcast()函数)中发出 的信号,然后再继续执行。然而,虽然看起
来我们陷入了我们的互斥体中--如果我们一直保持着这个锁,我们的函数将永
远无法把数据放入到队列中去!但 是,SDL_CondWait()函数也为我们做了解锁
互斥量的动作然后才尝试着在得到信号后去重新锁定它。
意外情况
你们将会注意到我们有一个全局变量quit,我们用它来保证还没有设置程序退
出的信号(SDL 会自动处理TERM 类似的信 号)。否则,这个线程将不停地运行
直到我们使用kill -9 来结束程序。FFMPEG 同样也提供了一个函数来进行回调
并检查我们是否需要退出一些被阻塞的函数:这个函数就是
url_set_interrupt_cb。
int decode_interrupt_cb(void) {
return quit;
}
...
main() {
...
url_set_interrupt_cb(decode_interrupt_cb);
...
SDL_PollEvent(&event);
switch(event.type) {
case SDL_QUIT:
quit = 1;
...
当然,这仅仅是用来给ffmpeg 中的阻塞情况使用的,而不是SDL 中的。我们还
必需要设置quit 标志为1。
为队列提供包
剩下的我们唯一需要为队列所做的事就是提供包了:
PacketQueue audioq;
main() {
...
avcodec_open(aCodecCtx, aCodec);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
函数SDL_PauseAudio()让音频设备最终开始工作。如果没有立即供给足够的数
据,它会播放静音。
我们已经建立好我们的队列,现在我们准备为它提供包。先看一下我们的读取包
的循环:
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
....
}
} else if(packet.stream_index==audioStream) {
packet_queue_put(&audioq, &packet);
} else {
av_free_packet(&packet);
}
注意:我们没有在把包放到队列里的时候释放它,我们将在解码后来释放它。
取出包
现在,让我们最后让声音回调函数audio_callback 来从队列中取出包。回调函
数的格式必需为void callback(void *userdata, Uint8 *stream, int len),
这里的userdata 就是我们给到SDL 的指针,stream 是我们要把声音数据写入的
缓冲区指针,len 是缓冲区的大小。下面就是代码:
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
int len1, audio_size;
static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
static unsigned int audio_buf_size = 0;
static unsigned int audio_buf_index = 0;
while(len > 0) {
if(audio_buf_index >= audio_buf_size) {
audio_size = audio_decode_frame(aCodecCtx, audio_buf,
sizeof(audio_buf));
if(audio_size < 0) {
audio_buf_size = 1024;
memset(audio_buf, 0, audio_buf_size);
} else {
audio_buf_size = audio_size;
}
audio_buf_index = 0;
}
len1 = audio_buf_size - audio_buf_index;
if(len1 > len)
len1 = len;
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream += len1;
audio_buf_index += len1;
}
}
这基本上是一个简单的从另外一个我们将要写的audio_decode_frame()函数中概要 概要
电影文件有很多基本的组成部分。首先,文件本身被称为容 容容 容 器 器器 器 Container Container Container,容
器的类型决定了信息被存放在文件中的位置。AVI 和 Quicktime 就是容器的例
子。接着,你有一组流 流流 流, 例如,你经常有的是一个音频流和一个视频流。(一
个流只是一种想像出来的词语,用来表示一连 串的通过时间来串连的数据元
素)。在流中的数据元素被称为帧 帧帧 帧 Frame Frame 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>来替
换)
#include <avcodec.h>
#include <avformat.h>
...
int main(int argc, charg *argv[]) {
av_register_all();
这里注册了所有的文件格式和编解码器的库,所以它们将被自 动的使用在被打
开的合适格式的文件上。注意你只需要调用av_register_all()一次,因此我们
在主函数main()中来调用它。如果你喜欢, 也可以只注册特定的格式和编解码
器,但是通常你没有必要这样做。
现在我们可以真正的打开文件:
AVFormatContext *pFormatCtx;
// Open video file
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
return -1; // Couldn't open file
我们通过第一个参数来获得文件名。这个函数读取文件的头部并且把信息保存到
我们给的AVFormatContext 结构体中。最后三个参数用来指定特殊的 文件格式,
缓冲大小和格式参数,但如果把它们设置为空NULL 或者0,libavformat 将自动
检测这些参数。
这个函数只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
// Retrieve stream information
if(av_find_stream_info(pFormatCtx)<0)
return -1; // Couldn't find stream information
这个函数为pFormatCtx->streams 填充上正确的信息。我们引进一个手工调试的
函数来看一下里面有什么:
// Dump information about file onto standard error
dump_format(pFormatCtx, 0, argv[1], 0);
现在pFormatCtx->streams仅仅是一组大小为pFormatCtx->nb_streams的指针,
所以让我们先跳过它直到 我们找到一个视频流。
int i;
AVCodecContext *pCodecCtx;
// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1)
return -1; // Didn't find a video stream
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文)
的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一
个指向他的指针。但是我们必需要找到真正的 编解码器并且打开它:
AVCodec *pCodec;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec)<0)
return -1; // Could not open codec
有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加
CODEC_FLAG_TRUNCATED 到 pCodecCtx->flags 和添 加一个hack 来粗糙的修正帧
率。这两个修正已经不在存在于ffplay.c 中。因此,我必需假设它们不再必要。
我们移除了那些代码后还有一个需要指出的 不同点:pCodecCtx->time_base 现
在已经保存了帧率的信息。time_base 是一个结构体,它里面有一个分子和分母
(AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数
的帧率(例如NTSC 使用29.97fps)。
保存数据
现在我们需要找到一个地方来保存帧:
AVFrame *pFrame;
// Allocate video frame
pFrame=avcodec_alloc_frame();
因为我们准备输出保存24 位 RGB 色的PPM 文件,我们必需把帧的格式从原来的
转换为RGB。FFMPEG 将为我们做这些转换。在大多数项目中(包括我们 的这个)
我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内
存。
// Allocate an AVFrame structure
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
return -1;
即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始
的数据。我们使用avpicture_get_size 来获得我们需要的大小, 然后手工申请
内存空间:
uint8_t *buffer;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_malloc 是 ffmpeg 的 malloc,用来实现一个简单的malloc 的包装,这样来保
证内存地址是对齐的(4 字节对齐或者2 字节对齐)。它并不能保 护你不被内
存泄漏,重复释放或者其它malloc 的问题所困扰。
现在我们使用avpicture_fill 来把帧和我们新申请的内存来结合。关于
AVPicture 的结成:AVPicture 结构体是AVFrame 结 构体的子集――AVFrame 结
构体的开始部分与AVPicture 结构体是一样的。
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);
最后,我们已经准备好来从流中读取数据了。
读取数据
我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最好后转换
格式并且保存。
int frameFinished;
AVPacket packet;
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video(pCodecCtx, pFrame, &frameFinished,
packet.data, packet.size);
// Did we get a video frame?
if(frameFinished) {
// Convert the image from its native format to RGB
img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24,
(AVPicture*)pFrame, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height);
// Save the frame to disk
if(++i<=5)
SaveFrame(pFrameRGB, pCodecCtx->width,
pCodecCtx->height, i);
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
这个循环过程是比较简单的:av_read_frame()读取一个包并且把它保存到
AVPacket 结构体中。注意我们仅仅申请了一个包的结构体 ――ffmpeg 为我们申
请了内部的数据的内存并通过packet.data 指针来指向它。这些数据可以在后面
通过av_free_packet()来释 放。函数avcodec_decode_video()把包转换为帧。
然而当解码一个包的时候,我们可能没有得到我们需要的关于帧的信息。因此,
当我们得 到下一帧的时候,avcodec_decode_video()为我们设置了帧结束标志
frameFinished。最后,我们使用 img_convert()函数来把帧从原始格式
(pCodecCtx->pix_fmt)转换成为RGB 格式。要记住,你可以把一个 AVFrame
结构体的指针转换为AVPicture 结构体的指针。最后,我们把帧和高度宽度信息
传递给我们的SaveFrame 函数。
关于包Packets Packets 的注释 的注释
从技术上讲一个包可以包含部分或者其它的数据,但是 ffmpeg 的解释器保证了
我们得到的包Packets 包含的要么是完整的要么是多种完整的帧。
现在我们需要做的是让SaveFrame 函数能把RGB 信息定稿到一个PPM 格式的文件
中。我们将生成一个简单的PPM 格式文件,请相信,它是可以工作 的。
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// Open file
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile=fopen(szFilename, "wb");
if(pFile==NULL)
return;
// Write header
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data
for(y=0; y<height; y++)
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
// Close file
fclose(pFile);
}
我们做了一些标准的文件打开动作,然后写入RGB 数据。我们一次向文件写入一
行数据。PPM 格式文件的是一种包含一长串的RGB 数据的文件。如果你了解 HTML
色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,就像
#ff0000#ff0000....就表示了了个红色的屏幕。(它被保存成 二进制方式并且
没有分隔符,但是你自己是知道如何分隔的)。文件的头部表示了图像的宽度和
高度以及最大的RGB 值的大小。
现在,回顾我们的main()函数。一旦我们开始读取完视频流,我们必需清理一
切:
// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);
// Free the YUV frame
av_free(pFrame);
// Close the codec
avcodec_close(pCodecCtx);
// Close the video file
av_close_input_file(pFormatCtx);
return 0;
你会注意到我们使用av_free 来释放我们使用avcode_alloc_fram 和 av_malloc
来分配的内存。
上面的就是代码!下面,我们将使用 Linux 或者其它类似的平台,你将运行:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil
-lm
如果你使用的是老版本的ffmpeg,你可以去掉-lavutil 参数:
gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lm
大多数的图像处理函数可以打开PPM 文件。可以使用一些电影文件来进行测试
如何用FFmpeg FFmpeg 编写一个简单播放器详 编写一个简单播放器详
细步骤介绍(3) (3)
FFmpeg, 播放器, 编写
上来得到这个库的源代码或者如果有可能的话你可以直接下载开发包到你的操
作系统中。按照这个指导,你将需要编译这个库。(剩下的几个指导中也是一样)
SDL 库中有许多种方式来在屏幕上绘制图形,而且它有一个特殊的方式来在屏幕
上显示图像――这种方式叫做YUV 覆盖。YUV(从技术上来讲并不叫YUV 而是叫
做 YCbCr)是一种类似 于 RGB 方式的存储原始图像的格式。粗略的讲,Y 是亮度
分量,U 和 V 是色度分量。(这种格式比RGB 复杂的多,因为很多的颜色信息被
丢弃了,而且你可以每 2 个 Y 有 1 个 U 和 1 个 V)。SDL 的 YUV 覆盖使用一组原
始的YUV 数据并且在屏幕上显示出他们。它可以允许4 种不同的 YUV 格式,但
是其中的YV12 是最快的一种。还有一个叫做YUV420P 的 YUV 格式,它和YV12
是一样的,除了U 和 V 分量的位置被调换了以外。 420 意味着它以4:2:0 的
比例进行了二次抽样,基本上就意味着1 个颜色分量对应着4 个亮度分量。所以
它的色度信息只有原来的1/4。这是一种节省带宽 的好方式,因为人眼感觉不
到这种变化。在名称中的P 表示这种格式是平面的――简单的说就是Y,U 和 V
分量分别在不同的数组中。FFMPEG 可以把图像格式 转换为YUV420P,但是现在
很多视频流的格式已经是YUV420P 的了或者可以被很容易 的转换成YUV420P 格
式。
于是,我们现在计划把指导1 中的SaveFrame()函数替换掉,让它直接输出我们
的帧到屏幕上去。但一开始我们必需要先看一下如何使用SDL 库。首先 我们必
需先包含SDL 库的头文件并且初始化它。
#include <SDL.h>#include <SDL_thread.h> if(SDL_Init(SDL_INIT_VIDEO |
SDL_INIT_AUDIO | SDL_INIT_TIMER)) { fprintf(stderr, "Could not
initialize SDL - %s\n", SDL_GetError()); exit(1);}
SDL_Init()函数告诉了SDL 库,哪些特性我们将要用到。当然SDL_GetError()
是一个用来手工除错的函数。
创建一个显示
现在我们需要在屏幕上的一个地方放上一些东西。在 SDL 中显示图像的基本区域
叫做面 面面 面 surface surface surface。
SDL_Surface *screen; screen = SDL_SetVideoMode(pCodecCtx->width,
pCodecCtx->height, 0, 0);if(!screen) { fprintf(stderr, "SDL: could not
set video mode - exiting\n"); exit(1);}
这就创建了一个给定高度和宽度的屏幕。下一个选项是屏幕的颜色深度――0 表
示使用和当前一样的深度。(这个在OS X 系统上不能正常工作,原因请看源代
码)
现在我们在屏幕上来创建一个YUV 覆盖以便于我们输入视频上去:
SDL_Overlay *bmp; bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
pCodecCtx->height, SDL_YV12_OVERLAY,
screen);
正如前面我们所说的,我们使用YV12 来显示图像。
显示图像
前面那些都是很简单的。现在我们需要来显示图像。让我们看一下是如何来处理
完成后的帧的。我们将原来对RGB 处理的方式,并且替换SaveFrame() 为显示
到屏幕上的代码。为了显示到屏幕上,我们将先建立一个AVPicture 结构体并且
设置其数据指针和行尺寸来为我们的YUV 覆盖服务:
if(frameFinished) { SDL_LockYUVOverlay(bmp); AVPicture
pict; pict.data[0] = bmp->pixels[0]; pict.data[1] =
bmp->pixels[2]; pict.data[2] =
bmp->pixels[1]; pict.linesize[0] =
bmp->pitches[0]; pict.linesize[1] =
bmp->pitches[2]; pict.linesize[2] = bmp->pitches[1]; // Convert
the image into YUV format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); }
首先,我们锁定这个覆盖,因为我们将要去改写它。这是一个避免以后发生问题
的好习惯。正如前面所示的,这个AVPicture 结构体有一个数据指针指向一 个
有 4 个元素的指针数据。由于我们处理的是YUV420P,所以我们只需要3 个通道
即只要三组数据。其它的格式可能需要第四个指针来表示alpha 通道或 者其它
参数。行尺寸正如它的名字表示的意义一样。在 YUV 覆盖中相同功能的结构体是
像素pixel 和程度pitch。(程度pitch 是在SDL 里用来表 示指定行数据宽度
的值)。所以我们现在做的是让我们的覆盖中的pict.data 中的三个指针有一个
指向必要的空间的地址。类似的,我们可以直接从覆盖中得到行尺寸信息。像 前
面一样我们使用img_convert 来把格式转换成PIX_FMT_YUV420P。
绘制图像
但我们仍然需要告诉SDL 如何来实际显示我们给的数据。我们也会传递一个表明
电影位置、宽度、高度和缩放大小的矩形参数给SDL 的函数。这样,SDL 为我 们
做缩放并且它可以通过显卡的帮忙来进行快速缩放。
SDL_Rect rect; if(frameFinished) { // Convert the image into YUV
format that SDL uses img_convert(&pict,
PIX_FMT_YUV420P, (AVPicture *)pFrame,
pCodecCtx->pix_fmt, pCodecCtx->width,
pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); rect.x =
0; rect.y = 0; rect.w = pCodecCtx->width; rect.h =
pCodecCtx->height; SDL_DisplayYUVOverlay(bmp, &rect); }
现在我们的视频显示出来了!
让我们再花一点时间来看一下SDL 的特性:它的事件驱动系统。SDL 被 设置成
当你在SDL 中点击或者移动鼠标或者向它发送一个信号它都将产生一个事件的
驱动方式。如果你的程序想要处理用户输入的话,它就会检测这些事件。你的程
序也可以产生事件并且传递给SDL事件系统。当使用SDL进行多线程编程的时候,
这相当有用,这方面代码我们可以在指导4 中看到。在这个程序中,我们将在 处
理完包以后就立即轮询事件。现在而言,我们将处理SDL_QUIT 事件以便于我们
退出:
SDL_Event event; av_free_packet(&packet); SDL_PollEvent
(&event); switch(event.type) { case
SDL_QUIT: SDL_Quit(); exit(0); break; default:
break; }
让我们去掉旧的冗余代码,开始编译。如果你使用的是Linux 或者其变体,使用
SDL 库进行编译的最好方式 为:
gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lz -lm
\`sdl-config --cflags --libs`
这里的sdl-config 命令会打印出用于gcc 编译的包含正确SDL 库的适当参数。
为了进行编译,在你自己的平台你可能需要做的有点不同:请查阅一下 SDL 文
档中关于你的系统的那部分。一旦可以编译,就马上运行它。
当运行这个程序的时候会发生什么呢?电影简直跑疯了!实际上,我们只是以我
们能从文件中解码帧的最快速度显示了所有的电影的帧。现在我们没有任何代码
来计 算出我们什么时候需要显示电影的帧。最后(在指导5),我们将花足够
的时间来探讨同步问题。但一开始我们会先忽略这个,因为我们有更加重要的事
情要处理: 音频!
指 指指 指 导 导导 导 333 3: :: :播放声音 播放声音
现在我们要来播放声音。SDL 也为我们准备了输出声音的方法。函数
SDL_OpenAudio()本身就是用来打开声音设备的。它使用一个叫做
SDL_AudioSpec 结构体作为参数,这个结构体中包含了我们将要输出的音频的所
有信息。
在我们展示如何建立之前,让我们先解释一下电脑是如何处理音频的。数字音频
是由一长串的样本流组成的。每个样本表示声音波形中的一个值。声音按照一个
特定 的采样率来进行录制,采样率表示以多快的速度来播放这段样本流,它的
表示方式为每秒多少次采样。例如22050 和 44100 的采样率就是电台和CD 常用
的 采样率。此外,大多音频有不只一个通道来表示立体声或者环绕。例如,如
果采样是立体声,那么每次的采样数就为2 个。当我们从一个电影文件中等到数
据的时候,我们不知道我们将得到多少个样 本,但是ffmpeg 将不会给我们部分
的样本――这意味着它将不 会把立体声分割开来。
SDL 播放声音的方式是这样的:你先设置声音的选项:采样率(在 SDL 的结构体
中被叫做freq 的表示频率frequency),声音通道数和其它的参 数,然后我们
设置一个回调函数和一些用户数据userdata。当开始播放音频的时候,SDL 将不
断地调用这个回调函数并且要求它来向声音缓冲填入一个特 定的数量的字节。
当我们把这些信息放到SDL_AudioSpec 结构体中后,我们调用函数
SDL_OpenAudio()就会打开声音设备并且给我们送 回另外一个AudioSpec 结构
体。这个结构体是我们实际上用到的--因为我们不能保证得到我们所要求的。
设置音频
目前先把讲的记住,因为我们实际上还没有任何关于声音流的信息。让我们回过
头来看一下我们的代码,看我们是如何找到视频流的,同样我们也可以找到声音
流。
// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_VIDEO
&&
videoStream < 0) {
videoStream=i;
}
if(pFormatCtx->streams->codec->codec_type==CODEC_TYPE_AUDIO &&
audioStream < 0) {
audioStream=i;
}
}
if(videoStream==-1)
return -1; // Didn't find a video stream
if(audioStream==-1)
return -1;
从这里我们可以从描述流的AVCodecContext 中得到我们想要的信息,就像我们
得到视频流的信息一样。
AVCodecContext *aCodecCtx;
aCodecCtx=pFormatCtx->streams[audioStream]->codec;
包含在编解码上下文中的所有信息正是我们所需要的用来建立音频的信息:
wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
让我们浏览一下这些:
·freq 前面所讲的采样率
·format 告诉SDL 我们将要给的格式。在“S16SYS”中的S 表示有符号的signed,
16 表示每个样本是16 位长的,SYS 表示大小头的顺序是与使用的系统相同的。
这些格式是由 avcodec_decode_audio2 为我们给出来的输入音频的格式。
·channels 声音的通道数
·silence 这是用来表示静音的值。因为声音采样是有符号的,所以0 当然就
是这个值。
·samples 这是当我们想要更多声音的时候,我们想让SDL 给出来的声音缓冲
区的尺寸。一个比较合适的值在512 到 8192 之间;ffplay 使用1024。
·callback 这个是我们的回调函数。我们后面将会详细讨论。
·userdata 这个是SDL 供给回调函数运行的参数。我们将让回调函数得到整个
编解码的上下文;你将在后面知道原因。
最后,我们使用SDL_OpenAudio 函数来打开声音。
如果你还记得前面的指导,我们仍然需要打开声音编解码器本身。这是很显然的。
AVCodec *aCodec;
aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
avcodec_open(aCodecCtx, aCodec);
队列
嗯!现在我们已经准备好从流中取出声音信息。但是我们如何来处理这些信息
呢?我们将会不断地从文件中得到这些包,但同时SDL 也将调用回调函数。解决
方法 为创建一个全局的结构体变量以便于我们从文件中得到的声音包有地方存
放同时也保证SDL中的声音回调函数audio_callback能从这个地方得到声 音数
据。所以我们要做的是创建一个包的队列queue。在 ffmpeg 中有一个叫
AVPacketList 的结构体可以帮助我们,这个结构体实际是一串包 的链表。下面
就是我们的队列结构体:
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
首先,我们应当指出nb_packets 是与size 不一样的--size 表示我们从
packet->size 中得到的字节数。你会注意到我们有一 个互斥量mutex 和一个条
件变量cond 在结构体里面。这是因为SDL 是在一个独立的线程中来进行音频处
理的。如果我们没有正确的锁定这个队列,我们有 可能把数据搞乱。我们将来
看一个这个队列是如何来运行的。每一个程序员应当知道如何来生成的一个队
列,但是我们将把这 部分也来讨论从而可以学习到SDL 的函数。
一开始我们先创建一个函数来初始化队列:
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
接着我们再做一个函数来给队列中填入东西:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
SDL_LockMutex(q->mutex);
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
函数SDL_LockMutex()锁定队列的互斥量以便于我们向队列中添加东西,然后函
数 SDL_CondSignal()通过我们的条件变量为一个接 收函数(如果它在等待)发
出一个信号来告诉它现在已经有数据了,接着就会解锁互斥量并让队列可以自由
访问。
下面是相应的接收函数。注意函数SDL_CondWait()是如何按照我们的要求让函
数阻塞block 的(例如一直等到队列中有数据)。
int quit = 0;
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for(;;) {
if(quit) {
ret = -1;
break;
}
pkt1 = q->first_pkt;
if (pkt1) {
q->first_pkt = pkt1->next;
if (!q->first_pkt)
q->last_pkt = NULL;
q->nb_packets--;
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
正如你所看到的,我们已经用一个无限循环包装了这个函数以便于我们想用阻塞
的方式来得到数据。我们通过使用SDL 中的函数SDL_CondWait()来 避免无限循
环。基本上,所有的CondWait 只等待从SDL_CondSignal()函数(或者
SDL_CondBroadcast()函数)中发出 的信号,然后再继续执行。然而,虽然看起
来我们陷入了我们的互斥体中--如果我们一直保持着这个锁,我们的函数将永
远无法把数据放入到队列中去!但 是,SDL_CondWait()函数也为我们做了解锁
互斥量的动作然后才尝试着在得到信号后去重新锁定它。
意外情况
你们将会注意到我们有一个全局变量quit,我们用它来保证还没有设置程序退
出的信号(SDL 会自动处理TERM 类似的信 号)。否则,这个线程将不停地运行
直到我们使用kill -9 来结束程序。FFMPEG 同样也提供了一个函数来进行回调
并检查我们是否需要退出一些被阻塞的函数:这个函数就是
url_set_interrupt_cb。
int decode_interrupt_cb(void) {
return quit;
}
...
main() {
...
url_set_interrupt_cb(decode_interrupt_cb);
...
SDL_PollEvent(&event);
switch(event.type) {
case SDL_QUIT:
quit = 1;
...
当然,这仅仅是用来给ffmpeg 中的阻塞情况使用的,而不是SDL 中的。我们还
必需要设置quit 标志为1。
为队列提供包
剩下的我们唯一需要为队列所做的事就是提供包了:
PacketQueue audioq;
main() {
...
avcodec_open(aCodecCtx, aCodec);
packet_queue_init(&audioq);
SDL_PauseAudio(0);
函数SDL_PauseAudio()让音频设备最终开始工作。如果没有立即供给足够的数
据,它会播放静音。
我们已经建立好我们的队列,现在我们准备为它提供包。先看一下我们的读取包
的循环:
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
....
}
} else if(packet.stream_index==audioStream) {
packet_queue_put(&audioq, &packet);
} else {
av_free_packet(&packet);
}
注意:我们没有在把包放到队列里的时候释放它,我们将在解码后来释放它。
取出包
现在,让我们最后让声音回调函数audio_callback 来从队列中取出包。回调函
数的格式必需为void callback(void *userdata, Uint8 *stream, int len),
这里的userdata 就是我们给到SDL 的指针,stream 是我们要把声音数据写入的
缓冲区指针,len 是缓冲区的大小。下面就是代码:
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
int len1, audio_size;
static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
static unsigned int audio_buf_size = 0;
static unsigned int audio_buf_index = 0;
while(len > 0) {
if(audio_buf_index >= audio_buf_size) {
audio_size = audio_decode_frame(aCodecCtx, audio_buf,
sizeof(audio_buf));
if(audio_size < 0) {
audio_buf_size = 1024;
memset(audio_buf, 0, audio_buf_size);
} else {
audio_buf_size = audio_size;
}
audio_buf_index = 0;
}
len1 = audio_buf_size - audio_buf_index;
if(len1 > len)
len1 = len;
memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
len -= len1;
stream += len1;
audio_buf_index += len1;
}
}
这基本上是一个简单的从另外一个我们将要写的audio_decode_frame()函数中
获取数据的循环,这个循环把结果写入到中间缓冲区,尝试着向 流中写入len
字节并且在我们没有足够的数据的时候会获取更多的数据或者当我们有多余数
据的时候保存下来为后面使用。这个audio_buf 的大小为 1.5 倍的声音帧的大
小以便于有一个比较好的缓冲,这个声音帧的大小是ffmpeg 给出的。
最后解码音频
让我们看一下解码器的真正部分:audio_decode_frame
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf,
int buf_size) {
static AVPacket pkt;
static uint8_t *audio_pkt_data = NULL;
static int audio_pkt_size = 0;
int len1, data_size;
for(;;) {
while(audio_pkt_size > 0) {
data_size = buf_size;
len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf,
&data_size,
audio_pkt_data, audio_pkt_size);
if(len1 < 0) {
audio_pkt_size = 0;
break;
}
audio_pkt_data += len1;
audio_pkt_size -= len1;
if(data_size <= 0) {
continue;
}
return data_size;
}
if(pkt.data)
av_free_packet(&pkt);
if(quit) {
return -1;
}
if(packet_queue_get(&audioq, &pkt, 1) < 0) {
return -1;
}
audio_pkt_data = pkt.data;
audio_pkt_size = pkt.size;
}
}
整个过程实际上从函数的尾部开始,在这里我们调用了packet_queue_get()函
数。我们从队列中取出包,并且保存它的信息。然后,一旦我们有 了可以使用
的包,我们就调用函数avcodec_decode_audio2(),它的功能就像它的姐妹函数
avcodec_decode_video()一样,唯一的区别是它的一个包里可能有不止一个声音
帧,所以你可能要调用很多次来解码出包中所有的数据。同 时也要记住进行指
针 audio_buf 的强制转换,因为SDL 给出的是8 位整型缓冲指针而ffmpeg 给出
的数据是16 位的整型指针。你应该也会注意到 len1 和 data_size 的不同,len1
表示解码使用的数据的在包中的大小,data_size 表示实际返回的原始声音数据
的大小。
当我们得到一些数据的时候,我们立刻返回来看一下是否仍然需要从队列中得到
更加多的数据或者我们已经完成了。如果我们仍然有更加多的数据要处理,我们
把它 保存到下一次。如果我们完成了一个包的处理,我们最后要释放它。
就是这样。我们利用主的读取队列循环从文件得到音频并送到队列中,然后被
audio_callback 函数从队列中读取并处理,最后把数据送给SDL,于 是 SDL 就
相当于我们的声卡。让我们继续并且编译:
gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm \
`sdl-config --cflags --libs`
啊哈!视频虽然还是像原来那样快,但是声音可以正常播放了。这是为什么呢?
因为声音信息中的采样率--虽然我们把声音数据尽可能快的填充到声卡缓冲
中,但 是声音设备却会按照原来指定的采样率来进行播放。
我们几乎已经准备好来开始同步音频和视频了,但是首先我们需要的是一点程序
的组织。用队列的方式来组织和播放音频在一个独立的线程中工作的很好:它使
得程 序更加更加易于控制和模块化。在我们开始同步音视频之前,我们需要让
我们的代码更加容易处理。所以下次要讲的是:创建一个线程。
如何用FFmpeg FFmpeg 编写一个简单播放器详 编写一个简单播放器详
细步骤介绍(5) (5)
FFmpeg, 播放器, 编写
} else {
break;
}
}
// Is this a packet from the video stream?
if(packet->stream_index == is->videoStream) {
packet_queue_put(&is->videoq, packet);
} else if(packet->stream_index == is->audioStream) {
packet_queue_put(&is->audioq, packet);
} else {
av_free_packet(packet);
}
}
这里没有什么新东西,除了我们给音频和视频队列限定了一个最大值并且我们添
加一个检测读错误的 函数。格式上下文里面有一个叫做pb 的 ByteIOContext
类型结构体。这个结构体是用来保存一些低级的文件信息。函数url_ferror 用
来检测结构体并发 现是否有些读取文件错误。
在循环以后,我们的代码是用等待其余的程序结束和提示我们已经结束的。这些
代码是有益的,因为 它指示出了如何驱动事件--后面我们将显示影像。
while(!is->quit) {
SDL_Delay(100);
}
fail:
if(1){
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
}
return 0;
我们使用SDL 常量SDL_USEREVENT 来从用户事件中得到值。第一个用户事件的值
应当是SDL_USEREVENT,下一个是 SDL_USEREVENT+1 并且依此类推。在我们的
程序中FF_QUIT_EVENT 被定义成SDL_USEREVENT+2。如果喜欢,我们也可以 传
递用户数据,在这里我们传递的是大结构体的指针。最后我们调用
SDL_PushEvent()函数。在我们的事件分支中,我们只是像以前放入
SDL_QUIT_EVENT 部分一样。我们将在自己的事件队列中详细讨论,现在只是确
保我们正确放入了FF_QUIT_EVENT 事件,我们将在后面捕 捉到它并且设置我们
的退出标志quit。
得到帧: :: :video_thread video_thread video_thread
当我们准备好解码器后,我们开始视频线程。这个线程从视频队列中读取包,把
它解码成视频帧,然后调用queue_picture 函数把处理好的帧放入到图 片队列
中:
int video_thread(void *arg) {
VideoState *is = (VideoState *)arg;
AVPacket pkt1, *packet = &pkt1;
int len1, frameFinished;
AVFrame *pFrame;
pFrame = avcodec_alloc_frame();
for(;;) {
if(packet_queue_get(&is->videoq, packet, 1) < 0) {
// means we quit getting packets
break;
}
// Decode video frame
len1 = avcodec_decode_video(is->video_st->codec, pFrame,
&frameFinished,
packet->data, packet->size);
// Did we get a video frame?
if(frameFinished) {
if(queue_picture(is, pFrame) < 0) {
break;
}
}
av_free_packet(packet);
}
av_free(pFrame);
return 0;
}
在这里的很多函数应该很熟悉吧。我们把avcodec_decode_video 函数移到了这
里,替换了一些参数,例如:我们把AVStream 保存在我 们自己的大结构体中,
所以我们可以从那里得到编解码器的信息。我们仅仅是不断的从视频队列中取包
一直到有人告诉我们要停止或者出错为止。
把帧队列化
让我们看一下保存解码后的帧pFrame 到图像队列中去的函数。因为我们的图像
队列是SDL 的覆盖的集合(基本上不用让视频显示函数再做计算了),我们需 要
把帧转换成相应的格式。我们保存到图像队列中的数据是我们自己做的一个结构
体。
typedef struct VideoPicture {
SDL_Overlay *bmp;
int width, height;
int allocated;
} VideoPicture;
我们的大结构体有一个可以保存这些缓冲区。然而,我们需要自己来申请
SDL_Overlay(注意:allocated 标志会指明我们是否已经做了这个申 请的动作
与否)。
为了使用这个队列,我们有两个指针--写入指针和读取指针。我们也要保证一
定数量的实际数据在缓冲中。要写入到队列中,我们先要等待缓冲清空以便于有
位置 来保存我们的VideoPicture。然后我们检查看我们是否已经申请到了一个
可以写入覆盖的索引号。如果没有,我们要申请一段空间。我们也要重新申请 缓
冲如果窗口的大小已经改变。然而,为了避免被锁定,尽是避免在这里申请(我
现在还不太清楚原因;我相信是为了避免在其它线程中调用SDL 覆盖函数的原
因)。
int queue_picture(VideoState *is, AVFrame *pFrame) {
VideoPicture *vp;
int dst_pix_fmt;
AVPicture pict;
SDL_LockMutex(is->pictq_mutex);
while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
!is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if(is->quit)
return -1;
// windex is set to 0 initially
vp = &is->pictq[is->pictq_windex];
if(!vp->bmp ||
vp->width != is->video_st->codec->width ||
vp->height != is->video_st->codec->height) {
SDL_Event event;
vp->allocated = 0;
event.type = FF_ALLOC_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
SDL_LockMutex(is->pictq_mutex);
while(!vp->allocated && !is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if(is->quit) {
return -1;
}
}
这里的事件机制与前面我们想要退出的时候看到的一样。我们已经定义了事件
FF_ALLOC_EVENT 作为SDL_USEREVENT。我们把事件发到事 件队列中然后等待申
请内存的函数设置好条件变量。
让我们来看一看如何来修改事件循环:
for(;;) {
SDL_WaitEvent(&event);
switch(event.type) {
case FF_ALLOC_EVENT:
alloc_picture(event.user.data1);
break;
记住event.user.data1 是我们的大结构体。就这么简单。让我们看一下
alloc_picture()函数:
void alloc_picture(void *userdata) {
VideoState *is = (VideoState *)userdata;
VideoPicture *vp;
vp = &is->pictq[is->pictq_windex];
if(vp->bmp) {
// we already have one make another, bigger/smaller
SDL_FreeYUVOverlay(vp->bmp);
}
// Allocate a place to put our YUV image on that screen
vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
is->video_st->codec->height,
SDL_YV12_OVERLAY,
screen);
vp->width = is->video_st->codec->width;
vp->height = is->video_st->codec->height;
SDL_LockMutex(is->pictq_mutex);
vp->allocated = 1;
SDL_CondSignal(is->pictq_cond);
SDL_UnlockMutex(is->pictq_mutex);
}
你可以看到我们把SDL_CreateYUVOverlay 函数从主循环中移到了这里。这段代
码应该完全可以自我注释。记住我们把高度和宽度保存到 VideoPicture 结构体
中因为我们需要保存我们的视频的大小没有因为某些原因而改变。
好,我们几乎已经全部解决并且可以申请到YUV 覆盖和准备好接收图像。让我们
回顾一下queue_picture 并看一个拷贝帧到覆盖的代码。你应该能认 出其中的
一部分:
int queue_picture(VideoState *is, AVFrame *pFrame) {
if(vp->bmp) {
SDL_LockYUVOverlay(vp->bmp);
dst_pix_fmt = PIX_FMT_YUV420P;
pict.data[0] = vp->bmp->pixels[0];
pict.data[1] = vp->bmp->pixels[2];
pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0];
pict.linesize[1] = vp->bmp->pitches[2];
pict.linesize[2] = vp->bmp->pitches[1];
// Convert the image into YUV format that SDL uses
img_convert(&pict, dst_pix_fmt,
(AVPicture *)pFrame, is->video_st->codec->pix_fmt,
is->video_st->codec->width, is->video_st->codec->height);
SDL_UnlockYUVOverlay(vp->bmp);
if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_windex = 0;
}
SDL_LockMutex(is->pictq_mutex);
is->pictq_size++;
SDL_UnlockMutex(is->pictq_mutex);
}
return 0;
}
这部分代码和前面用到的一样,主要是简单的用我们的帧来填充YUV 覆盖。最后
一点只是简单的给队列加1。这个队列在写的时候会一直写入到满为止,在读的
时 候会一直读空为止。因此所有的都依赖于is->pictq_size 值,这要求我们必
需要锁定它。这里我们做的是增加写指针(在必要的时候采用轮转 的方式),
然后锁定队列并且增加尺寸。现在我们的读者函数将会知道队列中有了更多的信
息,当队列满的时候,我们的写入函数也会知道。
显示视频
这就是我们的视频线程。现在我们看过了几乎所有的线程除了一个--记得我们
调用schedule_refresh()函数吗?让我们看一下实际中是如何做 的:
static void schedule_refresh(VideoState *is, int delay) {
SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}
函数SDL_AddTimer()是 SDL 中的一个定时(特定的毫秒)执行用户定义的回调
函数(可以带一些参数user data)的简单函数。我们将用这个函数来定时刷新
视频--每次我们调用这个函数的时候,它将设置一个定时器来触发定时事件来
把一帧从图像队列中显示到屏 幕上。
但是,让我们先触发那个事件。
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
SDL_Event event;
event.type = FF_REFRESH_EVENT;
event.user.data1 = opaque;
SDL_PushEvent(&event);