上一篇我們侃侃而談了下Android下的App音視頻開發(fā)雜談音诈,我們從入手到深入再到實(shí)際項(xiàng)目的遇到的問題以及解決方案都聊了下,那么這一次我們來雜談下IOS項(xiàng)目中音視頻的內(nèi)容答渔,這篇內(nèi)容主要是對比上篇Android的內(nèi)容关带,為的是熟悉IOS的朋友方便閱讀觀看,讓我們開始吧:
首先需要了解的是音視頻處理的流程:
- 數(shù)據(jù)分別經(jīng)歷了解協(xié)議研儒,解封裝豫缨,音/視頻解碼,播放步驟端朵,再次請上這張圖:
其次是了解音頻PCM的數(shù)據(jù)是怎么來的包括:
- 怎么采樣采樣率是什么(8kHZ,44.1kHZ)燃箭,
- 單/雙通道冲呢,
- 樣本怎么存儲(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ù)是怎么來的包括:
- YUV數(shù)據(jù)的幾種格式(YUV420P裙戏,YUV420SP乘凸,NV12,NV21)的排布是怎么樣的
- 怎么計(jì)算例如YUV420P的大小
- 怎么分解明亮度與色度
既然是音視頻肯定要涉及壓縮編碼累榜,那么首先應(yīng)該要了解:
國際標(biāo)準(zhǔn)化組織(ISO)的MPEG-1营勤、MPEG-2與MPEG-4,的規(guī)范和標(biāo)準(zhǔn)是哪些
其次要了解這個(gè)這個(gè)主流標(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格式里面每個(gè)NALU的格式:包含頭與payload是什么樣的
AVCC里面extradata里面的數(shù)據(jù)格式是怎么樣的(包含SPS,PPS在里面)
H264里面的SPS猖凛,PPS赂蠢,I幀,P幀辨泳,B幀所表示的意義
說了編碼當(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平臺 EAGL的使用
其中OpenGLES 特別是可以作為一個(gè)分支來進(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的基本矩陣運(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)用層框架有哪些:
- 高級應(yīng)用框架:ffmpeg的基本使用
- 高級應(yīng)用框架:OpenCV的基本使用
額外需要掌握哪些技能:
- C/C++ 基礎(chǔ)院喜;(話說搞OC的工程師應(yīng)該都對于C有很好的理解才對)
以上是我認(rèn)為作為音視頻工程師入門應(yīng)該掌握的知識點(diǎn)亡蓉,我覺得掌握了這些不敢說成為了一個(gè)高手,但應(yīng)該是成為一個(gè)合格的音視頻工程師的 基本功
PS:基本功重要嗎喷舀?我認(rèn)為非常重要砍濒,往小了說基本功顯示了一個(gè)人的技能扎實(shí),擁有了扎實(shí)的基礎(chǔ)才能往更深的方向發(fā)展元咙;往大了說基本功顯示了一個(gè)人可靠梯影,處事沉穩(wěn)可以做到了解一個(gè)事物的本質(zhì)能做到萬變不離其中
有了這些基本功那么我們可以接觸一些實(shí)際的案例了,如果你想要更進(jìn)階那么我推薦一本我認(rèn)為音視頻內(nèi)容比較全庶香,而且里面有很多實(shí)戰(zhàn)例子作為參考的書甲棍,??再次請出這本書:
這本書我認(rèn)為有幾點(diǎn)比較好的:
第一是這本書出于實(shí)戰(zhàn)出發(fā)(據(jù)說是 唱吧App 架構(gòu)師在做唱吧的時(shí)候總結(jié)了很多經(jīng)驗(yàn)寫的),
第二這本書的內(nèi)容包含了Android,IOS兩個(gè)版本的所以有對比參考性感猛,第三這本書從基礎(chǔ)的音視頻到高級的應(yīng)用場景都介紹了七扰,可謂是內(nèi)容豐富;
說了這么多好的再說說這本書的一些不好的地方:
首先就是我認(rèn)為這本書不太適合剛剛?cè)腴T的新手(注意是剛剛?cè)腴T)如果是這類的工程師一些概念都沒搞清楚的就看這個(gè)其實(shí)不是很合適;
其次就是里面的例子的代碼段過于松散陪白,閱讀起來需要不是很順暢颈走,而且git里面的Demo感覺也跟不上書里面的代碼,里面的Demo目錄結(jié)構(gòu)不是很清晰(一般來說我們見得多的是1章分為一個(gè)或多個(gè)項(xiàng)目咱士,分別講解對應(yīng)的內(nèi)容互相不會干擾立由,書里面是git commit來區(qū)分的感覺體驗(yàn)性不是很好)
但是瑕不掩瑜如果你是有基礎(chǔ)的話,那么這本書肯定能給你帶了項(xiàng)目中的幫助序厉。
好了锐膜,介紹了這么多基礎(chǔ)我們馬上進(jìn)入項(xiàng)目中去看看,IOS音視頻的項(xiàng)目問題以及解決方案
我們要實(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é)議頭:包括同步碼字段文捶,幀類型荷逞,數(shù)據(jù)長度,數(shù)據(jù)方向粹排,時(shí)間戳等等拿到數(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)前幀時(shí)間戳顽耳,精確到秒院促,非關(guān)鍵幀時(shí)間戳需根據(jù)幀率來計(jì)算(僅I幀有效)8字節(jié)
HLE_U32 length; //幀數(shù)據(jù)長度
HLE_U64 pts_msec; //毫秒級時(shí)間戳,一直累加斧抱,溢出后自動回繞
} P2P_FRAME_HDR; //32字節(jié)
App實(shí)現(xiàn)實(shí)時(shí)音視頻解碼:
硬件碼優(yōu)勢:更加省電,適合長時(shí)間的移動端視頻播放器和直播渐溶,手機(jī)電池有限的情況下辉浦,使用硬件解碼會更加好。減少CPU的占用茎辐,可以把CUP讓給別的線程使用宪郊,有利于手機(jī)的流暢度。
軟解碼優(yōu)勢:具有更好的適應(yīng)性拖陆,軟件解碼主要是會占用CUP的運(yùn)行弛槐,軟解不考慮社備的硬件解碼支持情況,有CPU就可以使用了依啰,但是占用了更多的CUP那就意味著很耗費(fèi)性能乎串,很耗電,在設(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的使用其實(shí)也很簡單钝侠,我們先來聊聊音頻的硬解碼 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 : 用戶自定義數(shù)據(jù)指針。
ioOutputDataPacketSize : 輸出數(shù)據(jù)包大小父腕。
outOutputData : 輸出數(shù)據(jù) AudioBufferList 指針弱匪。
outPacketDescription : 輸出包描述符。
解碼的具體步驟如下:首先璧亮,從媒體文件中取出一個(gè)音視幀萧诫。其次,設(shè)置輸出地址枝嘶。然后帘饶,調(diào)用 AudioConverterFillComplexBuffer 方法,該方法又會調(diào)用 inInputDataProc 回調(diào)函數(shù)群扶,將輸入數(shù)據(jù)拷貝到編碼器中及刻。最后,解碼竞阐。將解碼后的數(shù)據(jù)輸出到指定的輸出變量中缴饭。
- IOS視頻的硬解碼:剛剛聊了音頻的硬解碼是用 AudioToolBox ,下面到視頻的硬解碼實(shí)現(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實(shí)列仇奶。
然后開始解碼:
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 : 用戶自定義參數(shù)嫁艇。(輸出解碼數(shù)據(jù))
infoFlagsOut : 輸出參數(shù)標(biāo)記。
需要注意的是弦撩,如果你的硬解碼出來的數(shù)據(jù)是要轉(zhuǎn)換為 UIImage 貼圖顯示的話那么在配置解碼器的時(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 來配合出圖的(下面視頻播放的時(shí)候我會再提到這種方式)
- IOS音視頻的軟解碼:
軟解碼首推的就是ffmpeg,ffmpeg的使用還是很簡單的益楼,簡單的來說你只需要一開始初始化 編解碼格式對象 AVCodecContext 與編解碼器 AVCodec 猾漫,然后把數(shù)據(jù)填充AvPacket ,然后解碼成 AvFrame 就可以了感凤。
App實(shí)現(xiàn)音頻的播放:
- 音頻的重采樣:有時(shí)候在音頻播放的時(shí)候悯周,會出現(xiàn)你的音源與播放設(shè)備的硬件條件不匹配,例如播放每幀的樣本數(shù)不匹配陪竿,采樣位數(shù)不匹配的情況禽翼,那么這個(gè)時(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ù)夺脾,播放隊(duì)列空調(diào)用
(*pcmQue)->RegisterCallback(pcmQue,PcmCall,this);
//設(shè)置為播放狀態(tài)
(*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);
//啟動隊(duì)列回調(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ù)時(shí)就會調(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ù)分別貼圖來播放顯示颖变,例子如下:
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硬解碼出來的數(shù)據(jù) CVPixelBufferRef 轉(zhuǎn)換為 UIImage听想,這種方式看似簡單但是坑也最多腥刹,我總結(jié)了以下幾總轉(zhuǎn)換的方式以及測試結(jié)果,??敲黑板了注意聽講:
我的測試手機(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);
這種方式理論上說不能正常運(yùn)行,我調(diào)試了很久原因就在 CVPixelBufferRef 這個(gè)對象的釋放問題,因?yàn)橐婚_始就對他進(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); //去掉
很悲劇的這種方式直接空指針報(bào)錯(cuò),根據(jù)調(diào)試開看應(yīng)該是 CVPixelBuffer 被提前釋放了慨代,所以你生成的 UIImage 沒法在主線程使用
那把末尾的release去掉呢邢笙,
CVPixelBufferRelease(imageBuffer); //去掉
這種情況會出圖但是,你會發(fā)現(xiàn)你的內(nèi)存在暴漲侍匙,因?yàn)檫@個(gè) CVPixelBuffer 這個(gè)對象沒有手動釋放氮惯,(也說明了這種生成圖片的方式,對于 CVPixelBuffer 的釋放不太好處理至少SDK沒有什么好的辦法)想暗,我甚至想了個(gè)辦法把這個(gè) CVPixelBuffer 對象轉(zhuǎn)為OC對象想用ARC來管理它還是不行妇汗,這種生成圖片的方式Pass掉
- 第二種最簡單,也是網(wǎng)絡(luò)上經(jīng)辰酰看見的方法:
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
UIImage *image = [UIImage imageWithCIImage:ciImage];
簡單歸簡單铛纬,但是這種方式太耗內(nèi)存了,不是說內(nèi)存一直漲唬滑,而是固定就很高告唆,尤其是IphoneX上面非常明顯,為了性能著想不可染堋(其實(shí)也沒到使用不了的地步擒悬,只不過是想要最優(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)過長時(shí)間測試這種方式CPU與內(nèi)存都是穩(wěn)定輸出
經(jīng)過測試與觀察這種方式其實(shí)效率看起來并不低,Iphone5S都能正常的播放澎语,而且參照了同類方案商的SDK途事,分析了他們的顯示發(fā)現(xiàn)也是轉(zhuǎn)為 UIImage 來進(jìn)行顯示的验懊,說明這種顯示方式應(yīng)該是一種主流的方式,不像網(wǎng)絡(luò)上說的那樣性能低下尸变,性能低下很可能主要是使用方式不對造成的义图,最后要注意的是生成 CGContextRef 使用這個(gè)配置:
CGContextRef cgContext = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, rgbColorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
App實(shí)現(xiàn)截圖拍照:
- 不論是硬解碼,還是軟解碼最后出來的數(shù)據(jù)應(yīng)該都是YUV數(shù)據(jù)那么召烂,利用YUV數(shù)據(jù)生成圖片方法很多碱工,要看具體需求,例如 libyuv 庫來做這個(gè)骑晶;不過IOS平臺如果你是硬解碼成 CVPixelBufferRef 以后以 UIImage 來顯示的話痛垛,那么你直接可以利用 UIImage 來生成圖片更簡單(我們目前就是)
UIImage *getImage = [UIImage imageWithContentsOfFile:file];
NSData *data;
if (UIImagePNGRepresentation(getImage) == nil){
data = UIImageJPEGRepresentation(getImage, 1);
} else {
data = UIImagePNGRepresentation(getImage);
}
App實(shí)現(xiàn)錄制視頻:
錄制視頻說白了就是封包,把編碼過的音頻AAC桶蛔,視頻H264封裝為一個(gè)數(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封包的時(shí)候要注意幾個(gè)事項(xiàng)蹂析,我們是打算封裝成視頻H264 ,音頻AAC的Mp4文件在進(jìn)行的時(shí)候就總結(jié)出以下幾個(gè)問題:
1 如果是單獨(dú)封裝H264編碼過的視頻的話沒有問題,AVAssetWriter封裝Mp4能成功碟婆,傳到手機(jī)能播放电抚,第三方播放器可以播放
2 如果是音頻編碼過的AAC,視頻編碼過的H264竖共,利用AVAssetWriter封裝Mp4能輸出文件蝙叛,但是傳到手機(jī)就是不能正常播放,但是第三方部分播放器可以播放
3 后來再試了音頻PCM公给,視頻YUV進(jìn)行封包AVAssetWriter封裝Mp4能成功借帘,傳到手機(jī)能播放,第三方播放器可以播放
4 后來實(shí)在不行我們就試了音頻用PCM裸音源淌铐,視頻用H264來進(jìn)行Mp4封包就可以了肺然,傳到手機(jī)能播放,第三方播放器可以播放
5 再后來我們對比了同類產(chǎn)品的10秒Mp4封包體積腿准,發(fā)現(xiàn)個(gè)問題基本上同類產(chǎn)品的體積都比我的大际起,我們的體積是他們的1/3左右,估計(jì)他們就是PCM吐葱,YUV進(jìn)行封包的所以體積比較大街望,我們算是這個(gè)體驗(yàn)比對手產(chǎn)品的要好
- 如果是ffmpeg軟解碼的話那么ffmpeg的SDK里面就包含了封包的方法:
初始化三個(gè)** AVFormatContext** 容器,一個(gè)音頻一個(gè)視頻的用來作為輸入的AAC弟跑,H264的容器它匕,另外一個(gè)作為輸出的容器,還有一個(gè) AVOutputFormat
輸出格式化對象窖认,簡單的來說就是讀出一個(gè)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í)間戳,決定寫入視頻還是寫入音頻酝惧。這個(gè)函數(shù)相對要少見一些榴鼎。
av_read_frame():從輸入文件讀取一個(gè)AVPacket。
av_interleaved_write_frame():寫入一個(gè)AVPacket到輸出文件晚唇。
av_write_trailer():寫入文件尾巫财。
App實(shí)現(xiàn)音視頻同步:
- 音視頻同步的話選擇一般來說有以下三種:
將視頻同步到音頻上:就是以音頻的播放速度為基準(zhǔn)來同步視頻。
將音頻同步到視頻上:就是以視頻的播放速度為基準(zhǔn)來同步音頻哩陕。
將視頻和音頻同步外部的時(shí)鐘上:選擇一個(gè)外部時(shí)鐘為基準(zhǔn)平项,視頻和音頻的播放速度都以該時(shí)鐘為標(biāo)準(zhǔn)。
這三種是最基本的策略悍及,考慮到人對聲音的敏感度要強(qiáng)于視頻闽瓢,頻繁調(diào)節(jié)音頻會帶來較差的觀感體驗(yàn),且音頻的播放時(shí)鐘為線性增長心赶,所以一般會以音頻時(shí)鐘為參考時(shí)鐘扣讼,視頻同步到音頻上,音頻作為主導(dǎo)視頻作為次要,用視頻流來同步音頻流缨叫,由于不論是哪一個(gè)平臺播放音頻的引擎椭符,都可以保證播放音頻的時(shí)間長度與實(shí)際這段音頻所代表的時(shí)間長度是一致的,所以我們可以依賴于音頻的順序播放為我們提供的時(shí)間戳弯汰,當(dāng)客戶端代碼請求發(fā)送視頻幀的時(shí)候艰山,會先計(jì)算出當(dāng)前視頻隊(duì)列頭部的視頻幀元素的時(shí)間戳與當(dāng)前音頻播放幀的時(shí)間戳的差值。如果在閾值范圍內(nèi)咏闪,就可以渲染這一幀視頻幀曙搬;如果不在閾值范圍內(nèi),則要進(jìn)行對齊操作鸽嫂。具體的對齊操作方法就是:如果當(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)是視頻畫面有可能會有跳幀的操作挽唉,但是對于視頻畫面的丟幀和跳幀滤祖,用戶的眼睛是不太容易分辨得出來的
一般來說視頻丟幀是我們常見的處理視頻慢于音頻的方式,可以先計(jì)算出需要加快多少時(shí)間瓶籽,然后根據(jù)一個(gè)GOP算出每一幀的時(shí)間是多少匠童,可以得出需要丟多少幀,然后丟幀的時(shí)候要注意的是必須要判斷塑顺,不能把I幀丟了,否則接下來的P幀就根本用不了汤求,而應(yīng)該丟的是P幀,也就是一個(gè)GOP的后半部分严拒,最合適的情況就是丟一整個(gè)GOP扬绪,如果是丟GOP后半部分的話你需要一開始播放GOP的時(shí)候弄一個(gè)變量記錄當(dāng)前是第幾個(gè)P幀了,然后計(jì)算出需要丟幾個(gè)P幀才能和音頻同步裤唠,然后到了那一個(gè)需要丟的幀到來的時(shí)候直接拋棄挤牛,即到下一個(gè)I幀到來的時(shí)候才進(jìn)行渲染(這里面有可能丟的不是那么準(zhǔn)確,可能需要經(jīng)過幾個(gè)的丟幀步驟才能準(zhǔn)確同步)
好了巧骚,我們IOS開發(fā)中的音視頻雜談就到這里了赊颠,我們洋洋灑灑的談了這么多,主要是方案部分劈彪,也包括了項(xiàng)目中的一些“坑”竣蹦,如果大家喜歡的話接下來我會把細(xì)節(jié)部分再分別寫一些東西出來,??希望大家多多留言討論沧奴,想看Android音視頻開發(fā)雜談的出門左轉(zhuǎn)即可
《Android App項(xiàng)目中音視頻開發(fā)雜談》
···