轉載:http://www.lxweimin.com/p/9e078dfabf0a
轉載時間:2017年4月6日
在此之前我們通常使用的FFmpeg多媒體庫,利用CPU來進行視頻的編解碼,占用CPU資源,效率低下,俗稱軟編解碼.而蘋果在2014年的iOS8中,開放了VideoToolbox.framwork框架,此框架使用GPU或專用的處理器來進行編解碼,俗稱硬編解碼.而此框架在此之前只有MAC
OS系統中可以使用,在iOS作為私有框架.終于蘋果在iOS8.0中得到開放引入.
2014年的WWDCDirect Access to Video Encoding and Decoding中,蘋果介紹了使用videoToolbox硬編解碼.
使用硬編解碼有幾個優點: * 提高性能; * 增加效率; * 延長電量的使用
(一)對于編解碼,AVFoundation框架只有以下幾個功能:
1. 直接解壓后顯示;
2. 直接壓縮到一個文件當中;
(二)而對于Video Toolbox,我們可以通過以下功能獲取到數據,進行網絡流傳輸等多種保存:
1. 解壓為圖像的數據結構;
2. 壓縮為視頻圖像的容器數據結構.
一、videoToolbox的基本數據
Video Toolbox視頻編解碼前后概念說明:
CVPixelBuffer:編碼前和解碼后的圖像數據結構。此內容包含一系列的CVPixelBufferPool內容
CMTime、CMClock和CMTimebase:時間戳相關。時間以64-bit/32-bit的形式出現。
pixelBufferAttributes:字典設置.可能包括Width/height、pixel format type、? Compatibility (e.g., OpenGL ES, Core Animation)
CMBlockBuffer:編碼后,結果圖像的數據結構。
CMVideoFormatDescription:圖像存儲方式,編解碼器等格式描述。
(CMSampleBuffer:存放編解碼前后的視頻圖像的容器數據結構。
CMTimebase: 關于CMClock的一個控制視圖,包含CMClock、時間映射(Time mapping)、速率控制(Rate control)
由二、采集視頻數據可知,我們獲取到的數據(CMSampleBufferRef)sampleBuffer為未編碼的數據;

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

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

5.1 先判斷壓縮的數據是否正確
//不存在則代表壓縮不成功或幀丟失if(!sampleBuffer)return;if(status
!=
noErr)return;//返回sampleBuffer中包括可變字典的不可變數組,如果有錯誤則為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從字面意思理解即為以上的說明,但是網上看到很多都是做為查詢是否是視頻關鍵幀,而查詢文檔看到有此關鍵幀key值kCMSampleBufferAttachmentKey_ForceKeyFrame存在,因此對此值如若有了解情況者敬請告知詳情.
5.2 獲取CMVideoFormatDesc數據由三、解碼篇可知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參數集合中的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并轉換成數據
CMBlockBufferRef
blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);? ? size_t
lengthAtOffset,totalLength;char*dataPointer;//接收到的數據展示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個字節到目標dest所指的內存地址的起始位置中
*/memcpy(&NALUnitLength,
dataPointer + bufferOffset, AVCCHeaderLength);//字節從高位反轉到低位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數據應用于后面的RTMP協議做推流準備。