大白話iOS音視訊-01-音訊播放(FFMpeg+AudioUnit)
前言瞎扯
實際關於利用 FFmpeg
+ AudioUnit
,相關文章是有的,但是還是有所不足, 較多是隻言片語有的沒有Demo,所以我還是要寫這麼一篇, 我這篇的特點是, 閒扯中讓各位(讓我自己~)從 最基本的概念
-> 能搞出東西
.
ofollow,noindex">Demo地址 當然直接下載下來是不能跑的你要安裝我的 iOS/" target="_blank" rel="nofollow,noindex">===>編譯iOS能用的FFmpeg靜態庫 這篇文章裡說的把編譯好的FFmpeg拖到我的工程了,然後 Build Setting
—-> 搜尋 Header Search Paths
新增 $(PROJECT_DIR)/AudioUnitPlayerDemo/ffmpeg/include
基礎知識不太熟的同學看看我的這篇文章
=====>音視訊基礎知識, 只是為看懂本文的話, 看音訊部分就好啦.看我這篇文章你能幹嘛?
你可以完成一個音訊播放 Demo . 用AudioUnit播放一個 mp3
, aac
, 這樣的檔案, 或者視訊檔案的音訊也就是說只播放MP4檔案聲音. 播放一幀一幀的音訊資料(實際上是音訊裸資料PCM, 而PCM是沒有 幀
的概念的.PCM說的取樣..). 播放本地檔案呢,是為後面播放網路過來的資料打個基礎, 因為 解碼
, 解封裝
, AudioUnit 相關API
等相關知識是直播也好播本地檔案也好是相同的程式碼, 多的只是處理網路流部分的邏輯.
大概怎麼做?
FFmpeg解碼 mp3
, aac
, MP4
, 這類的 封裝格式
拿到 裸資料(pcm)
, 然後 喂
給 AudioUnit
材料
FFmpeg
+ AudioUnit
+ 音視訊檔案
FFmpeg
是編譯iOS能用的靜態庫檔案如圖

fffmpeg_iOS.png
看看我這個文章,如果你本地沒有編譯好的.
===>編譯iOS能用的FFmpeg靜態庫往下就是具體邏輯講解了, 預設你懂了關於音訊的基礎知識和已經編譯好iOS能用的靜態庫了哈, 那啥要不再看看
===>編譯iOS能用的FFmpeg靜態庫1.AudioUnit
1.1大概原理閒扯
啥也不說. 看看一幅圖.

AudioUnitJG.png
嗯嗯看看圖,AudioUnit在下去就是硬體了.用它處理音視訊資料確實略微"複雜"."複雜"的話功能就會有點騷.
AudioUnit 就一個小孩, 需要一直喂東西.我要做的就是不斷喂他東西.
....或者說 AudioUnit就是一臺機器,它生產的產品是聲音, 我們要做的就是不斷的給他填原料
, 本篇文章就當他是 打米機
好了, FFmpeg
就是水稻收割機.

FFmpeg_AudioUnit.png
如上圖 水稻收割機
( FFmpeg
)從 田裡
( 音視訊檔案
)收穫 稻穀
( PCM
),然後進過我們排程給 打米機
( AudioUnit
),然後生產 大米
( 聲音
)..城裡的同學請自行研究 水稻
--> 稻穀
--> 大米
全過程.
打米機
如圖右邊那個漏斗是填稻穀的, 然後下面中間的出口產生大米,右邊產生米糠(稻穀的殼). 當我們買來零件組裝好一臺打米機插上電就可以讓它執行起來你要是填稻米它就生產大米,你沒稻米填給它就在那白跑著浪費電, 打米機
它不管 稻米
哪來的它只要人給它填稻米,是不是 水稻收割機
從田裡採集的還是農民通過人工採集的它不管, 它只是說 給我稻米給我電我給你大米
. 然後 AudioUnit
這傢伙跟它一個意思.
如圖,AudioUnit跟打米機一樣也是一個漏斗填音訊(aac,pcm)資料給他,然後它讓揚聲器或者耳機出聲.

