上一篇我們侃侃而談了下Android下的App音視頻開(kāi)發(fā)雜談,我們從入手到深入再到實(shí)際項(xiàng)目的遇到的問(wèn)題以及解決方案都聊了下,那么這一次我們來(lái)雜談下IOS項(xiàng)目中音視頻的內(nèi)容,這篇內(nèi)容主要是對(duì)比上篇Android的內(nèi)容,為的是熟悉IOS的朋友方便閱讀觀看,讓我們開(kāi)始吧:
首先需要了解的是音視頻處理的流程:
- 數(shù)據(jù)分別經(jīng)歷了解協(xié)議,解封裝,音/視頻解碼,播放步驟,再次請(qǐng)上這張圖:

其次是了解音頻PCM的數(shù)據(jù)是怎么來(lái)的包括:
- 怎么采樣采樣率是什么(8kHZ,44.1kHZ),
- 單/雙通道,
- 樣本怎么存儲(chǔ)(8bit/16bit),
- 一幀音頻為多少樣本(通常是按1024個(gè)采樣點(diǎn)一幀,每幀采樣間隔為23.22ms)
- 每幀PCM數(shù)據(jù)大小:(PCM Buffersize=采樣率采樣時(shí)間采樣位深/8*通道數(shù)(Bytes))
- 每秒的PCM數(shù)據(jù)大?。海ú蓸勇省敛蓸游簧?8×聲道數(shù)bps)
了解視頻YUV數(shù)據(jù)是怎么來(lái)的包括:
- YUV數(shù)據(jù)的幾種格式(YUV420P,YUV420SP,NV12,NV21)的排布是怎么樣的
- 怎么計(jì)算例如YUV420P的大小
- 怎么分解明亮度與色度
既然是音視頻肯定要涉及壓縮編碼,那么首先應(yīng)該要了解:
國(guó)際標(biāo)準(zhǔn)化組織(ISO)的MPEG-1、MPEG-2與MPEG-4,的規(guī)范和標(biāo)準(zhǔn)是哪些
其次要了解這個(gè)這個(gè)主流標(biāo)準(zhǔn)里面MPEG-4的音頻/視頻具體的一種編碼格式,一般來(lái)說(shuō)是AAC(MP3)與H264
AAC編碼格式數(shù)據(jù):要了解AAC編碼的ADTS frame與ADTS頭是怎么樣子的
H264編碼格式數(shù)據(jù):要了解H264的編碼格式一般主流是兩種AVCC(IOS默認(rèn)硬編碼),Annex-B(Android默認(rèn)硬編碼)
Annex-B格式里面每個(gè)NALU的格式:包含頭與payload是什么樣的
AVCC里面extradata里面的數(shù)據(jù)格式是怎么樣的(包含SPS,PPS在里面)
H264里面的SPS,PPS,I幀,P幀,B幀所表示的意義
說(shuō)了編碼當(dāng)然要有解碼:
- IOS里面音頻的硬解(VideoToolbox),軟解(ffpmeg)怎么實(shí)現(xiàn)
- IOS里面視頻的硬解(AudioToolbox),軟解(ffmpeg)怎么實(shí)現(xiàn);
解碼以后怎么播放,音頻播放:
- IOS :(包括不限于:AudioUnit ,OpenAL);
- 播放中音頻重采樣(播放環(huán)境如果與樣本環(huán)境不兼容則需要重采樣);
解碼后視頻播放:
- IOS:(包括不限于:CMSampleBuffer ,OpenGLES);
- IOS平臺(tái) EAGL的使用
其中OpenGLES 特別是可以作為一個(gè)分支來(lái)進(jìn)行加強(qiáng):
- 物體坐標(biāo)系:是指繪制物體的坐標(biāo)系。
- 世界坐標(biāo)系:是指擺放物體的坐標(biāo)系。
- 攝像機(jī)坐標(biāo)系:攝像機(jī)的在三維空間的位置,攝像機(jī)lookat的方向向量,攝像機(jī)的up方向向量
- 簡(jiǎn)單的繪制一些基本圖形:三角形,正方形,球形
- 紋理坐標(biāo):紋理貼圖的方向以及大小
兩種投影:正射投影,透視投影 - 著色器語(yǔ)言GLSL的基本語(yǔ)法以及使用
- 紋理貼圖顯示圖片
- 處理平移、旋轉(zhuǎn)、縮放等一些3x3 ,4X4的基本矩陣運(yùn)算
- FBO離屏渲染
什么是封包:
- 然后是數(shù)據(jù)封包格式:包括MP4,TS的格式大致是什么樣子的,支持哪幾種音視頻的編碼格式;
- DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)代表的意義;
- TimeBase時(shí)間基在做音視頻同步的意義;
音視頻流媒體在網(wǎng)絡(luò)上怎么傳輸:
- 音視頻在網(wǎng)絡(luò)傳輸方式:HTTP,HLS,RTMP,HttpFlv
音視頻應(yīng)用層框架有哪些:
- 高級(jí)應(yīng)用框架:ffmpeg的基本使用
- 高級(jí)應(yīng)用框架:OpenCV的基本使用
額外需要掌握哪些技能:
- C/C++ 基礎(chǔ);(話(huà)說(shuō)搞OC的工程師應(yīng)該都對(duì)于C有很好的理解才對(duì))
以上是我認(rèn)為作為音視頻工程師入門(mén)應(yīng)該掌握的知識(shí)點(diǎn),我覺(jué)得掌握了這些不敢說(shuō)成為了一個(gè)高手,但應(yīng)該是成為一個(gè)合格的音視頻工程師的 基本功
PS:基本功重要嗎?我認(rèn)為非常重要,往小了說(shuō)基本功顯示了一個(gè)人的技能扎實(shí),擁有了扎實(shí)的基礎(chǔ)才能往更深的方向發(fā)展;往大了說(shuō)基本功顯示了一個(gè)人可靠,處事沉穩(wěn)可以做到了解一個(gè)事物的本質(zhì)能做到萬(wàn)變不離其中
有了這些基本功那么我們可以接觸一些實(shí)際的案例了,如果你想要更進(jìn)階那么我推薦一本我認(rèn)為音視頻內(nèi)容比較全,而且里面有很多實(shí)戰(zhàn)例子作為參考的書(shū),??再次請(qǐng)出這本書(shū):

