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;
}
}
复制代码
此文章是本身学习音视频的笔记记录,也参考了网上不少的资料和文章,在这里推荐一下: