前言

承接上篇的《AV Foundation开发秘籍——实践掌握iOS & OS X应用的视听处理技术 阅读指南》 今天这篇给大家讲解下如何利用AVFoundation设计一套通用稳定的音视频框架。

核心

AVCaptureSession开启捕获任务,配置AVCaptureDeviceInput定制捕获任务的输入源(多种摄像头),通过AVFoundation内各种Data output输出数据(元数据、视频帧、音频帧),AVAssetWriter开启写任务,将音视频数据归档为媒体文件。

实现功能

视频流预览、录像归档、捕获相片、切换摄像头、人脸检测、帧率配置、相机详细配置

框架源码

github.com/caixindong/…

具体设计

核心模块 XDCaptureService & XDVideoWritter

XDCaptureService是对外API的总入口,也是框架的核心类,主要做音视频输入输出配置工作和调度工作。 XDVideoWritter是音视频写模块,主要提供写数据和归档数据的基础操作,不对外暴露。 对外API设计:

@class XDCaptureService;@protocol XDCaptureServiceDelegate <NSObject>@optional
//service生命周期
- (void)captureServiceDidStartService:(XDCaptureService *)service;- (void)captureService:(XDCaptureService *)service serviceDidFailWithError:(NSError *)error;- (void)captureServiceDidStopService:(XDCaptureService *)service;- (void)captureService:(XDCaptureService *)service getPreviewLayer:(AVCaptureVideoPreviewLayer *)previewLayer;- (void)captureService:(XDCaptureService *)service outputSampleBuffer:(CMSampleBufferRef)sampleBuffer;//录像相关
- (void)captureServiceRecorderDidStart:(XDCaptureService *)service ;- (void)captureService:(XDCaptureService *)service recorderDidFailWithError:(NSError *)error;- (void)captureServiceRecorderDidStop:(XDCaptureService *)service;//照片捕获
- (void)captureService:(XDCaptureService *)service capturePhoto:(UIImage *)photo;//人脸检测
- (void)captureService:(XDCaptureService *)service outputFaceDetectData:(NSArray <AVMetadataFaceObject*>*) faces;//景深数据
- (void)captureService:(XDCaptureService *)service captureTrueDepth:(AVDepthData *)depthData API_AVAILABLE(ios(11.0));@end@protocol XDCaptureServicePreViewSource <NSObject>- (AVCaptureVideoPreviewLayer *)preViewLayerSource;@end@interface XDCaptureService : NSObject//是否录制音频,默认是NO
@property (nonatomic, assign) BOOL shouldRecordAudio;//iOS原生人脸检测,默认是NO
@property (nonatomic, assign) BOOL openNativeFaceDetect;//摄像头的方向,默认是AVCaptureDevicePositionFront(前置)
@property (nonatomic, assign) AVCaptureDevicePosition devicePosition;//判断是否支持景深模式,当前只支持7p、8p、X的后置摄像头及X的前后摄像头,系统要求是iOS 11以上
@property (nonatomic, assign, readonly) BOOL depthSupported;//是否开启景深模式,默认是NO
@property (nonatomic, assign) BOOL openDepth;//只有以下指定的sessionPreset才有depth数据:AVCaptureSessionPresetPhoto、AVCaptureSessionPreset1280x720、AVCaptureSessionPreset640x480
@property (nonatomic, assign) AVCaptureSessionPreset sessionPreset;//帧率,默认是30
@property (nonatomic, assign) int frameRate;//录像的临时存储地址,建议每次录完视频做下重定向
@property (nonatomic, strong, readonly) NSURL *recordURL;//如果设置preViewSource则内部不生成AVCaptureVideoPreviewLayer
@property (nonatomic, assign) id<XDCaptureServicePreViewSource> preViewSource;@property (nonatomic, assign) id<XDCaptureServiceDelegate> delegate;@property (nonatomic, assign, readonly) BOOL isRunning;//视频编码设置(影响录制的视频的编码和大小)
@property (nonatomic, strong) NSDictionary *videoSetting;///相机专业设置,除非特定需求,一般不设置
//感光度(iOS8以上)
@property (nonatomic, assign, readonly) CGFloat deviceISO;
@property (nonatomic, assign, readonly) CGFloat deviceMinISO;
@property (nonatomic, assign, readonly) CGFloat deviceMaxISO;//镜头光圈大小
@property (nonatomic, assign, readonly) CGFloat deviceAperture;//曝光
@property (nonatomic, assign, readonly) BOOL supportsTapToExpose;
@property (nonatomic, assign) AVCaptureExposureMode exposureMode;
@property (nonatomic, assign) CGPoint exposurePoint;
@property (nonatomic, assign, readonly) CMTime deviceExposureDuration;//聚焦
@property (nonatomic, assign, readonly) BOOL supportsTapToFocus;
@property (nonatomic, assign) AVCaptureFocusMode focusMode;
@property (nonatomic, assign) CGPoint focusPoint;//白平衡
@property (nonatomic, assign) AVCaptureWhiteBalanceMode whiteBalanceMode;//手电筒
@property (nonatomic, assign, readonly) BOOL hasTorch;
@property (nonatomic, assign) AVCaptureTorchMode torchMode;//闪光灯
@property (nonatomic, assign, readonly) BOOL hasFlash;
@property (nonatomic, assign) AVCaptureFlashMode flashMode;//相机权限判断
+ (BOOL)videoGranted;//麦克风权限判断
+ (BOOL)audioGranted;//切换摄像机
- (void)switchCamera;//启动
- (void)startRunning;//关闭
- (void)stopRunning;//开始录像
- (void)startRecording;//取消录像
- (void)cancleRecording;//停止录像
- (void)stopRecording;//拍照
- (void)capturePhoto;@end
复制代码

