四、視頻的編解碼-編碼篇
時間?2016-08-05 10:22:59Twenty's 時間念
原文http://blog.img421.com/si-shi-pin-de-bian-jie-ma-bian-ma-pian/
在此之前我們通常使用的FFmpeg多媒體庫,利用CPU來進行視頻的編解碼,占用CPU資源,效率低下,俗稱軟編解碼.而蘋果在2014年的iOS8中,開放了VideoToolbox.framwork框架,此框架使用GPU或專用的處理器來進行編解碼,俗稱硬編解碼.而此框架在此之前只有MAC OS系統(tǒng)中可以使用,在iOS作為私有框架.終于蘋果在iOS8.0中得到開放引入.
2014年的WWDCDirect Access to Video Encoding and Decoding中,蘋果介紹了使用videoToolbox硬編解碼.
使用硬編解碼有幾個優(yōu)點: * 提高性能; * 增加效率; * 延長電量的使用
對于編解碼,AVFoundation框架只有以下幾個功能: 1. 直接解壓后顯示;
2. 直接壓縮到一個文件當中;
而對于Video Toolbox,我們可以通過以下功能獲取到數(shù)據(jù),進行網(wǎng)絡流傳輸?shù)榷喾N保存: 1. 解壓為圖像的數(shù)據(jù)結構;
2. 壓縮為視頻圖像的容器數(shù)據(jù)結構.
一、videoToolbox的基本數(shù)據(jù)
Video Toolbox視頻編解碼前后需要應用的數(shù)據(jù)結構進行說明。
CVPixelBuffer:編碼前和解碼后的圖像數(shù)據(jù)結構。此內容包含一系列的CVPixelBufferPool內容
CMTime、CMClock和CMTimebase:時間戳相關。時間以64-bit/32-bit的形式出現(xiàn)。
pixelBufferAttributes:字典設置.可能包括Width/height、pixel format type、? Compatibility (e.g., OpenGL ES, Core Animation)
CMBlockBuffer:編碼后,結果圖像的數(shù)據(jù)結構。
CMVideoFormatDescription:圖像存儲方式,編解碼器等格式描述。
(CMSampleBuffer:存放編解碼前后的視頻圖像的容器數(shù)據(jù)結構。
CMClock
CMTimebase: 關于CMClock的一個控制視圖,包含CMClock、時間映射(Time mapping)、速率控制(Rate control)
由二、采集視頻數(shù)據(jù)可知,我們獲取到的數(shù)據(jù)(CMSampleBufferRef)sampleBuffer為未編碼的數(shù)據(jù);
圖1.1
上圖中,編碼前后的視頻圖像都封裝在CMSampleBuffer中,編碼前以CVPixelBuffer進行存儲;編碼后以CMBlockBuffer進行存儲。除此之外兩者都包括CMTime、CMVideoFormatDesc.
二、視頻數(shù)據(jù)流編碼并上傳到服務器
1.將CVPixelBuffer使用VTCompressionSession進行數(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ù)說明:
allocator:分配器,設置NULL為默認分配
width: 寬
height: 高
codecType: 編碼類型,如kCMVideoCodecType_H264
encoderSpecification: 編碼規(guī)范。設置NULL由videoToolbox自己選擇
sourceImageBufferAttributes: 源像素緩沖區(qū)屬性.設置NULL不讓videToolbox創(chuàng)建,而自己創(chuàng)建
compressedDataAllocator: 壓縮數(shù)據(jù)分配器.設置NULL,默認的分配
outputCallback: 當VTCompressionSessionEncodeFrame被調用壓縮一次后會被異步調用.注:當你設置NULL的時候,你需要調用VTCompressionSessionEncodeFrameWithOutputHandler方法進行壓縮幀處理,支持iOS9.0以上
outputCallbackRefCon: 回調客戶定義的參考值.
compressionSessionOut: 壓縮會話變量。
(2)配置VTCompressionSession
使用VTSessionSetProperty()調用進行配置compression。 * kVTCompressionPropertyKeyAllowFrameReordering: 允許幀重新排序.默認為true * kVTCompressionPropertyKeyAverageBitRate: 設置需要的平均編碼率 * kVTCompressionPropertyKeyH264EntropyMode:H264的熵編碼模式。有兩種模式:一種基于上下文的二進制算數(shù)編碼CABAC和可變長編碼VLC.在slice層之上(picture和sequence)使用定長或變長的二進制編碼,slice層及其以下使用VLC或CABAC.詳情請參考* kVTCompressionPropertyKeyRealTime: 視頻編碼壓縮是否是實時壓縮。可設置CFBoolean或NULL.默認為NULL * kVTCompressionPropertyKeyProfileLevel: 對于編碼流指定配置和標準 .比如kVTProfileLevelH264MainAutoLevel
配置過VTCompressionSession后,可以可選的調用VTCompressionSessionPrepareToEncodeFrames進行準備工作編碼幀。
(3)開始硬編碼流入的數(shù)據(jù)
使用VTCompressionSessionEncodeFrame方法進行編碼.當編碼結束后調用outputCallback回調函數(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: 獲取到的這個sample buffer數(shù)據(jù)的展示時間戳。每一個傳給這個session的時間戳都要大于前一個展示時間戳.
duration: 對于獲取到sample buffer數(shù)據(jù),這個幀的展示時間.如果沒有時間信息,可設置kCMTimeInvalid.
frameProperties: 包含這個幀的屬性.幀的改變會影響后邊的編碼幀.
sourceFrameRefCon: 回調函數(shù)會引用你設置的這個幀的參考值.
infoFlagsOut: 指向一個VTEncodeInfoFlags來接受一個編碼操作.如果使用異步運行,kVTEncodeInfo_Asynchronous被設置;同步運行,kVTEncodeInfo_FrameDropped被設置;設置NULL為不想接受這個信息.
(4)執(zhí)行VTCompressionOutputCallback回調函數(shù)
typedefvoid(*VTCompressionOutputCallback)(void* CM_NULLABLE outputCallbackRefCon,void* CM_NULLABLE sourceFrameRefCon,? ? ? ? OSStatus status,? ? ? ? VTEncodeInfoFlags infoFlags,? ? ? ? CM_NULLABLE CMSampleBufferRef sampleBuffer );
outputCallbackRefCon: 回調函數(shù)的參考值
sourceFrameRefCon: VTCompressionSessionEncodeFrame函數(shù)中設置的幀的參考值
status: 壓縮的成功為noErr,如失敗有錯誤碼
infoFlags: 包含編碼操作的信息標識
sampleBuffer: 如果壓縮成功或者幀不丟失,則包含這個已壓縮的數(shù)據(jù)CMSampleBuffer,否則為NULL
(5)將壓縮成功的sampleBuffer數(shù)據(jù)進行處理為基本流NSData上傳到服務器
MPEG-4是一套用于音頻、視頻信息的壓縮編碼標準.
由圖1.1可知,已壓縮 $$CMSampleBuffer = CMTime(可選) + CMBlockBuffer + CMVideoFormatDesc$$。
5.1 先判斷壓縮的數(shù)據(jù)是否正確
//不存在則代表壓縮不成功或幀丟失if(!sampleBuffer)return;if(status != noErr)return;//返回sampleBuffer中包括可變字典的不可變數(shù)組,如果有錯誤則為NULLCFArrayRefarray=? CMSampleBufferGetSampleAttachmentsArray(sampleBuffer,true);if(!array)return;? CFDictionaryRef dic = CFArrayGetValueAtIndex(array,0);if(!dic)return;//issue 3:kCMSampleAttachmentKey_NotSync:沒有這個鍵意味著同步, yes: 異步. no:同步BOOL keyframe = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);//此代表為同步
而對于issue 3從字面意思理解即為以上的說明,但是網(wǎng)上看到很多都是做為查詢是否是視頻關鍵幀,而查詢文檔看到有此關鍵幀key值kCMSampleBufferAttachmentKey_ForceKeyFrame存在,因此對此值如若有了解情況者敬請告知詳情.
5.2 獲取CMVideoFormatDesc數(shù)據(jù)由三、解碼篇可知CMVideoFormatDesc 包括編碼所用的profile,level,圖像的寬和高,deblock濾波器等.具體包含第一個NALU的SPS(Sequence Parameter Set)和第二個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并轉換成數(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個字節(jié)到目標dest所指的內存地址的起始位置中
*/memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);//字節(jié)從高位反轉到低位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ù)應用于后面的RTMP協(xié)議做推流準備。