視頻采集
視頻采集部分,上一篇文章已經(jīng)提及
iOS視頻采集
視頻編碼
首先初始化編碼器
- (void)initEncoder{
//視頻寬、高、幀率、碼率
int width = 1280, height = 720, FPS = 25, bitrate = 2048;
//默認(rèn)H264編碼,H265編碼需要iOS 11
encoderType = kCMVideoCodecType_H264;
// if (@available(iOS 11.0, *)) {
// if ([[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality]){
// ///是否支持H265
// encoderType = kCMVideoCodecType_HEVC;
// }
// }
lock = [[NSLock alloc] init];
[lock lock];
//創(chuàng)建session
VTCompressionSessionRef session;
OSStatus status = VTCompressionSessionCreate(NULL,
width,
height,
encoderType,
NULL,
NULL,
NULL,
videoEncoderCallBack,
(__bridge void*)self,
&session);
if (status == noErr) {
}else{
NSLog(@"創(chuàng)建session失敗");
}
///設(shè)置session屬性
// 設(shè)置實(shí)時(shí)編碼輸出(避免延遲)
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
if (encoderType == kCMVideoCodecType_H264) {
if ((YES)/*支持實(shí)時(shí)編碼*/) {
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Main_AutoLevel);
}else{
//表示使用H264的Profile規(guī)格,可以設(shè)置Hight的AutoLevel規(guī)格
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
//若是支持h264該屬性設(shè)置編碼器是否應(yīng)該使用基于CAVLC 或是 CABAC
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CAVLC);
}
}else if (encoderType == kCMVideoCodecType_HEVC){
///H265
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_HEVC_Main_AutoLevel);
}
// 設(shè)置關(guān)鍵幀(GOPsize)間隔
int frameInterval = 10;
CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
// 設(shè)置期望幀率
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &FPS);
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
//設(shè)置碼率,上限,單位是bps
int bitRate = width * height * 3 * 4 * 8;
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
//設(shè)置碼率,均值,單位是byte
int bitRateLimit = width * height * 3 * 4;
CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRateLimit);
status = VTSessionSetProperty(session, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
//告訴編碼器開始編碼
status = VTCompressionSessionPrepareToEncodeFrames(session);
[lock unlock];
if (status != noErr) {
NSLog(@"失敗,需要排查問題和參數(shù)");
}else{
videoSession = session;
}
}
傳入需要編碼的數(shù)據(jù)
/// 傳入需要編碼的數(shù)據(jù)進(jìn)行編碼
/// @param sampleBuffer 需要編碼的原始數(shù)據(jù)
/// @param isNeedFreeBuffer 是否需要釋放,如果自己組裝的,需要手動(dòng)釋放,系統(tǒng)返回的不需要
- (void)startEncodeWithSampleBuffer:(CMSampleBufferRef)sampleBuffer isNeedFreeBuffer:(BOOL)isNeedFreeBuffer{
[lock lock];
//第一幀必須是iframe,然后創(chuàng)建引用的時(shí)間戳
static BOOL isFirstFrame = YES;
if (isFirstFrame && encode_capture_base_time == 0) {
CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
encode_capture_base_time = CMTimeGetSeconds(pts);
isFirstFrame = NO;
}
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CMTime presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
// 切換不同源數(shù)據(jù)會(huì)顯示馬賽克,因?yàn)闀r(shí)間戳不同步
static int64_t lastPts = 0;
int64_t currentPts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
lastPts = currentPts;
OSStatus status = noErr;
//如果編碼中途進(jìn)行其他操作,這時(shí)候強(qiáng)制添加關(guān)鍵幀
BOOL needForceInsertKeyFrame = NO;//是否需要強(qiáng)制插入關(guān)鍵幀
NSDictionary *properties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame:@(needForceInsertKeyFrame)};
//編碼
status = VTCompressionSessionEncodeFrame(videoSession,
imageBuffer,
presentationTimeStamp,
kCMTimeInvalid,
(__bridge CFDictionaryRef)properties,
NULL,
NULL);
if (status != noErr) {
[self handleEncodeFailedWithIsNeedFreeBuffer:isNeedFreeBuffer sampleBuffer:sampleBuffer];
}
[lock unlock];
if (isNeedFreeBuffer) {
if (sampleBuffer != NULL) {
CFRelease(sampleBuffer);
}
}
}
編碼回調(diào)
#pragma mark - callback -
static void videoEncoderCallBack(void *outputCallbackRefCon,
void *souceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer) {
(當(dāng)前類)*encoder = (__bridge (當(dāng)前類)*)outputCallbackRefCon;
if (status != noErr) {
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
NSLog(@"編碼返回失敗 %@", error);
return;
}
CMBlockBufferRef block = CMSampleBufferGetDataBuffer(sampleBuffer);
CMTime pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
CMTime dts = CMSampleBufferGetDecodeTimeStamp(sampleBuffer);
// 利用我們定義的時(shí)間。(此時(shí)間用于同步音頻和視頻)
int64_t ptsAfter = (int64_t)((CMTimeGetSeconds(pts) - encode_capture_base_time) * 1000);
int64_t dtsAfter = (int64_t)((CMTimeGetSeconds(dts) - encode_capture_base_time) * 1000);
dtsAfter = ptsAfter;
/*有時(shí)相對(duì)DTS為零,提供一個(gè)恢復(fù)DTS的工作區(qū)*/
static int64_t last_dts = 0;
if(dtsAfter == 0){
dtsAfter = last_dts +33;
}else if (dtsAfter == last_dts){
dtsAfter = dtsAfter + 1;
}
BOOL isKeyFrame = NO;
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
if(attachments != NULL) {
CFDictionaryRef attachment =(CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFBooleanRef dependsOnOthers = (CFBooleanRef)CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
isKeyFrame = (dependsOnOthers == kCFBooleanFalse);
}
if(isKeyFrame) {
static uint8_t *keyParameterSetBuffer = NULL;
static size_t keyParameterSetBufferSize = 0;
// 注意:如果視頻分辨率不改變,NALU頭不會(huì)改變。
if (keyParameterSetBufferSize == 0 || YES == encoder->needResetKeyParamSetBuffer) {
const uint8_t *vps, *sps, *pps;
size_t vpsSize, spsSize, ppsSize;
int NALUnitHeaderLengthOut;
size_t parmCount;
if (keyParameterSetBuffer != NULL) {
free(keyParameterSetBuffer);
}
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
if (encoder->encoderType == kCMVideoCodecType_H264) {
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &parmCount, &NALUnitHeaderLengthOut);
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &parmCount, &NALUnitHeaderLengthOut);
keyParameterSetBufferSize = spsSize+4+ppsSize+4;
keyParameterSetBuffer = (uint8_t*)malloc(keyParameterSetBufferSize);
memcpy(keyParameterSetBuffer, "\x00\x00\x00\x01", 4);
memcpy(&keyParameterSetBuffer[4], sps, spsSize);
memcpy(&keyParameterSetBuffer[4+spsSize], "\x00\x00\x00\x01", 4);
memcpy(&keyParameterSetBuffer[4+spsSize+4], pps, ppsSize);
NSLog(@ "H264 find IDR frame, spsSize : %zu, ppsSize : %zu",spsSize, ppsSize);
}else if (encoder->encoderType == kCMVideoCodecType_HEVC) {
CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 0, &vps, &vpsSize, &parmCount, &NALUnitHeaderLengthOut);
CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 1, &sps, &spsSize, &parmCount, &NALUnitHeaderLengthOut);
CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 2, &pps, &ppsSize, &parmCount, &NALUnitHeaderLengthOut);
keyParameterSetBufferSize = vpsSize+4+spsSize+4+ppsSize+4;
keyParameterSetBuffer = (uint8_t*)malloc(keyParameterSetBufferSize);
memcpy(keyParameterSetBuffer, "\x00\x00\x00\x01", 4);
memcpy(&keyParameterSetBuffer[4], vps, vpsSize);
memcpy(&keyParameterSetBuffer[4+vpsSize], "\x00\x00\x00\x01", 4);
memcpy(&keyParameterSetBuffer[4+vpsSize+4], sps, spsSize);
memcpy(&keyParameterSetBuffer[4+vpsSize+4+spsSize], "\x00\x00\x00\x01", 4);
memcpy(&keyParameterSetBuffer[4+vpsSize+4+spsSize+4], pps, ppsSize);
NSLog(@ "H265 find IDR frame, vpsSize : %zu, spsSize : %zu, ppsSize : %zu",vpsSize,spsSize, ppsSize);
}
encoder->needResetKeyParamSetBuffer = NO;
}
// 是否是關(guān)鍵幀 NO
// 是否是額外數(shù)據(jù) YES
// keyParameterSetBuffer 就是編碼后的數(shù)據(jù)
// keyParameterSetBufferSize 就是編碼后數(shù)據(jù)的size
// dtsAfter timeStamp
// 回調(diào)數(shù)據(jù)的地方 (2-1)
NSLog(@"視頻編碼,加載 I 幀");
}
size_t blockBufferLength;
uint8_t *bufferDataPointer = NULL;
CMBlockBufferGetDataPointer(block, 0, NULL, &blockBufferLength, (char **)&bufferDataPointer);
size_t bufferOffset = 0;
while (bufferOffset < blockBufferLength - kStartCodeLength)
{
uint32_t NALUnitLength = 0;
memcpy(&NALUnitLength, bufferDataPointer+bufferOffset, kStartCodeLength);
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
memcpy(bufferDataPointer+bufferOffset, kStartCode, kStartCodeLength);
bufferOffset += kStartCodeLength + NALUnitLength;
}
// 是否是關(guān)鍵幀 isKeyFrame
// 是否是額外數(shù)據(jù) NO
// bufferDataPointer 就是編碼后的數(shù)據(jù)
// blockBufferLength 就是編碼后數(shù)據(jù)的size
// dtsAfter timeStamp
// 回調(diào)數(shù)據(jù)的地方 (2-2)
last_dts = dtsAfter;
}
其他參數(shù):
{
VTCompressionSessionRef videoSession;
NSLock *lock;
///是否需要重置
BOOL needResetKeyParamSetBuffer;
///編碼方式
CMVideoCodecType encoderType;
}
static const size_t kStartCodeLength = 4;
static const uint8_t kStartCode[] = {0x00, 0x00, 0x00, 0x01};
uint32_t encode_capture_base_time = 0;
Demo地址整理后奉上。
有其他不明白的,可以留言,看到就會(huì)回復(fù)。
如果喜歡,請(qǐng)幫忙點(diǎn)贊。支持轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)附原文鏈接。