四、視頻的編解碼-編碼篇
時(shí)間?2016-08-05 10:22:59Twenty's 時(shí)間念
原文http://blog.img421.com/si-shi-pin-de-bian-jie-ma-bian-ma-pian/
在此之前我們通常使用的FFmpeg多媒體庫(kù),利用CPU來(lái)進(jìn)行視頻的編解碼,占用CPU資源,效率低下,俗稱軟編解碼.而蘋(píng)果在2014年的iOS8中,開(kāi)放了VideoToolbox.framwork框架,此框架使用GPU或?qū)S玫奶幚砥鱽?lái)進(jìn)行編解碼,俗稱硬編解碼.而此框架在此之前只有MAC OS系統(tǒng)中可以使用,在iOS作為私有框架.終于蘋(píng)果在iOS8.0中得到開(kāi)放引入.
2014年的WWDCDirect Access to Video Encoding and Decoding中,蘋(píng)果介紹了使用videoToolbox硬編解碼.
使用硬編解碼有幾個(gè)優(yōu)點(diǎn): * 提高性能; * 增加效率; * 延長(zhǎng)電量的使用
對(duì)于編解碼,AVFoundation框架只有以下幾個(gè)功能: 1. 直接解壓后顯示;
2. 直接壓縮到一個(gè)文件當(dāng)中;
而對(duì)于Video Toolbox,我們可以通過(guò)以下功能獲取到數(shù)據(jù),進(jìn)行網(wǎng)絡(luò)流傳輸?shù)榷喾N保存: 1. 解壓為圖像的數(shù)據(jù)結(jié)構(gòu);
2. 壓縮為視頻圖像的容器數(shù)據(jù)結(jié)構(gòu).
一、videoToolbox的基本數(shù)據(jù)
Video Toolbox視頻編解碼前后需要應(yīng)用的數(shù)據(jù)結(jié)構(gòu)進(jìn)行說(shuō)明。
CVPixelBuffer:編碼前和解碼后的圖像數(shù)據(jù)結(jié)構(gòu)。此內(nèi)容包含一系列的CVPixelBufferPool內(nèi)容
CMTime、CMClock和CMTimebase:時(shí)間戳相關(guān)。時(shí)間以64-bit/32-bit的形式出現(xiàn)。
pixelBufferAttributes:字典設(shè)置.可能包括Width/height、pixel format type、? Compatibility (e.g., OpenGL ES, Core Animation)
CMBlockBuffer:編碼后,結(jié)果圖像的數(shù)據(jù)結(jié)構(gòu)。
CMVideoFormatDescription:圖像存儲(chǔ)方式,編解碼器等格式描述。
(CMSampleBuffer:存放編解碼前后的視頻圖像的容器數(shù)據(jù)結(jié)構(gòu)。
CMClock
CMTimebase: 關(guān)于CMClock的一個(gè)控制視圖,包含CMClock、時(shí)間映射(Time mapping)、速率控制(Rate control)
由二、采集視頻數(shù)據(jù)可知,我們獲取到的數(shù)據(jù)(CMSampleBufferRef)sampleBuffer為未編碼的數(shù)據(jù);
圖1.1

上圖中,編碼前后的視頻圖像都封裝在CMSampleBuffer中,編碼前以CVPixelBuffer進(jìn)行存儲(chǔ);編碼后以CMBlockBuffer進(jìn)行存儲(chǔ)。除此之外兩者都包括CMTime、CMVideoFormatDesc.
二、視頻數(shù)據(jù)流編碼并上傳到服務(wù)器

1.將CVPixelBuffer使用VTCompressionSession進(jìn)行數(shù)據(jù)流的硬編碼。
(1)初始化VTCompressionSession
VT_EXPORT OSStatus VTCompressionSessionCreate(? ? CM_NULLABLE CFAllocatorRef? ? ? ? ? ? ? ? ? ? ? ? ? allocator,? ? int32_t? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? width,? ? int32_t? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? height,? ? CMVideoCodecType? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? codecType,? ? CM_NULLABLE CFDictionaryRef? ? ? ? ? ? ? ? ? ? ? ? encoderSpecification,? ? CM_NULLABLE CFDictionaryRef? ? ? ? ? ? ? ? ? ? ? ? sourceImageBufferAttributes,? ? CM_NULLABLE CFAllocatorRef? ? ? ? ? ? ? ? ? ? ? ? ? compressedDataAllocator,? ? CM_NULLABLE VTCompressionOutputCallback? ? ? ? ? ? outputCallback,? ? void * CM_NULLABLE? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? outputCallbackRefCon,? ? CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut)? ? __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_8_0);
VTCompressionSession的初始化參數(shù)說(shuō)明:
allocator:分配器,設(shè)置NULL為默認(rèn)分配
width: 寬
height: 高
codecType: 編碼類型,如kCMVideoCodecType_H264
encoderSpecification: 編碼規(guī)范。設(shè)置NULL由videoToolbox自己選擇
sourceImageBufferAttributes: 源像素緩沖區(qū)屬性.設(shè)置NULL不讓videToolbox創(chuàng)建,而自己創(chuàng)建
compressedDataAllocator: 壓縮數(shù)據(jù)分配器.設(shè)置NULL,默認(rèn)的分配
outputCallback: 當(dāng)VTCompressionSessionEncodeFrame被調(diào)用壓縮一次后會(huì)被異步調(diào)用.注:當(dāng)你設(shè)置NULL的時(shí)候,你需要調(diào)用VTCompressionSessionEncodeFrameWithOutputHandler方法進(jìn)行壓縮幀處理,支持iOS9.0以上
outputCallbackRefCon: 回調(diào)客戶定義的參考值.
compressionSessionOut: 壓縮會(huì)話變量。
(2)配置VTCompressionSession
使用VTSessionSetProperty()調(diào)用進(jìn)行配置compression。 * kVTCompressionPropertyKeyAllowFrameReordering: 允許幀重新排序.默認(rèn)為true * kVTCompressionPropertyKeyAverageBitRate: 設(shè)置需要的平均編碼率 * kVTCompressionPropertyKeyH264EntropyMode:H264的熵編碼模式。有兩種模式:一種基于上下文的二進(jìn)制算數(shù)編碼CABAC和可變長(zhǎng)編碼VLC.在slice層之上(picture和sequence)使用定長(zhǎng)或變長(zhǎng)的二進(jìn)制編碼,slice層及其以下使用VLC或CABAC.詳情請(qǐng)參考* kVTCompressionPropertyKeyRealTime: 視頻編碼壓縮是否是實(shí)時(shí)壓縮??稍O(shè)置CFBoolean或NULL.默認(rèn)為NULL * kVTCompressionPropertyKeyProfileLevel: 對(duì)于編碼流指定配置和標(biāo)準(zhǔn) .比如kVTProfileLevelH264MainAutoLevel
配置過(guò)VTCompressionSession后,可以可選的調(diào)用VTCompressionSessionPrepareToEncodeFrames進(jìn)行準(zhǔn)備工作編碼幀。
(3)開(kāi)始硬編碼流入的數(shù)據(jù)
使用VTCompressionSessionEncodeFrame方法進(jìn)行編碼.當(dāng)編碼結(jié)束后調(diào)用outputCallback回調(diào)函數(shù)。
VT_EXPORT OSStatus? VTCompressionSessionEncodeFrame(? ? ? CM_NONNULL VTCompressionSessionRef? session,? ? CM_NONNULL CVImageBufferRef? ? ? ? imageBuffer,? ? CMTime? ? ? ? ? ? ? ? ? ? ? ? ? ? ? presentationTimeStamp,? ? CMTime? ? ? ? ? ? ? ? ? ? ? ? ? ? ? duration,// may be kCMTimeInvalidCM_NULLABLE CFDictionaryRef? ? ? ? frameProperties,void* CM_NULLABLE? ? ? ? ? ? ? ? ? sourceFrameRefCon,? ? VTEncodeInfoFlags * CM_NULLABLE? ? infoFlagsOut )? ? __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_8_0);
presentationTimeStamp: 獲取到的這個(gè)sample buffer數(shù)據(jù)的展示時(shí)間戳。每一個(gè)傳給這個(gè)session的時(shí)間戳都要大于前一個(gè)展示時(shí)間戳.
duration: 對(duì)于獲取到sample buffer數(shù)據(jù),這個(gè)幀的展示時(shí)間.如果沒(méi)有時(shí)間信息,可設(shè)置kCMTimeInvalid.
frameProperties: 包含這個(gè)幀的屬性.幀的改變會(huì)影響后邊的編碼幀.
sourceFrameRefCon: 回調(diào)函數(shù)會(huì)引用你設(shè)置的這個(gè)幀的參考值.
infoFlagsOut: 指向一個(gè)VTEncodeInfoFlags來(lái)接受一個(gè)編碼操作.如果使用異步運(yùn)行,kVTEncodeInfo_Asynchronous被設(shè)置;同步運(yùn)行,kVTEncodeInfo_FrameDropped被設(shè)置;設(shè)置NULL為不想接受這個(gè)信息.
(4)執(zhí)行VTCompressionOutputCallback回調(diào)函數(shù)
typedefvoid(*VTCompressionOutputCallback)(void* CM_NULLABLE outputCallbackRefCon,void* CM_NULLABLE sourceFrameRefCon,? ? ? ? OSStatus status,? ? ? ? VTEncodeInfoFlags infoFlags,? ? ? ? CM_NULLABLE CMSampleBufferRef sampleBuffer );
outputCallbackRefCon: 回調(diào)函數(shù)的參考值
sourceFrameRefCon: VTCompressionSessionEncodeFrame函數(shù)中設(shè)置的幀的參考值
status: 壓縮的成功為noErr,如失敗有錯(cuò)誤碼
infoFlags: 包含編碼操作的信息標(biāo)識(shí)
sampleBuffer: 如果壓縮成功或者幀不丟失,則包含這個(gè)已壓縮的數(shù)據(jù)CMSampleBuffer,否則為NULL
(5)將壓縮成功的sampleBuffer數(shù)據(jù)進(jìn)行處理為基本流NSData上傳到服務(wù)器
MPEG-4是一套用于音頻、視頻信息的壓縮編碼標(biāo)準(zhǔn).
由圖1.1可知,已壓縮 $$CMSampleBuffer = CMTime(可選) + CMBlockBuffer + CMVideoFormatDesc$$。

5.1 先判斷壓縮的數(shù)據(jù)是否正確
//不存在則代表壓縮不成功或幀丟失if(!sampleBuffer)return;if(status != noErr)return;//返回sampleBuffer中包括可變字典的不可變數(shù)組,如果有錯(cuò)誤則為NULLCFArrayRefarray=? CMSampleBufferGetSampleAttachmentsArray(sampleBuffer,true);if(!array)return;? CFDictionaryRef dic = CFArrayGetValueAtIndex(array,0);if(!dic)return;//issue 3:kCMSampleAttachmentKey_NotSync:沒(méi)有這個(gè)鍵意味著同步, yes: 異步. no:同步BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);//此代表為同步
而對(duì)于issue 3從字面意思理解即為以上的說(shuō)明,但是網(wǎng)上看到很多都是做為查詢是否是視頻關(guān)鍵幀,而查詢文檔看到有此關(guān)鍵幀key值kCMSampleBufferAttachmentKey_ForceKeyFrame存在,因此對(duì)此值如若有了解情況者敬請(qǐng)告知詳情.
5.2 獲取CMVideoFormatDesc數(shù)據(jù)由三、解碼篇可知CMVideoFormatDesc 包括編碼所用的profile,level,圖像的寬和高,deblock濾波器等.具體包含第一個(gè)NALU的SPS(Sequence Parameter Set)和第二個(gè)NALU的PPS(Picture Parameter Set).
//if (keyframe && !encoder -> sps) {? ? //獲取sample buffer 中的 CMVideoFormatDesc? ? CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);? ? //獲取H264參數(shù)集合中的SPS和PPS? ? const uint8_t * sparameterSet;size_t sparameterSetSize,sparameterSetCount ;? OSStatus statusCode =? ? CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount,0);if (statusCode == noErr) {? ? ? ? size_t pparameterSetSize, pparameterSetCount;? ? ? ? const uint8_t *pparameterSet;OSStatus statusCode =? ? CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount,0);if (statusCode == noErr) {? ? ? ? ? ? encoder->sps = [NSData dataWithBytes:sparameterSetlength:sparameterSetSize];encoder->pps = [NSData dataWithBytes:pparameterSetlength:pparameterSetSize];}? ? }}
5.3 獲取CMBlockBuffer并轉(zhuǎn)換成數(shù)據(jù)
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);? ? size_t? lengthAtOffset,totalLength;char*dataPointer;//接收到的數(shù)據(jù)展示OSStatus blockBufferStatus = CMBlockBufferGetDataPointer(blockBuffer,0, &lengthAtOffset, &totalLength, &dataPointer);if(blockBufferStatus != kCMBlockBufferNoErr)? ? {? ? ? ? size_t bufferOffset =0;staticconstintAVCCHeaderLength =4;while(bufferOffset < totalLength -? AVCCHeaderLength) {// Read the NAL unit lengthuint32_t NALUnitLength =0;/**
*? void *memcpy(void *dest, const void *src, size_t n);
*? 從源src所指的內(nèi)存地址的起始位置開(kāi)始拷貝n個(gè)字節(jié)到目標(biāo)dest所指的內(nèi)存地址的起始位置中
*/memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);//字節(jié)從高位反轉(zhuǎn)到低位NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);? ? ? ? ? ? RTAVVideoFrame * frame = [RTAVVideoFramenew];? ? ? ? ? ? frame.sps = encoder -> sps;? ? ? ? ? ? frame.pps = encoder -> pps;? ? ? ? ? ? frame.data = [NSData dataWithBytes:(dataPointer+bufferOffset+AVCCHeaderLength) length:NALUnitLength];? ? ? ? ? ? bufferOffset += NALUnitLength + AVCCHeaderLength;? ? ? ? }? ? }
此得到的H264數(shù)據(jù)應(yīng)用于后面的RTMP協(xié)議做推流準(zhǔn)備。