音視頻學(xué)習(xí)從零到整--(1)
音視頻學(xué)習(xí)從零到整--(2)
音視頻學(xué)習(xí)從零到整--(3)
音視頻學(xué)習(xí)從零到整--(4)
音視頻學(xué)習(xí)從零到整--(6)
音視頻學(xué)習(xí)從零到整--(7)
音視頻學(xué)習(xí)從零到整--(8)
音視頻學(xué)習(xí)從零到整--(9)
音視頻學(xué)習(xí)從零到整--(10)
一.了解VideoToolBox 硬編碼
在iOS4.0,蘋果就已經(jīng)支持硬編解碼.但是硬編解碼在當(dāng)時屬于私有API. 不提供給開發(fā)者使用
在2014年的WWDC大會上,iOS 8.0 之后,蘋果開放了硬編解碼的API。就是VideoToolbox.framework的API。VideoToolbox 是一套純C語言API。其中包含了很多C語言函數(shù).VideoToolbox.framework 是基于Core Foundation庫函數(shù),基于C語言
VideoToolBox實際上屬于低級框架,它是可以直接訪問硬件編碼器和解碼器.它存在于視頻壓縮和解壓縮以及存儲在像素緩存區(qū)中的數(shù)據(jù)轉(zhuǎn)換提供服務(wù).
硬編碼的優(yōu)點
- 提高編碼性能(使用CPU的使用率大大降低,傾向使用GPU)
- 增加編碼效率(將編碼一幀的時間縮短)
- 延長電量使用(耗電量大大降低)
這個框架在音視頻項目開發(fā)中,也是會要頻繁使用的.如果大家有想法去從事音視頻的開發(fā).那么這個框架將會是你學(xué)習(xí)的一個重點.
VideoToolBox框架的流程
- 創(chuàng)建session
- 設(shè)置編碼相關(guān)參數(shù)
- 開始編碼
- 循環(huán)獲取采集數(shù)據(jù)
- 獲取編碼后數(shù)據(jù)
- 將數(shù)據(jù)寫入H264文件
1.1 編碼的輸入和輸出
在我們開始編碼工作之前,需要了解VideoToolBox進行編解碼的輸入輸出分別是什么? 只有了解了這個,我們才能清楚知道如何去向VideoToolBox添加數(shù)據(jù),并且如何獲取數(shù)據(jù).

如圖所示,左邊的三幀視頻幀是發(fā)送給編碼器之前的數(shù)據(jù),開發(fā)者必須將原始圖像數(shù)據(jù)封裝為CVPixelBuufer的數(shù)據(jù)結(jié)構(gòu).該數(shù)據(jù)結(jié)構(gòu)是使用VideoToolBox的核心.
Apple Developer CVPixelBuffer 官方文檔介紹
1.2 CVPixelBuffer 解析
在這個官方文檔的介紹中,CVPixelBuffer 給的官方解釋,是其主內(nèi)存存儲所有像素點數(shù)據(jù)的一個對象.那么什么是主內(nèi)存了?
其實它并不是我們平常所操作的內(nèi)存,它指的是存儲區(qū)域存在于緩存之中. 我們在訪問這個塊內(nèi)存區(qū)域,需要先鎖定這塊內(nèi)存區(qū)域.
//1.鎖定內(nèi)存區(qū)域:
CVPixelBufferLockBaseAddress(pixel_buffer,0);
//2.讀取該內(nèi)存區(qū)域數(shù)據(jù)到NSData對象中
Void *data = CVPixelBufferGetBaseAddress(pixel_buffer);
//3.數(shù)據(jù)讀取完畢后,需要釋放鎖定區(qū)域
CVPixelBufferRelease(pixel_buffer);
單純從它的使用方式,我們就可以知道這一塊內(nèi)存區(qū)域不是普通內(nèi)存區(qū)域.它需要加鎖,解鎖等一系列操作.
作為視頻開發(fā),盡量減少進行顯存和內(nèi)存的交換.所以在iOS開發(fā)過程中也要盡量減少對它的內(nèi)存區(qū)域訪問.建議使用iOS平臺提供的對應(yīng)的API來完成相應(yīng)的一系列操作.
在AVFoundation 回調(diào)方法中,它有提供我們的數(shù)據(jù)其實就是CVPixelBuffer.只不過當(dāng)時使用的是引用類型CVImageBufferRef,其實就是CVPixelBuffer的另外一個定義.
Camera 返回的CVImageBuffer 中存儲的數(shù)據(jù)是一個CVPixelBuffer,而經(jīng)過VideoToolBox編碼輸出的CMSampleBuffer中存儲的數(shù)據(jù)是一個CMBlockBuffer的引用.

