| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- //
- // CameraUtility.m
- // AIIM
- //
- // Created by qitewei on 2025/6/5.
- //
- #import "CameraUtility.h"
- // 默认视频保存路径
- #define DEFAULT_VIDEO_DIRECTORY @"Videos"
- @interface CameraUtility () <AVCaptureFileOutputRecordingDelegate, AVCapturePhotoCaptureDelegate>{
- 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<AVCaptureConnection *> *)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 {
- NSInteger now = [[NSDate date] timeIntervalSince1970];
- NSString *fileName = [NSString stringWithFormat:@"video_%ld.mp4", now];
-
- // 获取文档目录
- 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 = [documentsURL URLByAppendingPathComponent:fileName];
- return _lastVideoOutputURL;
- }
- @end
|