VideoToolbox
是 Apple
在 iOS 8
以後推出的用於視頻硬編碼、解碼的工具庫。 平時所說的軟編解碼是指使用 ffmpeg
這個第三方庫去作編碼解碼。bash
通常在作音視頻應用開發的時候,咱們都是用 AVFoundation
去作原始數據採集的,使用前置攝像頭或者後置攝像頭採集視頻數據,使用麥克風採集音頻數據。app
在 AVCaptureVideoDataOutputSampleBufferDelegate
這個代理的async
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
複製代碼
回調方法裏面能夠獲取採集的視頻裸流信息。注意AVCaptureAudioDataOutputSampleBufferDelegate
音頻輸出的代理方法也是這個,那麼咱們如何區分究竟是音頻數據仍是視頻數據呢?這裏有兩種方案:ide
output
是 AVCaptureAudioDataOutput
(音頻) 仍是 AVCaptureVideoDataOutput
(視頻)connection
是 audioConnection
(音頻) 仍是 videoConnection
(視頻), 我本身在代碼裏使用屬性聲明瞭 audioConnection
和 videoConnection
。建立編碼會話函數
建立編碼會話的時候注意傳入了咱們的編碼回調函數 VideoEncodeCallback
,這個函數會屢次調用。工具
//建立編碼會話
OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, (int32_t)_config.width, (int32_t)_config.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoEncodeCallback, (__bridge void * _Nullable)(self), &_encodeSession);
if (status != noErr) {
NSLog(@"VTCompressionSession create failed. status=%d", (int)status);
return self;
} else {
NSLog(@"VTCompressionSession create success");
}
複製代碼
設置編碼會話屬性學習
//設置實時編碼
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
//指定編碼比特流的配置文件和級別。直播通常使用baseline,可減小因爲b幀帶來的延時
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
//設置碼率均值(比特率能夠高於此。默認比特率爲零,表示視頻編碼器。應該肯定壓縮數據的大小。注意,比特率設置只在定時時有效)
CFNumberRef bit = (__bridge CFNumberRef)@(_config.bitrate);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_AverageBitRate, bit);
//設置碼率上限
CFArrayRef limits = (__bridge CFArrayRef)@[@(_config.bitrate / 4), @(_config.bitrate * 4)];
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_DataRateLimits,limits);
//設置關鍵幀間隔(GOPSize)GOP太大圖像會模糊,越小圖像質量越高,固然數據量也會隨之變大
CFNumberRef maxKeyFrameInterval = (__bridge CFNumberRef)@(_config.fps * 2);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, maxKeyFrameInterval);
//設置fps(預期)
CFNumberRef expectedFrameRate = (__bridge CFNumberRef)@(_config.fps);
VTSessionSetProperty(_encodeSession, kVTCompressionPropertyKey_ExpectedFrameRate, expectedFrameRate);
複製代碼
準備編碼ui
OSStatus status = VTCompressionSessionPrepareToEncodeFrames(_encodeSession);
複製代碼
CMSampleBuffer
獲取原始圖像信息 CVImageBuffer
Presentation Time Stamp
,PTS 主要用於度量解碼後的視頻幀何時被顯示出來Decode Time Stamp
,DTS 主要是標識讀入內存中的比特流在何時開始送入解碼器中進行解碼duration
爲 kCMTimeInvalid
,表示會一直進行解碼VTEncodeInfoFlags
來記錄編碼信息VTCompressionSessionEncodeFrame()
開始解碼- (void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
CFRetain(sampleBuffer);
dispatch_async(_encodeQueue, ^{
// 幀數據
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// 該幀的顯示時間戳 (PTS: 用於視頻顯示的時間戳)
CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
//持續時間
CMTime duration = kCMTimeInvalid;
//編碼
VTEncodeInfoFlags flags;
OSStatus status = VTCompressionSessionEncodeFrame(self.encodeSesion, imageBuffer, presentationTimeStamp, duration, NULL, NULL, &flags);
if (status != noErr) {
NSLog(@"VTCompression: encode failed: status=%d",(int)status);
}
CFRelease(sampleBuffer);
});
}
複製代碼
這裏的
frameID
是咱們聲明的圖像幀的遞增序標識,每次編碼自增就能夠了。編碼
keyFrame
CMFormatDescriptionRef
, 處理 SPS 數據和 PPS 數據
bufferOffset
, 而後 while
循環讀取 NALU
數據,注意手動添加起始碼 "\x00\x00\x00\x01"
// startCode 長度 4
const Byte startCode[] = "\x00\x00\x00\x01";
//編碼成功回調
void VideoEncodeCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon,OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer ) {
if (status != noErr) {
NSLog(@"VideoEncodeCallback: encode error, status = %d", (int)status);
return;
}
if (!CMSampleBufferDataIsReady(sampleBuffer)) {
NSLog(@"VideoEncodeCallback: data is not ready");
return;
}
CCVideoEncoder *encoder = (__bridge CCVideoEncoder *)(outputCallbackRefCon);
//判斷是否爲關鍵幀
CFArrayRef attachArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
BOOL keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(attachArray, 0), kCMSampleAttachmentKey_NotSync);//(注意取反符號)
//獲取 sps & pps 數據 ,只需獲取一次,保存在h264文件開頭便可
if (keyFrame) {
//獲取圖像源格式
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
// 聲明 sps 數據大小, 個數 以及 數據內容, 先獲取 sps 數據
size_t spsSize, spsCount;
const uint8_t *spsData;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0);
if (spsStatus == noErr) {
// 聲明 pps 數據大小, 個數 以及 數據內容, 後獲取 pps 數據
size_t ppsSize, ppsCount;
const uint8_t *ppsData;
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0);
if (ppsStatus == noErr) {
NSLog(@"VideoEncodeCallback:got both sps and pps successfully");
encoder->hasSpsPps = true;
//sps data
NSMutableData *sps = [NSMutableData dataWithCapacity:4 + spsSize];
[sps appendBytes:startCode length:4];
[sps appendBytes:spsData length:spsSize];
//pps data
NSMutableData *pps = [NSMutableData dataWithCapacity:4 + ppsSize];
[pps appendBytes:startCode length:4];
[pps appendBytes:ppsData length:ppsSize];
dispatch_async(encoder.callbackQueue, ^{
//回調方法傳遞sps/pps
[encoder.delegate videoEncodeCallbacksps:sps pps:pps];
});
} else {
NSLog(@"VideoEncodeCallback: get pps failed ppsStatus=%d", (int)ppsStatus);
}
} else {
NSLog(@"VideoEncodeCallback: get sps failed spsStatus=%d", (int)spsStatus);
}
}
//獲取NALU數據
size_t lengthAtOffset, totalLength;
char *dataPoint;
// 從 CMSampleBuffer 獲取 DataBuffer, 將數據複製到 dataPoint
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
OSStatus dataBufferStatus = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPoint);
if (dataBufferStatus == noErr) {
// 循環獲取 nalu 數據
size_t bufferOffset = 0;
// 這個常量 4 是大端模式的幀長度length, 而不是 nalu 數據前四個字節(0001 的 startcode)
static const int headerLength = 4;
while (bufferOffset < totalLength - headerLength) {
uint32_t nalUnitLength = 0;
// 讀取 nalu 長度的數據
memcpy(&nalUnitLength, dataPoint + bufferOffset, headerLength);
// 大端轉系統端(iOS 的系統端是小端模式)
nalUnitLength = CFSwapInt32BigToHost(nalUnitLength);
// 獲取到編碼好的視頻數據
NSMutableData *data = [NSMutableData dataWithCapacity:4 + nalUnitLength];
// 先添加 startcode
[data appendBytes:startCode length:4];
// 再拼接 nalu 數據
[data appendBytes:dataPoint + bufferOffset + headerLength length:nalUnitLength];
//將NALU數據回調到代理中
dispatch_async(encoder.callbackQueue, ^{
[encoder.delegate videoEncodeCallback:data];
});
// 移動偏移量,繼續讀取下一個 NALU 數據
bufferOffset += headerLength + nalUnitLength;
}
} else {
NSLog(@"VideoEncodeCallback: get datapoint failed, status = %d", (int)dataBufferStatus);
}
}
複製代碼
編碼結束了之後,在合理的地方要釋放資源spa
- (void)releaseEncodeSession
{
if (_encodeSession) {
VTCompressionSessionCompleteFrames(_encodeSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(_encodeSession);
CFRelease(_encodeSession);
_encodeSession = NULL;
}
}
複製代碼
此文章是本身學習音視頻的筆記記錄,也參考了網上不少的資料和文章,在這裏推薦一下: