Moves everything Camera related into `core/` / `Core/` so that it is better encapsulated from React Native.
Benefits:
1. Code is much better organized. Should be easier for collaborators now, and cleaner codebase for me.
2. Locking is fully atomically as you can now only configure the session through a lock/Mutex which is batch-overridable
    * On iOS, this makes Camera startup time **MUCH** faster, I measured speedups from **1.5 seconds** to only **240 milliseconds** since we only lock/commit once! 🚀 
    * On Android, this fixes a few out-of-sync/concurrency issues like "Capture Request contains unconfigured Input/Output Surface!" since it is now a single lock-operation! 💪 
3. It is easier to integrate VisionCamera outside of React Native (e.g. Native iOS Apps, NativeScript, Flutter, etc)
With this PR, VisionCamera V3 is up to **7x** faster than V2
		
	
		
			
				
	
	
		
			108 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			108 lines
		
	
	
		
			4.0 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
| //
 | |
| //  CameraSession+Photo.swift
 | |
| //  VisionCamera
 | |
| //
 | |
| //  Created by Marc Rousavy on 11.10.23.
 | |
| //  Copyright © 2023 mrousavy. All rights reserved.
 | |
| //
 | |
| 
 | |
| import AVFoundation
 | |
| import Foundation
 | |
| 
 | |
| extension CameraSession {
 | |
|   /**
 | |
|    Takes a photo.
 | |
|    `takePhoto` is only available if `photo={true}`.
 | |
|    */
 | |
|   func takePhoto(options: NSDictionary, promise: Promise) {
 | |
|     // Run on Camera Queue
 | |
|     CameraQueues.cameraQueue.async {
 | |
|       // Get Photo Output configuration
 | |
|       guard let configuration = self.configuration else {
 | |
|         promise.reject(error: .session(.cameraNotReady))
 | |
|         return
 | |
|       }
 | |
|       guard case let .enabled(config: photo) = configuration.photo else {
 | |
|         // User needs to enable photo={true}
 | |
|         promise.reject(error: .capture(.photoNotEnabled))
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       // Check if Photo Output is available
 | |
|       guard let photoOutput = self.photoOutput,
 | |
|             let videoDeviceInput = self.videoDeviceInput else {
 | |
|         // Camera is not yet ready
 | |
|         promise.reject(error: .session(.cameraNotReady))
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       ReactLogger.log(level: .info, message: "Capturing photo...")
 | |
| 
 | |
|       // Create photo settings
 | |
|       let photoSettings = AVCapturePhotoSettings()
 | |
| 
 | |
|       // default, overridable settings if high quality capture was enabled
 | |
|       if photo.enableHighQualityPhotos {
 | |
|         // TODO: On iOS 16+ this will be removed in favor of maxPhotoDimensions.
 | |
|         photoSettings.isHighResolutionPhotoEnabled = true
 | |
|         if #available(iOS 13.0, *) {
 | |
|           photoSettings.photoQualityPrioritization = .quality
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // flash
 | |
|       if videoDeviceInput.device.isFlashAvailable, let flash = options["flash"] as? String {
 | |
|         guard let flashMode = AVCaptureDevice.FlashMode(withString: flash) else {
 | |
|           promise.reject(error: .parameter(.invalid(unionName: "FlashMode", receivedValue: flash)))
 | |
|           return
 | |
|         }
 | |
|         photoSettings.flashMode = flashMode
 | |
|       }
 | |
| 
 | |
|       // shutter sound
 | |
|       let enableShutterSound = options["enableShutterSound"] as? Bool ?? true
 | |
| 
 | |
|       // depth data
 | |
|       photoSettings.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliveryEnabled
 | |
|       if #available(iOS 12.0, *) {
 | |
|         photoSettings.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliveryEnabled
 | |
|       }
 | |
| 
 | |
|       // quality prioritization
 | |
|       if #available(iOS 13.0, *), let qualityPrioritization = options["qualityPrioritization"] as? String {
 | |
|         guard let photoQualityPrioritization = AVCapturePhotoOutput.QualityPrioritization(withString: qualityPrioritization) else {
 | |
|           promise.reject(error: .parameter(.invalid(unionName: "QualityPrioritization", receivedValue: qualityPrioritization)))
 | |
|           return
 | |
|         }
 | |
|         photoSettings.photoQualityPrioritization = photoQualityPrioritization
 | |
|       }
 | |
| 
 | |
|       // photo size is always the one selected in the format
 | |
|       if #available(iOS 16.0, *) {
 | |
|         photoSettings.maxPhotoDimensions = photoOutput.maxPhotoDimensions
 | |
|       }
 | |
| 
 | |
|       // red-eye reduction
 | |
|       if #available(iOS 12.0, *), let autoRedEyeReduction = options["enableAutoRedEyeReduction"] as? Bool {
 | |
|         photoSettings.isAutoRedEyeReductionEnabled = autoRedEyeReduction
 | |
|       }
 | |
| 
 | |
|       // stabilization
 | |
|       if let enableAutoStabilization = options["enableAutoStabilization"] as? Bool {
 | |
|         photoSettings.isAutoStillImageStabilizationEnabled = enableAutoStabilization
 | |
|       }
 | |
| 
 | |
|       // distortion correction
 | |
|       if #available(iOS 14.1, *), let enableAutoDistortionCorrection = options["enableAutoDistortionCorrection"] as? Bool {
 | |
|         photoSettings.isAutoContentAwareDistortionCorrectionEnabled = enableAutoDistortionCorrection
 | |
|       }
 | |
| 
 | |
|       // Actually do the capture!
 | |
|       photoOutput.capturePhoto(with: photoSettings, delegate: PhotoCaptureDelegate(promise: promise, enableShutterSound: enableShutterSound))
 | |
| 
 | |
|       // Assume that `takePhoto` is always called with the same parameters, so prepare the next call too.
 | |
|       photoOutput.setPreparedPhotoSettingsArray([photoSettings], completionHandler: nil)
 | |
|     }
 | |
|   }
 | |
| }
 |