為什么視頻可以壓縮編碼?
-
存在冗余信息
- 空間冗余:圖像相鄰像素之間有較強(qiáng)的相關(guān)性
- 時(shí)間冗余:視頻序列的相鄰圖像之間內(nèi)容相似
- 視覺冗余:人的視覺系統(tǒng)對(duì)某些細(xì)節(jié)不敏感
- 其他冗余信息
-
空間冗余
- 同一張圖像中茬暇,有很多像素點(diǎn)表示的信息是完全一樣的
- 如果對(duì)每一個(gè)像素進(jìn)行單獨(dú)的存儲(chǔ)舅世,必然會(huì)非常浪費(fèi)空間辛慰,也完全沒有必要
-
時(shí)間冗余
- 多張圖像之間螺垢,有非常多的相關(guān)性尖昏,由于一些小運(yùn)動(dòng)造成了細(xì)小差別
- 如果對(duì)每張圖像進(jìn)行單獨(dú)的像素存儲(chǔ)瓷翻,在下一張圖片中又出現(xiàn)了相同的聚凹。那么相當(dāng)于很多像素都存儲(chǔ)了多份,必然會(huì)非常浪費(fèi)空間齐帚,也是完全沒有必要的
-
視覺冗余
- 人類視覺系統(tǒng)HVS(Human Visual System)
- 對(duì)高頻信息不敏感
- 對(duì)高對(duì)比度更敏感
- 對(duì)亮度信息比色度信息更敏感
- 對(duì)運(yùn)動(dòng)的信息更敏感
- 數(shù)字視頻系統(tǒng)的設(shè)計(jì)應(yīng)該考慮HVS的特點(diǎn):
- 丟棄高頻信息妒牙,只編碼低頻信息
- 提高邊緣信息的主觀質(zhì)量
- 降低色度的解析度
- 對(duì)感興趣區(qū)域(Region of Interesting,ROI)進(jìn)行特殊處理
- 人類視覺系統(tǒng)HVS(Human Visual System)
壓縮編碼的標(biāo)準(zhǔn)
- ITU:International Telecommunications Union VECG:Video Coding Experts Group(國際電傳視訊聯(lián)盟)
- ISO:International Standards Organization MPEG:Motion Picture Experts Group(國際標(biāo)準(zhǔn)組織機(jī)構(gòu))
ios8.0 之后 使用VideoToolBox框架
流程:
- 采集
- 獲取到視頻幀
- 對(duì)視頻幀進(jìn)行編碼
- 獲取到視頻幀信息
- 將編碼后的數(shù)據(jù)以NALU方式寫入到文件
視頻采集
視頻硬件編碼
-
初始化壓縮編碼會(huì)話(VTCompressionSessionRef)
- 在VideoToolbox框架的使用過程中对妄,基本都是C語言函數(shù)
-
初始化后通過VTSessionSetProperty設(shè)置對(duì)象屬性
- 編碼方式:H.264編碼
- 幀率:每秒鐘多少幀畫面
- 碼率:?jiǎn)挝粫r(shí)間內(nèi)保存的數(shù)據(jù)量
- 關(guān)鍵幀(GOPsize)間隔:多少幀為一個(gè)GOP
準(zhǔn)備編碼
- (void)prepareEncodeWithWidth:(int)width height:(int)height{
//0 定義幀的下標(biāo)值
frameIndex = 0;
//1.創(chuàng)建VTCompressionSessionRef 對(duì)象
//1.創(chuàng)建VTCompressionSessionRef 對(duì)象
// 參數(shù)一: CoreFoundation 創(chuàng)建對(duì)象的方式 湘今,NULL -> Default
// 參數(shù)二:編碼的視頻寬度
// 參數(shù)三: 編碼的視頻高度
// 參數(shù)四: 編碼的標(biāo)準(zhǔn) H.264/ H.265
// 參數(shù)五 ~ 參數(shù)七 NULL
// 參數(shù)八: 編碼成功一幀數(shù)據(jù)后的函數(shù)回調(diào)
// 參數(shù)九: 回調(diào)函數(shù)的第一個(gè)參數(shù)
// VTCompressionSessionRef session;
VTCompressionSessionCreate(kCFAllocatorDefault,
width, height, kCMVideoCodecType_H264,
NULL, NULL, NULL,
compressionCallback, (__bridge void * _Nullable)(self),
&_session);
//2.設(shè)置VTCompressionSessionRef 屬性
// 2.1 如果是直播,需要設(shè)置視頻編碼是實(shí)時(shí)輸出
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_RealTime, (__bridge CFTypeRef _Nullable)(@YES));
// 2.2 設(shè)置幀率 (16/24/30)
// 幀/s
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef _Nullable)(@30));
//2.3 設(shè)置比特率 (碼率) bit/s 單位時(shí)間的數(shù)據(jù)量
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef _Nullable)(@(1500000))); // bit
CFArrayRef dataLimits = (__bridge CFArrayRef)(@[@(1500000/8),@1]); //byte
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_DataRateLimits, dataLimits);
// 2.4 設(shè)置GOP的大小
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nullable)(@(20)));
//3.準(zhǔn)備開始編碼
VTCompressionSessionPrepareToEncodeFrames(self.session);
}
總結(jié)一下常用設(shè)置屬性:
kVTCompressionPropertyKey_RealTime
編碼是否實(shí)時(shí)輸出kVTCompressionPropertyKey_ExpectedFrameRate
幀率剪菱,也就是一秒輸出多少幀圖像摩瞎,16張就可以形成動(dòng)畫,一般默認(rèn)30kVTCompressionPropertyKey_AverageBitRate
碼率孝常,一般是單位時(shí)間內(nèi)的數(shù)據(jù)量 必須同時(shí)設(shè)置kVTCompressionPropertyKey_DataRateLimits
kVTCompressionPropertyKey_MaxKeyFrameInterval
GOP,默認(rèn)設(shè)置20開始編碼
- (void)encodeFrame:(CMSampleBufferRef)sampleBuffer{
//1.從CMSampleBufferRef 中獲取 CVImageBufferRef
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
//利用 VTCompressionSessionRef 編碼 CMSampleBufferRef
//pts(presentationTimeStamp):展示時(shí)間戳旗们,用來解碼時(shí),計(jì)算每一幀時(shí)間的
//dts(DecodeTimeStamp): 解碼時(shí)間戳,決定該幀在什么時(shí)間展示
frameIndex ++;
// 第幾幀 幀率
CMTime pts = CMTimeMake(frameIndex, 30);
VTCompressionSessionEncodeFrame(self.session,
imageBuffer, pts, kCMTimeInvalid, NULL, NULL, NULL);
}
CMSampleBuffer = CMTime(時(shí)間戳) +CMVideoFormatDesc(圖片存儲(chǔ)方式) + CMBlockBuffer(編碼后的數(shù)據(jù))
- 編碼成功一幀數(shù)據(jù)后的函數(shù)回調(diào)
void compressionCallback(void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer){
// 0 獲取到當(dāng)前對(duì)象
H264Encoder *encoder = (__bridge H264Encoder *)(outputCallbackRefCon);
// 1.CMSampleBufferRef
// 2.判斷該幀是否是關(guān)鍵幀
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0);
BOOL iskeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
// 3. 如果是關(guān)鍵幀构灸,那么將關(guān)鍵幀寫入文件之前上渴,先寫入 PPS / SPS數(shù)據(jù)
if (iskeyFrame) {
//3.1 獲取參數(shù)信息
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
//3.2 從format 中獲取sps信息
//
//參數(shù)二 : sps 0 pps 1
//參數(shù)三
const uint8_t *spsPointer;
size_t spsSize,spsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &spsPointer, &spsSize, &spsCount, NULL);
//3.3 從format 中獲取pps信息
const uint8_t *ppsPointer;
size_t ppsSize,ppsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &ppsPointer, &ppsSize, &ppsCount, NULL);
// 3.4 將sps/pps 寫入 NAL單元
NSData *spsData = [NSData dataWithBytes:spsPointer length:spsSize];
NSData *ppsData = [NSData dataWithBytes:ppsPointer length:ppsSize];
[encoder writeData:spsData];
[encoder writeData:ppsData];
}
// 4.將編碼后的數(shù)據(jù)寫入文件
// 4.1 獲取CMSampleBufferRef
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
// 4.2 CMSampleBufferRef獲取內(nèi)存地址/長(zhǎng)度
size_t totalLength;
char *dataPointer;
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totalLength, &dataPointer);
// 4.3 從dataPointer開始讀取數(shù)據(jù),并且寫入NALU -> slice
static const int h264HeaderLength = 4;
size_t offsetLength = 0;
// 4.4 通過循環(huán),不斷的讀取slice的切片數(shù)據(jù)驰贷,并且封裝成NALU 寫入文件
while (offsetLength < totalLength - h264HeaderLength) {
// 4.5 讀取slice的長(zhǎng)度
uint32_t naluLength;
memcpy(&naluLength, dataPointer+offsetLength, h264HeaderLength);
// 4.6 H264 大端字節(jié)序/ 小端字節(jié)序
naluLength = CFSwapInt32BigToHost(naluLength);
// 4.7 根據(jù)長(zhǎng)度讀取字節(jié),并轉(zhuǎn)成NSData
NSData *data = [NSData dataWithBytes:dataPointer+offsetLength+h264HeaderLength length:naluLength];
//4.8 寫入文件
[encoder writeData:data];
//4.9 設(shè)置offsetLength
offsetLength += naluLength + h264HeaderLength;
}
}
需要注意的一點(diǎn)是,編碼后的數(shù)據(jù)需要通過切片的方式讀取數(shù)據(jù)洛巢,h264已經(jīng)提供好了切片后的數(shù)據(jù)括袒,并且默認(rèn)使用4個(gè)字節(jié)提供每一個(gè)切片的數(shù)據(jù)的長(zhǎng)度,在寫入文件時(shí)候是不能包括這個(gè)4字節(jié)長(zhǎng)度的稿茉。
- 寫入數(shù)據(jù)
- (void)writeData:(NSData *)data{
// NALU 的形式寫入
// NALU 頭 0x 表示 16進(jìn)制的某個(gè)數(shù)字 x 表示16進(jìn)制的某個(gè)字節(jié)
const char bytes[] = "\x00\x00\x00\x01";
int headerLength = sizeof(bytes) - 1;
NSData *headerData = [NSData dataWithBytes:bytes length:headerLength];
// NALU 體
[self.fileHandle writeData:headerData];
[self.fileHandle writeData:data];
}
- 結(jié)束編碼
- (void)endEncoding{
VTCompressionSessionInvalidate(self.session);
CFRelease(self.session);
}