| |
快进快退
处理快进快退命令
现在我们来为我们的播放器加入一些快进和快退的功能,因为如果你不能全局搜索一部电影是很让人讨厌的。同时,这将告诉你av_seek_frame函数是多么容易使用。
我们将在电影播放中使用左方向键和右方向键来表示向后和向前一小段,使用向上和向下键来表示向前和向后一大段。这里一小段是10秒,一大段是60秒。所以我们需要设置我们的主循环来捕捉键盘事件。然而当我们捕捉到键盘事件后我们不能直接调用av_seek_frame函数。我们要主要的解码线程decode_thread的循环中做这些。所以,我们要添加一些变量到大结构体中,用来包含新的跳转位置和一些跳转标志:
int seek_req;
int seek_flags;
int64_t seek_pos;
现在让我们在主循环中捕捉按键:
for(;;) {
double incr, pos;
SDL_WaitEvent(&event);
switch(event.type) {
case SDL_KEYDOWN:
switch(event.key.keysym.sym) {
case SDLK_LEFT:
incr = -10.0;
goto do_seek;
case SDLK_RIGHT:
incr = 10.0;
goto do_seek;
case SDLK_UP:
incr = 60.0;
goto do_seek;
case SDLK_DOWN:
incr = -60.0;
goto do_seek;
do_seek:
if(global_video_state) {
pos = get_master_clock(global_video_state);
pos += incr;
stream_seek(global_video_state,
(int64_t)(pos * AV_TIME_BASE), incr);
}
break;
default:
break;
}
break;
为了检测按键,我们先查了一下是否有SDL_KEYDOWN事件。然后我们使用event.key.keysym.sym来判断哪个按键被按下。一旦我们知道了如何来跳转,我们就来计算新的时间,方法为把增加的时间值加到从函数get_master_clock中得到的时间值上。然后我们调用stream_seek函数来设置seek_pos等变量。我们把新的时间转换成为avcodec中的内部时间戳单位。在流中调用那个时间戳将使用帧而不是用秒来计算,公式为seconds = frames *time_base(fps)。默认的avcodec值为1,000,000fps(所以2秒的内部时间戳为2,000,000)。在后面我们来看一下为什么要把这个值进行一下转换。
这就是我们的stream_seek函数。请注意我们设置了一个标志为后退服务:
void stream_seek(VideoState *is, int64_t pos, int rel) {
if(!is->seek_req) {
is->seek_pos = pos;
is->seek_flags = rel < 0 ? AVSEEK_FLAG_BACKWARD : 0;
is->seek_req = 1;
}
}
现在让我们看一下如果在decode_thread中实现跳转。你会注意到我们已经在源文件中标记了一个叫做“seek stuff goes here”的部分。现在我们将把代码写在这里。
跳转是围绕着av_seek_frame函数的。这个函数用到了一个格式上下文,一个流,一个时间戳和一组标记来作为它的参数。这个函数将会跳转到你所给的时间戳的位置。时间戳的单位是你传递给函数的流的时基time_base。然而,你并不是必需要传给它一个流(流可以用-1来代替)。如果你这样做了,时基time_base将会是avcodec中的内部时间戳单位,或者是1000000fps。这就是为什么我们在设置seek_pos的时候会把位置乘以AV_TIME_BASER的原因。
但是,如果给av_seek_frame函数的stream参数传递传-1,你有时会在播放某些文件的时候遇到问题(比较少见),所以我们会取文件中的第一个流并且把它传递到av_seek_frame函数。不要忘记我们也要把时间戳timestamp的单位进行转化。
if(is->seek_req) {
int stream_index= -1;
int64_t seek_target = is->seek_pos;
if (is->videoStream >= 0) stream_index = is->videoStream;
else if(is->audioStream >= 0) stream_index = is->audioStream;
if(stream_index>=0){
seek_target= av_rescale_q(seek_target, AV_TIME_BASE_Q,
pFormatCtx->streams[stream_index]->time_base);
}
if(av_seek_frame(is->pFormatCtx, stream_index,
seek_target, is->seek_flags) < 0) {
fprintf(stderr, "%s: error while seeking/n",
is->pFormatCtx->filename);
} else {
这里av_rescale_q(a,b,c)是用来把时间戳从一个时基调整到另外一个时基时候用的函数。它基本的动作是计算a*b/c,但是这个函数还是必需的,因为直接计算会有溢出的情况发生。AV_TIME_BASE_Q是AV_TIME_BASE作为分母后的版本。它们是很不相同的:AV_TIME_BASE * time_in_seconds = avcodec_timestamp而AV_TIME_BASE_Q *avcodec_timestamp =time_in_seconds(注意AV_TIME_BASE_Q实际上是一个AVRational对象,所以你必需使用avcodec中特定的q函数来处理它)。
清空我们的缓冲
我们已经正确设定了跳转位置,但是我们还没有结束。记住我们有一个堆放了很多包的队列。既然我们跳到了不同的位置,我们必需把队列中的内容清空否则电影是不会跳转的。不仅如此,avcodec也有它自己的内部缓冲,也需要每次被清空。
要实现这个,我们需要首先写一个函数来清空我们的包队列。然后我们需要一种命令声音和视频线程来清空avcodec内部缓冲的办法。我们可以在清空队列后把特定的包放入到队列中,然后当它们检测到特定的包的时候,它们就会把自己的内部缓冲清空。
让我们开始写清空函数。其实很简单的,所以我直接把代码写在下面:
static void packet_queue_flush(PacketQueue *q) {
AVPacketList *pkt, *pkt1;
SDL_LockMutex(q->mutex);
for(pkt = q->first_pkt; pkt != NULL; pkt = pkt1) {
pkt1 = pkt->next;
av_free_packet(&pkt->pkt);
av_freep(&pkt);
}
q->last_pkt = NULL;
q->first_pkt = NULL;
q->nb_packets = 0;
q->size = 0;
SDL_UnlockMutex(q->mutex);
}
既然队列已经清空了,我们放入“清空包”。但是开始我们要定义和创建这个包:
AVPacket flush_pkt;
main() {
...
av_init_packet(&flush_pkt);
flush_pkt.data = "FLUSH";
...
}
现在我们把这个包放到队列中:
} else {
if(is->audioStream >= 0) {
packet_queue_flush(&is->audioq);
packet_queue_put(&is->audioq, &flush_pkt);
}
if(is->videoStream >= 0) {
packet_queue_flush(&is->videoq);
packet_queue_put(&is->videoq, &flush_pkt);
}
}
is->seek_req = 0;
}
(这些代码片段是接着前面decode_thread中的代码片段的)我们也需要修改packet_queue_put函数才不至于直接简单复制了这个包:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if(pkt != &flush_pkt && av_dup_packet(pkt) < 0) {
return -1;
}
然后在声音线程和视频线程中,我们在packet_queue_get后立即调用函数avcodec_flush_buffers:
if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
return -1;
}
if(packet->data == flush_pkt.data) {
avcodec_flush_buffers(is->audio_st->codec);
continue;
}
上面的代码片段与视频线程中的一样,只要把“audio”换成“video”。
就这样,让我们编译我们的播放器:
gcc -o tutorial07 tutorial07.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs`
试一下!我们几乎已经都做完了;下次我们只要做一点小的改动就好了,那就是检测ffmpeg提供的小的软件缩放采样。
软件缩放
软件缩放库libswscale
近来ffmpeg添加了新的接口:libswscale来处理图像缩放。
但是在前面我们使用img_convert来把RGB转换成YUV12,我们现在使用新的接口。新接口更加标准和快速,而且我相信里面有了MMX优化代码。换句话说,它是做缩放更好的方式。
我们将用来缩放的基本函数是sws_scale。但一开始,我们必需建立一个SwsContext的概念。这将让我们进行想要的转换,然后把它传递给sws_scale函数。类似于在SQL中的预备阶段或者是在Python中编译的规则表达式regexp。要准备这个上下文,我们使用sws_getContext函数,它需要我们源的宽度和高度,我们想要的宽度和高度,源的格式和想要转换成的格式,同时还有一些其它的参数和标志。然后我们像使用img_convert一样来使用sws_scale函数,唯一不同的是我们传递给的是SwsContext:
#include <ffmpeg/swscale.h> // include the header!
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
static struct SwsContext *img_convert_ctx;
...
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
if(img_convert_ctx == NULL) {
int w = is->video_st->codec->width;
int h = is->video_st->codec->height;
img_convert_ctx = sws_getContext(w, h,
is->video_st->codec->pix_fmt,
w, h, dst_pix_fmt, SWS_BICUBIC,
NULL, NULL, NULL);
if(img_convert_ctx == NULL) {
fprintf(stderr, "Cannot initialize the conversion context!/n");
exit(1);
}
}
sws_scale(img_convert_ctx, pFrame->data,
pFrame->linesize, 0,
is->video_st->codec->height,
pict.data, pict.linesize);
我们把新的缩放器放到了合适的位置。希望这会让你知道libswscale能做什么。
就这样,我们做完了!编译我们的播放器:
gcc -o tutorial08 tutorial08.c -lavutil -lavformat -lavcodec -lz -lm `sdl-config --cflags --libs`
享受我们用C写的少于1000行的电影播放器吧。
当然,还有很多事情要做。
现在还要做什么?
我们已经有了一个可以工作的播放器,但是它肯定还不够好。我们做了很多,但是还有很多要添加的性能:
·错误处理。我们代码中的错误处理是无穷的,多处理一些会更好。
·暂停。我们不能暂停电影,这是一个很有用的功能。我们可以在大结构体中使用一个内部暂停变量,当用户暂停的时候就设置它。然后我们的音频,视频和解码线程检测到它后就不再输出任何东西。我们也使用av_read_play来支持网络。这很容易解释,但是你却不能明显的计算出,所以把这个作为一个家庭作业,如果你想尝试的话。提示,可以参考ffplay.c。
·支持视频硬件特性。一个参考的例子,请参考Frame Grabbing在Martin的旧的指导中的相关部分。http://www.inb.uni-luebeck.de/~boehme/libavcodec_update.html
·按字节跳转。如果你可以按照字节而不是秒的方式来计算出跳转位置,那么对于像VOB文件一样的有不连续时间戳的视频文件来说,定位会更加精确。
·丢弃帧。如果视频落后的太多,我们应当把下一帧丢弃掉而不是设置一个短的刷新时间。
·支持网络。现在的电影播放器还不能播放网络流媒体。
·支持像YUV文件一样的原始视频流。如果我们的播放器支持的话,因为我们不能猜测出时基和大小,我们应该加入一些参数来进行相应的设置。
·全屏。
·多种参数,例如:不同图像格式;参考ffplay.c中的命令开关。
·其它事情,例如:在结构体中的音频缓冲区应该对齐。