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

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

這本書我認(rèn)為有幾點比較好的:
第一是這本書出于實戰(zhàn)出發(fā)(據(jù)說是 唱吧App 架構(gòu)師在做唱吧的時候總結(jié)了很多經(jīng)驗寫的),
第二這本書的內(nèi)容包含了Android,IOS兩個版本的所以有對比參考性,第三這本書從基礎(chǔ)的音視頻到高級的應(yīng)用場景都介紹了,可謂是內(nèi)容豐富;
說了這么多好的再說說這本書的一些不好的地方:
首先就是我認(rèn)為這本書不太適合剛剛?cè)腴T的新手(注意是剛剛?cè)腴T)如果是這類的工程師一些概念都沒搞清楚的就看這個其實不是很合適;
其次就是里面的例子的代碼段過于松散,閱讀起來需要不是很順暢,而且git里面的Demo感覺也跟不上書里面的代碼,里面的Demo目錄結(jié)構(gòu)不是很清晰(一般來說我們見得多的是1章分為一個或多個項目,分別講解對應(yīng)的內(nèi)容互相不會干擾,書里面是git commit來區(qū)分的感覺體驗性不是很好)
但是瑕不掩瑜如果你是有基礎(chǔ)的話,那么這本書肯定能給你帶了項目中的幫助。
好了,介紹了這么多基礎(chǔ)我們馬上進(jìn)入項目中去看看,IOS音視頻的項目問題以及解決方案
我們要實現(xiàn)的功能:
- App音視頻的數(shù)據(jù)怎么傳輸
- App實現(xiàn)音視頻解碼
- App實現(xiàn)音視頻播放
- App實現(xiàn)截圖拍照
- App實現(xiàn)錄制視頻
- App實現(xiàn)音視頻同步
App音視頻的數(shù)據(jù)怎么傳輸:
- App這邊與嵌入式定好傳輸協(xié)議,協(xié)議數(shù)據(jù)大致分為協(xié)議頭,協(xié)議體,協(xié)議頭:包括同步碼字段,幀類型,數(shù)據(jù)長度,數(shù)據(jù)方向,時間戳等等拿到數(shù)據(jù)頭以后
就可以按照長度拿到協(xié)議體數(shù)據(jù)就可以開始解碼了
typedef struct
{
HLE_U8 sync_code[3]; /*幀頭同步碼,固定為0x00,0x00,0x01*/
HLE_U8 type; /*幀類型, */
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)前幀時間戳,精確到秒,非關(guān)鍵幀時間戳需根據(jù)幀率來計算(僅I幀有效)8字節(jié)
HLE_U32 length; //幀數(shù)據(jù)長度
HLE_U64 pts_msec; //毫秒級時間戳,一直累加,溢出后自動回繞
} P2P_FRAME_HDR; //32字節(jié)
App實現(xiàn)實時音視頻解碼:
硬件碼優(yōu)勢:更加省電,適合長時間的移動端視頻播放器和直播,手機(jī)電池有限的情況下,使用硬件解碼會更加好。減少CPU的占用,可以把CUP讓給別的線程使用,有利于手機(jī)的流暢度。
軟解碼優(yōu)勢:具有更好的適應(yīng)性,軟件解碼主要是會占用CUP的運行,軟解不考慮社備的硬件解碼支持情況,有CPU就可以使用了,但是占用了更多的CUP那就意味著很耗費性能,很耗電,在設(shè)備電量充足的情況下,或者設(shè)備硬件解碼支持不足的情況下使用軟件解碼更加好!
- IOS音頻的硬解碼:IOS的硬解碼比Android的硬解碼要好上太多了,IOS從8.0就開始加入了 AudioToolBox 與 VideoToolbox 來進(jìn)行音視頻的硬編解碼,目前Iphone手機(jī)基本上都是8.0了,而且Iphone4S以上都支持硬解碼所以兼容性肯定沒的說(封閉也有封閉的好處,標(biāo)準(zhǔn)全部統(tǒng)一,對于開發(fā)來說就簡單),而且SDK的使用其實也很簡單,我們先來聊聊音頻的硬解碼 AudioToolBox 的使用,主要是這個方法:
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 : 用戶自定義數(shù)據(jù)指針。
ioOutputDataPacketSize : 輸出數(shù)據(jù)包大小。
outOutputData : 輸出數(shù)據(jù) AudioBufferList 指針。
outPacketDescription : 輸出包描述符。
解碼的具體步驟如下:首先,從媒體文件中取出一個音視幀。其次,設(shè)置輸出地址。然后,調(diào)用 AudioConverterFillComplexBuffer 方法,該方法又會調(diào)用 inInputDataProc 回調(diào)函數(shù),將輸入數(shù)據(jù)拷貝到編碼器中。最后,解碼。將解碼后的數(shù)據(jù)輸出到指定的輸出變量中。
- IOS視頻的硬解碼:剛剛聊了音頻的硬解碼是用 AudioToolBox ,下面到視頻的硬解碼實現(xiàn),下面請出 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實列。
然后開始解碼:
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)建解碼器時創(chuàng)建的 Session。
sampleBuffer : 準(zhǔn)備被解碼的視頻幀。
decodeFlags : 解碼標(biāo)志符。 0:代表異步解碼。
sourceFrameRefCon : 用戶自定義參數(shù)。(輸出解碼數(shù)據(jù))
infoFlagsOut : 輸出參數(shù)標(biāo)記。
需要注意的是,如果你的硬解碼出來的數(shù)據(jù)是要轉(zhuǎn)換為 UIImage 貼圖顯示的話那么在配置解碼器的時候要注意配置參數(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 來配合出圖的(下面視頻播放的時候我會再提到這種方式)
- IOS音視頻的軟解碼:
軟解碼首推的就是ffmpeg,ffmpeg的使用還是很簡單的,簡單的來說你只需要一開始初始化 編解碼格式對象 AVCodecContext 與編解碼器 AVCodec ,然后把數(shù)據(jù)填充AvPacket ,然后解碼成 AvFrame 就可以了。
App實現(xiàn)音頻的播放:
- 音頻的重采樣:有時候在音頻播放的時候,會出現(xiàn)你的音源與播放設(shè)備的硬件條件不匹配,例如播放每幀的樣本數(shù)不匹配,采樣位數(shù)不匹配的情況,那么這個時候需要用到對于音源PCM重采樣,重采樣以后才能正常播放,
int len = swr_convert(actx,outArr,frame->nb_samples,(const uint8_t **)frame->data,frame->nb_samples);
主要是通過 swr_convert 來進(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ù)再拿來播放就可以在指定的條件進(jìn)行指定的播放
- 音頻軟解碼的播放:這種情況下一般我們推薦的還是利用 OpenSLES 來播放
//設(shè)置回調(diào)函數(shù),播放隊列空調(diào)用
(*pcmQue)->RegisterCallback(pcmQue,PcmCall,this);
//設(shè)置為播放狀態(tài)
(*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);
//啟動隊列回調(diào)
(*pcmQue)->Enqueue(pcmQue,"",1);
- 音頻的硬解碼播放:這種情況下播放使用SDK自帶的 AudioUnit 來進(jìn)行播放,首先創(chuàng)建對象:
// 獲得 Audio Unit
status = AudioComponentInstanceNew(inputComponent, &audioUnit);
然后配置屬性:
// 為播放打開 IO
status = AudioUnitSetProperty(audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
kOutputBus,
&flag,
sizeof(flag));
checkStatus(status);
// 設(shè)置播放格式
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
kOutputBus,
& outputFormat, //參見編碼器格式
sizeof(audioFormat));
checkStatus(status);
// 設(shè)置聲音輸出回調(diào)函數(shù)。當(dāng)speaker需要數(shù)據(jù)時就會調(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 視頻的播放:
- 視頻軟解播放:這個當(dāng)然是首先 opengles ,拿到Y(jié)UV數(shù)據(jù),設(shè)置好貼圖坐標(biāo),使用YUV數(shù)據(jù)分別貼圖來播放顯示,例子如下:
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();
- 視頻硬解的播放:這個方式非常直接,利用SDK硬解碼出來的數(shù)據(jù) CVPixelBufferRef 轉(zhuǎn)換為 UIImage,這種方式看似簡單但是坑也最多,我總結(jié)了以下幾總轉(zhuǎn)換的方式以及測試結(jié)果,??敲黑板了注意聽講:
我的測試手機(jī)為兩部一部IphoneX,一部為Iphone5S(一部高端的一部低端的), didDecompress 方法是硬解碼的回調(diào)函數(shù),這個不解釋了
- 第一種是:
- 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);
這種方式理論上說不能正常運行,我調(diào)試了很久原因就在 CVPixelBufferRef 這個對象的釋放問題,因為一開始就對他進(jìn)行了Retain(CVPixelBufferRef 是C對象不是OC對象所以沒有辦法進(jìn)行ARC,需要手動的Retain,Release)
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
但是你最后這句releases會引發(fā)空指針問題,
CVPixelBufferRelease(imageBuffer);
究其原因我猜想是由于,生成的 UIImage 正在使用,雖然你在他后面才進(jìn)行了release,但是這種還是會影響他這塊內(nèi)存所以會有空指針問題(網(wǎng)絡(luò)上基本上搜不到答案,我的結(jié)論是我自己測試出來的,聽我往下講)
于是我把前面的retain ,release 去掉:也就是這三句話去掉
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon; //去掉
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer); //去掉
CVPixelBufferRelease(imageBuffer); //去掉
很悲劇的這種方式直接空指針報錯,根據(jù)調(diào)試開看應(yīng)該是 CVPixelBuffer 被提前釋放了,所以你生成的 UIImage 沒法在主線程使用
那把末尾的release去掉呢,
CVPixelBufferRelease(imageBuffer); //去掉
這種情況會出圖但是,你會發(fā)現(xiàn)你的內(nèi)存在暴漲,因為這個 CVPixelBuffer 這個對象沒有手動釋放,(也說明了這種生成圖片的方式,對于 CVPixelBuffer 的釋放不太好處理至少SDK沒有什么好的辦法),我甚至想了個辦法把這個 CVPixelBuffer 對象轉(zhuǎn)為OC對象想用ARC來管理它還是不行,這種生成圖片的方式Pass掉
- 第二種最簡單,也是網(wǎng)絡(luò)上經(jīng)??匆姷姆椒ǎ?/li>
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
UIImage *image = [UIImage imageWithCIImage:ciImage];
簡單歸簡單,但是這種方式太耗內(nèi)存了,不是說內(nèi)存一直漲,而是固定就很高,尤其是IphoneX上面非常明顯,為了性能著想不可取(其實也沒到使用不了的地步,只不過是想要最優(yōu)的方案,才有了下面的嘗試)
- 第三總在第二總的方式上面做了些許改動:
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); //沒有此句話無法釋放內(nèi)存
這種方式內(nèi)存沒有那么夸張了,但是CPU使用卻上來了,而且上升很明顯,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);
這種方式不需要手動retain,release CVPixelBuffer 了,而且使用 CGContextRef 代替了 CGDataProviderRef 去生成 CGImageRef ,經(jīng)過長時間測試這種方式CPU與內(nèi)存都是穩(wěn)定輸出
經(jīng)過測試與觀察這種方式其實效率看起來并不低,Iphone5S都能正常的播放,而且參照了同類方案商的SDK,分析了他們的顯示發(fā)現(xiàn)也是轉(zhuǎn)為 UIImage 來進(jìn)行顯示的,說明這種顯示方式應(yīng)該是一種主流的方式,不像網(wǎng)絡(luò)上說的那樣性能低下,性能低下很可能主要是使用方式不對造成的,最后要注意的是生成 CGContextRef 使用這個配置:
CGContextRef cgContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, rgbColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
App實現(xiàn)截圖拍照:
- 不論是硬解碼,還是軟解碼最后出來的數(shù)據(jù)應(yīng)該都是YUV數(shù)據(jù)那么,利用YUV數(shù)據(jù)生成圖片方法很多,要看具體需求,例如 libyuv 庫來做這個;不過IOS平臺如果你是硬解碼成 CVPixelBufferRef 以后以 UIImage 來顯示的話,那么你直接可以利用 UIImage 來生成圖片更簡單(我們目前就是)
UIImage *getImage = [UIImage imageWithContentsOfFile:file];
NSData *data;
if (UIImagePNGRepresentation(getImage) == nil){
data = UIImageJPEGRepresentation(getImage, 1);
} else {
data = UIImagePNGRepresentation(getImage);
}
App實現(xiàn)錄制視頻:
錄制視頻說白了就是封包,把編碼過的音頻AAC,視頻H264封裝為一個數(shù)據(jù)格式,常見的格式Mp4,TS等等
- 音視頻硬解碼的封包:
如果是通過 AudioToolBox 與 VideoToolbox 硬解碼音視頻的話,那么封包就就是用SDK里面的 AVFoundation中的AVAssetWriter 來進(jìn)行寫封包:
assetVideoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:compressionVideoSetting];
assetAudioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:compressionAudioSetting];
[assetVideoWriterInput appendSampleBuffer:buffer];
[assetAudioWriterInput appendSampleBuffer:buffer];
但是這種SDK封包的時候要注意幾個事項,我們是打算封裝成視頻H264 ,音頻AAC的Mp4文件在進(jìn)行的時候就總結(jié)出以下幾個問題:
1 如果是單獨封裝H264編碼過的視頻的話沒有問題,AVAssetWriter封裝Mp4能成功,傳到手機(jī)能播放,第三方播放器可以播放
2 如果是音頻編碼過的AAC,視頻編碼過的H264,利用AVAssetWriter封裝Mp4能輸出文件,但是傳到手機(jī)就是不能正常播放,但是第三方部分播放器可以播放
3 后來再試了音頻PCM,視頻YUV進(jìn)行封包AVAssetWriter封裝Mp4能成功,傳到手機(jī)能播放,第三方播放器可以播放
4 后來實在不行我們就試了音頻用PCM裸音源,視頻用H264來進(jìn)行Mp4封包就可以了,傳到手機(jī)能播放,第三方播放器可以播放
5 再后來我們對比了同類產(chǎn)品的10秒Mp4封包體積,發(fā)現(xiàn)個問題基本上同類產(chǎn)品的體積都比我的大,我們的體積是他們的1/3左右,估計他們就是PCM,YUV進(jìn)行封包的所以體積比較大,我們算是這個體驗比對手產(chǎn)品的要好
- 如果是ffmpeg軟解碼的話那么ffmpeg的SDK里面就包含了封包的方法:
初始化三個** AVFormatContext** 容器,一個音頻一個視頻的用來作為輸入的AAC,H264的容器,另外一個作為輸出的容器,還有一個 AVOutputFormat
輸出格式化對象,簡單的來說就是讀出一個AvPacket然后處理好PTS,DTS以后往對應(yīng)流的輸出容器去寫即可,涉及的函數(shù):
avformat_open_input():打開輸入文件。
avcodec_copy_context():賦值A(chǔ)VCodecContext的參數(shù)。
avformat_alloc_output_context2():初始化輸出文件。
avio_open():打開輸出文件。
avformat_write_header():寫入文件頭。
av_compare_ts():比較時間戳,決定寫入視頻還是寫入音頻。這個函數(shù)相對要少見一些。
av_read_frame():從輸入文件讀取一個AVPacket。
av_interleaved_write_frame():寫入一個AVPacket到輸出文件。
av_write_trailer():寫入文件尾。
App實現(xiàn)音視頻同步:
- 音視頻同步的話選擇一般來說有以下三種:
將視頻同步到音頻上:就是以音頻的播放速度為基準(zhǔn)來同步視頻。
將音頻同步到視頻上:就是以視頻的播放速度為基準(zhǔn)來同步音頻。
將視頻和音頻同步外部的時鐘上:選擇一個外部時鐘為基準(zhǔn),視頻和音頻的播放速度都以該時鐘為標(biāo)準(zhǔn)。
這三種是最基本的策略,考慮到人對聲音的敏感度要強(qiáng)于視頻,頻繁調(diào)節(jié)音頻會帶來較差的觀感體驗,且音頻的播放時鐘為線性增長,所以一般會以音頻時鐘為參考時鐘,視頻同步到音頻上,音頻作為主導(dǎo)視頻作為次要,用視頻流來同步音頻流,由于不論是哪一個平臺播放音頻的引擎,都可以保證播放音頻的時間長度與實際這段音頻所代表的時間長度是一致的,所以我們可以依賴于音頻的順序播放為我們提供的時間戳,當(dāng)客戶端代碼請求發(fā)送視頻幀的時候,會先計算出當(dāng)前視頻隊列頭部的視頻幀元素的時間戳與當(dāng)前音頻播放幀的時間戳的差值。如果在閾值范圍內(nèi),就可以渲染這一幀視頻幀;如果不在閾值范圍內(nèi),則要進(jìn)行對齊操作。具體的對齊操作方法就是:如果當(dāng)前隊列頭部的視頻幀的時間戳小于當(dāng)前播放音頻幀的時間戳,那么就進(jìn)行跳幀操作(具體的跳幀操作可以是加快速度播放的實現(xiàn),也可以是丟棄一部分視頻幀的實現(xiàn) );如果大于當(dāng)前播放音頻幀的時間戳,那么就進(jìn)行等待(重復(fù)渲染上一幀或者不進(jìn)行渲染)的操作。其優(yōu)點是音頻可以連續(xù)地播放,缺點是視頻畫面有可能會有跳幀的操作,但是對于視頻畫面的丟幀和跳幀,用戶的眼睛是不太容易分辨得出來的
一般來說視頻丟幀是我們常見的處理視頻慢于音頻的方式,可以先計算出需要加快多少時間,然后根據(jù)一個GOP算出每一幀的時間是多少,可以得出需要丟多少幀,然后丟幀的時候要注意的是必須要判斷,不能把I幀丟了,否則接下來的P幀就根本用不了,而應(yīng)該丟的是P幀,也就是一個GOP的后半部分,最合適的情況就是丟一整個GOP,如果是丟GOP后半部分的話你需要一開始播放GOP的時候弄一個變量記錄當(dāng)前是第幾個P幀了,然后計算出需要丟幾個P幀才能和音頻同步,然后到了那一個需要丟的幀到來的時候直接拋棄,即到下一個I幀到來的時候才進(jìn)行渲染(這里面有可能丟的不是那么準(zhǔn)確,可能需要經(jīng)過幾個的丟幀步驟才能準(zhǔn)確同步)
好了,我們IOS開發(fā)中的音視頻雜談就到這里了,我們洋洋灑灑的談了這么多,主要是方案部分,也包括了項目中的一些“坑”,如果大家喜歡的話接下來我會把細(xì)節(jié)部分再分別寫一些東西出來,??希望大家多多留言討論,想看Android音視頻開發(fā)雜談的出門左轉(zhuǎn)即可
···