這本書(shū)我認(rèn)為有幾點(diǎn)比較好的:
第一是這本書(shū)出于實(shí)戰(zhàn)出發(fā)(據(jù)說(shuō)是 唱吧App 架構(gòu)師在做唱吧的時(shí)候總結(jié)了很多經(jīng)驗(yàn)寫(xiě)的),
第二這本書(shū)的內(nèi)容包含了Android,IOS兩個(gè)版本的所以有對(duì)比參考性,第三這本書(shū)從基礎(chǔ)的音視頻到高級(jí)的應(yīng)用場(chǎng)景都介紹了,可謂是內(nèi)容豐富;
說(shuō)了這么多好的再說(shuō)說(shuō)這本書(shū)的一些不好的地方:
首先就是我認(rèn)為這本書(shū)不太適合剛剛?cè)腴T(mén)的新手(注意是剛剛?cè)腴T(mén))如果是這類(lèi)的工程師一些概念都沒(méi)搞清楚的就看這個(gè)其實(shí)不是很合適;
其次就是里面的例子的代碼段過(guò)于松散,閱讀起來(lái)需要不是很順暢,而且git里面的Demo感覺(jué)也跟不上書(shū)里面的代碼,里面的Demo目錄結(jié)構(gòu)不是很清晰(一般來(lái)說(shuō)我們見(jiàn)得多的是1章分為一個(gè)或多個(gè)項(xiàng)目,分別講解對(duì)應(yīng)的內(nèi)容互相不會(huì)干擾,書(shū)里面是git commit來(lái)區(qū)分的感覺(jué)體驗(yàn)性不是很好)
但是瑕不掩瑜如果你是有基礎(chǔ)的話(huà),那么這本書(shū)肯定能給你帶了項(xiàng)目中的幫助。
好了,介紹了這么多基礎(chǔ)我們馬上進(jìn)入項(xiàng)目中去看看,IOS音視頻的項(xiàng)目問(wèn)題以及解決方案
我們要實(shí)現(xiàn)的功能:
- App音視頻的數(shù)據(jù)怎么傳輸
- App實(shí)現(xiàn)音視頻解碼
- App實(shí)現(xiàn)音視頻播放
- App實(shí)現(xiàn)截圖拍照
- App實(shí)現(xiàn)錄制視頻
- App實(shí)現(xiàn)音視頻同步
App音視頻的數(shù)據(jù)怎么傳輸:
- App這邊與嵌入式定好傳輸協(xié)議,協(xié)議數(shù)據(jù)大致分為協(xié)議頭,協(xié)議體,協(xié)議頭:包括同步碼字段,幀類(lèi)型,數(shù)據(jù)長(zhǎng)度,數(shù)據(jù)方向,時(shí)間戳等等拿到數(shù)據(jù)頭以后
就可以按照長(zhǎng)度拿到協(xié)議體數(shù)據(jù)就可以開(kāi)始解碼了
typedef struct
{
HLE_U8 sync_code[3]; /*幀頭同步碼,固定為0x00,0x00,0x01*/
HLE_U8 type; /*幀類(lèi)型, */
HLE_U8 enc_std; //編碼標(biāo)準(zhǔn),0:H264 ; 1:H265
HLE_U8 framerate; //幀率(僅I幀有效)
HLE_U16 reserved; //保留位
HLE_U16 pic_width; //圖片寬(僅I幀有效)
HLE_U16 pic_height; //圖片高(僅I幀有效)
HLE_SYS_TIME rtc_time; //當(dāng)前幀時(shí)間戳,精確到秒,非關(guān)鍵幀時(shí)間戳需根據(jù)幀率來(lái)計(jì)算(僅I幀有效)8字節(jié)
HLE_U32 length; //幀數(shù)據(jù)長(zhǎng)度
HLE_U64 pts_msec; //毫秒級(jí)時(shí)間戳,一直累加,溢出后自動(dòng)回繞
} P2P_FRAME_HDR; //32字節(jié)
App實(shí)現(xiàn)實(shí)時(shí)音視頻解碼:
硬件碼優(yōu)勢(shì):更加省電,適合長(zhǎng)時(shí)間的移動(dòng)端視頻播放器和直播,手機(jī)電池有限的情況下,使用硬件解碼會(huì)更加好。減少CPU的占用,可以把CUP讓給別的線(xiàn)程使用,有利于手機(jī)的流暢度。
軟解碼優(yōu)勢(shì):具有更好的適應(yīng)性,軟件解碼主要是會(huì)占用CUP的運(yùn)行,軟解不考慮社備的硬件解碼支持情況,有CPU就可以使用了,但是占用了更多的CUP那就意味著很耗費(fèi)性能,很耗電,在設(shè)備電量充足的情況下,或者設(shè)備硬件解碼支持不足的情況下使用軟件解碼更加好!
- IOS音頻的硬解碼:IOS的硬解碼比Android的硬解碼要好上太多了,IOS從8.0就開(kāi)始加入了 AudioToolBox 與 VideoToolbox 來(lái)進(jìn)行音視頻的硬編解碼,目前Iphone手機(jī)基本上都是8.0了,而且Iphone4S以上都支持硬解碼所以兼容性肯定沒(méi)的說(shuō)(封閉也有封閉的好處,標(biāo)準(zhǔn)全部統(tǒng)一,對(duì)于開(kāi)發(fā)來(lái)說(shuō)就簡(jiǎn)單),而且SDK的使用其實(shí)也很簡(jiǎn)單,我們先來(lái)聊聊音頻的硬解碼 AudioToolBox 的使用,主要是這個(gè)方法:
AudioConverterFillComplexBuffer( AudioConverterRef inAudioConverter,
AudioConverterComplexInputDataProc inInputDataProc,
void * __nullable inInputDataProcUserData,
UInt32 * ioOutputDataPacketSize,
AudioBufferList * outOutputData,
AudioStreamPacketDescription * __nullable outPacketDescription)
inAudioConverter : 轉(zhuǎn)碼器
inInputDataProc : 回調(diào)函數(shù)。用于將AAC數(shù)據(jù)喂給解碼器。
inInputDataProcUserData : 用戶(hù)自定義數(shù)據(jù)指針。
ioOutputDataPacketSize : 輸出數(shù)據(jù)包大小。
outOutputData : 輸出數(shù)據(jù) AudioBufferList 指針。
outPacketDescription : 輸出包描述符。
解碼的具體步驟如下:首先,從媒體文件中取出一個(gè)音視幀。其次,設(shè)置輸出地址。然后,調(diào)用 AudioConverterFillComplexBuffer 方法,該方法又會(huì)調(diào)用 inInputDataProc 回調(diào)函數(shù),將輸入數(shù)據(jù)拷貝到編碼器中。最后,解碼。將解碼后的數(shù)據(jù)輸出到指定的輸出變量中。
- IOS視頻的硬解碼:剛剛聊了音頻的硬解碼是用 AudioToolBox ,下面到視頻的硬解碼實(shí)現(xiàn),下面請(qǐng)出 VideoToolbox ,首先創(chuàng)建解碼器:
VTDecompressionSessionCreate(
CM_NULLABLE CFAllocatorRef allocator,
CM_NONNULL CMVideoFormatDescriptionRef videoFormatDescription,
CM_NULLABLE CFDictionaryRef videoDecoderSpecification,
CM_NULLABLE CFDictionaryRef destinationImageBufferAttributes,
const VTDecompressionOutputCallbackRecord * CM_NULLABLE outputCallback,
CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTDecompressionSessionRef * CM_NONNULL decompressionSessionOut)
各參數(shù)詳細(xì)介紹:
allocator : session分配器,NULL使用默認(rèn)分配器。
videoFormatDescription : 源視頻幀格式描述信息。
videoDecoderSpecification : 視頻解碼器。如果是NULL表式讓 VideoToolbox自己選擇視頻解碼器。
destinationImageBufferAttributes: 像素緩沖區(qū)要求的屬性。
outputCallback: 解碼后的回調(diào)函數(shù)。
decompressionSessionOut: 輸出Session實(shí)列。
然后開(kāi)始解碼:
VT_EXPORT OSStatus
VTDecompressionSessionDecodeFrame(
CM_NONNULL VTDecompressionSessionRef session,
CM_NONNULL CMSampleBufferRef sampleBuffer,
VTDecodeFrameFlags decodeFlags, // bit 0 is enableAsynchronousDecompression
void * CM_NULLABLE sourceFrameRefCon,
VTDecodeInfoFlags * CM_NULLABLE infoFlagsOut) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));
session : 創(chuàng)建解碼器時(shí)創(chuàng)建的 Session。
sampleBuffer : 準(zhǔn)備被解碼的視頻幀。
decodeFlags : 解碼標(biāo)志符。 0:代表異步解碼。
sourceFrameRefCon : 用戶(hù)自定義參數(shù)。(輸出解碼數(shù)據(jù))
infoFlagsOut : 輸出參數(shù)標(biāo)記。
需要注意的是,如果你的硬解碼出來(lái)的數(shù)據(jù)是要轉(zhuǎn)換為 UIImage 貼圖顯示的話(huà)那么在配置解碼器的時(shí)候要注意配置參數(shù):
// kCVPixelFormatType_420YpCbCr8Planar is YUV420
// kCVPixelFormatType_420YpCbCr8BiPlanarFullRange is NV12
// kCVPixelFormatType_24RGB //使用24位bitsPerPixel
// kCVPixelFormatType_32BGRA //使用32位bitsPerPixel,kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst
uint32_t pixelFormatType = kCVPixelFormatType_32BGRA;
const void *keys[] = { kCVPixelBufferPixelFormatTypeKey };
const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &pixelFormatType) };
CFDictionaryRef attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = didDecompress;
callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
status = VTDecompressionSessionCreate(kCFAllocatorDefault,
mDecoderFormatDescription,
NULL,
attrs,
&callBackRecord,
&mDeocderSession);
我是利用 kCVPixelFormatType_32BGRA 與 kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst 來(lái)配合出圖的(下面視頻播放的時(shí)候我會(huì)再提到這種方式)
- IOS音視頻的軟解碼:
軟解碼首推的就是ffmpeg,ffmpeg的使用還是很簡(jiǎn)單的,簡(jiǎn)單的來(lái)說(shuō)你只需要一開(kāi)始初始化 編解碼格式對(duì)象 AVCodecContext 與編解碼器 AVCodec ,然后把數(shù)據(jù)填充AvPacket ,然后解碼成 AvFrame 就可以了。
App實(shí)現(xiàn)音頻的播放:
- 音頻的重采樣:有時(shí)候在音頻播放的時(shí)候,會(huì)出現(xiàn)你的音源與播放設(shè)備的硬件條件不匹配,例如播放每幀的樣本數(shù)不匹配,采樣位數(shù)不匹配的情況,那么這個(gè)時(shí)候需要用到對(duì)于音源PCM重采樣,重采樣以后才能正常播放,
int len = swr_convert(actx,outArr,frame->nb_samples,(const uint8_t **)frame->data,frame->nb_samples);
主要是通過(guò) swr_convert 來(lái)進(jìn)行轉(zhuǎn)換
/** Convert audio.
*
* in and in_count can be set to 0 to flush the last few samples out at the
* end.
*
* If more input is provided than output space, then the input will be buffered.
* You can avoid this buffering by using swr_get_out_samples() to retrieve an
* upper bound on the required number of output samples for the given number of
* input samples. Conversion will run directly without copying whenever possible.
*
* @param s allocated Swr context, with parameters set
* @param out output buffers, only the first one need be set in case of packed audio
* @param out_count amount of space available for output in samples per channel
* @param in input buffers, only the first one need to be set in case of packed audio
* @param in_count number of input samples available in one channel
*
* @return number of samples output per channel, negative value on error
*/
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count,
const uint8_t **in , int in_count);
out表示的是輸出buffer的指針;
out_count表示的是輸出的樣本大小;
in表示的輸入buffer的指針;
in_count表示的是輸入樣品的大??;
轉(zhuǎn)換成功后輸出的音頻數(shù)據(jù)再拿來(lái)播放就可以在指定的條件進(jìn)行指定的播放
- 音頻軟解碼的播放:這種情況下一般我們推薦的還是利用 OpenSLES 來(lái)播放
//設(shè)置回調(diào)函數(shù),播放隊(duì)列空調(diào)用
(*pcmQue)->RegisterCallback(pcmQue,PcmCall,this);
//設(shè)置為播放狀態(tài)
(*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);
//啟動(dòng)隊(duì)列回調(diào)
(*pcmQue)->Enqueue(pcmQue,"",1);
- 音頻的硬解碼播放:這種情況下播放使用SDK自帶的 AudioUnit 來(lái)進(jìn)行播放,首先創(chuàng)建對(duì)象:
// 獲得 Audio Unit
status = AudioComponentInstanceNew(inputComponent, &audioUnit);
然后配置屬性:
// 為播放打開(kāi) IO
status = AudioUnitSetProperty(audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
kOutputBus,
&flag,
sizeof(flag));
checkStatus(status);
// 設(shè)置播放格式
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
kOutputBus,
& outputFormat, //參見(jiàn)編碼器格式
sizeof(audioFormat));
checkStatus(status);
// 設(shè)置聲音輸出回調(diào)函數(shù)。當(dāng)speaker需要數(shù)據(jù)時(shí)就會(huì)調(diào)用回調(diào)函數(shù)去獲取數(shù)據(jù)。它是 "拉" 數(shù)據(jù)的概念。
callbackStruct.inputProc = playbackCallback;
callbackStruct.inputProcRefCon = self;
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
kOutputBus,
&callbackStruct,
sizeof(callbackStruct));
然后播放PCM:
AudioOutputUnitStart(audioUnit);
App 視頻的播放:
- 視頻軟解播放:這個(gè)當(dāng)然是首先 opengles ,拿到Y(jié)UV數(shù)據(jù),設(shè)置好貼圖坐標(biāo),使用YUV數(shù)據(jù)分別貼圖來(lái)播放顯示,例子如下:
sh.GetTexture(0,width,height,data[0]); // Y
if(type == XTEXTURE_YUV420P)
{
sh.GetTexture(1,width/2,height/2,data[1]); // U
sh.GetTexture(2,width/2,height/2,data[2]); // V
}
else
{
sh.GetTexture(1,width/2,height/2,data[1], true); // UV
}
sh.Draw();
- 視頻硬解的播放:這個(gè)方式非常直接,利用SDK硬解碼出來(lái)的數(shù)據(jù) CVPixelBufferRef 轉(zhuǎn)換為 UIImage,這種方式看似簡(jiǎn)單但是坑也最多,我總結(jié)了以下幾總轉(zhuǎn)換的方式以及測(cè)試結(jié)果,??敲黑板了注意聽(tīng)講:
我的測(cè)試手機(jī)為兩部一部IphoneX,一部為Iphone5S(一部高端的一部低端的), didDecompress 方法是硬解碼的回調(diào)函數(shù),這個(gè)不解釋了
- 第一種是:
- static void didDecompress(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration )
{
VDh264Decoder *delegateSelf = (__bridge VDh264Decoder *)decompressionOutputRefCon;
if (pixelBuffer==nil) {
return;
}
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
}
CVImageBufferRef imageBuffer = pixelBuffer;
CVPixelBufferLockBaseAddress(imageBuffer, 0);
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t bufferSize = CVPixelBufferGetDataSize(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, baseAddress, bufferSize, NULL);
CGImageRef cgImage= CGImageCreate(width, height, 8,32, bytesPerRow, rgbColorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little, provider, NULL, false, kCGRenderingIntentDefault);
UIImage * image = [UIImage imageWithCGImage:cgImage];
if (delegateSelf.delegate && [delegateSelf.delegate respondsToSelector:@selector(decoderSuccessGetImg:saveImg:)]) {
[delegateSelf.delegate decoderSuccessGetImg:nil saveImg:image];
}
CGImageRelease(cgImage);
CGDataProviderRelease(provider);
CGColorSpaceRelease(rgbColorSpace);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
CVPixelBufferRelease(imageBuffer);
這種方式理論上說(shuō)不能正常運(yùn)行,我調(diào)試了很久原因就在 CVPixelBufferRef 這個(gè)對(duì)象的釋放問(wèn)題,因?yàn)橐婚_(kāi)始就對(duì)他進(jìn)行了Retain(CVPixelBufferRef 是C對(duì)象不是OC對(duì)象所以沒(méi)有辦法進(jìn)行ARC,需要手動(dòng)的Retain,Release)
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
但是你最后這句releases會(huì)引發(fā)空指針問(wèn)題,
CVPixelBufferRelease(imageBuffer);
究其原因我猜想是由于,生成的 UIImage 正在使用,雖然你在他后面才進(jìn)行了release,但是這種還是會(huì)影響他這塊內(nèi)存所以會(huì)有空指針問(wèn)題(網(wǎng)絡(luò)上基本上搜不到答案,我的結(jié)論是我自己測(cè)試出來(lái)的,聽(tīng)我往下講)
于是我把前面的retain ,release 去掉:也就是這三句話(huà)去掉
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon; //去掉
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer); //去掉
CVPixelBufferRelease(imageBuffer); //去掉
很悲劇的這種方式直接空指針報(bào)錯(cuò),根據(jù)調(diào)試開(kāi)看應(yīng)該是 CVPixelBuffer 被提前釋放了,所以你生成的 UIImage 沒(méi)法在主線(xiàn)程使用
那把末尾的release去掉呢,
CVPixelBufferRelease(imageBuffer); //去掉
這種情況會(huì)出圖但是,你會(huì)發(fā)現(xiàn)你的內(nèi)存在暴漲,因?yàn)檫@個(gè) CVPixelBuffer 這個(gè)對(duì)象沒(méi)有手動(dòng)釋放,(也說(shuō)明了這種生成圖片的方式,對(duì)于 CVPixelBuffer 的釋放不太好處理至少SDK沒(méi)有什么好的辦法),我甚至想了個(gè)辦法把這個(gè) CVPixelBuffer 對(duì)象轉(zhuǎn)為OC對(duì)象想用ARC來(lái)管理它還是不行,這種生成圖片的方式Pass掉
- 第二種最簡(jiǎn)單,也是網(wǎng)絡(luò)上經(jīng)??匆?jiàn)的方法:
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
UIImage *image = [UIImage imageWithCIImage:ciImage];
簡(jiǎn)單歸簡(jiǎn)單,但是這種方式太耗內(nèi)存了,不是說(shuō)內(nèi)存一直漲,而是固定就很高,尤其是IphoneX上面非常明顯,為了性能著想不可取(其實(shí)也沒(méi)到使用不了的地步,只不過(guò)是想要最優(yōu)的方案,才有了下面的嘗試)
- 第三總在第二總的方式上面做了些許改動(dòng):
CIContext *context = [CIContext contextWithOptions:nil];
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
UIImage *image = [[UIImage alloc] initWithCGImage:cgImage];
CGImageRelease(cgImage); //沒(méi)有此句話(huà)無(wú)法釋放內(nèi)存
這種方式內(nèi)存沒(méi)有那么夸張了,但是CPU使用卻上來(lái)了,而且上升很明顯,Iphone快達(dá)到了50%,Iphone5S已經(jīng)接近90%,也不可取
- 最后一種穩(wěn)定的方式:
CVImageBufferRef imageBuffer = pixelBuffer;
CVPixelBufferLockBaseAddress(imageBuffer, 0);
uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer);
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef cgContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, rgbColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
CGImageRef cgImage = CGBitmapContextCreateImage(cgContext);
UIImage *image = [UIImage imageWithCGImage:cgImage];
if (delegateSelf.delegate && [delegateSelf.delegate respondsToSelector:@selector(decoderSuccessGetImg:saveImg:)]) {
[delegateSelf.delegate decoderSuccessGetImg:nil saveImg:image];
}
CGImageRelease(cgImage);
CGContextRelease(cgContext);
CGColorSpaceRelease(rgbColorSpace);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
這種方式不需要手動(dòng)retain,release CVPixelBuffer 了,而且使用 CGContextRef 代替了 CGDataProviderRef 去生成 CGImageRef ,經(jīng)過(guò)長(zhǎng)時(shí)間測(cè)試這種方式CPU與內(nèi)存都是穩(wěn)定輸出
經(jīng)過(guò)測(cè)試與觀察這種方式其實(shí)效率看起來(lái)并不低,Iphone5S都能正常的播放,而且參照了同類(lèi)方案商的SDK,分析了他們的顯示發(fā)現(xiàn)也是轉(zhuǎn)為 UIImage 來(lái)進(jìn)行顯示的,說(shuō)明這種顯示方式應(yīng)該是一種主流的方式,不像網(wǎng)絡(luò)上說(shuō)的那樣性能低下,性能低下很可能主要是使用方式不對(duì)造成的,最后要注意的是生成 CGContextRef 使用這個(gè)配置:
CGContextRef cgContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, rgbColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
App實(shí)現(xiàn)截圖拍照:
- 不論是硬解碼,還是軟解碼最后出來(lái)的數(shù)據(jù)應(yīng)該都是YUV數(shù)據(jù)那么,利用YUV數(shù)據(jù)生成圖片方法很多,要看具體需求,例如 libyuv 庫(kù)來(lái)做這個(gè);不過(guò)IOS平臺(tái)如果你是硬解碼成 CVPixelBufferRef 以后以 UIImage 來(lái)顯示的話(huà),那么你直接可以利用 UIImage 來(lái)生成圖片更簡(jiǎn)單(我們目前就是)
UIImage *getImage = [UIImage imageWithContentsOfFile:file];
NSData *data;
if (UIImagePNGRepresentation(getImage) == nil){
data = UIImageJPEGRepresentation(getImage, 1);
} else {
data = UIImagePNGRepresentation(getImage);
}
App實(shí)現(xiàn)錄制視頻:
錄制視頻說(shuō)白了就是封包,把編碼過(guò)的音頻AAC,視頻H264封裝為一個(gè)數(shù)據(jù)格式,常見(jiàn)的格式Mp4,TS等等
- 音視頻硬解碼的封包:
如果是通過(guò) AudioToolBox 與 VideoToolbox 硬解碼音視頻的話(huà),那么封包就就是用SDK里面的 AVFoundation中的AVAssetWriter 來(lái)進(jìn)行寫(xiě)封包:
assetVideoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:compressionVideoSetting];
assetAudioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:compressionAudioSetting];
[assetVideoWriterInput appendSampleBuffer:buffer];
[assetAudioWriterInput appendSampleBuffer:buffer];
但是這種SDK封包的時(shí)候要注意幾個(gè)事項(xiàng),我們是打算封裝成視頻H264 ,音頻AAC的Mp4文件在進(jìn)行的時(shí)候就總結(jié)出以下幾個(gè)問(wèn)題:
1 如果是單獨(dú)封裝H264編碼過(guò)的視頻的話(huà)沒(méi)有問(wèn)題,AVAssetWriter封裝Mp4能成功,傳到手機(jī)能播放,第三方播放器可以播放
2 如果是音頻編碼過(guò)的AAC,視頻編碼過(guò)的H264,利用AVAssetWriter封裝Mp4能輸出文件,但是傳到手機(jī)就是不能正常播放,但是第三方部分播放器可以播放
3 后來(lái)再試了音頻PCM,視頻YUV進(jìn)行封包AVAssetWriter封裝Mp4能成功,傳到手機(jī)能播放,第三方播放器可以播放
4 后來(lái)實(shí)在不行我們就試了音頻用PCM裸音源,視頻用H264來(lái)進(jìn)行Mp4封包就可以了,傳到手機(jī)能播放,第三方播放器可以播放
5 再后來(lái)我們對(duì)比了同類(lèi)產(chǎn)品的10秒Mp4封包體積,發(fā)現(xiàn)個(gè)問(wèn)題基本上同類(lèi)產(chǎn)品的體積都比我的大,我們的體積是他們的1/3左右,估計(jì)他們就是PCM,YUV進(jìn)行封包的所以體積比較大,我們算是這個(gè)體驗(yàn)比對(duì)手產(chǎn)品的要好
- 如果是ffmpeg軟解碼的話(huà)那么ffmpeg的SDK里面就包含了封包的方法:
初始化三個(gè)** AVFormatContext** 容器,一個(gè)音頻一個(gè)視頻的用來(lái)作為輸入的AAC,H264的容器,另外一個(gè)作為輸出的容器,還有一個(gè) AVOutputFormat
輸出格式化對(duì)象,簡(jiǎn)單的來(lái)說(shuō)就是讀出一個(gè)AvPacket然后處理好PTS,DTS以后往對(duì)應(yīng)流的輸出容器去寫(xiě)即可,涉及的函數(shù):
avformat_open_input():打開(kāi)輸入文件。
avcodec_copy_context():賦值A(chǔ)VCodecContext的參數(shù)。
avformat_alloc_output_context2():初始化輸出文件。
avio_open():打開(kāi)輸出文件。
avformat_write_header():寫(xiě)入文件頭。
av_compare_ts():比較時(shí)間戳,決定寫(xiě)入視頻還是寫(xiě)入音頻。這個(gè)函數(shù)相對(duì)要少見(jiàn)一些。
av_read_frame():從輸入文件讀取一個(gè)AVPacket。
av_interleaved_write_frame():寫(xiě)入一個(gè)AVPacket到輸出文件。
av_write_trailer():寫(xiě)入文件尾。
App實(shí)現(xiàn)音視頻同步:
- 音視頻同步的話(huà)選擇一般來(lái)說(shuō)有以下三種:
將視頻同步到音頻上:就是以音頻的播放速度為基準(zhǔn)來(lái)同步視頻。
將音頻同步到視頻上:就是以視頻的播放速度為基準(zhǔn)來(lái)同步音頻。
將視頻和音頻同步外部的時(shí)鐘上:選擇一個(gè)外部時(shí)鐘為基準(zhǔn),視頻和音頻的播放速度都以該時(shí)鐘為標(biāo)準(zhǔn)。
這三種是最基本的策略,考慮到人對(duì)聲音的敏感度要強(qiáng)于視頻,頻繁調(diào)節(jié)音頻會(huì)帶來(lái)較差的觀感體驗(yàn),且音頻的播放時(shí)鐘為線(xiàn)性增長(zhǎng),所以一般會(huì)以音頻時(shí)鐘為參考時(shí)鐘,視頻同步到音頻上,音頻作為主導(dǎo)視頻作為次要,用視頻流來(lái)同步音頻流,由于不論是哪一個(gè)平臺(tái)播放音頻的引擎,都可以保證播放音頻的時(shí)間長(zhǎng)度與實(shí)際這段音頻所代表的時(shí)間長(zhǎng)度是一致的,所以我們可以依賴(lài)于音頻的順序播放為我們提供的時(shí)間戳,當(dāng)客戶(hù)端代碼請(qǐng)求發(fā)送視頻幀的時(shí)候,會(huì)先計(jì)算出當(dāng)前視頻隊(duì)列頭部的視頻幀元素的時(shí)間戳與當(dāng)前音頻播放幀的時(shí)間戳的差值。如果在閾值范圍內(nèi),就可以渲染這一幀視頻幀;如果不在閾值范圍內(nèi),則要進(jìn)行對(duì)齊操作。具體的對(duì)齊操作方法就是:如果當(dāng)前隊(duì)列頭部的視頻幀的時(shí)間戳小于當(dāng)前播放音頻幀的時(shí)間戳,那么就進(jìn)行跳幀操作(具體的跳幀操作可以是加快速度播放的實(shí)現(xiàn),也可以是丟棄一部分視頻幀的實(shí)現(xiàn) );如果大于當(dāng)前播放音頻幀的時(shí)間戳,那么就進(jìn)行等待(重復(fù)渲染上一幀或者不進(jìn)行渲染)的操作。其優(yōu)點(diǎn)是音頻可以連續(xù)地播放,缺點(diǎn)是視頻畫(huà)面有可能會(huì)有跳幀的操作,但是對(duì)于視頻畫(huà)面的丟幀和跳幀,用戶(hù)的眼睛是不太容易分辨得出來(lái)的
一般來(lái)說(shuō)視頻丟幀是我們常見(jiàn)的處理視頻慢于音頻的方式,可以先計(jì)算出需要加快多少時(shí)間,然后根據(jù)一個(gè)GOP算出每一幀的時(shí)間是多少,可以得出需要丟多少幀,然后丟幀的時(shí)候要注意的是必須要判斷,不能把I幀丟了,否則接下來(lái)的P幀就根本用不了,而應(yīng)該丟的是P幀,也就是一個(gè)GOP的后半部分,最合適的情況就是丟一整個(gè)GOP,如果是丟GOP后半部分的話(huà)你需要一開(kāi)始播放GOP的時(shí)候弄一個(gè)變量記錄當(dāng)前是第幾個(gè)P幀了,然后計(jì)算出需要丟幾個(gè)P幀才能和音頻同步,然后到了那一個(gè)需要丟的幀到來(lái)的時(shí)候直接拋棄,即到下一個(gè)I幀到來(lái)的時(shí)候才進(jìn)行渲染(這里面有可能丟的不是那么準(zhǔn)確,可能需要經(jīng)過(guò)幾個(gè)的丟幀步驟才能準(zhǔn)確同步)
好了,我們IOS開(kāi)發(fā)中的音視頻雜談就到這里了,我們洋洋灑灑的談了這么多,主要是方案部分,也包括了項(xiàng)目中的一些“坑”,如果大家喜歡的話(huà)接下來(lái)我會(huì)把細(xì)節(jié)部分再分別寫(xiě)一些東西出來(lái),??希望大家多多留言討論,想看Android音視頻開(kāi)發(fā)雜談的出門(mén)左轉(zhuǎn)即可
《Android App項(xiàng)目中音視頻開(kāi)發(fā)雜談》
···