audioIO.png
1.2 相關API混臉熟
好啦廢話說了那麼多了,基本上知道AudioUnit是一個什麼樣尿性的傢伙了.下面說具體的類、結構體、函式、方法什麼的了.
原料有: AVAudioSession
, AudioComponentDescription
, AUNode
, AUGraph
, AudioStreamBasicDescription
, AURenderCallbackStruct
差不多這些結構體類啥的(並不是~),
函式方法~(先寫兩個):
AUGraphNodeInfo(AUGraphinGraph, AUNodeinNode, AudioComponentDescription * __nullableoutDescription, AudioUnit __nullable * __nullableoutAudioUnit)__OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0); AudioUnitSetProperty(AudioUnitinUnit, AudioUnitPropertyIDinID, AudioUnitScopeinScope, AudioUnitElementinElement, const void * __nullable inData, UInt32inDataSize) __OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0);
開始有點程式碼了哈, 上面提到的類結構體方法函式先混個臉熟吧, 花30秒過一遍....
1.3操作過程和具體API講解
AudioUnit的使用一句話講解是這樣的: 首先使用 AVAudioSession
會話用來管理獲取硬體資訊, 然後利用一個描述結構體( AudioComponentDescription
)確定 AudioUnit
的型別(AudioUnit能做很多事情的,不同的型別幹不同的事,我們這裡是找能播放音訊的那個),然後通過 AUNode
, AUGraph
拿到我們的 AudioUnit
, 然後設定 AudioUnit
的入口出口等資訊, 最後連線.