CDG队列分流

因为在主线程启动音视频捕获及音视频读写会阻塞主线程,所以我们需要将这些任务派发到子线程中执行。我们选择GCD队列帮我们做这个视频。我们框架总共配置3个队列,分别是sessionQueue、writtingQueue、outputQueue,这些队列都是串行队列,因为音视频相关操作都是有顺序(时序)要求,保证当前队列只有一个操作的执行(配置、写数据、读数据)。sessionQueue主要负责音视频任务启动的调度,writtingQueue主要负责写数据的调度,保证数据帧能够准确归档到文件,outputQueue主要负责数据帧的输出。

@property (nonatomic, strong) dispatch_queue_t sessionQueue;
@property (nonatomic, strong) dispatch_queue_t writtingQueue;
@property (nonatomic, strong) dispatch_queue_t outputQueue;_sessionQueue = dispatch_queue_create("com.caixindong.captureservice.session", DISPATCH_QUEUE_SERIAL);
_writtingQueue = dispatch_queue_create("com.caixindong.captureservice.writting", DISPATCH_QUEUE_SERIAL);
_outputQueue = dispatch_queue_create("com.caixindong.captureservice.output", DISPATCH_QUEUE_SERIAL);
复制代码

音视频捕获

初始化捕获任务

sessionPreset指定了输出的视频帧的像素,例如640*480

@property (nonatomic, strong) AVCaptureSession *captureSession;_captureSession = [[AVCaptureSession alloc] init];
_captureSession.sessionPreset = _sessionPreset;
复制代码

配置捕获的输入

获取输入源设备,通过_cameraWithPosition可以获取摄像头的抽象表示,因为红外摄像头、双摄像头只能从较新API中获取,所以方法里已经做了兼容处理。并用输入源设备配置捕获输入AVCaptureDeviceInput

