指導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; |
爲了檢測按鍵,我們先查了一下是否有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提供的小的軟件縮放採樣。