本文轉自:FFmpeg 入門(7):Seeking | www.samirchen.comgit
咱們將爲播放器添加 seek 的能力。這個過程當中,咱們會看到 av_seek_frame
用起來有多方便。github
咱們添加的功能是經過上下左右鍵可以作快進或快退,其中左右鍵快進或快退的幅度較小,爲 10s,上下鍵快進或快退的幅度較大,爲 60s。因此咱們須要在咱們的事件處理循環中添加處理按鍵的邏輯。可是當咱們遇到按鍵事件時,咱們不能直接調用 av_seek_frame
,咱們須要在 decode loop 和 decode_thread loop 進行處理。因此咱們需呀在 VideoState
裏添加一些變量來記錄 seek 的位置以及一些 seek 的標記。ide
typedef struct VideoState { // ... code ... int seek_req; int seek_flags; int64_t seek_pos; // ... code ... }
咱們須要在主函數的事件循環中監聽按鍵事件:函數
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; // ... code ... }
當咱們監聽到按鍵事件並判斷出按鍵方向時,咱們就知道了咱們該如何 seek 了,這時候咱們經過 get_master_clock()
得到此時的時鐘值,並加上要 seek 的時間,而後調用 stream_seek()
函數來設置 seek_pos
等值。咱們轉換新的時間爲 avcodec
的內部時間戳單位。記住,在流中時間戳是經過幀數來度量而不是秒,公式是:seconds = frames * time_base (fps)
。avcodec
中 time_base
的默認值是 1000000 fps(也就是說 2s 的位置即時間戳爲 2000000)。咱們將看到咱們爲何要轉換這個值。oop
下面是 stream_seek()
函數,咱們設置了一個 flag 來標記是快進仍是快退:ui
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 操做。線程
咱們的 seek 操做是圍繞着 av_seek_frame()
函數進行的。這個函數的參數是:AVFormatContext *s, int stream_index, int64_t timestamp, int flags
。這個函數將 seek 到你給它的 timestamp
。timestamp
的單位是你傳入的流的 time_base
。可是,你能夠不用傳入一個流,經過傳一個 -1 來表示。若是這樣的話,time_base
就會是 avcodec
內部的時間戳單位,即 1000000 fps。這就是爲何咱們要在設置 seek_pos
時把 position 乘上 AV_TIME_BASE
的緣由。code
然而,有時候對於有些媒體文件,你傳給 av_seek_frame()
-1 做爲 stream_index
可能會遇到一些問題。因此咱們將選擇文件中的第一個流傳給 av_seek_frame()
。不要忘記,這時咱們也必須調整咱們的時間戳到新的單位。orm
// 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 { // ... code ... }
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 函數來處理它。視頻
咱們將 seek 調整完畢,可是還沒徹底完成。咱們有一個存放 packet 的隊列,既然如今咱們 seek 到別的位置了,那麼咱們就須要刷新一下這個隊列,不然視頻就無法 seek 了。不光如此,avcodec
也有它內部的緩衝區,也須要由各對應的線程來刷新。
爲此,首先,咱們須要寫一個函數來清理咱們的 packet 隊列。而後,咱們須要有一些機制來告訴音視頻線程來刷新 avcodec
的內部緩衝區。咱們能夠經過在刷新後的 packet 隊列中放一個特殊的 packet 來作到這一點,當 avcodec
探測到這個 packet 時,它就會刷新本身的緩衝區。
咱們實現的函數是 packet_queue_flush()
,代碼以下:
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_packet_unref(&pkt->pkt); av_freep(&pkt); } q->last_pkt = NULL; q->first_pkt = NULL; q->nb_packets = 0; q->size = 0; SDL_UnlockMutex(q->mutex); }
既然如今隊列已經刷新,接着就是放一個 flush packet,可是首先咱們要定義一下它:
AVPacket flush_pkt; int main(int argc, char *argv[]) { // ... code ... av_init_packet(&flush_pkt); flush_pkt.data = (unsigned char *) "FLUSH"; // ... code ... }
如今咱們把它放到刷新後的隊列中:
// Seek stuff goes here. if (is->seek_req) { // ... code ... } 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()
函數當是 flush packet 時不作拷貝:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) { AVPacketList *pkt1; if (pkt != &flush_pkt && av_packet_ref(pkt, pkt) < 0) { return -1; } // ... code ... }
接着是修改 audio thread 和 video thread,咱們在調用 packet_queue_put()
後即檢查 flush packet 並調用 avcodec_flush_buffers()
:
視頻線程,在 video_thread()
中:
if (packet_queue_get(&is->videoq, packet, 1) < 0) { // Means we quit getting packets. break; } if (packet->data == flush_pkt.data) { avcodec_flush_buffers(is->video_st->codec); continue; }
音頻線程,在 audio_decode_frame()
中:
// Next packet. if (packet_queue_get(&is->audioq, pkt, 1) < 0) { return -1; } if (pkt->data == flush_pkt.data) { avcodec_flush_buffers(is->audio_st->codec); continue; }
以上即是咱們這節教程的所有內容,其中的完整代碼你能夠從這裏得到:https://github.com/samirchen/TestFFmpeg
你可使用下面的命令編譯它:
$ gcc -o tutorial07 tutorial07.c -lavutil -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
找一個視頻文件,你能夠這樣執行一下試試:
$ tutorial07 myvideofile.mp4