@property (nonatomic, strong) AVCaptureDeviceInput *videoInput;- (BOOL)_setupVideoInputOutput:(NSError **) error {self.currentDevice = [self _cameraWithPosition:_devicePosition];self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_currentDevice error:error];if (_videoInput) {if ([_captureSession canAddInput:_videoInput]) {[_captureSession addInput:_videoInput];} else {*error = [NSError errorWithDomain:@"com.caixindong.captureservice.video" code:-2200 userInfo:@{NSLocalizedDescriptionKey:@"add video input fail"}];return NO;}} else {*error = [NSError errorWithDomain:@"com.caixindong.captureservice.video" code:-2201 userInfo:@{NSLocalizedDescriptionKey:@"video input is nil"}];return NO;}//稳定帧率CMTime frameDuration = CMTimeMake(1, _frameRate);if ([_currentDevice lockForConfiguration:error]) {_currentDevice.activeVideoMaxFrameDuration = frameDuration;_currentDevice.activeVideoMinFrameDuration = frameDuration;[_currentDevice unlockForConfiguration];} else {*error = [NSError errorWithDomain:@"com.caixindong.captureservice.video" code:-2203 userInfo:@{NSLocalizedDescriptionKey:@"device lock fail(input)"}];return NO;}……Other code
}- (AVCaptureDevice *)_cameraWithPosition:(AVCaptureDevicePosition)position {if (@available(iOS 10.0, *)) {//AVCaptureDeviceTypeBuiltInWideAngleCamera默认广角摄像头,AVCaptureDeviceTypeBuiltInTelephotoCamera长焦摄像头,AVCaptureDeviceTypeBuiltInDualCamera后置双摄像头,AVCaptureDeviceTypeBuiltInTrueDepthCamera红外前置摄像头NSMutableArray *mulArr = [NSMutableArray arrayWithObjects:AVCaptureDeviceTypeBuiltInWideAngleCamera,AVCaptureDeviceTypeBuiltInTelephotoCamera,nil];if (@available(iOS 10.2, *)) {[mulArr addObject:AVCaptureDeviceTypeBuiltInDualCamera];}if (@available(iOS 11.1, *)) {[mulArr addObject:AVCaptureDeviceTypeBuiltInTrueDepthCamera];}AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:[mulArr copy] mediaType:AVMediaTypeVideo position:position];return discoverySession.devices.firstObject;} else {NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];for (AVCaptureDevice *device in videoDevices) {if (device.position == position) {return device;}}}return nil;
}复制代码

配置捕获输出

根据不同的功能需求,我们可以往捕获任务里添加不同的输出,例如捕获基础的视频帧数据,我们添加AVCaptureVideoDataOutput,捕获音频数据,我们添加AVCaptureAudioDataOutput,捕获人脸数据,我们添加AVCaptureMetadataOutput。因为音频的输出和视频输出的设置方式大同小异,所以这里只列出视频输出的关键代码,这里有几个关键的设计:
1、因为相机传感器问题,输出的视频流的方向会有90度偏转,所以我们需要通过获取与输出连接的videoConnection进行偏转配置; 2、视频帧(或者音频帧)都是以CMSampleBufferRef格式输出,视频帧可能经过多个业务处理,例如写文件或者抛到上层业务处理,所以处理数据前都对视频帧数据进行retatin操作,保证各个业务线处理的视频帧是独立的,具体可以看_processVideoData;
3、为了及时清理临时变量(对视频帧处理的各种操作可能需要较多内存空间),所以将外抛的帧处理用autorelease pool包裹起来,防止出现内存高峰;

@property (nonatomic, strong) AVCaptureVideoDataOutput *videoOutput;- (BOOL)_setupVideoInputOutput:(NSError **) error {
……Other codeself.videoOutput = [[AVCaptureVideoDataOutput alloc] init];_videoOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)};//对迟到的帧做丢帧处理_videoOutput.alwaysDiscardsLateVideoFrames = YES;dispatch_queue_t videoQueue = dispatch_queue_create("com.caixindong.captureservice.video", DISPATCH_QUEUE_SERIAL);//设置数据输出的delegate[_videoOutput setSampleBufferDelegate:self queue:videoQueue];if ([_captureSession canAddOutput:_videoOutput]) {[_captureSession addOutput:_videoOutput];} else {*error = [NSError errorWithDomain:@"com.caixindong.captureservice.video" code:-2204 userInfo:@{NSLocalizedDescriptionKey:@"device lock fail(output)"}];return NO;}self.videoConnection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];//录制视频会有90度偏转,是因为相机传感器问题,所以在这里设置输出的视频流的方向if (_videoConnection.isVideoOrientationSupported) {_videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;}return YES;
}#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate && AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {//可以捕获到不同的线程if (connection == _videoConnection) {@synchronized(self) {[self _processVideoData:sampleBuffer];};} else if (connection == _audioConnection) {@synchronized(self) {[self _processAudioData:sampleBuffer];};}
}#pragma mark - process Data
- (void)_processVideoData:(CMSampleBufferRef)sampleBuffer {//CFRetain的目的是为了每条业务线(写视频、抛帧)的sampleBuffer都是独立的if (_videoWriter && _videoWriter.isWriting) {CFRetain(sampleBuffer);dispatch_async(_writtingQueue, ^{[_videoWriter appendSampleBuffer:sampleBuffer];CFRelease(sampleBuffer);});}CFRetain(sampleBuffer);//及时清理临时变量,防止出现内存高峰dispatch_async(_outputQueue, ^{@autoreleasepool{if (self.delegate && [self.delegate respondsToSelector:@selector(captureService:outputSampleBuffer:)]) {[self.delegate captureService:self outputSampleBuffer:sampleBuffer];}}CFRelease(sampleBuffer);});
}
复制代码

