IOS App項(xiàng)目中音視頻開發(fā)雜談

上一篇我們侃侃而談了下Android下的App音視頻開發(fā)雜談音诈,我們從入手到深入再到實(shí)際項(xiàng)目的遇到的問題以及解決方案都聊了下,那么這一次我們來雜談下IOS項(xiàng)目中音視頻的內(nèi)容答渔,這篇內(nèi)容主要是對比上篇Android的內(nèi)容关带,為的是熟悉IOS的朋友方便閱讀觀看,讓我們開始吧:

首先需要了解的是音視頻處理的流程:

  • 數(shù)據(jù)分別經(jīng)歷了解協(xié)議研儒,解封裝豫缨,音/視頻解碼,播放步驟端朵,再次請上這張圖:
image.png

其次是了解音頻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)例子作為參考的書甲棍,??再次請出這本書:

image.png

這本書我認(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就開始加入了 AudioToolBoxVideoToolbox 來進(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ā)雜談》

···

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痘括,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子滔吠,更是在濱河造成了極大的恐慌纲菌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疮绷,死亡現(xiàn)場離奇詭異翰舌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)冬骚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門椅贱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人只冻,你說我怎么就攤上這事庇麦。” “怎么了喜德?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵山橄,是天一觀的道長。 經(jīng)常有香客問我舍悯,道長航棱,這世上最難降的妖魔是什么睡雇? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮饮醇,結(jié)果婚禮上入桂,老公的妹妹穿的比我還像新娘。我一直安慰自己驳阎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布馁蒂。 她就那樣靜靜地躺著呵晚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沫屡。 梳的紋絲不亂的頭發(fā)上饵隙,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機(jī)與錄音沮脖,去河邊找鬼金矛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛勺届,可吹牛的內(nèi)容都是我干的驶俊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼免姿,長吁一口氣:“原來是場噩夢啊……” “哼饼酿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胚膊,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤故俐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后紊婉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體药版,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年喻犁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了槽片。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡株汉,死狀恐怖筐乳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情乔妈,我是刑警寧澤蝙云,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站路召,受9級特大地震影響勃刨,放射性物質(zhì)發(fā)生泄漏波材。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一身隐、第九天 我趴在偏房一處隱蔽的房頂上張望廷区。 院中可真熱鬧,春花似錦贾铝、人聲如沸隙轻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玖绿。三九已至,卻和暖如春叁巨,著一層夾襖步出監(jiān)牢的瞬間斑匪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工锋勺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚀瘸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓庶橱,卻偏偏與公主長得像贮勃,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子悬包,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容