AudioUnitSet.png
1.3.1 AVAudioSession
在iOS的音視訊開發中, 使用具體API之前都會先建立一個會話, 這裡也不例外.這是必須的第一步, 你在使用AudioUnit之前必須先建立會話並設定相關引數.
AVAudioSession 用於管理與獲取iOS裝置音訊的硬體資訊, 並且是以單例的形式存在.iOS7以前是使用 Audio Session
兩個實際上是幹一件事.就是 管理與獲取iOS裝置音訊的硬體資訊
, 你的聲音是揚聲器播勒還是耳機了,是藍芽耳機了還是插線耳機了這些資訊都由他管, 舉個例子:你用揚聲器播的好好的然後你插耳機了這時要他做一定邏輯處理.
AVAudioSession
AVAudioSession * audioSession = [AVAudioSessionsharedInstance];
Audio Session
AudioSessionInitialize( NULL,// Run loop (NULL = main run loop) kCFRunLoopDefaultMode, // Run loop mode (void(*)(void*,UInt32))XXXXXX, // Interruption callback NULL);
AVAudioSession
和 Audio Session
一個是類,一個是一個函式,使用起來還是很不同的, 我們這裡用前者. 我們將用一個包裝類來使用AVAudioSession, 下面是具體介紹
- 1.獲取AVAudioSession例項
AVAudioSession * audioSession = [AVAudioSessionsharedInstance];
- 2.設定硬體能力
我們要做什麼? 看我的標題,我們只要播放聲音,我們想要iPhone手機播放聲音.然後我們設定 AVAudioSessionCategoryPlayback
, 如果我們要手機採集又播放就是 AVAudioSessionCategoryPlayAndRecord
[audioSession setCategory:AVAudioSessionCategoryPlayback];
-
- 設定I/O的Buffer,
Buffer
越小則說明延遲越低
- 設定I/O的Buffer,

damijiBuffer.png
AudioUnit
的buffer就好像打米機的稻穀漏斗. 如圖打米機自帶的漏斗填滿稻穀可能需要1分鐘打完, 所以我們需要快1分鐘後就要再往裡面填稻穀, 如果我們換成左邊那個更小的漏斗( buffer
)可能40秒就打完了, 換個大的就時間長點. 小的漏斗呢就需要人不斷的加稻穀, 大的就不需要那麼頻繁.
[audioSession setPreferredIOBufferDuration:bufferDuration error:nil];
PCM資料是1024個取樣一個包, 所以一般就用1024取樣點的時間, 所以這裡的值最大是1024/sampleRate(取樣率), 只能比這個小, 越小的buffer, 延遲就越低, 一般設定成 1024/sampleRate(取樣率)
就行了.
如果取樣率是44100, 就是1024/44100=0.023, 具體看取樣率.
取樣率哪來?FFmpeg讀音視訊檔案得到.FFmpeg給的.
具體體現函式(看裡面的註釋~)
/** 這就是我們給AudioUnit餵食的函式, 也就是AudioUnit的漏斗,你上buffer設定的越小呢AudioUnit呼叫這個函式的頻率就越高, 然後每次問你要的inNumberFrames個數就越少 , 多少的基礎標準就是"1024/sampleRate"的值,實際上最大可以是"1024/sampleRate*1.4", 最小嘛就是"1.0/sampleRate"就是1buffer大小, 知道就行,然後設定成"1024/sampleRate"就行了, 這都是毫秒級別的了,各種直播協議延遲能到1秒就燒香拜佛了.(就算直接TCP協議用socket寫,網差也會超過3秒4秒啥的,閒扯的~) AURenderCallbackStruct callbackStruct; callbackStruct.inputProc = &STInputRenderCallback; */ typedef OSStatus (*AURenderCallback)(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32inBusNumber, UInt32inNumberFrames, AudioBufferList * __nullableioData);
- 4.設定取樣率(這個沒啥好說的直接上程式碼)
[audioSession setPreferredSampleRate:sampleRate error:nil];
- 5.啟用AVAudioSession
[audioSession setActive:YES error:nil];
到這裡哈和 AudioUnit
API還沒半毛錢關係的哈, 但是再看一下這個圖

AudioUnitJG.png
AudioUnit
下面就是驅動和硬體了意思是它是跟硬體和驅動直接大交道的, 所以使用AudioUnit之前必須要建立一個會話管理獲取硬體相關資訊.
雖然沒有用到AudioUnit, 但是卻對其有很大的影響,程式碼不多就⑤步~
1.3.2 建立AudioUnit
實際上 AudioUnit
是一個大類名稱,看圖

damijiBufferlei.png
還是打米機哈,不好意思哈我真的覺得這傢伙和打米機好像([捂臉] 哈哈哈哈~), 如圖打米機有很多種型號,有的打米機不只有"打米"的功能還有將小麥加工成麵粉呢(並沒有真的見過那種機器~瞎扯的).
AudioUnit
也一樣,它分為五大類,每個大類下面又有具體子類.它不只是播放聲音這麼簡單(就好像打米機並不只是簡單的將稻穀去殼一樣, 有的大米比較白是打米機給他拋光了~AudioUnit有做錄音播放的, 有做混音的等等...)但他們統一叫 AudioUnit
, 我們這篇文章用到的是 I/O Units
這個大類下的RemoteIO和 Format Converter Units
大類下 AUConverter
I/O Units這個大類型別是`kAudioUnitType_Output` RemoteIO: 子類型別是`kAudioUnitSubType_RemoteIO` Format Converter Units這個大類型別是`kAudioUnitType_FormatConverter` AUConverter: 子類型別是`kAudioUnitSubType_AUConverter`
I/O嘛就是播放和錄音嘛,我們只用它的播放功能.還記得上面 [audioSession setCategory:AVAudioSessionCategoryPlayback];
這個沒,如果你還要錄音就得改一下
再看一下1.3開頭說的這句話
AudioUnit的使用一句話講解是這樣的: 首先使用 AVAudioSession
會話用來管理獲取硬體資訊, 然後利用一個描述結構體( AudioComponentDescription
)確定 AudioUnit
的型別,然後通過 AUNode
, AUGraph
拿到我們的 AudioUnit
, 然後設定 AudioUnit
的入口出口等資訊, 最後連線.
-
- 第一步就是拿到
AUGraph
,AUNode
- 第一步就是拿到
首先要說的是我們是通過 AUGraph
, AUNode
去換AudioUnit, AUNode
我們可以理解為他是AudioUnit的包裝類.
我們上面說了, AudioUnit是分很多種的, 我們要用到的是I/O 和 Format Converter Units, 後者是做格式轉換的, 因為我們用FFmpeg解碼出來的PCM是SInt16表示的, AudioUnit要的Float32,所以要格式轉換一下.所以要用到 Format Converter Units
入下面程式碼我們得到兩個 AUNode
, 也就是兩個 AudioUnit
SStatus status = noErr; status = NewAUGraph(&_auGraph); AudioComponentDescription ioDescription; bzero(&ioDescription, sizeof(ioDescription)); ioDescription.componentManufacturer = kAudioUnitManufacturer_Apple; ioDescription.componentType = kAudioUnitType_Output; ioDescription.componentSubType = kAudioUnitSubType_RemoteIO; status = AUGraphAddNode(_auGraph, &ioDescription, &_ioNNode); CheckStatus(status, @"AUGraphAddNode create error", YES); AudioComponentDescription converDescription; bzero(&converDescription, sizeof(converDescription)); converDescription.componentManufacturer = kAudioUnitManufacturer_Apple; converDescription.componentType = kAudioUnitType_FormatConverter; converDescription.componentSubType = kAudioUnitSubType_AUConverter; status = AUGraphAddNode(_auGraph, &converDescription, &_convertNote); CheckStatus(status, @"AUGraphAddNode _convertNote create error", YES);
構建AudioUnit的時候需要指定 型別(Type), 子型別(subtype), 以及廠商(Manufacture). 這裡體現在 AudioComponentDescription
設定上.
型別(Type)就是大類了,上面簡單介紹過的東西
子型別(subtype)就是該大型別下面的子型別
廠商(Manufacture)一般情況比較固定, 直接寫成 kAudioUnitManufacturer_Apple
- 2.獲取我們要的AudioUnit
上面我們的到了 AUNode
和 AUGraph
, 現在我們可以通過他們召喚出真正的 AudioUnit
了, 操作順序是先開啟 AUGraph
, 然後再召喚,順序不能變.
AudioUnit convertUnit; OSStatus status = noErr; status = NewAUGraph(&_auGraph); status = AUGraphAddNode(_auGraph, &ioDescription, &_convertUnit); // 開啟AUGraph, 其實開啟AUGraph的過程也是間接例項化AUGraph中所有的AUNode. //注意, 必須在獲取AudioUnit之前開啟整個AUGraph, 否則我們將不能從對應的AUNode中獲取正確的AudioUnit status = AUGraphOpen(_auGraph); status = AUGraphNodeInfo(_auGraph, _ioNNode, NULL, &_convertUnit); status = AUGraphNodeInfo(_auGraph, _ioNNode, NULL, &_ioUnit);
至此我們拿到 AudioUnit
.
實際上是一個不完整的AudioUnit,還有些零件沒裝好.就好像打米機的漏斗和出口都沒裝.
1.3.3 設定AudioUnit
再看一下1.3開頭說的這句話, 之所以重複就是要知道我們離目的地有多遠,當前在哪
AudioUnit的使用一句話講解是這樣的: 首先使用 AVAudioSession
會話用來管理獲取硬體資訊, 然後利用一個描述結構體( AudioComponentDescription
)確定 AudioUnit
的型別,然後通過 AUNode
, AUGraph
拿到我們的 AudioUnit
, 然後設定 AudioUnit
的入口出口等資訊, 最後連線.
再來看看看下面那個圖
沒錯這就是我們使用的 I/O Unit
原理圖, 我們用的是 I/O Unit
大類下的 RemoteIO
. I就是輸入端,O是輸出端. 輸入端一般是麥克風或者網路流, 輸出端是揚聲器或者耳機. 就好像打米機的漏斗或者大米出口, 到目前為止 漏斗
和 出口
兩個元件還沒有裝上的,我們得把他倆裝上.
如圖 RemoteIO Unit
分為 Element 0
和 Element 1
, 其中 Element 0
控制輸出端, Element 1
控制輸入端. 同時每個Element 又分為 Input Scope
和 Output Scope
. 看圖中APP和 Element 1, Element 0
的連線, 如果我們只是想播放聲音就將我們的APP與 Element 0
的 Input Scope
連線起來, 如果我們只是想要通過麥克風錄音我們就將我們的APP與 Element 1
的 Output Scope
連線起來, 所謂的"連線"程式碼裡的體現就是設定兩個回撥函式

audioIO.png
本文是幹嘛的, 就音訊播放. 所以 我們只是想播放聲音就將我們的APP與
Element 0 的
Input Scope 連線起來
, 連線之前我們要告訴等會傳輸給他的音訊資料的引數(告訴他是什麼樣的音訊)
有關AudioUnit的設定都是使用 AudioUnitSetProperty
函式
extern OSStatus AudioUnitSetProperty(AudioUnitinUnit, AudioUnitPropertyIDinID, AudioUnitScopeinScope, AudioUnitElementinElement, const void * __nullable inData, UInt32inDataSize) __OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0);
做連線之前我們得先告訴AudioUnit我們給它的音訊的相關引數.取樣率是多少,聲道多少,是什麼音訊資料等等引數..通過 AudioStreamBasicDescription
結構體設定:
AudioStreamBasicDescription _clientFormat16int; UInt32 bytesPersample = sizeof(SInt16); bzero(&_clientFormat16int, sizeof(_clientFormat16int)); _clientFormat16int.mFormatID = kAudioFormatLinearPCM; _clientFormat16int.mSampleRate = _sampleRate; _clientFormat16int.mChannelsPerFrame = _channels; _clientFormat16int.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger; _clientFormat16int.mFramesPerPacket = 1; _clientFormat16int.mBytesPerPacket = bytesPersample * _channels; _clientFormat16int.mBytesPerFrame = bytesPersample * _channels; _clientFormat16int.mBitsPerChannel = 8 * bytesPersample;
上面這段程式碼展示瞭如何填充 AudioStreamBasicDescription
結構體, 其實在iOS平臺做音視訊開發: 不論音訊還是視訊的API都會接觸到很多StreamBasic Description. 該Description就是用來描述音視訊具體格式的.
下面是上述程式碼的分析
-
bytesPersample
是取樣深度(取樣精度, 量化格式)
, 三個都是一個意思哈. -
mFormatID
引數可用來指定音訊的編碼格式. 此處指定音訊的編碼格式為PCM格式.什麼樣的音訊資料, 這裡我設定裸資料PCM
-
mSampleRate
取樣率 -
mChannelsPerFrame
每一幀裡面有多少聲道, 實際上就是問聲道數.
-
mFormatFlags
是用來描述聲音表示格式的引數, 程式碼中的引數kLinearPCMFormatFlagIsSignedInteger
指定每個Sample的表示格式是SInt16格式, ..
-
mFramesPerPacket
這個說的是每一幀裡面有多少個包. PCM資料是沒有壓縮過的裸資料, 所以是一幀一個包, 壓縮編碼後的資料例如AAC, 一幀資料對應1024個包. 所以這裡我們寫1以後我們如果餵給AudioUnit的不是裸資料PCM的話,如果是AAC就寫1024
AudioStreamBasicDescription audio_desc = { 0 }; audio_desc.mFormatID= kAudioFormatMPEG4AAC; audio_desc.mFormatFlags= kMPEG4Object_AAC_LC; audio_desc.mFramesPerPacket= 1024;
-
mBytesPerPacket
每一個包裡面有多少個位元組, 這裡就涉及到你是怎樣填資料的, 就拿雙聲道來說, 兩個聲道就是兩路兩個, 我們可以將兩路資料放到一個數組裡給AudioUnit(這就是交叉), 我們也可以分兩個陣列給AudioUnit, 到底怎麼給了實際是看mFormatFlags
,kLinearPCMFormatFlagIsSignedInteger
這樣不只是說PCM資料是用SInt16表示還有交叉的PCM的意思. 那誰有是非交叉了? 這裡先不說...那具體是影響到哪裡了,答:是影響到AudioUnit問我們要資料的那個回撥函式.的AudioBufferList * __nullable ioData) (你可以理解這傢伙就是打米機的填稻穀那個漏斗)
, 實際上我們為了方便資料填入, 不管是播放聲音也錄音也會, 都是用的交叉(因為方便....) 所以就是bytesPersample * _channels;
-
mBytesPerFrame
每一幀有多少個位元組, 因為這裡是一幀一包, 所以就也是bytesPersample * _channels;
- mBitsPerChannel 表示的是
一個聲道的
音訊資料用多少位來表示, 前面已經提到過每個取樣使用SInt16來表示, 所以這裡是使用8乘以每個取樣的位元組數
來賦值
*** 描述結構體弄完了下一步我們就來設定Element 0的Input Scope***
status = AudioUnitSetProperty( _convertUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &_clientFormat16int, sizeof(_clientFormat16int));
-
_convertUnit
我們拿到的AudioUnit -
kAudioUnitProperty_StreamFormat
說的是本次呼叫AudioUnitSetProperty
函式時做連線, 然後告訴AudioUnit連線的資料流.AudioUnitSetProperty
函式可以做很多事情的具體什麼事情就看第二引數的值是什麼了 -
kAudioUnitScope_Input
就是上面說的Input Scope
-
0
就是Element 0
了 -
_clientFormat16int
就是描述了
前面說了,我們需要兩個 AudioUnit
一個"I/O"的一個"convert"的, 並且也已經拿到了, 也設定好"convert", 下面就可以做連線.
OSStatus status = noErr; status = AUGraphConnectNodeInput(_auGraph, _convertNote, 0, _ioNNode, 0); CheckStatus(status, @"Could not connect I/O node input to mixer node input", YES); AURenderCallbackStruct callbackStruct; callbackStruct.inputProc = &STInputRenderCallback; callbackStruct.inputProcRefCon = (__bridge void *)self; status = AudioUnitSetProperty(_convertUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct)); CheckStatus(status, @"Could not set render callback on mixer input scope, element 1", YES);
"I/O"才有輸入功能, 但是資料需要轉換所以先連線 _convertNote和_ioNNode. AUGraphConnectNodeInput(_auGraph, _convertNote, 0, _ioNNode, 0);
然後就是最重要的一步回撥函式的設定.,前面一系列操作相當於是製作打米機的漏斗,現在我們就要將漏斗裝上. &STInputRenderCallback;
這個回撥函式就是真正的AudioUnit的"漏斗", AudioUnit會按照我們設定的時間不斷的呼叫此回撥函式向我們索要音訊資料, 函式如下
static OSStatus STInputRenderCallback(void * inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32inBusNumber, UInt32inNumberFrames, AudioBufferList * __nullableioData) { NSLog(@"====> inBusNumber:%uinNumberFrames:%u", (unsigned int)inBusNumber, inNumberFrames); ST_AudioOutput *audioOutput = (__bridge id)inRefCon; return [audioOutput renderData:ioData atTimeStamp:inTimeStamp forElement:inBusNumber numberFrames:inNumberFrames flags:ioActionFlags]; }
這個函式不是亂寫的, 我們點選結構體 AURenderCallbackStruct
的 inputProc
可以看到函式原型如下. 我們要做的是實現該函式, 函式名由我們自己定義.我講函式名定義為 STInputRenderCallback
你們也可以隨意定義改函式名, 函式體是固定的.
typedef OSStatus (*AURenderCallback)(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32inBusNumber, UInt32inNumberFrames, AudioBufferList * __nullableioData);
連線完成後進行最後一步的操作, 啟動
,讓AudioUnit跑起來
CAShow(_auGraph); status = AUGraphInitialize(_auGraph); CheckStatus(status, @"Could not initialize AUGraph", YES);
執行完上面一步後AudioUnit就會不斷的呼叫回撥函式, 我們要做的就是不斷的給它音訊資料
至此有關AudioUnit操作相關原理就說完了.
實際上還有AudioUnit的分類沒有說可以看我這篇文章 AudioUnit的分類
2.FFmpeg操作
首先預設你們已經按照我的這篇文章 ===>編譯iOS能用的FFmpeg靜態庫 做好了靜態庫,工程相關配置也是按照文章做好了的哈..
然後再回顧一下 =====>音視訊基礎知識 , 如下圖 封裝格式
===> 編碼資料
===> 原始資料
, 我們用FFmpeg做解碼也都是按照這個順序使用它的相關資料結構和相關函式來的. 下面1-3小節是相關介紹
2.1 FFmpeg資料結構簡介
- AVFormatContext
封裝格式上下文結構體,也是統領全域性的結構體,儲存了視訊檔案封裝 格式相關資訊。
- AVInputFormat
每種封裝格式(例如FLV, MKV, MP4, AVI)對應一個該結構體。
-
AVStream
視訊檔案中每個視訊(音訊)流對應一個該結構體。
-
AVCodecContext
編碼器上下文結構體,儲存了視訊(音訊)編解碼相關資訊。
-
AVCodec
每種視訊(音訊)編解碼器(例如H.264解碼器)對應一個該結構體。
-
AVPacket
儲存一幀壓縮編碼資料。
-
AVFrame
儲存一幀解碼後像素(取樣)資料
2.2 FFmpeg解碼的資料結構