在iOS中,會經(jīng)常使用到session的方式.比如我們使用任何硬件設(shè)備都要使用對應(yīng)的session,麥克風(fēng)就要使用AudioSession,使用Camera就要使用AVCaptureSession,使用編碼則需要使用VTCompressionSession.解碼時,要使用VTDecompressionSessionRef.
1.3 視頻編碼步驟分解
第一步: 使用VTCompressionSessionCreate方法,創(chuàng)建編碼會話;
//1.調(diào)用VTCompressionSessionCreate創(chuàng)建編碼session
//參數(shù)1:NULL 分配器,設(shè)置NULL為默認(rèn)分配
//參數(shù)2:width
//參數(shù)3:height
//參數(shù)4:編碼類型,如kCMVideoCodecType_H264
//參數(shù)5:NULL encoderSpecification: 編碼規(guī)范。設(shè)置NULL由videoToolbox自己選擇
//參數(shù)6:NULL sourceImageBufferAttributes: 源像素緩沖區(qū)屬性.設(shè)置NULL不讓videToolbox創(chuàng)建,而自己創(chuàng)建
//參數(shù)7:NULL compressedDataAllocator: 壓縮數(shù)據(jù)分配器.設(shè)置NULL,默認(rèn)的分配
//參數(shù)8:回調(diào) 當(dāng)VTCompressionSessionEncodeFrame被調(diào)用壓縮一次后會被異步調(diào)用.注:當(dāng)你設(shè)置NULL的時候,你需要調(diào)用VTCompressionSessionEncodeFrameWithOutputHandler方法進行壓縮幀處理,支持iOS9.0以上
//參數(shù)9:outputCallbackRefCon: 回調(diào)客戶定義的參考值
//參數(shù)10:compressionSessionOut: 編碼會話變量
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &cEncodeingSession);
第二步:?設(shè)置相關(guān)的參數(shù)
/*
session: 會話
propertyKey: 屬性名稱
propertyValue: 屬性值
*/
VT_EXPORT OSStatus
VTSessionSetProperty(
CM_NONNULL VTSessionRef session,
CM_NONNULL CFStringRef propertyKey,
CM_NULLABLE CFTypeRef propertyValue ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));
- ?
kVTCompressionPropertyKey_RealTime:設(shè)置是否實時編碼 -
kVTProfileLevel_H264_Baseline_AutoLevel:表示使用H264的Profile規(guī)格,可以設(shè)置Hight的AutoLevel規(guī)格. -
kVTCompressionPropertyKey_AllowFrameReordering:表示是否使用產(chǎn)生B幀數(shù)據(jù)(因為B幀在解碼是非必要數(shù)據(jù),所以開發(fā)者可以拋棄B幀數(shù)據(jù)) -
kVTCompressionPropertyKey_MaxKeyFrameInterval: 表示關(guān)鍵幀的間隔,也就是我們常說的gop size. -
kVTCompressionPropertyKey_ExpectedFrameRate: 表示設(shè)置幀率 -
kVTCompressionPropertyKey_AverageBitRate/kVTCompressionPropertyKey_DataRateLimits設(shè)置編碼輸出的碼率.
第三步: 準(zhǔn)備編碼
//開始編碼
VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);
第四步: 捕獲編碼數(shù)據(jù)
- 通過AVFoundation 捕獲的視頻,這個時候我們會走到AVFoundation捕獲結(jié)果代理方法:
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
//AV Foundation 獲取到視頻流
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
//開始視頻錄制,獲取到攝像頭的視頻幀,傳入encode 方法中
dispatch_sync(cEncodeQueue, ^{
[self encode:sampleBuffer];
});
}
第五步:數(shù)據(jù)編碼
- 將獲取的視頻數(shù)據(jù)編碼
- (void) encode:(CMSampleBufferRef )sampleBuffer
{
//拿到每一幀未編碼數(shù)據(jù)
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
//設(shè)置幀時間,如果不設(shè)置會導(dǎo)致時間軸過長。時間戳以ms為單位
CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
VTEncodeInfoFlags flags;
//參數(shù)1:編碼會話變量
//參數(shù)2:未編碼數(shù)據(jù)
//參數(shù)3:獲取到的這個sample buffer數(shù)據(jù)的展示時間戳。每一個傳給這個session的時間戳都要大于前一個展示時間戳.
//參數(shù)4:對于獲取到sample buffer數(shù)據(jù),這個幀的展示時間.如果沒有時間信息,可設(shè)置kCMTimeInvalid.
//參數(shù)5:frameProperties: 包含這個幀的屬性.幀的改變會影響后邊的編碼幀.
//參數(shù)6:ourceFrameRefCon: 回調(diào)函數(shù)會引用你設(shè)置的這個幀的參考值.
//參數(shù)7:infoFlagsOut: 指向一個VTEncodeInfoFlags來接受一個編碼操作.如果使用異步運行,kVTEncodeInfo_Asynchronous被設(shè)置;同步運行,kVTEncodeInfo_FrameDropped被設(shè)置;設(shè)置NULL為不想接受這個信息.
OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);
if (statusCode != noErr) {
NSLog(@"H.264:VTCompressionSessionEncodeFrame faild with %d",(int)statusCode);
VTCompressionSessionInvalidate(cEncodeingSession);
CFRelease(cEncodeingSession);
cEncodeingSession = NULL;
return;
}
NSLog(@"H264:VTCompressionSessionEncodeFrame Success");
}
第六步: 編碼數(shù)據(jù)處理-獲取SPS/PPS
當(dāng)編碼成功后,就會回調(diào)到最開始初始化編碼器會話時傳入的回調(diào)函數(shù),回調(diào)函數(shù)的原型如下:
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
- 判斷status,如果成功則返回0(noErr);成功則繼續(xù)處理,不成功則不處理.
-
判斷是否關(guān)鍵幀
- 為什么要判斷關(guān)鍵幀呢?
- 因為VideoToolBox編碼器在每一個關(guān)鍵幀前面都會輸出SPS/PPS信息.所以如果本幀未關(guān)鍵幀,則可以取出對應(yīng)的SPS/PPS信息.
//判斷當(dāng)前幀是否為關(guān)鍵幀
//獲取sps & pps 數(shù)據(jù) 只獲取1次,保存在h264文件開頭的第一幀中
//sps(sample per second 采樣次數(shù)/s),是衡量模數(shù)轉(zhuǎn)換(ADC)時采樣速率的單位
//pps()
if (keyFrame) {
//圖像存儲方式,編碼器等格式描述
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
//sps
size_t sparameterSetSize,sparameterSetCount;
const uint8_t *sparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
if (statusCode == noErr) {
//獲取pps
size_t pparameterSetSize,pparameterSetCount;
const uint8_t *pparameterSet;
//從第一個關(guān)鍵幀獲取sps & pps
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
//獲取H264參數(shù)集合中的SPS和PPS
if (statusCode == noErr)
{
//Found pps & sps
NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
if(encoder)
{
[encoder gotSpsPps:sps pps:pps];
}
}
}
}
第七步 編碼壓縮數(shù)據(jù)并寫入H264文件
當(dāng)我們獲取了SPS/PPS信息之后,我們就獲取實際的內(nèi)容來進行處理了
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length,totalLength;
char *dataPointer;
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
if (statusCodeRet == noErr) {
size_t bufferOffset = 0;
static const int AVCCHeaderLength = 4;//返回的nalu數(shù)據(jù)前4個字節(jié)不是001的startcode,而是大端模式的幀長度length
//循環(huán)獲取nalu數(shù)據(jù)
while (bufferOffset < totalLength - AVCCHeaderLength) {
uint32_t NALUnitLength = 0;
//讀取 一單元長度的 nalu
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
//從大端模式轉(zhuǎn)換為系統(tǒng)端模式
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
//獲取nalu數(shù)據(jù)
NSData *data = [[NSData alloc]initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
//將nalu數(shù)據(jù)寫入到文件
[encoder gotEncodedData:data isKeyFrame:keyFrame];
//move to the next NAL unit in the block buffer
//讀取下一個nalu 一次回調(diào)可能包含多個nalu數(shù)據(jù)
bufferOffset += AVCCHeaderLength + NALUnitLength;
}
}
}
//第一幀寫入 sps & pps
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
NSLog(@"gotSpsPp %d %d",(int)[sps length],(int)[pps length]);
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1;
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
[fileHandele writeData:ByteHeader];
[fileHandele writeData:sps];
[fileHandele writeData:ByteHeader];
[fileHandele writeData:pps];
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
NSLog(@"gotEncodeData %d",(int)[data length]);
if (fileHandele != NULL) {
//添加4個字節(jié)的H264 協(xié)議 start code 分割符
//一般來說編碼器編出的首幀數(shù)據(jù)為PPS & SPS
//H264編碼時,在每個NAL前添加起始碼 0x000001,解碼器在碼流中檢測起始碼,當(dāng)前NAL結(jié)束。
/*
為了防止NAL內(nèi)部出現(xiàn)0x000001的數(shù)據(jù),h.264又提出'防止競爭 emulation prevention"機制,在編碼完一個NAL時,如果檢測出有連續(xù)兩個0x00字節(jié),就在后面插入一個0x03。當(dāng)解碼器在NAL內(nèi)部檢測到0x000003的數(shù)據(jù),就把0x03拋棄,恢復(fù)原始數(shù)據(jù)。
總的來說H264的碼流的打包方式有兩種,一種為annex-b byte stream format 的格式,這個是絕大部分編碼器的默認(rèn)輸出格式,就是每個幀的開頭的3~4個字節(jié)是H264的start_code,0x00000001或者0x000001。
另一種是原始的NAL打包格式,就是開始的若干字節(jié)(1,2,4字節(jié))是NAL的長度,而不是start_code,此時必須借助某個全局的數(shù)據(jù)來獲得編 碼器的profile,level,PPS,SPS等信息才可以解碼。
*/
const char bytes[] ="\x00\x00\x00\x01";
//長度
size_t length = (sizeof bytes) - 1;
//頭字節(jié)
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
//寫入頭字節(jié)
[fileHandele writeData:ByteHeader];
//寫入H264數(shù)據(jù)
[fileHandele writeData:data];
}
}
推薦文集
* 抖音效果實現(xiàn)
* BAT—最新iOS面試題總結(jié)
* iOS面試題合集
原文作者:集才華美貌于一身的—C姐