我们下面完成获取摄像头数据,并保存为h264格式的文件。
1、初始化获取摄像头数据相关代码
{
//创建AVCaptureDevice的视频设备对象
AVCaptureDevice* videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError* error;
//创建视频输入端对象
AVCaptureDeviceInput* input = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if (error) {
NSLog(@"创建输入端失败,%@",error);
return;
}
//创建功能会话对象
self.captureSession = [[AVCaptureSession alloc] init];
//设置会话输出的视频分辨率
[self.captureSession setSessionPreset:AVCaptureSessionPreset1280x720];
//添加输入端
if (![self.captureSession canAddInput:input]) {
NSLog(@"输入端添加失败");
return;
}
[self.captureSession addInput:input];
//显示摄像头捕捉到的数据
AVCaptureVideoPreviewLayer* layer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
layer.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 100);
[self.view.layer addSublayer:layer];
//创建输出端
AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
//会话对象添加输出端
if ([self.captureSession canAddOutput:videoDataOutput]) {
[self.captureSession addOutput:videoDataOutput];
self.videoDataOutput = videoDataOutput;
//创建输出调用的队列
dispatch_queue_t videoDataOutputQueue = dispatch_queue_create("videoDataOutputQueue", DISPATCH_QUEUE_SERIAL);
self.videoDataOutputQueue = videoDataOutputQueue;
//设置代理和调用的队列
[self.videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue];
//设置延时丢帧
self.videoDataOutput.alwaysDiscardsLateVideoFrames = NO;
}
}
然后我们实行代理方法
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
在代理方法里面可以获取到我们需要的CMSampleBufferRef对象。
我们调用会话的开始方法就可以获取到视频数据了。
[self.captureSession startRunning];
获取到CMSampleBufferRef对象后,我们对CMSampleBufferRef对象进行处理。
2、创建硬解码VideoToolBox会话对象
根据苹果文档,我们可以知道摄像头返回的数据为CVPixelBuffers数据,需要我们进行手动压缩为H264数据。
在此,我们使用VideoToolBox对数据进行压缩,我们也可以使用x264库对数据进行压缩。使用VideoToolBox可以进行硬编码,性能更优,使用x264则兼容性更高。
VideoToolBox为一套C语言函数库,使用相对有点复杂。流程如下:
{
self.frameID = 0;
int width = (int)self.width;
int height = (int)self.height;
//创建编码会话对象
//第八个参数为回调函数的函数名
OSStatus status = VTCompressionSessionCreate(NULL,
width,
height,
kCMVideoCodecType_H264,
NULL,
NULL,
NULL,
didCompressH264,
(__bridge void *)(self),
&self->_EncodingSession
);
NSLog(@"H264: VTCompressionSessionCreate %d", (int)status);
if (status != 0) {
NSLog(@"H264: Unable to create a H264 session");
self.EncodingSession = NULL;
return ;
}
// 设置实时编码输出(避免延迟)
VTSessionSetProperty(self->_EncodingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
// h264 profile, 直播一般使用baseline,可减少由于b帧带来的延时
VTSessionSetProperty(self->_EncodingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// 设置关键帧(GOPsize)间隔
int frameInterval = (int)(self.frameRate / 2);
CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
VTSessionSetProperty(self->_EncodingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
// 设置期望帧率
int fps = (int)self.frameRate;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(self->_EncodingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
//不产生B帧
VTSessionSetProperty(self->_EncodingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
// 设置编码码率(比特率),如果不设置,默认将会以很低的码率编码,导致编码出来的视频很模糊
// 设置码率,上限,单位是bps
int bitRate = width * height * 3 * 4 * 8;
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
VTSessionSetProperty(self->_EncodingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
// 设置码率,均值,单位是byte
int bitRateLimit = width * height * 3 * 4;
CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRateLimit);
VTSessionSetProperty(self->_EncodingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
// Tell the encoder to start encoding
status = VTCompressionSessionPrepareToEncodeFrames(self->_EncodingSession);
if (status == 0) {
self.initComplete = YES;
}else {
NSLog(@"init compression session prepare to encode frames failure");
}
}
3、实现回调函数
我们在第二步中注册了编码回调的函数名,我们实行回调函数:
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
//在此方法中处理获取H264编码后的数据
}
4、编码数据
把获取的sampleBuffer进行编码
- (void)encode:(CMSampleBufferRef)sampleBuffer {
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// 帧时间,如果不设置会导致时间轴过长。
CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
VTEncodeInfoFlags flags;
OSStatus statusCode = VTCompressionSessionEncodeFrame(_EncodingSession,
imageBuffer,
presentationTimeStamp,
kCMTimeInvalid,
NULL, NULL, &flags);
if (statusCode != noErr) {
if(_EncodingSession != NULL) {
NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
VTCompressionSessionInvalidate(_EncodingSession);
CFRelease(_EncodingSession);
_EncodingSession = NULL;
NSLog(@"encodingSession release");
return;
}
}
// NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
}
5、在回调函数中处理编码后的H264数据
根据H264的编码规范,我们需要先取出PPS和SPS数据保存,然后再依次保存H264的视频数据
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
// NSLog(@"didCompressH264 called with status %d infoFlags %d", (int)status, (int)infoFlags);
if (status != 0) {
return;
}
if (!CMSampleBufferDataIsReady(sampleBuffer)) {
NSLog(@"didCompressH264 data is not ready ");
return;
}
ESCSaveToH264FileTool* encoder = (__bridge ESCSaveToH264FileTool *)outputCallbackRefCon;
bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
// 判断当前帧是否为关键帧
// 获取sps & pps数据
if (keyframe) {
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
size_t spsSize, spsCount;
const uint8_t *sparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &spsSize, &spsCount, 0 );
if (statusCode == noErr) {
// Found sps and now check for pps
size_t ppsSize, ppsCount;
const uint8_t *pparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &ppsSize, &ppsCount, 0 );
if (statusCode == noErr) {
// Found pps
NSData *sps = [NSData dataWithBytes:sparameterSet length:spsSize];
NSData *pps = [NSData dataWithBytes:pparameterSet length:ppsSize];
if (encoder) {
[encoder gotSpsPps:sps pps:pps];
}
}
}
}
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数据前四个字节不是0001的startcode,而是大端模式的帧长度length
// 循环获取nalu数据
while (bufferOffset < totalLength - AVCCHeaderLength) {
uint32_t NALUnitLength = 0;
// Read the NAL unit length
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
// 从大端转系统端
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
[encoder gotEncodedData:data isKeyFrame:keyframe];
// Move to the next NAL unit in the block buffer
bufferOffset += AVCCHeaderLength + NALUnitLength;
}
}
}
写入SPS和PPS信息
- (void)gotSpsPps:(NSData *)sps pps:(NSData *)pps {
// NSLog(@"gotSpsPps %d %d", (int)[sps length], (int)[pps length]);
const char bytes[] = "\x00\x00\x00\x01";
NSData *ByteHeader = [NSData dataWithBytes:bytes length:4];
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:sps];
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:pps];
}
写入H264裸数据
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame {
// NSLog(@"gotEncodedData %d", (int)[data length]);
if (self.fileHandle != NULL)
{
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:data];
}
}
6、结束录制时停止编码会话和视频流会话
停止编码会话
- (void)EndVideoToolBox {
VTCompressionSessionCompleteFrames(_EncodingSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(_EncodingSession);
CFRelease(_EncodingSession);
_EncodingSession = NULL;
}
停止视频会话
[self.captureSession stopRunning];
结束视频的录制。我们打开沙盒即可查看录制的H264文件。
可以使用VLC进行播放。
最后欢迎大家留言交流,同时附上Demo地址。
Demo地址:https://github.com/XMSECODE/ESCCameraToH264Demo