FFmpeg開發筆記(十九)FFmpeg開啟兩個執行緒分別解碼音影片

aqi00發表於2024-05-05
同步播放音影片的時候,《FFmpeg開發實戰:從零基礎到短影片上線》一書第10章的示例程式playsync.c採取一邊遍歷一邊播放的方式,在原始檔的音訊流和影片流交錯讀取的情況下,該方式可以很好地實現同步播放功能。

但個別格式的音訊流和影片流是分開儲存的,前面一大段放了所有的音訊幀,後面一大段放了所有的影片幀,並非音訊幀與影片幀交錯儲存的模式。對於這種格式,playsync.c播放時先放完所有的聲音,這期間畫面是空白的;再快速放完所有的影片畫面,這期間沒有聲音,顯然播放過程是有問題的。
若想糾正playsync.c的播放問題,就得重新設計音影片的同步播放機制,不能採取一邊遍歷一邊播放的方式,而要先把音訊幀和影片幀都讀到快取佇列中,再依次檢查音訊與影片的時間戳,從而決定在哪個時刻才播放對應時間戳的音影片。具體到程式碼實現上,需要補充下列幾點改造。
1、除了已有的影片處理執行緒和影片包佇列之外,還要增加宣告音訊處理執行緒和音訊包佇列,當然音訊包佇列配套的佇列鎖也要補充宣告。增補後的宣告程式碼如下所示:

SDL_mutex *audio_list_lock = NULL; // 宣告一個音訊包佇列鎖,防止執行緒間同時操作包佇列
SDL_Thread *audio_thread = NULL; // 宣告一個音訊處理執行緒
PacketQueue packet_audio_list; // 存放音訊包的佇列
SDL_mutex *video_list_lock = NULL; // 宣告一個影片包的佇列鎖,防止執行緒間同時操作包佇列
SDL_Thread *video_thread = NULL; // 宣告一個影片處理執行緒
PacketQueue packet_video_list; // 存放影片包的佇列

2、在程式初始化的時候,不但要建立影片處理執行緒和影片佇列的互斥鎖,還要建立音訊處理執行緒和音訊佇列的互斥鎖。修改後的初始化程式碼如下所示:

audio_list_lock = SDL_CreateMutex(); // 建立互斥鎖,用於排程佇列
// 建立SDL執行緒,指定任務處理函式,並返回執行緒編號
audio_thread = SDL_CreateThread(thread_work_audio, "thread_work_audio", NULL);
if (!audio_thread) {
    av_log(NULL, AV_LOG_ERROR, "sdl create audio thread occur error\n");
    return -1;
}
video_list_lock = SDL_CreateMutex(); // 建立互斥鎖,用於排程佇列
// 建立SDL執行緒,指定任務處理函式,並返回執行緒編號
video_thread = SDL_CreateThread(thread_work_video, "thread_work_video", NULL);
if (!video_thread) {
    av_log(NULL, AV_LOG_ERROR, "sdl create video thread occur error\n");
    return -1;
}

3、對音影片檔案遍歷資料包時,不能立即渲染音訊,而要把音訊包加入音訊佇列,把影片包加入影片佇列,由兩個處理執行緒根據時間戳來排程具體的播放進度。另外,在所有資料包都遍歷完之後,影片包佇列可能還有剩餘的資料,所以程式末尾得輪詢影片包佇列,直至所有影片幀都渲染結束才算完成播放。據此修改音影片檔案的遍歷與輪詢程式碼如下所示:

while (av_read_frame(in_fmt_ctx, packet) >= 0) { // 輪詢資料包
    if (packet->stream_index == audio_index) { // 音訊包需要解碼
        SDL_LockMutex(audio_list_lock); // 對音訊佇列鎖加鎖
        push_packet(&packet_audio_list, *packet); // 把音訊包加入佇列
        SDL_UnlockMutex(audio_list_lock); // 對音訊佇列鎖解鎖
    } else if (packet->stream_index == video_index) { // 影片包需要解碼
        SDL_LockMutex(video_list_lock); // 對影片佇列鎖加鎖
        push_packet(&packet_video_list, *packet); // 把影片包加入佇列
        SDL_UnlockMutex(video_list_lock); // 對影片佇列鎖解鎖
        if (!has_audio) { // 不存在音訊流
            SDL_Delay(interval); // 延遲若干時間,單位毫秒
        }
    }
    if (play_video_frame() == -1) { // 播放影片畫面
        goto __QUIT;
    }
}
while (!is_empty(packet_video_list)) { // 播放剩餘的影片畫面
    if (play_video_frame() == -1) {
        goto __QUIT;
    }
    SDL_Delay(5); // 延遲若干時間,單位毫秒
}

除了上述的三大塊改造,尚有下面四個函式要補充修改:
thread_work_audio函式:這是音訊處理執行緒新增的工作函式,主要從音訊包佇列取資料,然後解碼為音訊幀再重取樣,並將重取樣的結果資料送給揚聲器。
thread_work_video函式:這是影片處理執行緒原有的工作函式,除了給影片包佇列及其對應的互斥鎖改名之外,其他程式碼照搬即可。
play_video_frame函式:這是播放影片畫面的新增函式,就是把原來SDL渲染畫面的程式碼塊重新包裝成獨立的函式,方便多次呼叫罷了。
release函式:這是釋放音影片資源的函式,與之前的釋放程式碼相比,主要增加了音訊處理執行緒的等待操作,以及音訊佇列鎖的銷燬操作。
上述修改後的程式碼已經附在了《FFmpeg開發實戰:從零基礎到短影片上線》一書第10章的原始碼chapter10/playsync2.c,這個c程式碼是playsync.c的改進版,能夠正常播放音訊流和影片流分開儲存的影片檔案。
接著執行下面的編譯命令。

gcc playsync2.c -o playsync2 -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -I/usr/local/sdl2/include -L/usr/local/sdl2/lib -lsdl2 -lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

編譯完成後執行以下命令啟動測試程式,期望播放影片檔案fuzhou.mp4。

./playsync2 ../fuzhou.mp4

程式執行完畢,發現控制檯輸出以下的日誌資訊。

Success open input_file ../fuzhou.mp4.
out_sample_rate=44100, out_nb_samples=1024
thread_work_video
video_index 0
thread_work_audio
audio_index 0
……
9216 10240 11264 12288 13312 14336 15360 16384 17408 18432 19456 20480 21504 22528 23552 24576 25600 26624 27648 28672 29696 30720 31744 32768 33792 34816 35840 36864 37888 38912 39936 ……
Close window.
begin release audio resource
audio_thread audio_status=0
end release audio resource
begin release video resource
video_thread video_status=0
end release video resource
Quit SDL.

同時彈出SDL視窗播放影片畫面,並且揚聲器傳來了陣陣歌聲,表示上述程式碼正確實現了同步播放音影片的功能。

相關文章