// // CameraUtility.m // AIIM // // Created by qitewei on 2025/6/5. // #import "CameraUtility.h" // 默认视频保存路径 #define DEFAULT_VIDEO_DIRECTORY @"Videos" @interface CameraUtility () { NSURL *_lastVideoOutputURL; } @property (nonatomic, strong) AVCaptureSession *captureSession; @property (nonatomic, strong) AVCaptureDeviceInput *videoInput; @property (nonatomic, strong) AVCapturePhotoOutput *photoOutput; @property (nonatomic, strong) AVCaptureMovieFileOutput *movieFileOutput; @property (nonatomic, weak) UIView *previewView; @property (nonatomic, strong) NSTimer *progressTimer; @property (nonatomic, assign) CGFloat recordingDuration; @property (nonatomic, assign) BOOL isRecording; @end @implementation CameraUtility static const CGFloat kMaxVideoDuration = 60.0f; // 最大录制时长60秒 #pragma mark - 生命周期方法 - (instancetype)initWithPreviewView:(UIView *)previewView { self = [super init]; if (self) { _previewView = previewView; _flashMode = CameraUtilityFlashModeOff; _isFrontCamera = NO; _recordingDuration = 0; _isRecording = NO; [self setupCaptureSession]; } return self; } - (void)dealloc { [self stopRunning]; [self cleanTempFiles]; } #pragma mark - 公开方法 - (void)configureCaptureButton:(UIButton *)button { // 单击手势 - 拍照 UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; tapGesture.numberOfTapsRequired = 1; [button addGestureRecognizer:tapGesture]; // 长按手势 - 拍视频 UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; longPressGesture.minimumPressDuration = 0.3; [button addGestureRecognizer:longPressGesture]; // 确保单击不会被长按阻塞 [tapGesture requireGestureRecognizerToFail:longPressGesture]; } - (void)startRunning { if (![self.captureSession isRunning]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.captureSession startRunning]; }); } } - (void)stopRunning { if ([self.captureSession isRunning]) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self.captureSession stopRunning]; }); } } - (void)switchCamera { AVCaptureDevicePosition position = self.isFrontCamera ? AVCaptureDevicePositionBack : AVCaptureDevicePositionFront; AVCaptureDevice *device = [self cameraWithPosition:position]; if (!device) return; [self.captureSession beginConfiguration]; // 移除原有输入 [self.captureSession removeInput:self.videoInput]; // 创建新输入 NSError *error = nil; AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error]; if (newVideoInput) { if ([self.captureSession canAddInput:newVideoInput]) { [self.captureSession addInput:newVideoInput]; self.videoInput = newVideoInput; self.isFrontCamera = !self.isFrontCamera; } } else { if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) { [self.delegate cameraUtilityDidOccurError:error]; } } [self.captureSession commitConfiguration]; // 重新设置闪光灯模式 [self setFlashMode:self.flashMode]; } - (void)takePhoto { AVCaptureConnection *connection = [self.photoOutput connectionWithMediaType:AVMediaTypeVideo]; if (connection.isVideoOrientationSupported) { connection.videoOrientation = [self currentVideoOrientation]; } // 创建照片设置 AVCapturePhotoSettings *photoSettings; if (@available(iOS 11.0, *)) { // iOS 11+ 使用正确的格式设置 NSDictionary *format = @{ AVVideoCodecKey: AVVideoCodecTypeJPEG, AVVideoCompressionPropertiesKey: @{ AVVideoQualityKey: @0.9 // 质量参数放在压缩属性字典内 } }; photoSettings = [AVCapturePhotoSettings photoSettingsWithFormat:format]; } else { // iOS 10 回退方案 photoSettings = [AVCapturePhotoSettings photoSettings]; } // 设置闪光灯模式 photoSettings.flashMode = (AVCaptureFlashMode)self.flashMode; // 触发拍照 [self.photoOutput capturePhotoWithSettings:photoSettings delegate:self]; } - (void)startRecordingVideo { if (self.isRecording) return; NSURL *outputURL = [self videoOutputURL]; // 如果文件已存在则删除 if ([[NSFileManager defaultManager] fileExistsAtPath:outputURL.path]) { [[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil]; } AVCaptureConnection *connection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo]; if (connection.isVideoOrientationSupported) { connection.videoOrientation = [self currentVideoOrientation]; } [self.movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self]; self.isRecording = YES; self.recordingDuration = 0; self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(updateRecordingProgress) userInfo:nil repeats:YES]; } - (void)stopRecordingVideo { if (!self.isRecording) return; [self.movieFileOutput stopRecording]; [self.progressTimer invalidate]; self.progressTimer = nil; self.isRecording = NO; } - (void)cleanTempFiles { NSString *tempPath = NSTemporaryDirectory(); NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tempPath error:nil]; for (NSString *file in contents) { if ([file hasPrefix:@"tempVideo"] && [file hasSuffix:@".mp4"]) { NSString *filePath = [tempPath stringByAppendingPathComponent:file]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; } } } #pragma mark - 私有方法 - (void)setupCaptureSession { self.captureSession = [[AVCaptureSession alloc] init]; // 根据设备性能选择合适的分辨率 if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) { self.captureSession.sessionPreset = AVCaptureSessionPreset3840x2160; } else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) { self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080; } else { self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720; } // 设置视频输入 AVCaptureDevice *videoDevice = [self cameraWithPosition:AVCaptureDevicePositionBack]; if (!videoDevice) { if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) { NSError *error = [NSError errorWithDomain:@"CameraUtility" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"无法获取摄像头设备"}]; [self.delegate cameraUtilityDidOccurError:error]; } return; } NSError *error = nil; self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:&error]; if (error) { if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) { [self.delegate cameraUtilityDidOccurError:error]; } return; } if ([self.captureSession canAddInput:self.videoInput]) { [self.captureSession addInput:self.videoInput]; } // 设置音频输入 AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error]; if (!error && [self.captureSession canAddInput:audioInput]) { [self.captureSession addInput:audioInput]; } // 设置照片输出 self.photoOutput = [[AVCapturePhotoOutput alloc] init]; // 检查是否支持高质量照片捕获 if ([self.photoOutput isHighResolutionCaptureEnabled]) { self.photoOutput.highResolutionCaptureEnabled = YES; } if ([self.captureSession canAddOutput:self.photoOutput]) { [self.captureSession addOutput:self.photoOutput]; } // 设置视频文件输出 self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init]; // 设置最大录制时长 CMTime maxDuration = CMTimeMakeWithSeconds(kMaxVideoDuration, 1); self.movieFileOutput.maxRecordedDuration = maxDuration; if ([self.captureSession canAddOutput:self.movieFileOutput]) { [self.captureSession addOutput:self.movieFileOutput]; } // 设置预览层 self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession]; self.previewLayer.frame = self.previewView.bounds; self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; [self.previewView.layer insertSublayer:self.previewLayer atIndex:0]; } - (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position { if (@available(iOS 10.0, *)) { AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:position]; return discoverySession.devices.firstObject; } else { // iOS 10 以下版本的兼容代码 NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *device in devices) { if (device.position == position) { return device; } } return nil; } } - (AVCaptureVideoOrientation)currentVideoOrientation { UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; switch (orientation) { case UIInterfaceOrientationPortrait: return AVCaptureVideoOrientationPortrait; case UIInterfaceOrientationLandscapeLeft: return AVCaptureVideoOrientationLandscapeLeft; case UIInterfaceOrientationLandscapeRight: return AVCaptureVideoOrientationLandscapeRight; case UIInterfaceOrientationPortraitUpsideDown: return AVCaptureVideoOrientationPortraitUpsideDown; default: return AVCaptureVideoOrientationPortrait; } } - (NSString *)tempVideoFilePath { NSString *tempPath = NSTemporaryDirectory(); NSString *fileName = [NSString stringWithFormat:@"tempVideo_%@.mp4", [[NSUUID UUID] UUIDString]]; return [tempPath stringByAppendingPathComponent:fileName]; } - (void)setFlashMode:(CameraUtilityFlashMode)flashMode { _flashMode = flashMode; AVCaptureDevice *device = self.videoInput.device; if ([device hasFlash] && [[self.photoOutput supportedFlashModes] containsObject:[NSNumber numberWithInteger:flashMode]]) { NSError *error = nil; if ([device lockForConfiguration:&error]) { self.photoOutput.photoSettingsForSceneMonitoring.flashMode = (AVCaptureFlashMode)flashMode; // device.flashMode = (AVCaptureFlashMode)flashMode; [device unlockForConfiguration]; } else { if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) { [self.delegate cameraUtilityDidOccurError:error]; } } } } - (void)updateRecordingProgress { if (!self.isRecording) return; self.recordingDuration += 0.05; CGFloat progress = self.recordingDuration / kMaxVideoDuration; if (progress >= 1.0) { [self stopRecordingVideo]; progress = 1.0; } if ([self.delegate respondsToSelector:@selector(cameraUtilityRecordingProgress:)]) { [self.delegate cameraUtilityRecordingProgress:progress]; } } - (UIImage *)adjustImageToScreenRatio:(UIImage *)image { if (self.aspectRatioMode == CameraAspectRatioModeOriginal) { return image; } CGSize screenSize = [UIScreen mainScreen].bounds.size; CGFloat screenRatio = screenSize.width / screenSize.height; CGSize imageSize = image.size; CGFloat imageRatio = imageSize.width / imageSize.height; // 计算目标尺寸 CGSize targetSize; if (self.aspectRatioMode == CameraAspectRatioModeSquare) { CGFloat size = MIN(imageSize.width, imageSize.height); targetSize = CGSizeMake(size, size); } else { if (imageRatio > screenRatio) { // 图片比屏幕宽,以高度为基准 targetSize = CGSizeMake(imageSize.height * screenRatio, imageSize.height); } else { // 图片比屏幕高,以宽度为基准 targetSize = CGSizeMake(imageSize.width, imageSize.width / screenRatio); } } // 高性能裁剪方法 return [self cropImage:image toRect:CGRectMake( (imageSize.width - targetSize.width)/2, (imageSize.height - targetSize.height)/2, targetSize.width, targetSize.height )]; } - (UIImage *)cropImage:(UIImage *)image toRect:(CGRect)rect { // 转换为像素精确坐标 rect = CGRectMake(rect.origin.x * image.scale, rect.origin.y * image.scale, rect.size.width * image.scale, rect.size.height * image.scale); CGImageRef imageRef = CGImageCreateWithImageInRect(image.CGImage, rect); UIImage *result = [UIImage imageWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation]; CGImageRelease(imageRef); return result; } - (UIImage *)safeCropImageToScreenRatio:(UIImage *)originalImage { // 先校正方向 UIImage *normalizedImage = [self correctedImage:originalImage]; // 现在获取的尺寸是显示方向正确的尺寸 CGSize imageSize = normalizedImage.size; CGFloat imageRatio = imageSize.width / imageSize.height; // 获取屏幕比例(注意考虑界面当前方向) CGSize screenSize = [UIScreen mainScreen].bounds.size; CGFloat screenRatio = screenSize.width / screenSize.height; // 计算裁剪区域(基于校正后的图像) CGRect cropRect; if (imageRatio > screenRatio) { // 裁剪宽度 CGFloat newWidth = imageSize.height * screenRatio; cropRect = CGRectMake((imageSize.width - newWidth)/2, 0, newWidth, imageSize.height); } else { // 裁剪高度 CGFloat newHeight = imageSize.width / screenRatio; cropRect = CGRectMake(0, (imageSize.height - newHeight)/2, imageSize.width, newHeight); } // 执行裁剪 return [self cropImage:normalizedImage toRect:cropRect]; } - (UIImage *)correctedImage:(UIImage *)image { if (image.imageOrientation == UIImageOrientationUp) return image; UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return normalizedImage; } #pragma mark - 手势处理 - (void)handleTapGesture:(UITapGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateEnded) { [self takePhoto]; } } - (void)handleLongPressGesture:(UILongPressGestureRecognizer *)gesture { switch (gesture.state) { case UIGestureRecognizerStateBegan: [self startRecordingVideo]; break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateFailed: [self stopRecordingVideo]; break; default: break; } } #pragma mark - AVCapturePhotoCaptureDelegate - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error { if (error) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate cameraUtilityDidOccurError:error]; }); return; } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSData *imageData = [photo fileDataRepresentation]; UIImage *image = [UIImage imageWithData:imageData]; // 在后台线程处理图片 image = [self safeCropImageToScreenRatio:image]; if (self.isFrontCamera) { image = [UIImage imageWithCGImage:image.CGImage scale:image.scale orientation:UIImageOrientationLeftMirrored]; } dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate cameraUtilityDidFinishTakingPhoto:image]; }); }); } #pragma mark - AVCaptureFileOutputRecordingDelegate - (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error { [self.progressTimer invalidate]; self.progressTimer = nil; self.isRecording = NO; if (error) { // 删除可能不完整的视频文件 if ([[NSFileManager defaultManager] fileExistsAtPath:outputFileURL.path]) { [[NSFileManager defaultManager] removeItemAtURL:outputFileURL error:nil]; } if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) { [self.delegate cameraUtilityDidOccurError:error]; } return; } // 确保文件已保存 if ([[NSFileManager defaultManager] fileExistsAtPath:outputFileURL.path]) { _lastVideoOutputURL = outputFileURL; if ([self.delegate respondsToSelector:@selector(cameraUtilityDidFinishRecordingVideo:)]) { [self.delegate cameraUtilityDidFinishRecordingVideo:outputFileURL]; } } else { NSError *fileError = [NSError errorWithDomain:@"CameraUtility" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"视频文件保存失败"}]; if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) { [self.delegate cameraUtilityDidOccurError:fileError]; } } } #pragma mark - 视频路径处理 - (NSString *)videoSaveDirectory { if (!_videoSaveDirectory) { _videoSaveDirectory = DEFAULT_VIDEO_DIRECTORY; } return _videoSaveDirectory; } - (NSURL *)lastVideoOutputURL { return _lastVideoOutputURL; } // 获取视频保存路径 - (NSURL *)videoOutputURL { NSString *fileName = [NSString stringWithFormat:@"video_%@.mp4", [NSUUID UUID].UUIDString]; // 获取文档目录 NSURL *documentsURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject]; NSURL *videoDirURL = [documentsURL URLByAppendingPathComponent:self.videoSaveDirectory isDirectory:YES]; // 创建目录(如果不存在) [[NSFileManager defaultManager] createDirectoryAtURL:videoDirURL withIntermediateDirectories:YES attributes:nil error:nil]; _lastVideoOutputURL = [videoDirURL URLByAppendingPathComponent:fileName]; return _lastVideoOutputURL; } @end