配置图片数据输出

配置图片数据输出的目的是为了实现相片捕获功能,通过setOutputSettings,我们可以配置我们输出的图片格式。

@property (nonatomic, strong) AVCaptureStillImageOutput *imageOutput;- (BOOL)_setupImageOutput:(NSError **)error {self.imageOutput = [[AVCaptureStillImageOutput alloc] init];NSDictionary *outputSetting = @{AVVideoCodecKey: AVVideoCodecJPEG};[_imageOutput setOutputSettings:outputSetting];if ([_captureSession canAddOutput:_imageOutput]) {[_captureSession addOutput:_imageOutput];return YES;} else {*error = [NSError errorWithDomain:@"com.caixindong.captureservice.image" code:-2205 userInfo:@{NSLocalizedDescriptionKey:@"device lock fail(output)"}];return NO;}
}//拍照功能实现
- (void)capturePhoto {AVCaptureConnection *connection = [_imageOutput connectionWithMediaType:AVMediaTypeVideo];if (connection.isVideoOrientationSupported) {connection.videoOrientation = AVCaptureVideoOrientationPortrait;}__weak typeof(self) weakSelf = self;[_imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:^(CMSampleBufferRef  _Nullable imageDataSampleBuffer, NSError * _Nullable error) {__strong typeof(weakSelf) strongSelf = weakSelf;if (imageDataSampleBuffer != NULL) {NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];UIImage *image = [UIImage imageWithData:imageData];if (strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(captureService:capturePhoto:)]) {[strongSelf.delegate captureService:strongSelf capturePhoto:image];}}}];
}
复制代码

配置人脸数据输出

关键是配置人脸元数据输出AVCaptureMetadataOutput,并指定metadataObjectTypes为AVMetadataObjectTypeFace,捕获的人脸数据包含当前视频帧中所有的人脸,可以从数据中提取人脸的范围、位置、偏转角,但这个有个注意点,就是原始的人脸数据的坐标是相机坐标系,我们需要转化为屏幕坐标,这样才方便我们的业务处理,具体可以看人脸数据输出那一块。

