公司的項(xiàng)目里有拉取H.264視頻流進(jìn)行解碼播放的功能,本來是采用FFMpeg多媒體庫,用CPU做視頻的編碼和解碼,就是大家常說的軟編軟解。但是軟解存在太占用CPU,解碼效率低等缺點(diǎn),所以我們一合計(jì)干脆用硬解碼代替原來的方案。當(dāng)然硬件解碼使用的當(dāng)然就是蘋果大名鼎鼎的Video ToolBox
框架,眾所周知,蘋果在iOS8開始才可以在iOS系統(tǒng)中調(diào)用該框架中的API
。
Video ToolBox H.264解碼
iOS媒體接口結(jié)構(gòu).png
AVFoundation:
- 解壓視頻后直接播放
- 直接將視頻壓縮成文件
Video Toolbox:
- 將視頻解壓成 CVPixelBuffer
- 直接將視頻壓縮成CMSampleBuffer
Video ToolBox 數(shù)據(jù)結(jié)構(gòu)
- CVPixelBuffer:
typealias CVPixelBuffer = CVImageBuffer
,CVImageBuffer
是一種保存圖像數(shù)據(jù)的抽象類型,表示未經(jīng)編碼或解碼后的圖像數(shù)據(jù)結(jié)構(gòu)。
- CVPixelBufferPool:存放和管理
CVPixelBuffer
的數(shù)據(jù)結(jié)構(gòu)(具有回收循環(huán)利用的妙處)。
- pixelBufferAttributes - CFDictionary對(duì)象,一般包含了視頻的寬高,像素格式類型(
32RGBA, YCbCr420
),是否兼容OpenGL ES
,Core Animation
等相關(guān)信息
- CTime:分子是
64-bit
的時(shí)間值,分母是32-bit
的時(shí)標(biāo)(time scale
)。
CMVideoFormatDescription:視頻寬高,格式
(kCMPixelFormat_32RGBA, kCMVideoCodecType_H264)
, 其他諸如顏色空間等信息的擴(kuò)展。CMBlockBuffer:
CMBlockBuffer
是一個(gè)CFType
對(duì)象,表示數(shù)據(jù)偏移量的連續(xù)范圍。用來存放編碼后的數(shù)據(jù)。CMSampleBuffer:對(duì)于編碼后的數(shù)據(jù),包含了
CMTime
,CMVideoFormatDesc
和CMBlockBuffer
;對(duì)于解碼后的數(shù)據(jù),則包含了CMTime
,CMVideoFormatDesc
和CMPixelBuffer
。
圖1.1
- CMClock - 封裝了時(shí)間源,其中
CMClockGetHostTimeClock()
封裝了mach_absolute_time()
。 - CMTimebase - CMClock上的控制視圖。提供了時(shí)間的映射:
CMTimebaseSetTime(timebase, kCMTimeZero)
;速率控制:CMTimebaseSetRate(timebase, 1.0)
。
圖2.1展示的是通過AVSampleBufferDisplaylayer
播放網(wǎng)絡(luò)上獲取的H.264碼流。
圖2.1.png
但并不是說AVSampleBufferDisplaylayer
能直接播放H.264碼流,需要將H.264碼流包裝成SampleBuffer
傳給給AVSampleBufferDisplaylayer
解碼播放。
圖2.2.png
再來看一下H.264碼流的構(gòu)成,H.264碼流由一系列的NAL
單元組成。
NAL
單元一般包含:
- 視頻幀(或視頻幀片)
- H.264參數(shù)集
-序列參數(shù)集(Sequence Parameter Set(SPS)
)
-圖像參數(shù)集(Picture Parameter Set(PPS)
)
所以如果要將H.264解碼播放就需要將H.264碼流包裝成CMSampleBuffer。由圖1.1可得CMSampleBuffer = CMTime + CMVideoFormatDesc + CMBlockBuffer
。
解碼步驟:
1.從網(wǎng)絡(luò)獲取的碼流中獲取SPS和PPS生成CMVideoFormatDesc。
(1)H.264 NALU
單元的Start Code
是"0x 00 00 01" 或"0x 00 01",按照Start Code
定位NALU。
(2)通過類型信息找到SPS
和PPS
并提取,開始碼后第一個(gè)byte
的第5位,7代表SPS
,8代表PPS
。
(3)使用CMVideoFormatDescriptionCreateFromH264ParameterSets函數(shù)來構(gòu)建CMVideoFormatDescription。
// 設(shè)置H264Parameter
uint8_t* parameterSetPointers[2] = {sps, pps};
size_t parameterSetSizes[2] = {_spsSize-4, _ppsSize-4};
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2,
(const uint8_t *const*)parameterSetPointers,
parameterSetSizes, 4,
&_formatDesc);
2.提取視頻圖像數(shù)據(jù)生成CMBlockBuffer。
(1)按照Start Code
定位NALU。
(2)CMBlockBuffer數(shù)據(jù)需要的頭部碼為4個(gè)字節(jié)的長(zhǎng)度,為:0x 00 80 00,所以需要將H.264的header給替換掉。
//找到偏移量,或者SPS和PPS NALUs結(jié)束IDR幀NALU開始
int offset = (int)(_spsSize + _ppsSize);
blockLength = frameSize - offset;
data = malloc(blockLength);
data = memcpy(data, &frame[offset], blockLength);
//替換該NALU相應(yīng)長(zhǎng)度start code頭(AVCC format需要這樣)
// htonl 將數(shù)據(jù)類型轉(zhuǎn)換為unsigned int
uint32_t dataLength32 = htonl (blockLength - 4);
memcpy (data, &dataLength32, sizeof (uint32_t));
(3)CMBlockBufferCreateWithMemoryBlock
接口構(gòu)造CMBlockBufferRef
。
status = CMBlockBufferCreateWithMemoryBlock(NULL, data,
blockLength,
kCFAllocatorNull, NULL,
0,
blockLength,
0, &blockBuffer);
3.根據(jù)自己的需要設(shè)置CMTime
我的項(xiàng)目中的拉取的實(shí)時(shí)流需要實(shí)時(shí)播放,不需要設(shè)置時(shí)間間隔,所以不用設(shè)置CMTime。
4.根據(jù)上述得到CMVideoFormatDescriptionRef
、CMBlockBufferRef
和可選的時(shí)間信息,使用CMSampleBufferCreate
接口得到CMSampleBuffer
數(shù)據(jù)這個(gè)待解碼的原始的數(shù)據(jù)。
5.用AVSampleBufferDisplayLayer
處理得到sampleBuffer
來顯示圖像。
[_displayLayer enqueueSampleBuffer:sampleBuffer];
至此成功用Video Toolbox
硬件解碼H.264碼流,并在設(shè)備上播放視頻。
可是,如果我們要拿到每一幀圖像進(jìn)行處理呢,那該怎么得到?
那么我們還需要用VTDecompressionSession
解碼成CVPixelBuffer
,通過UIImageView
或者OpenGL ES
上顯示。
(1)創(chuàng)建VTDecompressionSession
,需要以下參數(shù):
-
CMVideoFormatDescription
(見上面的第(3)步) - 對(duì)所輸出數(shù)據(jù)的需求——
pixelBufferAttributes
- 解碼結(jié)果回調(diào)函數(shù)
VTDecompressionSessionOutputCallback
-(void) createDecompressionSession
{
//創(chuàng)建VTDecompressionSession
_decompressionSession = NULL;
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = decompressionSessionDecodeFrameCallback;
callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
NSDictionary* destinationPixelBufferAttributes = @{
(id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
//硬解必須是 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
// 或者是kCVPixelFormatType_420YpCbCr8Planar
//因?yàn)閕OS是 nv12 其他是nv21
(id)kCVPixelBufferWidthKey : [NSNumber numberWithInt:h264outputHeight*2],
(id)kCVPixelBufferHeightKey : [NSNumber numberWithInt:h264outputWidth*2],
//這里寬高和編碼反的
(id)kCVPixelBufferOpenGLCompatibilityKey : [NSNumber numberWithBool:YES]
};
OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,
_formatDesc,
NULL,
(__bridge CFDictionaryRef)destinationPixelBufferAttributes,
&callBackRecord,
&_decompressionSession);
if (status == noErr) {
NSLog(@"Video Decompression Session 創(chuàng)建成功!");
}else{
NSLog(@"Video Decompression Session 創(chuàng)建失敗,錯(cuò)誤碼: %d",(int)status);
}
}
(2)調(diào)用VTDecompresSessionDecodeFrame
接口進(jìn)行解碼。
VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression;
VTDecodeInfoFlags flagOut;
NSDate* currentTime = [NSDate date];
VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flags,
(void*)CFBridgingRetain(currentTime), &flagOut);
CFRelease(sampleBuffer);
(3)VTDecompressionSessionOutputCallback回調(diào)函數(shù)中可以得到解碼后的結(jié)果CVPixelBuffer
,可以將CVPixelBuffer
轉(zhuǎn)換成UIImage圖像顯示在ImageView上或者用OpenGL ES
渲染圖像。
void decompressionSessionDecodeFrameCallback(void *decompressionOutputRefCon,
void *sourceFrameRefCon,
OSStatus status,
VTDecodeInfoFlags infoFlags,
CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime presentationDuration)
{
if (status != noErr)
{
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
NSLog(@"解碼錯(cuò)誤: %@", error);
}
else
{
NSLog(@"解碼成功");
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
H264HWDecoder *decoder = (__bridge H264HWDecoder *)decompressionOutputRefCon;
if (decoder.delegate!=nil)
{
[decoder.delegate displayImageBuffer:imageBuffer];
}
}
}