FFmpeg 入門(7):Seeking

本文轉自:FFmpeg 入門(7):Seeking | www.samirchen.comgit

處理 seek 命令

咱們將爲播放器添加 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)avcodectime_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 到你給它的 timestamptimestamp 的單位是你傳入的流的 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_QAV_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
相關文章
相關標籤/搜索