@property (nonatomic, strong) AVCaptureMetadataOutput *metadataOutput;-(void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {NSMutableArray *transformedFaces = [NSMutableArray array];for (AVMetadataObject *face in metadataObjects) {@autoreleasepool{AVMetadataFaceObject *transformedFace = (AVMetadataFaceObject*)[self.previewLayer transformedMetadataObjectForMetadataObject:face];if (transformedFace) {[transformedFaces addObject:transformedFace];}};}@autoreleasepool{if (self.delegate && [self.delegate respondsToSelector:@selector(captureService:outputFaceDetectData:)]) {[self.delegate captureService:self outputFaceDetectData:[transformedFaces copy]];}};
}复制代码

配置预览源

这里有两种方式,一种是外部已经通过实现预览数据源方法配置了数据源,另外一种是内部自己生成AVCaptureVideoPreviewLayer配置为预览源。

   if (self.preViewSource && [self.preViewSource respondsToSelector:@selector(preViewLayerSource)]) {self.previewLayer = [self.preViewSource preViewLayerSource];[_previewLayer setSession:_captureSession];[_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];} else {self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];//充满整个屏幕[_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];if (self.delegate && [self.delegate respondsToSelector:@selector(captureService:getPreviewLayer:)]) {[self.delegate captureService:self getPreviewLayer:_previewLayer];}}
复制代码

处理音视频前后台状态变化

iOS的音视频前后台机制较复杂,有各种生命周期变化,为了保证我们框架在正确状态下做正确的事,我们将数据帧的读和写的状态处理进行解耦,各位维护自己的通知状态变化处理。外层业务无需监听AVFoundation的通知手动处理视频流的状态变化。 读模块音视频通知配置:

//CaptureService和VideoWritter各自维护自己的生命周期,捕获视频流的状态与写入视频流的状态解耦分离,音视频状态变迁由captureservice内部管理,外层业务无需手动处理视频流变化[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_captureSessionNotification:) name:nil object:self.captureSession];//为了适配低于iOS 9的版本,在iOS 9以前,当session start 还没完成就退到后台,回到前台会捕获AVCaptureSessionRuntimeErrorNotification,这时需要手动重新启动session,iOS 9以后系统对此做了优化,系统退到后台后会将session start缓存起来,回到前台会自动调用缓存的session start,无需手动调用[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_enterForegroundNotification:) name:UIApplicationWillEnterForegroundNotification object:nil];#pragma mark - CaptureSession Notification
- (void)_captureSessionNotification:(NSNotification *)notification {NSLog(@"_captureSessionNotification:%@",notification.name);if ([notification.name isEqualToString:AVCaptureSessionDidStartRunningNotification]) {if (!_firstStartRunning) {NSLog(@"session start running");_firstStartRunning = YES;if (self.delegate && [self.delegate respondsToSelector:@selector(captureServiceDidStartService:)]) {[self.delegate captureServiceDidStartService:self];}} else {NSLog(@"session resunme running");}} else if ([notification.name isEqualToString:AVCaptureSessionDidStopRunningNotification]) {if (!_isRunning) {NSLog(@"session stop running");if (self.delegate && [self.delegate respondsToSelector:@selector(captureServiceDidStopService:)]) {[self.delegate captureServiceDidStopService:self];}} else {NSLog(@"interupte session stop running");}} else if ([notification.name isEqualToString:AVCaptureSessionWasInterruptedNotification]) {NSLog(@"session was interupted, userInfo: %@",notification.userInfo);} else if ([notification.name isEqualToString:AVCaptureSessionInterruptionEndedNotification]) {NSLog(@"session interupted end");} else if ([notification.name isEqualToString:AVCaptureSessionRuntimeErrorNotification]) {NSError *error = notification.userInfo[AVCaptureSessionErrorKey];if (error.code == AVErrorDeviceIsNotAvailableInBackground) {NSLog(@"session runtime error : AVErrorDeviceIsNotAvailableInBackground");_startSessionOnEnteringForeground = YES;} else {if (self.delegate && [self.delegate respondsToSelector:@selector(captureService:serviceDidFailWithError:)]) {[self.delegate captureService:self serviceDidFailWithError:error];}}} else {NSLog(@"handel other notification : %@",notification.name);}
}#pragma mark - UIApplicationWillEnterForegroundNotification
- (void)_enterForegroundNotification:(NSNotification *)notification {if (_startSessionOnEnteringForeground == YES) {NSLog(@"为了适配低于iOS 9的版本,在iOS 9以前,当session start 还没完成就退到后台,回到前台会捕获AVCaptureSessionRuntimeErrorNotification,这时需要手动重新启动session,iOS 9以后系统对此做了优化,系统退到后台后会将session start缓存起来,回到前台会自动调用缓存的session start,无需手动调用");_startSessionOnEnteringForeground = NO;[self startRunning];}
}
复制代码

写模块音视频通知配置:

//写模块注册通知,只负责写相关的状态处理[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_assetWritterInterruptedNotification:) name:AVCaptureSessionWasInterruptedNotification object:nil];- (void)_assetWritterInterruptedNotification:(NSNotification *)notification {NSLog(@"assetWritterInterruptedNotification");[self cancleWriting];
}
复制代码

启动捕获 & 关闭捕获

异步启动,防止阻塞主线程。串行队列中执行启动和关闭,保证不会出现启动到一半就关闭这种异常case。

- (void)startRunning {dispatch_async(_sessionQueue, ^{NSError *error = nil;BOOL result =  [self _setupSession:&error];if (result) {_isRunning = YES;[_captureSession startRunning];}else{if (self.delegate && [self.delegate respondsToSelector:@selector(captureService:serviceDidFailWithError:)]) {[self.delegate captureService:self serviceDidFailWithError:error];}}});
}- (void)stopRunning {dispatch_async(_sessionQueue, ^{_isRunning = NO;NSError *error = nil;[self _clearVideoFile:&error];if (error) {if (self.delegate && [self.delegate respondsToSelector:@selector(captureService:serviceDidFailWithError:)]) {[self.delegate captureService:self serviceDidFailWithError:error];}}[_captureSession stopRunning];});
}复制代码

