CameraUtility.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. //
  2. // CameraUtility.m
  3. // AIIM
  4. //
  5. // Created by qitewei on 2025/6/5.
  6. //
  7. #import "CameraUtility.h"
  8. // 默认视频保存路径
  9. #define DEFAULT_VIDEO_DIRECTORY @"Videos"
  10. @interface CameraUtility () <AVCaptureFileOutputRecordingDelegate, AVCapturePhotoCaptureDelegate>{
  11. NSURL *_lastVideoOutputURL;
  12. }
  13. @property (nonatomic, strong) AVCaptureSession *captureSession;
  14. @property (nonatomic, strong) AVCaptureDeviceInput *videoInput;
  15. @property (nonatomic, strong) AVCapturePhotoOutput *photoOutput;
  16. @property (nonatomic, strong) AVCaptureMovieFileOutput *movieFileOutput;
  17. @property (nonatomic, weak) UIView *previewView;
  18. @property (nonatomic, strong) NSTimer *progressTimer;
  19. @property (nonatomic, assign) CGFloat recordingDuration;
  20. @property (nonatomic, assign) BOOL isRecording;
  21. @end
  22. @implementation CameraUtility
  23. static const CGFloat kMaxVideoDuration = 60.0f; // 最大录制时长60秒
  24. #pragma mark - 生命周期方法
  25. - (instancetype)initWithPreviewView:(UIView *)previewView {
  26. self = [super init];
  27. if (self) {
  28. _previewView = previewView;
  29. _flashMode = CameraUtilityFlashModeOff;
  30. _isFrontCamera = NO;
  31. _recordingDuration = 0;
  32. _isRecording = NO;
  33. [self setupCaptureSession];
  34. }
  35. return self;
  36. }
  37. - (void)dealloc {
  38. [self stopRunning];
  39. [self cleanTempFiles];
  40. }
  41. #pragma mark - 公开方法
  42. - (void)configureCaptureButton:(UIButton *)button {
  43. // 单击手势 - 拍照
  44. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
  45. tapGesture.numberOfTapsRequired = 1;
  46. [button addGestureRecognizer:tapGesture];
  47. // 长按手势 - 拍视频
  48. UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
  49. longPressGesture.minimumPressDuration = 0.3;
  50. [button addGestureRecognizer:longPressGesture];
  51. // 确保单击不会被长按阻塞
  52. [tapGesture requireGestureRecognizerToFail:longPressGesture];
  53. }
  54. - (void)startRunning {
  55. if (![self.captureSession isRunning]) {
  56. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  57. [self.captureSession startRunning];
  58. });
  59. }
  60. }
  61. - (void)stopRunning {
  62. if ([self.captureSession isRunning]) {
  63. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  64. [self.captureSession stopRunning];
  65. });
  66. }
  67. }
  68. - (void)switchCamera {
  69. AVCaptureDevicePosition position = self.isFrontCamera ? AVCaptureDevicePositionBack : AVCaptureDevicePositionFront;
  70. AVCaptureDevice *device = [self cameraWithPosition:position];
  71. if (!device) return;
  72. [self.captureSession beginConfiguration];
  73. // 移除原有输入
  74. [self.captureSession removeInput:self.videoInput];
  75. // 创建新输入
  76. NSError *error = nil;
  77. AVCaptureDeviceInput *newVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
  78. if (newVideoInput) {
  79. if ([self.captureSession canAddInput:newVideoInput]) {
  80. [self.captureSession addInput:newVideoInput];
  81. self.videoInput = newVideoInput;
  82. self.isFrontCamera = !self.isFrontCamera;
  83. }
  84. } else {
  85. if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) {
  86. [self.delegate cameraUtilityDidOccurError:error];
  87. }
  88. }
  89. [self.captureSession commitConfiguration];
  90. // 重新设置闪光灯模式
  91. [self setFlashMode:self.flashMode];
  92. }
  93. - (void)takePhoto {
  94. AVCaptureConnection *connection = [self.photoOutput connectionWithMediaType:AVMediaTypeVideo];
  95. if (connection.isVideoOrientationSupported) {
  96. connection.videoOrientation = [self currentVideoOrientation];
  97. }
  98. // 创建照片设置
  99. AVCapturePhotoSettings *photoSettings;
  100. if (@available(iOS 11.0, *)) {
  101. // iOS 11+ 使用正确的格式设置
  102. NSDictionary *format = @{
  103. AVVideoCodecKey: AVVideoCodecTypeJPEG,
  104. AVVideoCompressionPropertiesKey: @{
  105. AVVideoQualityKey: @0.9 // 质量参数放在压缩属性字典内
  106. }
  107. };
  108. photoSettings = [AVCapturePhotoSettings photoSettingsWithFormat:format];
  109. } else {
  110. // iOS 10 回退方案
  111. photoSettings = [AVCapturePhotoSettings photoSettings];
  112. }
  113. // 设置闪光灯模式
  114. photoSettings.flashMode = (AVCaptureFlashMode)self.flashMode;
  115. // 触发拍照
  116. [self.photoOutput capturePhotoWithSettings:photoSettings delegate:self];
  117. }
  118. - (void)startRecordingVideo {
  119. if (self.isRecording) return;
  120. NSURL *outputURL = [self videoOutputURL];
  121. // 如果文件已存在则删除
  122. if ([[NSFileManager defaultManager] fileExistsAtPath:outputURL.path]) {
  123. [[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
  124. }
  125. AVCaptureConnection *connection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
  126. if (connection.isVideoOrientationSupported) {
  127. connection.videoOrientation = [self currentVideoOrientation];
  128. }
  129. [self.movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];
  130. self.isRecording = YES;
  131. self.recordingDuration = 0;
  132. self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.05
  133. target:self
  134. selector:@selector(updateRecordingProgress)
  135. userInfo:nil
  136. repeats:YES];
  137. }
  138. - (void)stopRecordingVideo {
  139. if (!self.isRecording) return;
  140. [self.movieFileOutput stopRecording];
  141. [self.progressTimer invalidate];
  142. self.progressTimer = nil;
  143. self.isRecording = NO;
  144. }
  145. - (void)cleanTempFiles {
  146. NSString *tempPath = NSTemporaryDirectory();
  147. NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tempPath error:nil];
  148. for (NSString *file in contents) {
  149. if ([file hasPrefix:@"tempVideo"] && [file hasSuffix:@".mp4"]) {
  150. NSString *filePath = [tempPath stringByAppendingPathComponent:file];
  151. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
  152. }
  153. }
  154. }
  155. #pragma mark - 私有方法
  156. - (void)setupCaptureSession {
  157. self.captureSession = [[AVCaptureSession alloc] init];
  158. // 根据设备性能选择合适的分辨率
  159. if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) {
  160. self.captureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
  161. } else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
  162. self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
  163. } else {
  164. self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
  165. }
  166. // 设置视频输入
  167. AVCaptureDevice *videoDevice = [self cameraWithPosition:AVCaptureDevicePositionBack];
  168. if (!videoDevice) {
  169. if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) {
  170. NSError *error = [NSError errorWithDomain:@"CameraUtility" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"无法获取摄像头设备"}];
  171. [self.delegate cameraUtilityDidOccurError:error];
  172. }
  173. return;
  174. }
  175. NSError *error = nil;
  176. self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:&error];
  177. if (error) {
  178. if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) {
  179. [self.delegate cameraUtilityDidOccurError:error];
  180. }
  181. return;
  182. }
  183. if ([self.captureSession canAddInput:self.videoInput]) {
  184. [self.captureSession addInput:self.videoInput];
  185. }
  186. // 设置音频输入
  187. AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
  188. AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
  189. if (!error && [self.captureSession canAddInput:audioInput]) {
  190. [self.captureSession addInput:audioInput];
  191. }
  192. // 设置照片输出
  193. self.photoOutput = [[AVCapturePhotoOutput alloc] init];
  194. // 检查是否支持高质量照片捕获
  195. if ([self.photoOutput isHighResolutionCaptureEnabled]) {
  196. self.photoOutput.highResolutionCaptureEnabled = YES;
  197. }
  198. if ([self.captureSession canAddOutput:self.photoOutput]) {
  199. [self.captureSession addOutput:self.photoOutput];
  200. }
  201. // 设置视频文件输出
  202. self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
  203. // 设置最大录制时长
  204. CMTime maxDuration = CMTimeMakeWithSeconds(kMaxVideoDuration, 1);
  205. self.movieFileOutput.maxRecordedDuration = maxDuration;
  206. if ([self.captureSession canAddOutput:self.movieFileOutput]) {
  207. [self.captureSession addOutput:self.movieFileOutput];
  208. }
  209. // 设置预览层
  210. self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
  211. self.previewLayer.frame = self.previewView.bounds;
  212. self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
  213. [self.previewView.layer insertSublayer:self.previewLayer atIndex:0];
  214. }
  215. - (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
  216. if (@available(iOS 10.0, *)) {
  217. AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera] mediaType:AVMediaTypeVideo position:position];
  218. return discoverySession.devices.firstObject;
  219. } else {
  220. // iOS 10 以下版本的兼容代码
  221. NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
  222. for (AVCaptureDevice *device in devices) {
  223. if (device.position == position) {
  224. return device;
  225. }
  226. }
  227. return nil;
  228. }
  229. }
  230. - (AVCaptureVideoOrientation)currentVideoOrientation {
  231. UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
  232. switch (orientation) {
  233. case UIInterfaceOrientationPortrait:
  234. return AVCaptureVideoOrientationPortrait;
  235. case UIInterfaceOrientationLandscapeLeft:
  236. return AVCaptureVideoOrientationLandscapeLeft;
  237. case UIInterfaceOrientationLandscapeRight:
  238. return AVCaptureVideoOrientationLandscapeRight;
  239. case UIInterfaceOrientationPortraitUpsideDown:
  240. return AVCaptureVideoOrientationPortraitUpsideDown;
  241. default:
  242. return AVCaptureVideoOrientationPortrait;
  243. }
  244. }
  245. - (NSString *)tempVideoFilePath {
  246. NSString *tempPath = NSTemporaryDirectory();
  247. NSString *fileName = [NSString stringWithFormat:@"tempVideo_%@.mp4", [[NSUUID UUID] UUIDString]];
  248. return [tempPath stringByAppendingPathComponent:fileName];
  249. }
  250. - (void)setFlashMode:(CameraUtilityFlashMode)flashMode {
  251. _flashMode = flashMode;
  252. AVCaptureDevice *device = self.videoInput.device;
  253. if ([device hasFlash] && [[self.photoOutput supportedFlashModes] containsObject:[NSNumber numberWithInteger:flashMode]]) {
  254. NSError *error = nil;
  255. if ([device lockForConfiguration:&error]) {
  256. self.photoOutput.photoSettingsForSceneMonitoring.flashMode = (AVCaptureFlashMode)flashMode;
  257. // device.flashMode = (AVCaptureFlashMode)flashMode;
  258. [device unlockForConfiguration];
  259. } else {
  260. if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) {
  261. [self.delegate cameraUtilityDidOccurError:error];
  262. }
  263. }
  264. }
  265. }
  266. - (void)updateRecordingProgress {
  267. if (!self.isRecording) return;
  268. self.recordingDuration += 0.05;
  269. CGFloat progress = self.recordingDuration / kMaxVideoDuration;
  270. if (progress >= 1.0) {
  271. [self stopRecordingVideo];
  272. progress = 1.0;
  273. }
  274. if ([self.delegate respondsToSelector:@selector(cameraUtilityRecordingProgress:)]) {
  275. [self.delegate cameraUtilityRecordingProgress:progress];
  276. }
  277. }
  278. - (UIImage *)adjustImageToScreenRatio:(UIImage *)image {
  279. if (self.aspectRatioMode == CameraAspectRatioModeOriginal) {
  280. return image;
  281. }
  282. CGSize screenSize = [UIScreen mainScreen].bounds.size;
  283. CGFloat screenRatio = screenSize.width / screenSize.height;
  284. CGSize imageSize = image.size;
  285. CGFloat imageRatio = imageSize.width / imageSize.height;
  286. // 计算目标尺寸
  287. CGSize targetSize;
  288. if (self.aspectRatioMode == CameraAspectRatioModeSquare) {
  289. CGFloat size = MIN(imageSize.width, imageSize.height);
  290. targetSize = CGSizeMake(size, size);
  291. } else {
  292. if (imageRatio > screenRatio) {
  293. // 图片比屏幕宽,以高度为基准
  294. targetSize = CGSizeMake(imageSize.height * screenRatio, imageSize.height);
  295. } else {
  296. // 图片比屏幕高,以宽度为基准
  297. targetSize = CGSizeMake(imageSize.width, imageSize.width / screenRatio);
  298. }
  299. }
  300. // 高性能裁剪方法
  301. return [self cropImage:image toRect:CGRectMake(
  302. (imageSize.width - targetSize.width)/2,
  303. (imageSize.height - targetSize.height)/2,
  304. targetSize.width,
  305. targetSize.height
  306. )];
  307. }
  308. - (UIImage *)cropImage:(UIImage *)image toRect:(CGRect)rect {
  309. // 转换为像素精确坐标
  310. rect = CGRectMake(rect.origin.x * image.scale,
  311. rect.origin.y * image.scale,
  312. rect.size.width * image.scale,
  313. rect.size.height * image.scale);
  314. CGImageRef imageRef = CGImageCreateWithImageInRect(image.CGImage, rect);
  315. UIImage *result = [UIImage imageWithCGImage:imageRef
  316. scale:image.scale
  317. orientation:image.imageOrientation];
  318. CGImageRelease(imageRef);
  319. return result;
  320. }
  321. - (UIImage *)safeCropImageToScreenRatio:(UIImage *)originalImage {
  322. // 先校正方向
  323. UIImage *normalizedImage = [self correctedImage:originalImage];
  324. // 现在获取的尺寸是显示方向正确的尺寸
  325. CGSize imageSize = normalizedImage.size;
  326. CGFloat imageRatio = imageSize.width / imageSize.height;
  327. // 获取屏幕比例(注意考虑界面当前方向)
  328. CGSize screenSize = [UIScreen mainScreen].bounds.size;
  329. CGFloat screenRatio = screenSize.width / screenSize.height;
  330. // 计算裁剪区域(基于校正后的图像)
  331. CGRect cropRect;
  332. if (imageRatio > screenRatio) {
  333. // 裁剪宽度
  334. CGFloat newWidth = imageSize.height * screenRatio;
  335. cropRect = CGRectMake((imageSize.width - newWidth)/2, 0, newWidth, imageSize.height);
  336. } else {
  337. // 裁剪高度
  338. CGFloat newHeight = imageSize.width / screenRatio;
  339. cropRect = CGRectMake(0, (imageSize.height - newHeight)/2, imageSize.width, newHeight);
  340. }
  341. // 执行裁剪
  342. return [self cropImage:normalizedImage toRect:cropRect];
  343. }
  344. - (UIImage *)correctedImage:(UIImage *)image {
  345. if (image.imageOrientation == UIImageOrientationUp) return image;
  346. UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
  347. [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
  348. UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
  349. UIGraphicsEndImageContext();
  350. return normalizedImage;
  351. }
  352. #pragma mark - 手势处理
  353. - (void)handleTapGesture:(UITapGestureRecognizer *)gesture {
  354. if (gesture.state == UIGestureRecognizerStateEnded) {
  355. [self takePhoto];
  356. }
  357. }
  358. - (void)handleLongPressGesture:(UILongPressGestureRecognizer *)gesture {
  359. switch (gesture.state) {
  360. case UIGestureRecognizerStateBegan:
  361. [self startRecordingVideo];
  362. break;
  363. case UIGestureRecognizerStateEnded:
  364. case UIGestureRecognizerStateCancelled:
  365. case UIGestureRecognizerStateFailed:
  366. [self stopRecordingVideo];
  367. break;
  368. default:
  369. break;
  370. }
  371. }
  372. #pragma mark - AVCapturePhotoCaptureDelegate
  373. - (void)captureOutput:(AVCapturePhotoOutput *)output
  374. didFinishProcessingPhoto:(AVCapturePhoto *)photo
  375. error:(NSError *)error {
  376. if (error) {
  377. dispatch_async(dispatch_get_main_queue(), ^{
  378. [self.delegate cameraUtilityDidOccurError:error];
  379. });
  380. return;
  381. }
  382. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  383. NSData *imageData = [photo fileDataRepresentation];
  384. UIImage *image = [UIImage imageWithData:imageData];
  385. // 在后台线程处理图片
  386. image = [self safeCropImageToScreenRatio:image];
  387. if (self.isFrontCamera) {
  388. image = [UIImage imageWithCGImage:image.CGImage
  389. scale:image.scale
  390. orientation:UIImageOrientationLeftMirrored];
  391. }
  392. dispatch_async(dispatch_get_main_queue(), ^{
  393. [self.delegate cameraUtilityDidFinishTakingPhoto:image];
  394. });
  395. });
  396. }
  397. #pragma mark - AVCaptureFileOutputRecordingDelegate
  398. - (void)captureOutput:(AVCaptureFileOutput *)output
  399. didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
  400. fromConnections:(NSArray<AVCaptureConnection *> *)connections
  401. error:(NSError *)error {
  402. [self.progressTimer invalidate];
  403. self.progressTimer = nil;
  404. self.isRecording = NO;
  405. if (error) {
  406. // 删除可能不完整的视频文件
  407. if ([[NSFileManager defaultManager] fileExistsAtPath:outputFileURL.path]) {
  408. [[NSFileManager defaultManager] removeItemAtURL:outputFileURL error:nil];
  409. }
  410. if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) {
  411. [self.delegate cameraUtilityDidOccurError:error];
  412. }
  413. return;
  414. }
  415. // 确保文件已保存
  416. if ([[NSFileManager defaultManager] fileExistsAtPath:outputFileURL.path]) {
  417. _lastVideoOutputURL = outputFileURL;
  418. if ([self.delegate respondsToSelector:@selector(cameraUtilityDidFinishRecordingVideo:)]) {
  419. [self.delegate cameraUtilityDidFinishRecordingVideo:outputFileURL];
  420. }
  421. } else {
  422. NSError *fileError = [NSError errorWithDomain:@"CameraUtility"
  423. code:1001
  424. userInfo:@{NSLocalizedDescriptionKey: @"视频文件保存失败"}];
  425. if ([self.delegate respondsToSelector:@selector(cameraUtilityDidOccurError:)]) {
  426. [self.delegate cameraUtilityDidOccurError:fileError];
  427. }
  428. }
  429. }
  430. #pragma mark - 视频路径处理
  431. - (NSString *)videoSaveDirectory {
  432. if (!_videoSaveDirectory) {
  433. _videoSaveDirectory = DEFAULT_VIDEO_DIRECTORY;
  434. }
  435. return _videoSaveDirectory;
  436. }
  437. - (NSURL *)lastVideoOutputURL {
  438. return _lastVideoOutputURL;
  439. }
  440. // 获取视频保存路径
  441. - (NSURL *)videoOutputURL {
  442. NSString *fileName = [NSString stringWithFormat:@"video_%@.mp4", [NSUUID UUID].UUIDString];
  443. // 获取文档目录
  444. NSURL *documentsURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
  445. NSURL *videoDirURL = [documentsURL URLByAppendingPathComponent:self.videoSaveDirectory isDirectory:YES];
  446. // 创建目录(如果不存在)
  447. [[NSFileManager defaultManager] createDirectoryAtURL:videoDirURL
  448. withIntermediateDirectories:YES
  449. attributes:nil
  450. error:nil];
  451. _lastVideoOutputURL = [videoDirURL URLByAppendingPathComponent:fileName];
  452. return _lastVideoOutputURL;
  453. }
  454. @end