本代码案例实现: iOS端要实现拍摄采集 h264编码写入document下的本地文件
相机准备、编码准备
1、开始相机头像捕捉
- (void)startCapture
{
//1. 相机相关初始化及配置
self.captureSession = [[AVCaptureSession alloc]init];
self.captureSession.sessionPreset = AVCaptureSessionPreset640x480;
cCaptureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
cEncodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
AVCaptureDevice *inputCamera = nil;
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if ([device position] == AVCaptureDevicePositionBack) {
inputCamera = device;
}
}
self.cCaptureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:inputCamera error:nil];
if ([self.captureSession canAddInput:self.cCaptureDeviceInput]) {
[self.captureSession addInput:self.cCaptureDeviceInput];
}
//2. 相机输出设置
self.cCaptureDataOutput = [[AVCaptureVideoDataOutput alloc]init];
[self.cCaptureDataOutput setAlwaysDiscardsLateVideoFrames:NO];
[self.cCaptureDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
[self.cCaptureDataOutput setSampleBufferDelegate:self queue:cCaptureQueue];
if ([self.captureSession canAddOutput:self.cCaptureDataOutput]) {
[self.captureSession addOutput:self.cCaptureDataOutput];
}
//3. 输出连接AVCaptureConnection 捕获会话中特定捕获输入对和捕获输出对象之间的连接。
AVCaptureConnection *connection = [self.cCaptureDataOutput connectionWithMediaType:AVMediaTypeVideo];
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
//4. 拍摄画面预览
self.cPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];
[self.cPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
[self.cPreviewLayer setFrame:self.view.bounds];
[self.view.layer addSublayer:self.cPreviewLayer];
//5. 设置即将写入到的目录文件
NSString *filePath = [NSHomeDirectory()stringByAppendingPathComponent:@"/Documents/video.h264"];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
if (!createFile) {
NSLog(@"create file failed");
}else
{
NSLog(@"create file success");
}
NSLog(@"filePaht = %@",filePath);
fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
//6. 初始化videoToolbBox
[self initVideoToolBox];
//7. 开始捕捉
[self.captureSession startRunning];
}
//停止捕捉
- (void)stopCapture
{
[self.captureSession stopRunning];
[self.cPreviewLayer removeFromSuperlayer];
[self endVideoToolBox];
[fileHandle closeFile];
fileHandle = NULL;
}
复制代码
2. 初始化videoToolBox
-(void)initVideoToolBox
{
dispatch_sync(cEncodeQueue, ^{
frameID = 0;
int width = 480, height = 640;
// 第一步:创建编码会话
/*
参数
1. NULL 默认分配器
2. width
3. height 分辨率 像素为单位
4. 编码类型
5. NULL 编码规范, videoToolBox自动选择
6. NULL 源像素缓存区
7. NULL 默认分配压缩数据分配器
8. 回调函数:函数指针 指向函数名 didCompressH264
9. 桥接self
10.编码会话变量
*/
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void * _Nullable)(self), &cEncodeingSession);
NSLog(@"H264:VTCompressionSessionCreate %d", (int)status);
// 0 : noErr = 0
if (status != noErr) {
NSLog(@"H264:unable to creat H264 Session");
return;
}
/*
第二步:设置编码参数
*/
//1.1.设置实时编码参数、实时编码输出(避免延迟)
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
//1.2.设置 ProfileLevel
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
//1.3.设置关键
int frameInterval = 10;
CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);//类型转换 int -> CFNumberRef
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
//1.4. 设置期望帧率,不是实际帧率,设置关键帧GOPSize间隔,gop太小会模糊
int fps = 10;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
//1.5. 设置码率上限,单位bps
int bitRate = width * height * 3 * 4 * 8; //码率计算公式 极高码率 (width * height * 3)* 4 ,
//这里再 * 8 或者 *10 都可以
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &bitRate);
VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
//第二步 准备编码
VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);
//
//AVFoundation捕捉的时候才有数据, 编码
//这里就来到 输出cmsamplebuffer里
});
}
//结束VideoToolBox
-(void)endVideoToolBox
{
VTCompressionSessionCompleteFrames(cEncodeingSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(cEncodeingSession);
CFRelease(cEncodeingSession);
cEncodeingSession = NULL;
}
复制代码
以上两步相机准备、编码准备 工作基本完成,下一步就是接收samplebuffer 并编码
3. 接收samplebuffer进行编码
#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
//区分音/视频
// 可以根据 captureOutput 是 videocaptureOutput 还是 audiocaptureOutput
dispatch_sync(cEncodeQueue, ^{
[self encode:sampleBuffer];
});
}
//编码
- (void) encode:(CMSampleBufferRef )sampleBuffer
{
//拿到每一帧未编码的数据
//视频中1帧数据
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
//设置帧时间,如果不设置导致时间轴过长
CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
VTEncodeInfoFlags flags;//
//第三步:开始编码
/*
参数:
1.编码会话变量
2.未编码数据
3.获取到的sample buffer数据的展示时间戳。每一个传给这个session的时间戳都要大于前一个展示时间戳
4.对于获取到sample buffer数据,这个帧的展示时间。如果没有时间信息,可设置KCMTimeInvalid
5.frameProperties 包含这个帧的属性. 帧的改变会影响后边的编码帧
6. ourceFrameRefCom 回调函数会引用你设置的这个帧的参考值
7.infoFlagsOut 指向一个VTEncodeInfoFlags来接受一个编码操作,如果使用异步运行,KVTEncodeInfo_Asynchronous被设置;同步运行,KVTEncodeInfo_FrameDropped被设置;设置NULL为不想接收这个信息
*/
OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);
if (statusCode != noErr) {
//error
NSLog(@"H264:VTCompressionSessionEncodeFrame fail with %d", statusCode);
//释放
VTCompressionSessionInvalidate(cEncodeingSession);
CFRelease(cEncodeingSession);
cEncodeingSession = NULL;
return;
}
NSLog(@"H264:VTCompressionSessionEncodeFrame Success");
//完成编码
}
复制代码
4. 编码后写入文件
// h264文件格式 sps+pps+流数据
//序列参数集sps (Sequence Parameter Sets)
//图像参数集pps (Picture Parameter Sets)
// 00 00 00 01 间隔符
//编码完成回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
if (status != 0) { // 0 / noError
return;
}
//没准备好
if (!CMSampleBufferDataIsReady(sampleBuffer)) {
NSLog(@"H264: CMSampleBufferDataIsReady is not");
return;
}
// encoder: 桥接过来的self
ViewController *encoder = (__bridge ViewController *)(outputCallbackRefCon);
//判断当前是否为关键帧
bool keyFrame = CFDictionaryContainsKey(CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0), kCMSampleAttachmentKey_NotSync);
//判断当前帧是否为关键帧
//获取sps & pps 数据 只获取1次,保存在h264文件的第一帧中
//sps(sample per second 采样次数/s ),是衡量模数转换(ADC)时采样速率的单位
//pps()
if (keyFrame) {
//图像存储方式,编码器等格式描述(获取原图像存储格式)
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
//sps
size_t sparameterSetSize, sparameterSetCount; //数据大小 数据个数
const uint8_t *sparameterSet; //数据内容指针
//获取sps
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
if (statusCode == noErr) {
//pps
size_t pparameterSetSize, pparameterSetCount; //数据大小 数据个数
const uint8_t *pparameterSet; //数据内容指针
//获取sps
OSStatus pStatusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
if (pStatusCode == noErr) {
// C语言的数据 转为 oc的NSData
//获取sps data
NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
//获取pps data
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
if (encoder) {
//将 sps 和pps 的头写入file
[encoder gotSpsPps:sps pps:pps];
}
}
}
}
//前面头已经写入
// 接下来写其他数据
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
//数据的 总长度,单个数据长度,存储地址
size_t length, totalLength;
char *dataPointer;
//获取 数据信息
OSStatus bStatusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
//大端模式 转 小端模式
/*
大端模式
高字节的数据在前面,低字节的数据在后面
12 34 大端
*/
/*
小端模式
低字节的数据在前面,高字节的数据在后面
34 12 小端
*/
if (bStatusCodeRet == noErr) {
size_t bufferOffset = 0; //数据偏移量
static const int AVCCHeaderLength = 4; //
//返回的nalu数据前4个字节不是001的startCode,而是大端模式的帧长度length
//循环获取nalu数据
while (bufferOffset < totalLength - AVCCHeaderLength) {
//当还没偏移完时
uint32_t NALUnitLength = 0;
//读/获取 1单元的 nalu 数据
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
//从大端模式 转换为 系统端模式
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
//获取nalu数据 流数据
NSData *data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
//将nalu写入文件
[encoder gotEncodedData:data isKeyFrame:keyFrame];
//移到下一个NAL unit in the block buffer
bufferOffset += AVCCHeaderLength + NALUnitLength;
}
}
}
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
//
NSLog(@"gotSpsPps length: %d pps: %d", (int)[sps length], (int)[pps length]);
const char bytes[] = "\x00\x00\x00\x01";
//为什么 -1 , 在 C语言中 字符处 以\0结束,"\x00\x00\x00\x01\0"; 在计算的时候 -1
size_t length = sizeof(bytes) - 1;
// (sizeof bytes);
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
[fileHandle writeData:ByteHeader]; //4
[fileHandle writeData:sps];
[fileHandle writeData:ByteHeader]; //间隔
[fileHandle writeData:pps];
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
NSLog(@"gotEncodedData length: %d ", (int)[data length]);
const char bytes[] = "\x00\x00\x00\x01";
//长度
size_t length = sizeof(bytes) - 1;
//头字节
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
//写入头字节
[fileHandle writeData:ByteHeader]; //4
//写入h264数据
[fileHandle writeData:data];
}
复制代码