切换摄像头

切换不仅仅是切换device,同时还要将旧的捕获input移除,添加新的device input。切换摄像头时,videoConnection会变化,所以需要重新获取。

- (void)switchCamera {if (_openDepth) {return;}NSError *error;AVCaptureDevice *videoDevice = [self _inactiveCamera];AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];if (videoInput) {[_captureSession beginConfiguration];[_captureSession removeInput:self.videoInput];if ([self.captureSession canAddInput:videoInput]) {[self.captureSession addInput:videoInput];self.videoInput = videoInput;//切换摄像头videoConnection会变化,所以需要重新获取self.videoConnection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];if (_videoConnection.isVideoOrientationSupported) {_videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;}} else {[self.captureSession addInput:self.videoInput];}[self.captureSession commitConfiguration];}_devicePosition = _devicePosition == AVCaptureDevicePositionFront?AVCaptureDevicePositionBack:AVCaptureDevicePositionFront;
}- (AVCaptureDevice *)_inactiveCamera {AVCaptureDevice *device = nil;if (_devicePosition == AVCaptureDevicePositionBack) {device = [self _cameraWithPosition:AVCaptureDevicePositionFront];} else {device = [self _cameraWithPosition:AVCaptureDevicePositionBack];}return device;
}
复制代码

录像功能

通过videoSetting配置录制完的视频的编码格式,框架默认的编码格式是H.264,H.264是一种高效的视频编码格式,之后再出篇文章讲下这种编码格式,现阶段你只需要知道这是一种常用的编码格式,想了解更多编码格式,可以看下AVVideoCodecType里面的内容。在XDCaptureService的startRecording方法中,我们初始化我们的写模块XDVideoWritter,XDVideoWritter根据videoSetting配置对应的编码格式。

- (void)startRecording {dispatch_async(_writtingQueue, ^{@synchronized(self) {NSString *videoFilePath = [_videoDir stringByAppendingPathComponent:[NSString stringWithFormat:@"Record-%llu.mp4",mach_absolute_time()]];_recordURL = [[NSURL alloc] initFileURLWithPath:videoFilePath];if (_recordURL) {_videoWriter = [[XDVideoWritter alloc] initWithURL:_recordURL VideoSettings:_videoSetting audioSetting:_audioSetting];_videoWriter.delegate = self;[_videoWriter startWriting];if (self.delegate && [self.delegate respondsToSelector:@selector(captureServiceRecorderDidStart:)]) {[self.delegate captureServiceRecorderDidStart:self];}} else {NSLog(@"No record URL");}}});
}//XDVideoWritter.m
- (void)startWriting {if (_assetWriter) {_assetWriter = nil;}NSError *error = nil;NSString *fileType = AVFileTypeMPEG4;_assetWriter = [[AVAssetWriter alloc] initWithURL:_outputURL fileType:fileType error:&error];if (!_assetWriter || error) {if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:didFailWithError:)]){[self.delegate videoWritter:self didFailWithError:error];}}if (_videoSetting) {_videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:_videoSetting];_videoInput.expectsMediaDataInRealTime = YES;if ([_assetWriter canAddInput:_videoInput]) {[_assetWriter addInput:_videoInput];} else {NSError *error = [NSError errorWithDomain:@"com.caixindong.captureservice.writter" code:-2210 userInfo:@{NSLocalizedDescriptionKey:@"VideoWritter unable to add video input"}];if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:didFailWithError:)]) {[self.delegate videoWritter:self didFailWithError:error];}return;}} else {NSLog(@"warning: no video setting");}if (_audioSetting) {_audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:_audioSetting];_audioInput.expectsMediaDataInRealTime = YES;if ([_assetWriter canAddInput:_audioInput]) {[_assetWriter addInput:_audioInput];} else {NSError *error = [NSError errorWithDomain:@"com.caixindong.captureservice.writter" code:-2211 userInfo:@{NSLocalizedDescriptionKey:@"VideoWritter unable to add audio input"}];if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:didFailWithError:)]) {[self.delegate videoWritter:self didFailWithError:error];}return;}} else {NSLog(@"warning: no audio setting");}if ([_assetWriter startWriting]) {self.isWriting = YES;} else {NSError *error = [NSError errorWithDomain:@"com.xindong.captureservice.writter" code:-2212 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"VideoWritter startWriting fail error: %@",_assetWriter.error.localizedDescription]}];if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:didFailWithError:)]) {[self.delegate videoWritter:self didFailWithError:error];}}[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_assetWritterInterruptedNotification:) name:AVCaptureSessionWasInterruptedNotification object:nil];
}复制代码