ffmpegDecoder01.png
2.3 FFmpeg解碼的流程

ffmpegPlayAudio.png
2.4 API部分說明
FFmpeg其他的功能先不說, 再看看本文的標題. 是的我這篇文章是用它來搞音訊的, 解碼音訊的. 我們這篇文章是播放一個檔案(說這句話是相對於網路流來說),
然後請再看一遍這個圖, AudioUnit
要的是 音訊取樣資料PCM
, 我們現在有的是什麼? 是一個mp4檔案或者一個mp3檔案, 是檔案! FFmpeg
我們用它幹嘛? 我們用它扣出PCM資料,然後餵給 AudioUnit
. 說到頭就是解碼. 解碼就是用的解碼流程裡的 avcodec_decode_audio4
扣PCM餵給 AudioUnit
, 到底怎麼扣?
還是前面那個套路哈, 一句話簡單說就是: 不管用 FFmpeg
解碼音訊也會視訊也好,第一步都是先註冊, 第二步就是去拿封裝格式上下文 AVFormatContext
, 第三部用 AVFormatContext
換 AVStream
,拿到流後第四部用它換解碼器上下文 AVCodecContext
, 然後第五步我們就要用解碼器上下文去讀取編碼資料 AVPacket
, 最後第七步我們解碼編碼資料通過 avcodec_decode_audio4
函式換取PCM裸資料 AVFrame
- AVFormatContext封裝格式講解
關於封裝格式的話, 先看程式碼吧
_avFormatContext = avformat_alloc_context(); int result = avformat_open_input(&_avFormatContext, [audioFileStr UTF8String], NULL, NULL); intresult = avformat_find_stream_info(_avFormatContext, NULL);
其實這塊都不用怎麼解釋我們相信大家都能看懂
-
AVStream音訊流講解
先看上面的
封裝格式
那個圖.封裝格式
由音訊編碼資料和視訊編碼資料組成(有的還有字幕資料), 我從網上下來部星爺
的賭聖
, mkv格式的電影, 然後使用ffmpeg命令ffprobe -show_format /Users/codew/Desktop/賭聖.mkv
看看封裝格式的組成. 它由7部分組成, 視訊編碼資料一個Video: h264 (High)
, 有兩個音訊編碼資料都是Audio: aac (HE-AACv2)
, 然後四個字幕資料Subtitle: subrip
如圖

