ffmpeg解碼API
版本迭代
ffmpeg解碼API經過了好幾個版本的迭代,上一個版本的API是
- 解碼視訊:avcodec_decode_video2
- 解碼音訊:avcodec_decode_audio4
我們現在能看到的很多解碼例子用的都是這兩個,不過現在ffmpeg更推薦用新一代的API
- 向解碼器輸送資料包:avcodec_send_packet
- 從解碼器獲取幀:avcodec_receive_frame
通常來說,一個packet會被解碼出一個frame,不過也存在一個packet被解碼出多個frame或者多個packet才能解碼出一個frame的情況,甚至也有些解碼器在輸入以及輸出端上可能會有延遲。因此原來的API在某種程度上存在對呼叫者誤導的可能,使得呼叫者認為輸入的一個或者多個Packet就對應著解碼器所輸出的一個frame,但實際上可能並非如此。
新的API完全隱藏了“解碼”這一概念,只提供一個輸入packet的介面以及輸出frame的介面,如此一來呼叫者可以不必瞭解解碼器的具體細節,只需要瞭解這兩個介面的呼叫規則就能寫出適用於所有解碼器的程式碼。
狀態機
新一代API是一個狀態機。呼叫API是一種動作,API的返回值就是一種狀態,通過動作可以進行狀態的轉換。正常情況下,狀態機有6種狀態:
- send 0 :send_packet返回值為0,正常狀態,意味著輸入的packet被解碼器正常接收。
- send EAGAIN :send_packet返回值為EAGAIN,輸入的packet未被接收,需要輸出一個或多個的frame後才能重新輸入當前packet。
- send EOF :send_packet返回值為EOF,當send_packet輸入為NULL時才會觸發該狀態,用於通知解碼器輸入packet已結束。
- receive 0
- receive EAGAIN:receive_frame返回值為EAGAIN,未能輸出frame,需要輸入更多的packet才能輸出當前frame。
- receive EOF :receive_frame返回值為EOF,當處於send EOF狀態後,呼叫一次或者多次receive_frame後就能得到該狀態,表示所有的幀已經被輸出。
如上圖所示,儘管狀態轉換稍微有些繁瑣,但該狀態轉換圖實際上包含了兩種策略,對兩種策略分別進行分析能對狀態機有一個更為清晰的瞭解。
以消耗packet為主的策略
雖然我們前面說過輸入的packet並不一定對應於所輸出的frame,不過在這裡為了方便語言上的描述,在這裡我們可以認為receive_frame是對輸入的packet的一種消耗,當receive_frame返回EAGAIN時就認為所輸入的packet被完全消耗。這裡的策略就是對每次所輸入的一個packet,都迴圈呼叫receive_frame對該packet進行消耗,直到所輸入的packet消耗完成。
在消耗完一個packet後輸入下一個packet
當所有的packet都消耗完成後,呼叫send_packet輸入NULL,把狀態轉換為send EOF,最後呼叫receive_frame把狀態轉換為receive EOF即完成所有解碼任務。
以獲取frame為主的策略
本策略是先迴圈呼叫send_packet直到返回EAGAIN,此時肯定可以輸出frame了
然後呼叫receive_frame輸出一幀
當所有的packet都輸入完成後,呼叫send_packet輸入NULL,把狀態轉換為send EOF,最後呼叫receive_frame把狀態轉換為receive EOF即完成所有解碼任務。
API程式碼分析
avcodec_send_packet
avcodec_send_packet有如下結構:
首先粗略瞭解一下bsf,即bitstream filter。音訊與視訊編碼後資料會以一定的語法結構進行構建,除了編碼後的資料之外還有一些並非解碼所必須的語法元素,這些語法元素通常只是在解碼、顯示等過程起到輔助作用,這些語法元素很少使用到,它們的位置一般是位於在編碼後的資料之前,如h264中的SEI。bitstream filter就是對這些語法元素進行調整。
av_bsf_send_packet會把packet輸送到bitstream filter中,在av_bsf_send_packet當中,會判斷用於暫存輸入packet的buffer_pkt是否為有效packet,如果是有效packet,則表明上次傳入的packet仍未被解碼器消耗,因此無法接收這次傳入的packet,返回EAGAIN。
if (ctx->internal->buffer_pkt->data || ctx->internal->buffer_pkt->side_data_elems) return AVERROR(EAGAIN);
否則就把當前packet移動到用於暫存的buffer_pkt
av_packet_move_ref(ctx->internal->buffer_pkt, pkt);
decode_receive_frame_internal是實際的解碼入口,它有如下結構
decode_receive_frame_internal需要先從用於暫存的buffer_pkt中取出輸入的packet,這是呼叫bsfs_poll來實現的。bsfs_poll會執行所有的bitstream filter,最終會呼叫到ff_bsf_get_packet_ref,在該函式內,會先判斷用於暫存packet的buffer_pkt是否為有效packet,不是則返回EAGAIN
if (!ctx->internal->buffer_pkt->data && !ctx->internal->buffer_pkt->side_data_elems) return AVERROR(EAGAIN);
有效則取出該packet
av_packet_move_ref(pkt, ctx->internal->buffer_pkt);
取出該packet後就可以呼叫codec的decode函式來進行解碼。
avcodec_receive_frame
avcodec_receive_frame有如下結構:
avcodec_receive_frame會先進行判斷,如果解碼器解碼出了一幀,則會呼叫av_frame_move_ref輸出這一幀,否則繼續呼叫decode_receive_frame_internal繼續進行解碼。
if (avci->buffer_frame->buf[0]) { av_frame_move_ref(frame, avci->buffer_frame); } else { ret = decode_receive_frame_internal(avctx, frame); if (ret < 0) return ret; }
關於EAGAIN
我們前面討論過EAGAIN狀態:
- avcodec_send_packet返回EAGAIN表明無法輸入當前packet,需要呼叫avcodec_receive_frame進行消耗上一個packet。
- avcodec_receive_packet返回EAGAIN表明無法獲取當前frame,需要呼叫avcodec_send_packet輸入更多的packet。
一般來說,在實際的實現中,EAGAIN是由bsf相關的函式返回的。
- 呼叫avcodec_send_packet時,會先呼叫av_bsf_send_packet,此時如果用於暫存packet的buffer_pkt中含有有效packet時,av_bsf_send_packet會返回EAGAIN,這會導致avcodec_send_packet也返回EAGAIN。
- 呼叫avcodec_receive_frame時,如果沒有可輸出的frame,則會進入decode_receive_frame_internal分支。此時如果用於暫存packet的buffer_pkt中不含有效packet時,ff_bsf_get_packet_ref會返回EAGAIN,這會導致decode_receive_frame_internal返回EAGAIN,從而也使得avcodec_receive_frame也返回EAGAIN。
不過我們注意到avcodec_send_packet中也呼叫了decode_receive_frame_internal,不過avcodec_send_packet會忽視decode_receive_frame_internal所返回的EAGAIN。
ret = decode_receive_frame_internal(avctx, avci->buffer_frame); if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) return ret;