录像的原理就是在抛数据的同时调用XDVideoWritter的appendSampleBuffer方法将数据写入一个临时文件中,当调用stopRecording,也就是调用到XDVideoWritter的stopWriting方法停止写数据,将临时文件归档为MP4文件。

- (void)stopRecording {dispatch_async(_writtingQueue, ^{@synchronized(self) {if (_videoWriter) {[_videoWriter stopWriting];}}});
}//XDVideoWritter.m
- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer {CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);if (mediaType == kCMMediaType_Video) {CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);if (self.firstSample) {[_assetWriter startSessionAtSourceTime:timestamp];self.firstSample = NO;}if (_videoInput.readyForMoreMediaData) {if (![_videoInput appendSampleBuffer:sampleBuffer]) {NSError *error = [NSError errorWithDomain:@"com.caixindong.captureservice.writter" code:-2213 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat: @"VideoWritter appending video sample buffer fail error:%@",_assetWriter.error.localizedDescription]}];if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:didFailWithError:)]) {[self.delegate videoWritter:self didFailWithError:error];}}}} else if (!self.firstSample && mediaType == kCMMediaType_Audio) {if (_audioInput.readyForMoreMediaData) {if (![_audioInput appendSampleBuffer:sampleBuffer]) {NSError *error = [NSError errorWithDomain:@"com.caixindong.captureservice.writter" code:-2214 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"VideoWritter appending audio sample buffer fail error: %@",_assetWriter.error]}];if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:didFailWithError:)]) {[self.delegate videoWritter:self didFailWithError:error];}}}}
}- (void)stopWriting {if (_assetWriter.status == AVAssetWriterStatusWriting) {self.isWriting = NO;[_assetWriter finishWritingWithCompletionHandler:^{if (_assetWriter.status == AVAssetWriterStatusCompleted) {if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:completeWriting:)]) {[self.delegate videoWritter:self completeWriting:nil];}} else {if (self.delegate && [self.delegate respondsToSelector:@selector(videoWritter:completeWriting:)]) {[self.delegate videoWritter:self completeWriting:_assetWriter.error];}}}];} else {NSLog(@"warning : stop writing with unsuitable state : %ld",_assetWriter.status);}[[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionWasInterruptedNotification object:nil];
}复制代码

感谢

感觉每一位给XDCaptureService issue 和使用反馈的同学,开源完善靠大家!