ffmpegDu.png
這些視訊呀,音訊呀,字幕呀在FFmpeg資料結構裡面就是我們說的 AVStream
, 看見上圖中 Stream #0:0(chi)
, Stream #0:1(chi)
等等了嗎? 這些流是有序號的. 我們要用這個些流我們得找到流對應的序號就像下面這樣
_stream_index = av_find_best_stream(_avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
我們通過上面的程式碼拿到了序號, 我們就可以通過序號去拿音訊流資料了, 這裡是拿音訊序號因為本文是研究音訊播放的,所以 Demo 裡也只會出現如上拿音訊的API視訊呀字幕呀本文不會介紹. 下面是通過序號拿音訊流
AVStream *audioStream = _avFormatContext->streams[_stream_index];
- AVCodecContext解碼器上下文
我們要拿流資料 AVStream
是用來換取解碼器和解碼器上下文的.為什麼有了解碼器還要什麼解碼器上下文?因為我們後面解碼用到的函式 avcodec_decode_audio4
要傳的是上下文,第二解碼器上下文裡面包含了解碼器
// 獲得音訊流的解碼器上下文 _avCodecContext = audioStream->codec; // 根據解碼器上下文找到解碼器 AVCodec *avCodec = avcodec_find_decoder(_avCodecContext->codec_id); // 開啟解碼器 result = avcodec_open2(_avCodecContext, avCodec, NULL);
3. 工程Demo大概講解哈

Demo工程.png
AudioUnit主要邏輯在 ST_AudioOutput
裡面, AVAudioSession使用 ST_AudioSession
這個封裝類
FFmpeg使用在 STFFmpegLocalAudioDecoder
然後用到了生產模式消費模式搞了一個執行緒不間斷的生產資料,然後放到佇列中,系統快消耗完了就去補貨,具體體現在 STMediaCache
和 STLinkedBlockingQueue
後
實際上重要的先看懂上面的流程圖比較總要, FFmpeg API的使用實際上套路都差不多, 註冊找上下文找流找解碼器解碼....我個人覺得FFmpeg按文理來說我覺得它屬於文科.....那有人問了"我最開始應該怎麼學?"我的覺得哈買本我不是跟誰誰打廣告哈, 書是比較系統性的網上的多的是之言片語少了從頭到尾,我這篇也是.第二是FFmpeg原始碼裡的 examples
, 就好像ffmpeg-3.4.2原始碼裡 examples
的位置是 /ffmpeg-3.4.2/doc/examples
, 想學哪個學哪個差不多的功能都有了, 然後就是網上的各種部落格了.
我這篇文章是看了<<FFmpeg從入門到精通>>和<<音視訊開發進階指南>>還有雷霄驊博士部落格,當然也閱讀了些部落格, 實際上我這篇文章的Demo也是改寫了<<音視訊開發進階指南>>書裡的例子, 因為只是Demo嘛多少還有些問題, 希望能幫助你吧. 如果覺得還行記得給我點個贊,表揚我一下,啊哈哈哈哈哈~然後我將大白話iOS音視訊繼續扯下去?