项目中用到了拍照和拍视频功能,直接用系统的imagepickercontroller来实现的话,样式比较简单,不可以自定义样式,满足不了项目经理及UI的个性化需求。所以就要自定义拍照页面了,搜索了一些发现有一些内容,但是不够全面,下面内容是我搜索整理的,用在了自己的项目中
一、通用属性
- 初始化AVCaptureSession和AVCaptureVideoPreviewLayer,分别负责视频采集调度和视频预览
- (AVCaptureSession *)session { if (!_session) { _session = [[AVCaptureSession alloc] init]; if ([_session canSetSessionPreset:AVCaptureSessionPresetHigh]){ _session.sessionPreset = AVCaptureSessionPresetHigh; } else if ([_session canSetSessionPreset:AVCaptureSessionPresetiFrame1280x720]) { _session.sessionPreset = AVCaptureSessionPresetiFrame1280x720; } } return _session; } - (AVCaptureVideoPreviewLayer *)previewLayer { if (!_previewLayer) { _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session]; _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;//AVLayerVideoGravityResize; CGRect frame = CGRectMake(0, VIDEO_RECT_TOP, COMMON_SCREEN_WIDTH, COMMON_SCREEN_HEIGHT - VIDEO_RECT_TOP - VIDEO_RECT_BOTTOM); if (IS_NOTCH_SCREEN) { if (self.cameraType == CustomCameraTypeImage) { frame = CGRectMake(0, NOTCH_SCREEN_VIDEO_RECT_TOP, COMMON_SCREEN_WIDTH, COMMON_SCREEN_HEIGHT - NOTCH_SCREEN_VIDEO_RECT_TOP - NOTCH_SCREEN_VIDEO_RECT_BOTTOM); } else if (self.cameraType == CustomCameraTypeVideo) { } } else { if (self.cameraType == CustomCameraTypeImage) { } else if (self.cameraType == CustomCameraTypeVideo) { frame = CGRectMake(0, 0, COMMON_SCREEN_WIDTH, COMMON_SCREEN_HEIGHT); } } _previewLayer.frame = frame; } return _previewLayer; }
二、AVCapturePhotoOutput保存图片
- 初始化图像设备,包括前后摄像头
- (AVCaptureDevice *)backDevice { if (!_backDevice) { _backDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; } return _backDevice; } - (AVCaptureDevice *)frontDevice { if (!_frontDevice) { _frontDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront]; } return _frontDevice; } - (void)setImageDevice:(AVCaptureDevice *)imageDevice { _imageDevice = imageDevice; [self.session beginConfiguration]; for (AVCaptureDeviceInput *input in self.session.inputs) { if (input.device.deviceType == AVCaptureDeviceTypeBuiltInWideAngleCamera) { [self.session removeInput:input]; } } NSError *error; AVCaptureDeviceInput *imageInput = [AVCaptureDeviceInput deviceInputWithDevice:_imageDevice error:&error]; if (error) { NSLog(@"photoInput init error: %@", error); } else {//设置输入 if ([self.session canAddInput:imageInput]) { [self.session addInput:imageInput]; } } [self.session commitConfiguration]; }
- 设置图片输出
- (void)setupValue { if (self.cameraType == CustomCameraTypeImage) { self.imageDevice = self.backDevice; AVCapturePhotoOutput *photoOutput = [[AVCapturePhotoOutput alloc] init]; if ([self.session canAddOutput:photoOutput]) { [self.session addOutput:photoOutput]; } } }
- 点击拍照按钮
- (void)takePhoto { if (self.cameraType == CustomCameraTypeImage) { AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettingsWithFormat:@{ AVVideoCodecKey: AVVideoCodecJPEG, }]; settings.flashMode = self.flashButton.isSelected ? AVCaptureFlashModeOn : AVCaptureFlashModeOff; for (AVCaptureOutput *output in self.session.outputs) { if ([output isKindOfClass:[AVCapturePhotoOutput class]]) { AVCapturePhotoOutput *photoOutput = (AVCapturePhotoOutput *)output; [photoOutput capturePhotoWithSettings:settings delegate:self]; break; } } } }
- 拍照完成的回调
- (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings error:(NSError *)error { if (photoSampleBuffer) { [self.session stopRunning]; NSData *data = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer]; UIImage *image = [UIImage imageWithData:data]; UIEdgeInsets insets = UIEdgeInsetsZero; if (IS_NOTCH_SCREEN) { insets = UIEdgeInsetsMake(NOTCH_SCREEN_VIDEO_RECT_TOP - VIDEO_RECT_TOP, 0, NOTCH_SCREEN_VIDEO_RECT_BOTTOM - VIDEO_RECT_BOTTOM, 0); } else { insets = UIEdgeInsetsMake(VIDEO_RECT_TOP, 0, VIDEO_RECT_BOTTOM, 0); } self.image = [UIImage clipCameraPicture:image toInsets:insets];//[UIImage clipImage:image ofRect:CGRectMake(0, videoRectTop, COMMON_SCREEN_WIDTH, COMMON_SCREEN_HEIGHT - videoRectTop - videoRectBottom)]; if ([self.childViewControllers.firstObject isKindOfClass:[CustomImagePreviewViewController class]]) { CustomImagePreviewViewController *imagePreviewController = (CustomImagePreviewViewController *)self.childViewControllers.firstObject; imagePreviewController.previewImage = self.image; } } }
三、AVCaptureMovieFileOutput保存视频
- 这种方式可以保存视频文件,开始录制之后会将视频直接写入到设置好的路径里,完成之后可以从该路径读取到文件。最开始采用的这种方式,但是后来发现和需求有差异就换了下面一种更复杂但是更灵活的方式。
- 初始化音频设备
- (AVCaptureDevice *)audioDevice { if (!_audioDevice) { _audioDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInMicrophone mediaType:AVMediaTypeAudio position:AVCaptureDevicePositionUnspecified]; } return _audioDevice; }
- 设置视频和音频输入,及视频文件输出
if (self.cameraType == CustomCameraTypeVideo) { self.imageDevice = self.backDevice; AVCapturePhotoOutput *photoOutput = [[AVCapturePhotoOutput alloc] init]; if ([self.session canAddOutput:photoOutput]) { [self.session addOutput:photoOutput]; } NSError *error; AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:self.audioDevice error:&error]; if (error) { NSLog(@"create audioInput error: %@", error); } else { if ([self.session canAddInput:audioInput]) { [self.session addInput:audioInput]; } } AVCaptureMovieFileOutput *videoOutput = [[AVCaptureMovieFileOutput alloc] init]; if ([self.session canAddOutput:videoOutput]) { [self.session addOutput:videoOutput]; } }
- 开始和结束拍摄
if (self.cameraType == CustomCameraTypeVideo) { for (AVCaptureOutput *output in self.session.outputs) { if ([output isKindOfClass:[AVCaptureMovieFileOutput class]]) { AVCaptureMovieFileOutput *videoOutput = (AVCaptureMovieFileOutput *)output; if (sender.isSelected) { self.videoPath = nil; [videoOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:self.videoPath] recordingDelegate:self]; [self.videoTimer setFireDate:[NSDate date]]; } else { [videoOutput stopRecording]; [self.videoTimer setFireDate:[NSDate distantFuture]]; self.videoTimeInterval = 0; } break; } } }
- 拍摄视频完成回调
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error { if (error) { [CustomProgressHUD showTextHUD:error.userInfo[@"NSLocalizedDescription"]]; NSLog(@"finish recording error: %@", error); } else { [self.session stopRunning]; if ([self.childViewControllers.firstObject isKindOfClass:[CustomVideoPlayerViewController class]]) { CustomVideoPlayerViewController *videoPlayerController = (CustomVideoPlayerViewController *)self.childViewControllers.firstObject; videoPlayerController.videoPath = outputFileURL.path; } } }
四、AVAssetWriter保存视频
- 初始化及设置音频和视频输入设备类似三(2),三(3)部分
- 设置音频及视频输出
dispatch_queue_t audioQueue = dispatch_queue_create("com.kmelearning.audioDataOutputQueue", DISPATCH_QUEUE_SERIAL); AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init]; if ([self.session canAddOutput:audioOutput]) { [self.session addOutput:audioOutput]; } [audioOutput setSampleBufferDelegate:self queue:audioQueue]; dispatch_queue_t videoQueue = dispatch_queue_create("com.kmelearning.videoDataOutputQueue", DISPATCH_QUEUE_SERIAL); AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init]; if ([self.session canAddOutput:videoOutput]) { [self.session addOutput:videoOutput]; } [videoOutput setSampleBufferDelegate:self queue:videoQueue];
- 设置assetwriter,这里可以设置音频和视频的属性,已实现精确控制
- (AVAssetWriter *)assetWriter { if (!_assetWriter) { NSError *error; _assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:self.videoPath] fileType:AVFileTypeMPEG4 error:&error]; if (error) { NSLog(@"create assetWriter error: %@", error); return nil; } else { //写入视频大小 NSInteger numPixels = COMMON_SCREEN_WIDTH * COMMON_SCREEN_HEIGHT; //每像素比特 CGFloat bitsPerPixel = 12.0; NSInteger bitsPerSecond = numPixels * bitsPerPixel; // 码率和帧率设置 NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(bitsPerSecond), AVVideoExpectedSourceFrameRateKey : @(15), AVVideoMaxKeyFrameIntervalKey : @(15), AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel }; CGFloat width = COMMON_SCREEN_HEIGHT; CGFloat height = COMMON_SCREEN_WIDTH; if (IS_NOTCH_SCREEN) { width = COMMON_SCREEN_HEIGHT - VIDEO_RECT_TOP - VIDEO_RECT_BOTTOM; height = COMMON_SCREEN_WIDTH; } //视频属性 NSDictionary *videoSetting = @{ AVVideoCodecKey : AVVideoCodecH264, AVVideoWidthKey : @(width * 2), AVVideoHeightKey : @(height * 2), AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill, AVVideoCompressionPropertiesKey : compressionProperties }; NSDictionary *audioSetting = @{ AVFormatIDKey : @(kAudioFormatMPEG4AAC), AVEncoderBitRatePerChannelKey : @(28000), AVNumberOfChannelsKey : @(1), AVSampleRateKey : @(22050) }; self.audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioSetting]; self.audioWriterInput.expectsMediaDataInRealTime = YES; if ([_assetWriter canAddInput:self.audioWriterInput]) { [_assetWriter addInput:self.audioWriterInput]; } self.videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSetting]; self.videoWriterInput.expectsMediaDataInRealTime = YES; self.videoWriterInput.transform = CGAffineTransformMakeRotation(M_PI / 2.0); if ([_assetWriter canAddInput:self.videoWriterInput]) { [_assetWriter addInput:self.videoWriterInput]; } } } return _assetWriter; }
- 开始和结束拍摄
if (self.cameraType == CustomCameraTypeVideo) { if (sender.isSelected) { self.videoPath = nil; self.assetWriter = nil; [self assetWriter]; [self.videoTimer setFireDate:[NSDate date]]; self.startRecording = YES; } else { if (self.assetWriter.status == AVAssetWriterStatusWriting) { [self.session stopRunning]; for (AVAssetWriterInput *writerIntput in self.assetWriter.inputs) { [writerIntput markAsFinished]; } [self.assetWriter finishWritingWithCompletionHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ if ([self.childViewControllers.firstObject isKindOfClass:[CustomVideoPlayerViewController class]]) { CustomVideoPlayerViewController *videoPlayerController = (CustomVideoPlayerViewController *)self.childViewControllers.firstObject; videoPlayerController.videoPath = self.videoPath; } }); }]; [self.videoTimer setFireDate:[NSDate distantFuture]]; self.videoTimeInterval = 0; self.startRecording = NO; self.startWriting = NO; } else { NSLog(@"录制失败,请重试"); } } }
- 获取到音频和视频回调,在里面要区分接收到的是音频还是视频
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { // NSLog(@"output: %@", output); if (!self.startRecording || !CMSampleBufferDataIsReady(sampleBuffer)) { return; } CMFormatDescriptionRef desMedia = CMSampleBufferGetFormatDescription(sampleBuffer); CMMediaType mediaType = CMFormatDescriptionGetMediaType(desMedia); NSLog(@"status: %ld, mediaType: %u", (long)_assetWriter.status, (unsigned int)mediaType); if (mediaType == kCMMediaType_Video) {//audio数据一直都有 // setup the writer if (!self.startWriting && _assetWriter.status == AVAssetWriterStatusUnknown) { [[NSFileManager defaultManager] removeItemAtPath:self.videoPath error:nil]; [_assetWriter startWriting]; self.startWriting = YES; CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); [_assetWriter startSessionAtSourceTime:timestamp]; NSLog(@"started writing with status (%ld)", (long)_assetWriter.status); } } // check for completion state if (_assetWriter.status == AVAssetWriterStatusFailed) { NSLog(@"writer failure, (%@)", _assetWriter.error); [[NSFileManager defaultManager] removeItemAtPath:self.videoPath error:nil]; return; } if (_assetWriter.status == AVAssetWriterStatusCancelled) { NSLog(@"writer cancelled"); return; } if (_assetWriter.status == AVAssetWriterStatusCompleted) { NSLog(@"writer finished and completed"); return; } // perform write if (self.startWriting && _assetWriter.status == AVAssetWriterStatusWriting) { if (mediaType == kCMMediaType_Audio) { if (self.audioWriterInput.isReadyForMoreMediaData) { BOOL success = [self.audioWriterInput appendSampleBuffer:sampleBuffer]; if (success) { NSLog(@"AVCaptureAudioDataOutput appendSampleBuffer success"); } else { NSLog(@"AVCaptureAudioDataOutput appendSampleBuffer error:%@", _assetWriter.error); } } } else if (mediaType == kCMMediaType_Video) { if (self.videoWriterInput.isReadyForMoreMediaData) { BOOL success = [self.videoWriterInput appendSampleBuffer:sampleBuffer]; if (success) { // NSLog(@"AVCaptureVideoDataOutput appendSampleBuffer success"); } else { NSLog(@"AVCaptureVideoDataOutput appendSampleBuffer error:%@", _assetWriter.error); } } } } }
写完项目的时候这一部分没有总结,现在来回顾,当时的一些问题已经记不清楚了,以后还是养成边写项目边总结的习惯,把遇到的问题都记录下来,方便自己和别人查看和解决。
五、参考链接