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