FFmpeg基本知识
鼎鼎大名的FFmpeg不用多作介紹,基本是音視頻技術必備的基礎庫之一,提供了強大的音視頻處理方案。本文記錄FFmpeg的一些基本知識,基于4.0.2,有時間會慢慢增改。(PS:可能有錯誤)
FFmpeg最常用的結構體
解協議(http,rtsp,rtmp,mms)
協議(文件)操作的頂層結構是AVIOContext,這個對象實現了帶緩沖的讀寫操作;FFmpeg的輸入對象AVFormat的pb字段指向一個AVIOContext。
AVIOContext的opaque實際指向一個URLContext對象,這個對象封裝了協議對象及協議操作對象,其中prot指向具體的協議操作對象,priv_data指向具體的協議對象。
URLProtocol為協議操作對象,針對每種協議,會有一個這樣的對象,每個協議操作對象和一個協議對象關聯。
注意:FFmpeg中文件也被當做一種協議:file。
解封裝(flv,avi,rmvb,mp4)
AVFormatContext主要存儲視音頻封裝格式中包含的信息;AVInputFormat存儲輸入視音頻使用的封裝格式。每種視音頻封裝格式都對應一個AVInputFormat結構。
解碼(h264,mpeg2,aac,mp3)
每個AVStream存儲一個視頻/音頻流的相關數據;每個AVStream對應一個AVCodecContext,存儲該視頻/音頻流使用解碼方式的相關數據;每個AVCodecContext中對應一個AVCodec,包含該視頻/音頻對應的解碼器。每種解碼器都對應一個AVCodec結構。
編解碼數據
視頻每個Packet是一幀;音頻每個Packet可能包含若干幀。
解碼前數據:AVPacket
解碼后數據:AVFrame
解碼基本流程
從封裝文件中拿到流。
從流中讀取數據包到packet。
將packet解碼為frame。
處理frame。
轉到步驟2。
詳細:
創建AVFormatContext,用以管理文件的輸入輸出,可以直接賦予空指針,然后由后續函數分配內存:
|
|
也可以用函數分配內存:
|
|
然后用以下函數打開輸入:
|
|
該函數會讀取媒體文件的文件頭并將文件格式相關的信息存儲在我們作為第一個參數傳入的AVFormatContext中。第二個參數為視頻地址,這個可以是本地視頻文件地址,也可以是視頻流地址。第三個參數用于指定媒體文件格式,第四個參數是文件格式相關選項。如果后面這兩個參數傳入的是NULL,那么 libavformat 將自動探測文件格式。如果文件打開失敗,返回值為負值,要調用avformat_free_context()及時釋放掉AVFormatContext(好像會自動釋放?)。如果打開成功,返回值為0,且等到后面不再需要輸入文件的操作時,要調用avformat_close_input(AVFormatContext **s)來關閉輸入。
接著需要將視音頻流的信息讀取到AVFormatContext,AVFormatContext中有信息,才能進行查找視頻流、音頻流及相應的解碼器的操作:
|
|
第二個參數一般填NULL。返回值>=0表示成功。
調試函數:
|
|
可以為我們打印 AVFormatContext 中都有哪些信息。url是文件,index和output一般填0。AVFormatContext 里包含了下面這些跟媒體信息有關的成員:
struct AVInputFormat *iformat:輸入數據的封裝格式
AVIOContext *pb:輸入數據的緩存
unsigned int nb_streams:視音頻流的個數
AVStream **streams:視音頻流
char filename[1024]:文件名
int64_t duration:時長(單位:微秒us,轉換為秒需要除以1000000,即除以AV_TIME_BASE)
int bit_rate:比特率(單位bps,轉換為kbps需要除以1000)
AVDictionary *metadata:元數據
接下來需要初始化視音頻的AVCodec(解碼器)和AVCodecContext(解碼器上下文)。注意,這里音頻的AVCodec和AVCodecContext和視頻的是分開的,但是它們的流程是一模一樣的。首先根據類型找到音頻或視頻的序號,并在同時匹配到最適合的解碼器:
|
|
AVMediaType是AVMEDIA_TYPE_VIDEO或AVMEDIA_TYPE_AUDIO,wanted_stream_nb和related_stream傳-1。decoder_ret傳入一個新建的AVCodec *codec,這樣可以直接將查找到的解碼器填充進去,當然有可能查找失敗,所以應該判斷一下codec是否為NULL。flags傳0。函數返回值為相應流的序號,負值代表失敗。
通過序號就能找到視頻流或者音頻流:
|
|
接下來通過匹配到的解碼器創建AVCodecContext(解碼器上下文)并把視/音頻流里的參數傳到視/音頻解碼器中:
|
|
codecContext為NULL表示失敗。
|
|
返回負值表示失敗。
接下來就可以打開解碼器上下文準備進行解碼操作了:
|
|
最后一個參數填NULL,返回負值表示失敗。
分配AVPacket和AVFrame的內存:
|
|
這兩個語句分配的內存是AVPacket和AVFrame結構本身的內存,而不包括其指向的實際數據的部分,這些需要另外分配。失敗時返回NULL。此外AVPacket也可以不使用指針動態分配內存,而是直接定義AVPacket pkt,然后用其他方法獲得相應數據。
循環調用函數:
|
|
該函數從流中讀取一個數據包,把它存儲在AVPacket數據結構中,其中packet.data這個指針會指向這些數據。注意av_read_frame不會調用av_packet_unref,只會調用av_init_packet將引用計數的指針指向NULL。因此如果該packet沒有拷貝到別處用于其他用途,則在下一次av_read_frame前實際數據占用的內存需要手動通過av_packet_unref()函數來釋放(一般放在av_read_frame最后,保證該次循環不會再使用改packet)。
然后在循環中先調用avcodec_send_packet(AVCodecContextavctx, const AVPacketavpkt)發送
再調用avcodec_receive_frame(AVCodecContextavctx, AVFrameframe)接收。注意該函數會先調用av_frame_unref(frame),故如果每次使用的是同一個frame去接收解碼后的數據,那么每次傳進去就會把前面的數據釋放掉,導致就只有一個frame是有用的。
如果發送函數報AVERROR(EAGAIN)的錯,表示已發送的AVPacket還沒有被接收,不允許發送新的AVPacket。如果是接收函數報這個錯,表示沒有新的AVPacket可以接收,需要先發送AVPacket才能執行這個函數。而如果報AVERROR_EOF的錯,在以上4個函數中都表示編解碼器處于已經刷新完成的狀態,沒有數據可以進行發送和接收操作。
最后釋放內存
|
|
av_frame_free(&frame)和av_packet_free(&pkt)分別對應于av_frame_alloc()和av_packet_alloc(),不同之處在于av_frame_alloc()和av_packet_alloc()會先調用av_frame_unref()釋放buf內存,再釋放AVFrame本身的內存,av_packet_alloc()同理。
注意av_frame_ref對src的buf增加一個引用,即使用同一個數據,只是這個數據引用計數加1。av_frame_unref把自身對buf的引用釋放掉,數據的引用計數減1,當引用為0就釋放buf。
圖像/音頻數據內存分配與釋放
|
|
返回對應圖像格式和大小的圖像所占的字節數,最后一個參數是內存對齊的對齊數,也就是按多大的字節進行內存對齊。比如設置為1,表示按1字節對齊,那么得到的結果就是與實際的內存大小一樣。再比如設置為4,表示按4字節對齊。也就是內存的起始地址必須是4的整倍數。
|
|
申請指定字節數的內存,返回相應的指針,NULL表示分配內存失敗。
|
|
函數自身不具備內存申請的功能,此函數類似于格式化已經申請的內存,即通過av_malloc()函數申請的內存空間。再者,av_image_fill_arrays()中參數具體說明(中括號里表明是輸入還是輸出):
dst_data[4]:[out]對申請的內存格式化為三個通道后,分別保存其地址。
dst_linesize[4]: [out]格式化的內存的步長(即內存對齊后的寬度) 注:linesize每行的大小不一定等于圖像的寬度,因為pack格式圖像的所有分量儲存在同一個通道中,如data[0]。
*src: [in]av_malloc()函數申請的內存地址(av_malloc返回的指針)。
pix_fmt: [in] 申請 src內存時的像素格式。
[in]申請src內存時指定的寬度。
height: [in]申請scr內存時指定的高度。
align: [in]申請src內存時指定的對齊字節數。
通常上面三個函數一起調用。或者用下面的一步到位的簡化函數:
|
|
pointers[4]:保存圖像通道的地址。如果是RGB,則前三個指針分別指向R,G,B的內存地址。第四個指針保留不用。
linesizes[4]:保存圖像每個通道的內存對齊的步長,即一行的對齊內容的寬度,此值大小在planar圖像中等于圖像寬度。
w: 要申請內存的圖像寬度。
h: 要申請內存的圖像高度。
pix_fmt: 要申請內存的圖像的像素格式。
align: 用于內存對齊的值。一般為1。
返回值:所申請的內存空間的總大小。如果是負值,表示申請失敗。
同樣,音頻也有上述類似的函數:
|
|
linesize是函數里面計算出來的,可以傳NULL。
|
|
|
|
audio_data:保存音頻通道的地址,也有planar和pack之分。
linesize:允許為NULL。
|
|
釋放指針并置為NULL,上面分配的dst_data[4]/pointers[4]最后要用此函數釋放。當然也可以定義一個AVframe,用其相應的結構承載內存,最后由av_frame_free(&frame)負責釋放
圖像轉換libswscale
該庫可以改變圖像尺寸,轉換像素格式等,當對解碼后的圖像進行保存或顯示的時候需要用到,因為圖像原格式并不一定適合存儲或用SDL播放。總體流程是:
sws_getContext():初始化一個SwsContext。
sws_scale():處理圖像數據。
sws_freeContext():釋放一個SwsContext。
具體
|
|
AVPixelFormat 為輸入和輸出圖片數據的類型,eg:AV_PIX_FMT_YUV420、PAV_PIX_FMT_RGB24;int flags 為scale算法種類;后面三個指針一般為NULL。
出錯返回NULL。
|
|
const uint8_t *const srcSlice[],uint8_t *const dst[]:輸輸出圖像數據各顏色通道的buffer指針數組;
const int srcStride[],const int dstStride[]:輸入輸出圖像據各顏色通道每行存儲的字節數數組;
int srcSliceY:從輸入圖像數據的第多少列開始逐行掃描,通常設0;
int srcSliceH:需要掃描多少行,通常為輸入圖像數據的高度;
返回輸出圖像高度。
音頻轉換libswresample
該庫可以轉換音頻格式,當對解碼后的音頻進行保存或播放的時候需要用到,因為音頻原格式并不一定適合存儲或用SDL播放(比如SDL播放音頻不支持平面格式)。總體流程是:
創建SwrContext,并設置轉換所需的參數:通道數量、channel layout、sample rate。
設置了所有參數后,用swr_init(struct SwrContext *)初始化
調用swr_convert()進行轉換。
swr_free()釋放上下文。
具體
使用swr_alloc_set_opts設置SwrContext:
|
|
上述兩種方法設置的是將5.1聲道,采樣格式為AV_SAMPLE_FMT_FLTP,采樣率為48KHz的音頻轉換為2聲道,采樣格式為AV_SAMPLE_FMT_S16,采樣率為44.1KHz。
其中的參數channel_layout是一個64位整數,每個值為1的位對應一個通道。在頭文件channel_layout.h中為將每個通道定義了一個mask,一個channel_layout就是某些channel mask的組合。可以用以下函數根據channel_layout得到通道數:
|
|
也可以根據通道數得到默認的channel_layout:
|
|
調用swr_convert進行轉換
|
|
out:輸出緩沖區。
out_count:轉換后每個通道的sample個數
in:輸入緩沖區,一般為(const uint8_t **)frame->data
in_count:輸入音頻每個通道的sample個數,一般為frame->nb_samples
其返回值為轉換后每個通道的sample個數。
轉換后的sample個數的計算公式為:src_nb_samples * dst_sample_rate / src_sample_rate,其代碼如下:
|
|
函數av_rescale_rnd是按照指定的舍入方式計算a * b / c 。
函數swr_get_delay得到輸入sample和輸出sample之間的延遲,并且其返回值的根據傳入的第二個參數不同而不同。如果是輸入的采樣率,則返回值是輸入sample個數;如果輸入的是輸出采樣率,則返回值是輸出sample個數。
時間戳timestamp
時間戳是以時間基(timebase)為單位的具體時間表示,有PTS和DTS兩種,一般在有B幀編碼的情況下兩者都會用到,沒有B幀時,兩者一般保持一樣。
DTS(Decoding Time Stamp):即解碼時間戳,這個時間戳的意義在于告訴播放器該在什么時候解碼這一幀的數據。
PTS(Presentation Time Stamp):即顯示時間戳,這個時間戳用來告訴播放器該在什么時候顯示這一幀的數據。
時間基timebase
ffmpeg存在多個時間基(time_base),對應不同的階段(結構體),每個time_base具體的值不一樣,ffmpeg提供函數在各個time_base中進行切換。
首先要知道AVRatioal的定義如下:
|
|
ffmpeg提供了一個把AVRatioal結構轉換成double的函數:
|
|
不同的時間基
AV_TIME_BASE
ffmpeg中的“內部時間基”,以微秒為單位,作為某些變量的基本時間單位,比如AVFormatContext中的duration即以其倒數(AV_TIME_BASE_Q)為基本單位,意味著這個流的長度為duration微秒,要除以AV_TIME_BASE(即乘以AV_TIME_BASE_Q)才能得到單位是秒的結果。此處也說明它和別的time_base剛好相反,因為別的time_base都是直接用秒的倒數來表示的。AV_TIME_BASE定義為:
|
|
AV_TIME_BASE_Q
ffmpeg內部時間基的分數表示,實際上它是AV_TIME_BASE的倒數。從它的定義能很清楚的看到這點,1秒除以1000000即1微秒:
|
|
AVStream->time_base
根據時鐘采樣率來決定的,單位為秒,如:1/90000。根據封裝格式不一樣,avformat_write_header()可能修改AVStream->time_base,比如mpegts修改為90000,flv修改為1000,mp4根據設置time_base,如果小于10000,會將time_base*2的冪直到大于10000。AVPacket的pts和dts以AVStream的time_base為單位。
AVCodecContext->time_base
根據視頻幀率/音頻采樣率來決定的,單位為秒,如:通常視頻的該time_base值是 1/framerate,音頻則是1/samplerate。時間戳pts、dts每增加1實際上代表的是增加了一個time_base的時間。AVFrame的pts和dts以AVStream的time_base為單位,而AVFrame里面的pkt_pts和pkt_dts是拷貝自AVPacket,同樣以AVStream->time_base為單位。AVFrame的pts用av_frame_get_best_effort_timestamp獲取比較好。
InputStream這個結構的pts和dts以AV_TIME_BASE為單位
問題的關鍵是不同的場景下取到的數據幀的time是相對哪個時間體系的:
demux出來的幀的time:是相對于源AVStream的timebase。
編碼器出來的幀的time:是相對于源AVCodecContext的timebase。
mux存入文件等容器的time:是相對于目的AVStream的timebase。
這里的time指pts。
計算
根據pts來計算某一幀在整個視頻中的時間位置(PTS轉常規時間):
time(秒) = pts * av_q2d(st->time_base)
計算視頻長度:
time(秒) = st->duration * av_q2d(st->time_base)
常規時間轉PTS:(存疑?)
pts = time * 1/av_q2d(st->time_base)
這里的st是一個AVStream對象指針。pts應該也是AVStream->time_base為單位。似乎一般都是用AVStream的time_base和相應單位的pts來得到真實的時間。
ffmpeg提供了不同時間基之間的轉換函數:
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)
這個函數的作用是計算a * bq / cq,來把時間戳從一個時基調整到另外一個時基。在進行時基轉換的時候,應該首選這個函數,因為它可以避免溢出的情況發生。av_rescale_q(pts, timebase1, timebase2)的含義即是:
new_pts = pts(timebase1.num / timebase1.den )(timebase2.den / timebase2.num)
幾個含義
st為AVStream:
fps = st->avg_frame_rate:平均幀率
tbr = st->r_frame_rate:這是可以準確表示所有時間戳的最低幀率(它是流中所有幀率的最小公倍數),猜測值。
tbn = st->time_base:AVStream的timebase
tbc = st->codec->time_base:AVCodecContext的timebase。
總結
以上是生活随笔為你收集整理的FFmpeg基本知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为服务器设置iBMC管理网口IP地址,
- 下一篇: 何其有幸,年岁并进