如何利用 AVFoundation 设计一个通用稳定的音视频框架?相关推荐

  1. 利用 74390 设计一个模 6 计数器,要求从 000 计数至 101,利用D触发器使其暂态的高电平清零信号延长而稳定

    下面的图有错:请将inst3(非门)删去,图74390与ddf都是上升沿触发 任务 1:利用 74390 设计一个模 6 计数器,要求从 000 计数至 101,请用 Run Functional S ...

  2. 【openai】请帮我设计一个通用的ERP管理系统,涉及到的表结构用mysql语言表达出来,全部写出来

    背景 这周末把openAi集成到自己的web系统里面了 尝试提问了几个技术和日常问题,感觉回答的还不错 问题1:[请帮我设计一个通用的ERP管理系统,涉及到的表结构用mysql语言表达出来,全部写出来 ...

  3. java完成一个学生信息调查程序_利用Java设计一个简单的学生信息管理程序

    利用Java设计一个简单的控制台学生信息管理程序 此程序可作为课设的参考,其中信息存储于文件中. 创建了学生类Student,用于存储学号等的信息.创建StudentFunction类,用于实现诸如学 ...

  4. 如何设计一个通用的查询接口

    临近放假,手头的事情没那么多,老是摸鱼也不好,还是写写博客吧. 今天来聊聊:如何设计一个通用的查询接口. 从一个场景开始 首先,我们从一个简单的场景开始.现在,我需要一个订单列表,用来查询[我的订单] ...

  5. python-GUI:利用pyqt5设计一个bootloader上位机页面(ZLG驱动)及打包报错faild to execute script pyi_rth_multiprocessing精简方案

    python-GUI:利用pyqt5设计一个bootloader上位机页面 1.下载pyqt5和Qt Designer 2.利用Qt Designer设计页面 步骤一:打开Qt Designer 步骤 ...

  6. 数字逻辑练习题(十一)利用74LS161设计一个七进制计数器

    一.题目描述 已知74LS161为同步四位二进制加法计数器,其逻辑符号和功能表如下,请利用74LS161设计一个七进制计数器.应写出分析设计过程. 二.问题解答 (1)分析 采用同步置数法进行设计:

  7. 在matlab中实现累乘,如何利用matlab设计一个线性相位FIR带通滤波器,并在FPGA上实现...

    设计要求 利用matlab设计一个线性相位FIR带通滤波器,并在FPGA上实现. 1.滤波器指标:过渡带带宽分别为100~300HZ,500~700HZ,阻带允许误差为0.02,通带允许误差为0.01 ...

  8. 利用SpringCloud搭建一个最简单的微服务框架

    利用SpringCloud搭建一个最简单的微服务框架 https://blog.csdn.net/caicongyang/article/details/52974406 1.微服务 微服务主要包含服 ...

  9. 网站在线客服系统实时语音视频聊天实战开发,利用peerjs vue.js实现webRTC网页音视频客服系统...

    webRTC机制和peerjs库的介绍在其他博客中已经有了很多介绍,这里我直接搬运过来 一.webrtc回顾 WebRTC(Web Real-Time Communication)即:网页即时通信. ...

最新文章

  1. Microbiome:掠食性粘细菌通过调节土壤微生物群落来控制黄瓜枯萎病
  2. 被3整除的子序列(简单dp)
  3. SpringSecurity权限管理相关对象介绍
  4. 【简便解法】1084 外观数列 (20分)_24行代码AC
  5. 清默网络linux班,linux笔记(26)grep
  6. 人工智能与图像传感器
  7. 【逻辑与计算理论】Lambda 演算——开篇
  8. 基于AE的SimpleGIS框架的搭建
  9. jquery修改样式通过类
  10. delphi7 调用XE编译的DLL遇到的坑
  11. 用VSCode写IEEE论文
  12. 阿里异构离线数据同步工具/平台DataX
  13. 拜耳再投4亿元提升在华处方药产能;阿斯利康进博会公布新冠疫苗最新进展 | 美通企业日报...
  14. 容器技术介绍之docker核心技术概述
  15. 套壳截图王用户服务协议
  16. 笔记本电脑右下角网络图标显示红叉
  17. 手机编程python可以实现吗?有哪些软件值得推荐?
  18. mac无线网连上没网络连接网络连接服务器,无线网络连接上但上不了网
  19. P6SPY(JDBC SQL拦截)的安装和使用
  20. 基于知识图谱问答(KBQA)|数据集提供及获取工具开源

热门文章

  1. socket 例子 java_java socket例子
  2. gogs只支持mysql5.7_在docker中跑nginx,gogs,mysql服务
  3. 2017年计算机导论试题,2017年云南农业大学基础与信息工程学院813计算机导论与数据结构考研题库...
  4. java编写正则表达式引擎_从0到1打造正则表达式执行引擎(一)
  5. perl语言编程 第四版_被称作“胶水语言”的PERL,在芯片设计和验证中可以这样使用...
  6. dfmea文件_技术干货合集「失效分析、PFMEA DFMEA关系、文件结果化」
  7. linux make指定目标平台,CMake on Linux:目标平台不支持动态链接
  8. 怎么能把看不清的照片给看清_拍完照不会后期怎么办?教你一个懒人办法,能帮照片变“高级”...
  9. 较为综合的c语言题目,c语言考试综合题.doc
  10. 使命召唤ol服务器位置,服务器架构升级 使命召唤OL跨区作战时代来临!