指导7:控制进度
处理快进快退命令
现在我们来为我们的播放器加入一些快进和快退的功能,因为如果你不能全局
搜索一部视频是很让人讨厌的。同时,这将告诉你 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;
这就是我们的 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; } }
跳转是围绕着 av_seek_frame 函数的。这个函数用到了一个格式上下文,一个流, 一个时间戳和一组标记来作为它的参数。这个函数将会跳转到你所给的时间戳 的位置。时间戳的单位是你传递给函数的流的时基 time_base。然而,你并不必需要传给它一个流(流可以用-1 来代替)。如果这样做, 时基 time_base 将会以 avcodec 中的内部时间戳为单位,或者是 1000000fps。这就是为什么我们 在设置 seek_pos 的时候会把位置乘 以 AV_TIME_BASER 的原因。
但是,如果给 av_seek_frame 函数的 stream 参数传递传-1,你有时会在播放某些文件的时候遇到问题(比较少见),所以我们会取文件中的第一个流并且把它传递到 av_seek_frame 函数。不要忘记我们也要把时间戳 timestamp 的单位进行 转化。
// seek stuff goes here 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 提供的小的软件缩放采样。