feat: New Core/ library (#1975)
				
					
				
			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
			
			
This commit is contained in:
		| @@ -41,6 +41,7 @@ Pod::Spec.new do |s| | ||||
|   s.source_files = [ | ||||
|     # Core | ||||
|     "ios/*.{m,mm,swift}", | ||||
|     "ios/Core/*.{m,mm,swift}", | ||||
|     "ios/Extensions/*.{m,mm,swift}", | ||||
|     "ios/Parsers/*.{m,mm,swift}", | ||||
|     "ios/React Utils/*.{m,mm,swift}", | ||||
|   | ||||
| @@ -6,6 +6,9 @@ import com.facebook.react.bridge.ReactContext | ||||
| import com.facebook.react.bridge.WritableMap | ||||
| import com.facebook.react.uimanager.events.RCTEventEmitter | ||||
| import com.google.mlkit.vision.barcode.common.Barcode | ||||
| import com.mrousavy.camera.core.CameraError | ||||
| import com.mrousavy.camera.core.UnknownCameraError | ||||
| import com.mrousavy.camera.core.code | ||||
| import com.mrousavy.camera.parsers.CodeType | ||||
|  | ||||
| fun CameraView.invokeOnInitialized() { | ||||
|   | ||||
| @@ -5,7 +5,10 @@ import android.annotation.SuppressLint | ||||
| import android.content.pm.PackageManager | ||||
| import androidx.core.content.ContextCompat | ||||
| import com.facebook.react.bridge.* | ||||
| import com.mrousavy.camera.core.MicrophonePermissionError | ||||
| import com.mrousavy.camera.core.RecorderError | ||||
| import com.mrousavy.camera.core.RecordingSession | ||||
| import com.mrousavy.camera.core.code | ||||
| import com.mrousavy.camera.parsers.Torch | ||||
| import com.mrousavy.camera.parsers.VideoCodec | ||||
| import com.mrousavy.camera.parsers.VideoFileType | ||||
|   | ||||
| @@ -13,7 +13,10 @@ import android.view.Surface | ||||
| import android.widget.FrameLayout | ||||
| import androidx.core.content.ContextCompat | ||||
| import com.facebook.react.bridge.ReadableMap | ||||
| import com.mrousavy.camera.core.CameraPermissionError | ||||
| import com.mrousavy.camera.core.CameraQueues | ||||
| import com.mrousavy.camera.core.CameraSession | ||||
| import com.mrousavy.camera.core.NoCameraDeviceError | ||||
| import com.mrousavy.camera.core.PreviewView | ||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | ||||
| import com.mrousavy.camera.extensions.bigger | ||||
|   | ||||
| @@ -9,6 +9,8 @@ import com.facebook.react.module.annotations.ReactModule | ||||
| import com.facebook.react.modules.core.PermissionAwareActivity | ||||
| import com.facebook.react.modules.core.PermissionListener | ||||
| import com.facebook.react.uimanager.UIManagerHelper | ||||
| import com.mrousavy.camera.core.CameraError | ||||
| import com.mrousavy.camera.core.ViewNotFoundError | ||||
| import com.mrousavy.camera.frameprocessor.VisionCameraInstaller | ||||
| import com.mrousavy.camera.frameprocessor.VisionCameraProxy | ||||
| import com.mrousavy.camera.parsers.* | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.mrousavy.camera | ||||
| package com.mrousavy.camera.core | ||||
| 
 | ||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | ||||
| import com.mrousavy.camera.parsers.CameraDeviceError | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.mrousavy.camera | ||||
| package com.mrousavy.camera.core | ||||
| 
 | ||||
| import android.os.Handler | ||||
| import android.os.HandlerThread | ||||
| @@ -16,15 +16,7 @@ import android.os.Build | ||||
| import android.util.Log | ||||
| import android.util.Range | ||||
| import android.util.Size | ||||
| import com.mrousavy.camera.CameraNotReadyError | ||||
| import com.mrousavy.camera.CameraQueues | ||||
| import com.mrousavy.camera.CameraView | ||||
| import com.mrousavy.camera.CaptureAbortedError | ||||
| import com.mrousavy.camera.NoRecordingInProgressError | ||||
| import com.mrousavy.camera.PhotoNotEnabledError | ||||
| import com.mrousavy.camera.RecorderError | ||||
| import com.mrousavy.camera.RecordingInProgressError | ||||
| import com.mrousavy.camera.VideoNotEnabledError | ||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | ||||
| import com.mrousavy.camera.extensions.capture | ||||
| import com.mrousavy.camera.extensions.createCaptureSession | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import com.google.mlkit.vision.barcode.BarcodeScanner | ||||
| import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||||
| import com.google.mlkit.vision.barcode.BarcodeScanning | ||||
| import com.google.mlkit.vision.common.InputImage | ||||
| import com.mrousavy.camera.CameraQueues | ||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | ||||
| import com.mrousavy.camera.parsers.Orientation | ||||
| import java.io.Closeable | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import android.os.Build | ||||
| import android.util.Log | ||||
| import android.util.Size | ||||
| import android.view.Surface | ||||
| import com.mrousavy.camera.RecorderError | ||||
| import com.mrousavy.camera.parsers.Orientation | ||||
| import com.mrousavy.camera.parsers.VideoCodec | ||||
| import com.mrousavy.camera.parsers.VideoFileType | ||||
|   | ||||
| @@ -9,8 +9,6 @@ import android.os.Build | ||||
| import android.util.Log | ||||
| import android.view.Surface | ||||
| import com.facebook.jni.HybridData | ||||
| import com.mrousavy.camera.CameraQueues | ||||
| import com.mrousavy.camera.FrameProcessorsUnavailableError | ||||
| import com.mrousavy.camera.frameprocessor.Frame | ||||
| import com.mrousavy.camera.frameprocessor.FrameProcessor | ||||
| import com.mrousavy.camera.parsers.Orientation | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import android.util.Log | ||||
| import android.util.Size | ||||
| import android.view.Surface | ||||
| import com.google.mlkit.vision.barcode.common.Barcode | ||||
| import com.mrousavy.camera.CameraQueues | ||||
| import com.mrousavy.camera.core.CameraQueues | ||||
| import com.mrousavy.camera.core.CodeScannerPipeline | ||||
| import com.mrousavy.camera.core.VideoPipeline | ||||
| import com.mrousavy.camera.extensions.bigger | ||||
|   | ||||
| @@ -5,9 +5,9 @@ import android.hardware.camera2.CaptureFailure | ||||
| import android.hardware.camera2.CaptureRequest | ||||
| import android.hardware.camera2.TotalCaptureResult | ||||
| import android.media.MediaActionSound | ||||
| import com.mrousavy.camera.CameraQueues | ||||
| import com.mrousavy.camera.CaptureAbortedError | ||||
| import com.mrousavy.camera.UnknownCaptureError | ||||
| import com.mrousavy.camera.core.CameraQueues | ||||
| import com.mrousavy.camera.core.CaptureAbortedError | ||||
| import com.mrousavy.camera.core.UnknownCaptureError | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
| import kotlin.coroutines.suspendCoroutine | ||||
|   | ||||
| @@ -8,8 +8,8 @@ import android.hardware.camera2.params.OutputConfiguration | ||||
| import android.hardware.camera2.params.SessionConfiguration | ||||
| import android.os.Build | ||||
| import android.util.Log | ||||
| import com.mrousavy.camera.CameraQueues | ||||
| import com.mrousavy.camera.CameraSessionCannotBeConfiguredError | ||||
| import com.mrousavy.camera.core.CameraQueues | ||||
| import com.mrousavy.camera.core.CameraSessionCannotBeConfiguredError | ||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
|   | ||||
| @@ -5,9 +5,9 @@ import android.hardware.camera2.CameraDevice | ||||
| import android.hardware.camera2.CameraManager | ||||
| import android.os.Build | ||||
| import android.util.Log | ||||
| import com.mrousavy.camera.CameraCannotBeOpenedError | ||||
| import com.mrousavy.camera.CameraDisconnectedError | ||||
| import com.mrousavy.camera.CameraQueues | ||||
| import com.mrousavy.camera.core.CameraCannotBeOpenedError | ||||
| import com.mrousavy.camera.core.CameraDisconnectedError | ||||
| import com.mrousavy.camera.core.CameraQueues | ||||
| import com.mrousavy.camera.parsers.CameraDeviceError | ||||
| import kotlin.coroutines.resume | ||||
| import kotlin.coroutines.resumeWithException | ||||
|   | ||||
| @@ -4,12 +4,10 @@ import android.hardware.HardwareBuffer; | ||||
| import android.media.Image; | ||||
| import android.os.Build; | ||||
| import com.facebook.proguard.annotations.DoNotStrip; | ||||
| import com.mrousavy.camera.HardwareBuffersNotAvailableError; | ||||
| import com.mrousavy.camera.core.HardwareBuffersNotAvailableError; | ||||
| import com.mrousavy.camera.parsers.PixelFormat; | ||||
| import com.mrousavy.camera.parsers.Orientation; | ||||
|  | ||||
| import java.nio.ByteBuffer; | ||||
|  | ||||
| public class Frame { | ||||
|     private final Image image; | ||||
|     private final boolean isMirrored; | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import com.facebook.react.bridge.UiThreadUtil | ||||
| import com.facebook.react.turbomodule.core.CallInvokerHolderImpl | ||||
| import com.facebook.react.uimanager.UIManagerHelper | ||||
| import com.mrousavy.camera.CameraView | ||||
| import com.mrousavy.camera.ViewNotFoundError | ||||
| import com.mrousavy.camera.core.ViewNotFoundError | ||||
| import java.lang.ref.WeakReference | ||||
|  | ||||
| @Suppress("KotlinJniMissingFunction") // we use fbjni. | ||||
|   | ||||
| @@ -2,9 +2,7 @@ package com.mrousavy.camera.frameprocessor; | ||||
|  | ||||
| import com.facebook.jni.HybridData; | ||||
| import com.facebook.proguard.annotations.DoNotStrip; | ||||
| import com.mrousavy.camera.CameraQueues; | ||||
|  | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import com.mrousavy.camera.core.CameraQueues; | ||||
|  | ||||
| @SuppressWarnings("JavaJniMissingFunction") // using fbjni here | ||||
| public class VisionCameraScheduler { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package com.mrousavy.camera.parsers | ||||
|  | ||||
| import com.facebook.react.bridge.ReadableMap | ||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | ||||
| import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||
|  | ||||
| class CodeScanner(map: ReadableMap) { | ||||
|   val codeTypes: List<CodeType> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| package com.mrousavy.camera.parsers | ||||
|  | ||||
| import com.google.mlkit.vision.barcode.common.Barcode | ||||
| import com.mrousavy.camera.CodeTypeNotSupportedError | ||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | ||||
| import com.mrousavy.camera.core.CodeTypeNotSupportedError | ||||
| import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||
|  | ||||
| enum class CodeType(override val unionValue: String) : JSUnionValue { | ||||
|   CODE_128("code-128"), | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package com.mrousavy.camera.parsers | ||||
|  | ||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | ||||
| import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||
|  | ||||
| enum class Flash(override val unionValue: String) : JSUnionValue { | ||||
|   OFF("off"), | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package com.mrousavy.camera.parsers | ||||
|  | ||||
| import android.graphics.ImageFormat | ||||
| import com.mrousavy.camera.PixelFormatNotSupportedError | ||||
| import com.mrousavy.camera.core.PixelFormatNotSupportedError | ||||
|  | ||||
| enum class PixelFormat(override val unionValue: String) : JSUnionValue { | ||||
|   YUV("yuv"), | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package com.mrousavy.camera.parsers | ||||
|  | ||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | ||||
| import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||
|  | ||||
| enum class VideoFileType(override val unionValue: String) : JSUnionValue { | ||||
|   MOV("mov"), | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| package com.mrousavy.camera.utils | ||||
|  | ||||
| import com.facebook.react.bridge.Promise | ||||
| import com.mrousavy.camera.CameraError | ||||
| import com.mrousavy.camera.UnknownCameraError | ||||
| import com.mrousavy.camera.core.CameraError | ||||
| import com.mrousavy.camera.core.UnknownCameraError | ||||
|  | ||||
| inline fun withPromise(promise: Promise, closure: () -> Any?) { | ||||
|   try { | ||||
|   | ||||
| @@ -747,7 +747,7 @@ SPEC CHECKSUMS: | ||||
|   SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d | ||||
|   SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d | ||||
|   SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 | ||||
|   VisionCamera: f649cd0c0fa6266f1cd5e0787a7c9583ca143b3a | ||||
|   VisionCamera: f386aee60abb07d979c506ea9e6d4831e596cafe | ||||
|   Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce | ||||
|  | ||||
| PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb | ||||
|   | ||||
| @@ -47,7 +47,12 @@ export function CameraPage({ navigation }: Props): React.ReactElement { | ||||
|  | ||||
|   // camera device settings | ||||
|   const [preferredDevice] = usePreferredCameraDevice() | ||||
|   const device = useCameraDevice(cameraPosition) | ||||
|   let device = useCameraDevice(cameraPosition) | ||||
|  | ||||
|   if (preferredDevice != null && preferredDevice.position === cameraPosition) { | ||||
|     // override default device with the one selected by the user in settings | ||||
|     device = preferredDevice | ||||
|   } | ||||
|  | ||||
|   const [targetFps, setTargetFps] = useState(60) | ||||
|  | ||||
| @@ -172,7 +177,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { | ||||
|               <ReanimatedCamera | ||||
|                 ref={camera} | ||||
|                 style={StyleSheet.absoluteFill} | ||||
|                 device={preferredDevice ?? device} | ||||
|                 device={device} | ||||
|                 format={format} | ||||
|                 fps={fps} | ||||
|                 hdr={enableHdr} | ||||
|   | ||||
| @@ -1,151 +0,0 @@ | ||||
| // | ||||
| //  CameraView+AVAudioSession.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 26.03.21. | ||||
| //  Copyright © 2021 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  Extension for CameraView that sets up the AVAudioSession. | ||||
|  */ | ||||
| extension CameraView { | ||||
|   /** | ||||
|    Configures the Audio Capture Session with an audio input and audio data output. | ||||
|    */ | ||||
|   final func configureAudioSession() { | ||||
|     ReactLogger.log(level: .info, message: "Configuring Audio Session...") | ||||
|  | ||||
|     audioCaptureSession.beginConfiguration() | ||||
|     defer { | ||||
|       audioCaptureSession.commitConfiguration() | ||||
|     } | ||||
|  | ||||
|     audioCaptureSession.automaticallyConfiguresApplicationAudioSession = false | ||||
|     let enableAudio = audio?.boolValue == true | ||||
|  | ||||
|     // check microphone permission | ||||
|     if enableAudio { | ||||
|       let audioPermissionStatus = AVCaptureDevice.authorizationStatus(for: .audio) | ||||
|       if audioPermissionStatus != .authorized { | ||||
|         invokeOnError(.permission(.microphone)) | ||||
|         return | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Audio Input | ||||
|     do { | ||||
|       if let audioDeviceInput = audioDeviceInput { | ||||
|         audioCaptureSession.removeInput(audioDeviceInput) | ||||
|         self.audioDeviceInput = nil | ||||
|       } | ||||
|       if enableAudio { | ||||
|         ReactLogger.log(level: .info, message: "Adding Audio input...") | ||||
|         guard let audioDevice = AVCaptureDevice.default(for: .audio) else { | ||||
|           invokeOnError(.device(.microphoneUnavailable)) | ||||
|           return | ||||
|         } | ||||
|         audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice) | ||||
|         guard audioCaptureSession.canAddInput(audioDeviceInput!) else { | ||||
|           invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "audio-input"))) | ||||
|           return | ||||
|         } | ||||
|         audioCaptureSession.addInput(audioDeviceInput!) | ||||
|       } | ||||
|     } catch let error as NSError { | ||||
|       invokeOnError(.device(.microphoneUnavailable), cause: error) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Audio Output | ||||
|     if let audioOutput = audioOutput { | ||||
|       audioCaptureSession.removeOutput(audioOutput) | ||||
|       self.audioOutput = nil | ||||
|     } | ||||
|     if enableAudio { | ||||
|       ReactLogger.log(level: .info, message: "Adding Audio Data output...") | ||||
|       audioOutput = AVCaptureAudioDataOutput() | ||||
|       guard audioCaptureSession.canAddOutput(audioOutput!) else { | ||||
|         invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "audio-output"))) | ||||
|         return | ||||
|       } | ||||
|       audioOutput!.setSampleBufferDelegate(self, queue: CameraQueues.audioQueue) | ||||
|       audioCaptureSession.addOutput(audioOutput!) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Configures the Audio session and activates it. If the session was active it will shortly be deactivated before configuration. | ||||
|  | ||||
|    The Audio Session will be configured to allow background music, haptics (vibrations) and system sound playback while recording. | ||||
|    Background audio is allowed to play on speakers or bluetooth speakers. | ||||
|    */ | ||||
|   final func activateAudioSession() { | ||||
|     ReactLogger.log(level: .info, message: "Activating Audio Session...") | ||||
|  | ||||
|     do { | ||||
|       try AVAudioSession.sharedInstance().updateCategory(AVAudioSession.Category.playAndRecord, | ||||
|                                                          options: [.mixWithOthers, | ||||
|                                                                    .allowBluetoothA2DP, | ||||
|                                                                    .defaultToSpeaker, | ||||
|                                                                    .allowAirPlay]) | ||||
|  | ||||
|       if #available(iOS 14.5, *) { | ||||
|         // prevents the audio session from being interrupted by a phone call | ||||
|         try AVAudioSession.sharedInstance().setPrefersNoInterruptionsFromSystemAlerts(true) | ||||
|       } | ||||
|  | ||||
|       audioCaptureSession.startRunning() | ||||
|     } catch let error as NSError { | ||||
|       switch error.code { | ||||
|       case 561_017_449: | ||||
|         self.invokeOnError(.session(.audioInUseByOtherApp), cause: error) | ||||
|       default: | ||||
|         self.invokeOnError(.session(.audioSessionFailedToActivate), cause: error) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   final func deactivateAudioSession() { | ||||
|     ReactLogger.log(level: .info, message: "Deactivating Audio Session...") | ||||
|  | ||||
|     audioCaptureSession.stopRunning() | ||||
|   } | ||||
|  | ||||
|   @objc | ||||
|   func audioSessionInterrupted(notification: Notification) { | ||||
|     ReactLogger.log(level: .error, message: "Audio Session Interruption Notification!") | ||||
|     guard let userInfo = notification.userInfo, | ||||
|           let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, | ||||
|           let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // TODO: Add JS-Event for Audio Session interruptions? | ||||
|     switch type { | ||||
|     case .began: | ||||
|       // Something interrupted our Audio Session, stop recording audio. | ||||
|       ReactLogger.log(level: .error, message: "The Audio Session was interrupted!") | ||||
|     case .ended: | ||||
|       ReactLogger.log(level: .info, message: "The Audio Session interruption has ended.") | ||||
|       guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } | ||||
|       let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) | ||||
|       if options.contains(.shouldResume) { | ||||
|         if isRecording { | ||||
|           CameraQueues.audioQueue.async { | ||||
|             ReactLogger.log(level: .info, message: "Resuming interrupted Audio Session...") | ||||
|             // restart audio session because interruption is over | ||||
|             self.activateAudioSession() | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         ReactLogger.log(level: .error, message: "Cannot resume interrupted Audio Session!") | ||||
|       } | ||||
|     @unknown default: | ||||
|       () | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,369 +0,0 @@ | ||||
| // | ||||
| //  CameraView+AVCaptureSession.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 26.03.21. | ||||
| //  Copyright © 2021 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  Extension for CameraView that sets up the AVCaptureSession, Device and Format. | ||||
|  */ | ||||
| extension CameraView { | ||||
|   // pragma MARK: Configure Capture Session | ||||
|  | ||||
|   /** | ||||
|    Configures the Capture Session. | ||||
|    */ | ||||
|   final func configureCaptureSession() { | ||||
|     ReactLogger.log(level: .info, message: "Configuring Session...") | ||||
|     isReady = false | ||||
|  | ||||
|     #if targetEnvironment(simulator) | ||||
|       invokeOnError(.device(.notAvailableOnSimulator)) | ||||
|       return | ||||
|     #endif | ||||
|  | ||||
|     guard cameraId != nil else { | ||||
|       invokeOnError(.device(.noDevice)) | ||||
|       return | ||||
|     } | ||||
|     let cameraId = self.cameraId! as String | ||||
|  | ||||
|     ReactLogger.log(level: .info, message: "Initializing Camera with device \(cameraId)...") | ||||
|     captureSession.beginConfiguration() | ||||
|     defer { | ||||
|       captureSession.commitConfiguration() | ||||
|     } | ||||
|  | ||||
|     // pragma MARK: Capture Session Inputs | ||||
|     // Video Input | ||||
|     do { | ||||
|       if let videoDeviceInput = videoDeviceInput { | ||||
|         captureSession.removeInput(videoDeviceInput) | ||||
|         self.videoDeviceInput = nil | ||||
|       } | ||||
|       ReactLogger.log(level: .info, message: "Adding Video input...") | ||||
|       guard let videoDevice = AVCaptureDevice(uniqueID: cameraId) else { | ||||
|         invokeOnError(.device(.invalid)) | ||||
|         return | ||||
|       } | ||||
|       videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) | ||||
|       guard captureSession.canAddInput(videoDeviceInput!) else { | ||||
|         invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "video-input"))) | ||||
|         return | ||||
|       } | ||||
|       captureSession.addInput(videoDeviceInput!) | ||||
|     } catch { | ||||
|       invokeOnError(.device(.invalid)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // pragma MARK: Capture Session Outputs | ||||
|  | ||||
|     // Photo Output | ||||
|     if let photoOutput = photoOutput { | ||||
|       captureSession.removeOutput(photoOutput) | ||||
|       self.photoOutput = nil | ||||
|     } | ||||
|     if photo?.boolValue == true { | ||||
|       ReactLogger.log(level: .info, message: "Adding Photo output...") | ||||
|       photoOutput = AVCapturePhotoOutput() | ||||
|  | ||||
|       if enableHighQualityPhotos?.boolValue == true { | ||||
|         // TODO: In iOS 16 this will be removed in favor of maxPhotoDimensions. | ||||
|         photoOutput!.isHighResolutionCaptureEnabled = true | ||||
|         if #available(iOS 13.0, *) { | ||||
|           // TODO: Test if this actually does any fusion or if this just calls the captureOutput twice. If the latter, remove it. | ||||
|           photoOutput!.isVirtualDeviceConstituentPhotoDeliveryEnabled = photoOutput!.isVirtualDeviceConstituentPhotoDeliverySupported | ||||
|           photoOutput!.maxPhotoQualityPrioritization = .quality | ||||
|         } else { | ||||
|           photoOutput!.isDualCameraDualPhotoDeliveryEnabled = photoOutput!.isDualCameraDualPhotoDeliverySupported | ||||
|         } | ||||
|       } | ||||
|       // TODO: Enable isResponsiveCaptureEnabled? (iOS 17+) | ||||
|       // TODO: Enable isFastCapturePrioritizationEnabled? (iOS 17+) | ||||
|       if enableDepthData { | ||||
|         photoOutput!.isDepthDataDeliveryEnabled = photoOutput!.isDepthDataDeliverySupported | ||||
|       } | ||||
|       if #available(iOS 12.0, *), enablePortraitEffectsMatteDelivery { | ||||
|         photoOutput!.isPortraitEffectsMatteDeliveryEnabled = photoOutput!.isPortraitEffectsMatteDeliverySupported | ||||
|       } | ||||
|       guard captureSession.canAddOutput(photoOutput!) else { | ||||
|         invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "photo-output"))) | ||||
|         return | ||||
|       } | ||||
|       captureSession.addOutput(photoOutput!) | ||||
|       if videoDeviceInput!.device.position == .front { | ||||
|         photoOutput!.mirror() | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Video Output + Frame Processor | ||||
|     if let videoOutput = videoOutput { | ||||
|       captureSession.removeOutput(videoOutput) | ||||
|       self.videoOutput = nil | ||||
|     } | ||||
|     if video?.boolValue == true || enableFrameProcessor { | ||||
|       ReactLogger.log(level: .info, message: "Adding Video Data output...") | ||||
|       videoOutput = AVCaptureVideoDataOutput() | ||||
|       guard captureSession.canAddOutput(videoOutput!) else { | ||||
|         invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "video-output"))) | ||||
|         return | ||||
|       } | ||||
|       videoOutput!.setSampleBufferDelegate(self, queue: CameraQueues.videoQueue) | ||||
|       videoOutput!.alwaysDiscardsLateVideoFrames = false | ||||
|  | ||||
|       let pixelFormatType = getPixelFormat(videoOutput: videoOutput!) | ||||
|       videoOutput!.videoSettings = [ | ||||
|         String(kCVPixelBufferPixelFormatTypeKey): pixelFormatType, | ||||
|       ] | ||||
|       captureSession.addOutput(videoOutput!) | ||||
|     } | ||||
|  | ||||
|     // Code Scanner | ||||
|     if let codeScannerOptions = codeScannerOptions { | ||||
|       guard let codeScanner = try? CodeScanner(fromJsValue: codeScannerOptions) else { | ||||
|         invokeOnError(.parameter(.invalid(unionName: "codeScanner", receivedValue: codeScannerOptions.description))) | ||||
|         return | ||||
|       } | ||||
|       let metadataOutput = AVCaptureMetadataOutput() | ||||
|       guard captureSession.canAddOutput(metadataOutput) else { | ||||
|         invokeOnError(.codeScanner(.notCompatibleWithOutputs)) | ||||
|         return | ||||
|       } | ||||
|       captureSession.addOutput(metadataOutput) | ||||
|  | ||||
|       for codeType in codeScanner.codeTypes { | ||||
|         // swiftlint:disable:next for_where | ||||
|         if !metadataOutput.availableMetadataObjectTypes.contains(codeType) { | ||||
|           invokeOnError(.codeScanner(.codeTypeNotSupported(codeType: codeType.descriptor))) | ||||
|           return | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       metadataOutput.setMetadataObjectsDelegate(self, queue: CameraQueues.codeScannerQueue) | ||||
|       metadataOutput.metadataObjectTypes = codeScanner.codeTypes | ||||
|       if let rectOfInterest = codeScanner.regionOfInterest { | ||||
|         metadataOutput.rectOfInterest = rectOfInterest | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if outputOrientation != .portrait { | ||||
|       updateOrientation() | ||||
|     } | ||||
|  | ||||
|     invokeOnInitialized() | ||||
|     isReady = true | ||||
|     ReactLogger.log(level: .info, message: "Session successfully configured!") | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Returns the pixel format that should be used for the AVCaptureVideoDataOutput. | ||||
|    If HDR is enabled, this will return YUV 4:2:0 10-bit. | ||||
|    If HDR is disabled, this will return whatever the user specified as a pixelFormat, or the most efficient format as a fallback. | ||||
|    */ | ||||
|   private func getPixelFormat(videoOutput: AVCaptureVideoDataOutput) -> OSType { | ||||
|     // as per documentation, the first value is always the most efficient format | ||||
|     var defaultFormat = videoOutput.availableVideoPixelFormatTypes.first! | ||||
|     if enableBufferCompression { | ||||
|       // use compressed format instead if we enabled buffer compression | ||||
|       if defaultFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange && | ||||
|         videoOutput.availableVideoPixelFormatTypes.contains(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange) { | ||||
|         // YUV 4:2:0 8-bit (limited video colors; compressed) | ||||
|         defaultFormat = kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange | ||||
|       } | ||||
|       if defaultFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange && | ||||
|         videoOutput.availableVideoPixelFormatTypes.contains(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange) { | ||||
|         // YUV 4:2:0 8-bit (full video colors; compressed) | ||||
|         defaultFormat = kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // If the user enabled HDR, we can only use the YUV 4:2:0 10-bit pixel format. | ||||
|     if hdr == true { | ||||
|       guard pixelFormat == nil || pixelFormat == "yuv" else { | ||||
|         invokeOnError(.format(.incompatiblePixelFormatWithHDR)) | ||||
|         return defaultFormat | ||||
|       } | ||||
|  | ||||
|       var targetFormats = [kCVPixelFormatType_420YpCbCr10BiPlanarFullRange, | ||||
|                            kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange] | ||||
|       if enableBufferCompression { | ||||
|         // If we enable buffer compression, try to use a lossless compressed YUV format first, otherwise fall back to the others. | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr10PackedBiPlanarVideoRange, at: 0) | ||||
|       } | ||||
|  | ||||
|       // Find the best matching format | ||||
|       guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else { | ||||
|         invokeOnError(.format(.invalidHdr)) | ||||
|         return defaultFormat | ||||
|       } | ||||
|       // YUV 4:2:0 10-bit (compressed/uncompressed) | ||||
|       return format | ||||
|     } | ||||
|  | ||||
|     // If the user didn't specify a custom pixelFormat, just return the default one. | ||||
|     guard let pixelFormat = pixelFormat else { | ||||
|       return defaultFormat | ||||
|     } | ||||
|  | ||||
|     // If we don't use HDR, we can use any other custom pixel format. | ||||
|     switch pixelFormat { | ||||
|     case "yuv": | ||||
|       // YUV 4:2:0 8-bit (full/limited video colors; uncompressed) | ||||
|       var targetFormats = [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, | ||||
|                            kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] | ||||
|       if enableBufferCompression { | ||||
|         // YUV 4:2:0 8-bit (full/limited video colors; compressed) | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange, at: 0) | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange, at: 0) | ||||
|       } | ||||
|       guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else { | ||||
|         invokeOnError(.device(.pixelFormatNotSupported)) | ||||
|         return defaultFormat | ||||
|       } | ||||
|       return format | ||||
|     case "rgb": | ||||
|       // RGBA 8-bit (uncompressed) | ||||
|       var targetFormats = [kCVPixelFormatType_32BGRA] | ||||
|       if enableBufferCompression { | ||||
|         // RGBA 8-bit (compressed) | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_32BGRA, at: 0) | ||||
|       } | ||||
|       guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else { | ||||
|         invokeOnError(.device(.pixelFormatNotSupported)) | ||||
|         return defaultFormat | ||||
|       } | ||||
|       return format | ||||
|     case "native": | ||||
|       return defaultFormat | ||||
|     default: | ||||
|       invokeOnError(.parameter(.invalid(unionName: "pixelFormat", receivedValue: pixelFormat as String))) | ||||
|       return defaultFormat | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Configure Device | ||||
|  | ||||
|   /** | ||||
|    Configures the Video Device with the given FPS and HDR modes. | ||||
|    */ | ||||
|   final func configureDevice() { | ||||
|     ReactLogger.log(level: .info, message: "Configuring Device...") | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       invokeOnError(.session(.cameraNotReady)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     do { | ||||
|       try device.lockForConfiguration() | ||||
|  | ||||
|       // Configure FPS | ||||
|       if let fps = fps?.int32Value { | ||||
|         let supportsGivenFps = device.activeFormat.videoSupportedFrameRateRanges.contains { range in | ||||
|           return range.includes(fps: Double(fps)) | ||||
|         } | ||||
|         if !supportsGivenFps { | ||||
|           invokeOnError(.format(.invalidFps(fps: Int(fps)))) | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         let duration = CMTimeMake(value: 1, timescale: fps) | ||||
|         device.activeVideoMinFrameDuration = duration | ||||
|         device.activeVideoMaxFrameDuration = duration | ||||
|       } else { | ||||
|         device.activeVideoMinFrameDuration = CMTime.invalid | ||||
|         device.activeVideoMaxFrameDuration = CMTime.invalid | ||||
|       } | ||||
|  | ||||
|       // Configure Low-Light-Boost | ||||
|       if lowLightBoost != nil { | ||||
|         if lowLightBoost == true && !device.isLowLightBoostSupported { | ||||
|           invokeOnError(.device(.lowLightBoostNotSupported)) | ||||
|           return | ||||
|         } | ||||
|         device.automaticallyEnablesLowLightBoostWhenAvailable = lowLightBoost!.boolValue | ||||
|       } | ||||
|  | ||||
|       device.unlockForConfiguration() | ||||
|       ReactLogger.log(level: .info, message: "Device successfully configured!") | ||||
|     } catch let error as NSError { | ||||
|       invokeOnError(.device(.configureError), cause: error) | ||||
|       return | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Configure Format | ||||
|  | ||||
|   /** | ||||
|    Configures the Video Device to find the best matching Format. | ||||
|    */ | ||||
|   final func configureFormat() { | ||||
|     ReactLogger.log(level: .info, message: "Configuring Format...") | ||||
|     guard let jsFormat = format else { | ||||
|       // JS Format was null. Ignore it, use default. | ||||
|       return | ||||
|     } | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       invokeOnError(.session(.cameraNotReady)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     if device.activeFormat.isEqualTo(jsFormat: jsFormat) { | ||||
|       ReactLogger.log(level: .info, message: "Already selected active format.") | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // get matching format | ||||
|     let format = device.formats.first { $0.isEqualTo(jsFormat: jsFormat) } | ||||
|     guard let format else { | ||||
|       invokeOnError(.format(.invalidFormat)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     do { | ||||
|       try device.lockForConfiguration() | ||||
|       defer { | ||||
|         device.unlockForConfiguration() | ||||
|       } | ||||
|  | ||||
|       let shouldReconfigurePhotoOutput = device.activeFormat.photoDimensions.toCGSize() != format.photoDimensions.toCGSize() | ||||
|       device.activeFormat = format | ||||
|  | ||||
|       // The Photo Output uses the smallest available Dimension by default. We need to configure it for the maximum here | ||||
|       if shouldReconfigurePhotoOutput, #available(iOS 16.0, *) { | ||||
|         if let photoOutput = photoOutput { | ||||
|           photoOutput.maxPhotoDimensions = format.photoDimensions | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       ReactLogger.log(level: .info, message: "Format successfully configured!") | ||||
|     } catch let error as NSError { | ||||
|       invokeOnError(.device(.configureError), cause: error) | ||||
|       return | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Notifications/Interruptions | ||||
|  | ||||
|   @objc | ||||
|   func sessionRuntimeError(notification: Notification) { | ||||
|     ReactLogger.log(level: .error, message: "Unexpected Camera Runtime Error occured!") | ||||
|     guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     invokeOnError(.unknown(message: error._nsError.description), cause: error._nsError) | ||||
|  | ||||
|     if isActive { | ||||
|       // restart capture session after an error occured | ||||
|       CameraQueues.cameraQueue.async { | ||||
|         self.captureSession.startRunning() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| // | ||||
| //  CameraView+CodeScanner.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 03.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension CameraView: AVCaptureMetadataOutputObjectsDelegate { | ||||
|   public func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) { | ||||
|     guard let onCodeScanned = onCodeScanned else { | ||||
|       return | ||||
|     } | ||||
|     guard !metadataObjects.isEmpty else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Map codes to JS values | ||||
|     let codes = metadataObjects.map { object in | ||||
|       var value: String? | ||||
|       if let code = object as? AVMetadataMachineReadableCodeObject { | ||||
|         value = code.stringValue | ||||
|       } | ||||
|       let frame = previewView.layerRectConverted(fromMetadataOutputRect: object.bounds) | ||||
|  | ||||
|       return [ | ||||
|         "type": object.type.descriptor, | ||||
|         "value": value as Any, | ||||
|         "frame": [ | ||||
|           "x": frame.origin.x, | ||||
|           "y": frame.origin.y, | ||||
|           "width": frame.size.width, | ||||
|           "height": frame.size.height, | ||||
|         ], | ||||
|       ] | ||||
|     } | ||||
|     // Call JS event | ||||
|     onCodeScanned([ | ||||
|       "codes": codes, | ||||
|     ]) | ||||
|   } | ||||
| } | ||||
| @@ -1,93 +1,19 @@ | ||||
| // | ||||
| //  CameraView+Focus.swift | ||||
| //  mrousavy | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 19.02.21. | ||||
| //  Copyright © 2021 mrousavy. All rights reserved. | ||||
| //  Created by Marc Rousavy on 12.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension CameraView { | ||||
|   private func convertPreviewCoordinatesToCameraCoordinates(_ point: CGPoint) -> CGPoint { | ||||
|     return previewView.captureDevicePointConverted(fromLayerPoint: point) | ||||
|   } | ||||
|  | ||||
|   func focus(point: CGPoint, promise: Promise) { | ||||
|     withPromise(promise) { | ||||
|       guard let device = self.videoDeviceInput?.device else { | ||||
|         throw CameraError.session(SessionError.cameraNotReady) | ||||
|       } | ||||
|       if !device.isFocusPointOfInterestSupported { | ||||
|         throw CameraError.device(DeviceError.focusNotSupported) | ||||
|       } | ||||
|  | ||||
|       // in {0..1} system | ||||
|       let normalizedPoint = convertPreviewCoordinatesToCameraCoordinates(point) | ||||
|  | ||||
|       do { | ||||
|         try device.lockForConfiguration() | ||||
|         defer { | ||||
|           device.unlockForConfiguration() | ||||
|         } | ||||
|  | ||||
|         // Set Focus | ||||
|         if device.isFocusPointOfInterestSupported { | ||||
|           device.focusPointOfInterest = normalizedPoint | ||||
|           device.focusMode = .autoFocus | ||||
|         } | ||||
|  | ||||
|         // Set Exposure | ||||
|         if device.isExposurePointOfInterestSupported { | ||||
|           device.exposurePointOfInterest = normalizedPoint | ||||
|           device.exposureMode = .autoExpose | ||||
|         } | ||||
|  | ||||
|         // Remove any existing listeners | ||||
|         NotificationCenter.default.removeObserver(self, | ||||
|                                                   name: NSNotification.Name.AVCaptureDeviceSubjectAreaDidChange, | ||||
|                                                   object: nil) | ||||
|  | ||||
|         // Listen for focus completion | ||||
|         device.isSubjectAreaChangeMonitoringEnabled = true | ||||
|         NotificationCenter.default.addObserver(self, | ||||
|                                                selector: #selector(subjectAreaDidChange), | ||||
|                                                name: NSNotification.Name.AVCaptureDeviceSubjectAreaDidChange, | ||||
|                                                object: nil) | ||||
|       try cameraSession.focus(point: point) | ||||
|       return nil | ||||
|       } catch { | ||||
|         throw CameraError.device(DeviceError.configureError) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @objc | ||||
|   func subjectAreaDidChange(notification _: NSNotification) { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       invokeOnError(.session(.cameraNotReady)) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     do { | ||||
|       try device.lockForConfiguration() | ||||
|       defer { | ||||
|         device.unlockForConfiguration() | ||||
|       } | ||||
|  | ||||
|       // Reset Focus to continuous/auto | ||||
|       if device.isFocusPointOfInterestSupported { | ||||
|         device.focusMode = .continuousAutoFocus | ||||
|       } | ||||
|  | ||||
|       // Reset Exposure to continuous/auto | ||||
|       if device.isExposurePointOfInterestSupported { | ||||
|         device.exposureMode = .continuousAutoExposure | ||||
|       } | ||||
|  | ||||
|       // Disable listeners | ||||
|       device.isSubjectAreaChangeMonitoringEnabled = false | ||||
|     } catch { | ||||
|       invokeOnError(.device(.configureError)) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,45 +0,0 @@ | ||||
| // | ||||
| //  CameraView+Orientation.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 04.01.22. | ||||
| //  Copyright © 2022 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import UIKit | ||||
|  | ||||
| extension CameraView { | ||||
|   /// Orientation of the input connection (preview) | ||||
|   private var inputOrientation: UIInterfaceOrientation { | ||||
|     return .portrait | ||||
|   } | ||||
|  | ||||
|   // Orientation of the output connections (photo, video, frame processor) | ||||
|   var outputOrientation: UIInterfaceOrientation { | ||||
|     if let userOrientation = orientation as String?, | ||||
|        let parsedOrientation = try? UIInterfaceOrientation(withString: userOrientation) { | ||||
|       // user is overriding output orientation | ||||
|       return parsedOrientation | ||||
|     } else { | ||||
|       // use same as input orientation | ||||
|       return inputOrientation | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   func updateOrientation() { | ||||
|     // Updates the Orientation for all rotable | ||||
|     let isMirrored = videoDeviceInput?.device.position == .front | ||||
|  | ||||
|     let connectionOrientation = outputOrientation | ||||
|     captureSession.outputs.forEach { output in | ||||
|       output.connections.forEach { connection in | ||||
|         if connection.isVideoMirroringSupported { | ||||
|           connection.automaticallyAdjustsVideoMirroring = false | ||||
|           connection.isVideoMirrored = isMirrored | ||||
|         } | ||||
|         connection.setInterfaceOrientation(connectionOrientation) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -11,268 +11,42 @@ import AVFoundation | ||||
| // MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate | ||||
|  | ||||
| extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { | ||||
|   /** | ||||
|    Starts a video + audio recording with a custom Asset Writer. | ||||
|    */ | ||||
|   func startRecording(options: NSDictionary, callback jsCallbackFunc: @escaping RCTResponseSenderBlock) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       ReactLogger.log(level: .info, message: "Starting Video recording...") | ||||
|       let callback = Callback(jsCallbackFunc) | ||||
|   func startRecording(options: NSDictionary, callback jsCallback: @escaping RCTResponseSenderBlock) { | ||||
|     // Type-safety | ||||
|     let callback = Callback(jsCallback) | ||||
|  | ||||
|       var fileType = AVFileType.mov | ||||
|       if let fileTypeOption = options["fileType"] as? String { | ||||
|         guard let parsed = try? AVFileType(withString: fileTypeOption) else { | ||||
|           callback.reject(error: .parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption))) | ||||
|           return | ||||
|         } | ||||
|         fileType = parsed | ||||
|       } | ||||
|  | ||||
|       let errorPointer = ErrorPointer(nilLiteral: ()) | ||||
|       let fileExtension = fileType.descriptor ?? "mov" | ||||
|       guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else { | ||||
|         callback.reject(error: .capture(.createTempFileError), cause: errorPointer?.pointee) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       ReactLogger.log(level: .info, message: "File path: \(tempFilePath)") | ||||
|       let tempURL = URL(string: "file://\(tempFilePath)")! | ||||
|  | ||||
|       if let flashMode = options["flash"] as? String { | ||||
|         // use the torch as the video's flash | ||||
|         self.setTorchMode(flashMode) | ||||
|       } | ||||
|  | ||||
|       guard let videoOutput = self.videoOutput else { | ||||
|         if self.video?.boolValue == true { | ||||
|           callback.reject(error: .session(.cameraNotReady)) | ||||
|           return | ||||
|         } else { | ||||
|           callback.reject(error: .capture(.videoNotEnabled)) | ||||
|           return | ||||
|         } | ||||
|       } | ||||
|       guard let videoInput = self.videoDeviceInput else { | ||||
|         callback.reject(error: .session(.cameraNotReady)) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       // TODO: The startRecording() func cannot be async because RN doesn't allow | ||||
|       //       both a callback and a Promise in a single function. Wait for TurboModules? | ||||
|       //       This means that any errors that occur in this function have to be delegated through | ||||
|       //       the callback, but I'd prefer for them to throw for the original function instead. | ||||
|  | ||||
|       let enableAudio = self.audio?.boolValue == true | ||||
|  | ||||
|       let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in | ||||
|         defer { | ||||
|           if enableAudio { | ||||
|             CameraQueues.audioQueue.async { | ||||
|               self.deactivateAudioSession() | ||||
|             } | ||||
|           } | ||||
|           if options["flash"] != nil { | ||||
|             // Set torch mode back to what it was before if we used it for the video flash. | ||||
|             self.setTorchMode(self.torch) | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         self.recordingSession = nil | ||||
|         self.isRecording = false | ||||
|         ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).") | ||||
|  | ||||
|         if let error = error as NSError? { | ||||
|           if error.domain == "capture/aborted" { | ||||
|             callback.reject(error: .capture(.aborted), cause: error) | ||||
|           } else { | ||||
|             callback.reject(error: .capture(.unknown(message: "An unknown recording error occured! \(error.description)")), cause: error) | ||||
|           } | ||||
|         } else { | ||||
|           if status == .completed { | ||||
|             callback.resolve([ | ||||
|               "path": recordingSession.url.absoluteString, | ||||
|               "duration": recordingSession.duration, | ||||
|             ]) | ||||
|           } else { | ||||
|             callback.reject(error: .unknown(message: "AVAssetWriter completed with status: \(status.descriptor)")) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       let recordingSession: RecordingSession | ||||
|     do { | ||||
|         recordingSession = try RecordingSession(url: tempURL, | ||||
|                                                 fileType: fileType, | ||||
|                                                 completion: onFinish) | ||||
|       } catch let error as NSError { | ||||
|         callback.reject(error: .capture(.createRecorderError(message: nil)), cause: error) | ||||
|         return | ||||
|       } | ||||
|       self.recordingSession = recordingSession | ||||
|       let options = try RecordVideoOptions(fromJSValue: options) | ||||
|  | ||||
|       var videoCodec: AVVideoCodecType? | ||||
|       if let codecString = options["videoCodec"] as? String { | ||||
|         videoCodec = AVVideoCodecType(withString: codecString) | ||||
|       // Start Recording with success and error callbacks | ||||
|       cameraSession.startRecording( | ||||
|         options: options, | ||||
|         onVideoRecorded: { video in | ||||
|           callback.resolve(video.toJSValue()) | ||||
|         }, | ||||
|         onError: { error in | ||||
|           callback.reject(error: error) | ||||
|         } | ||||
|  | ||||
|       // Init Video | ||||
|       guard var videoSettings = self.recommendedVideoSettings(videoOutput: videoOutput, fileType: fileType, videoCodec: videoCodec), | ||||
|             !videoSettings.isEmpty else { | ||||
|         callback.reject(error: .capture(.createRecorderError(message: "Failed to get video settings!"))) | ||||
|         return | ||||
|       ) | ||||
|     } catch { | ||||
|       // Some error occured while initializing VideoSettings | ||||
|       if let error = error as? CameraError { | ||||
|         callback.reject(error: error) | ||||
|       } else { | ||||
|         callback.reject(error: .capture(.unknown(message: error.localizedDescription)), cause: error as NSError) | ||||
|       } | ||||
|  | ||||
|       // Custom Video Bit Rate (Mbps -> bps) | ||||
|       if let videoBitRate = options["videoBitRate"] as? NSNumber { | ||||
|         let bitsPerSecond = videoBitRate.doubleValue * 1_000_000 | ||||
|         videoSettings[AVVideoCompressionPropertiesKey] = [ | ||||
|           AVVideoAverageBitRateKey: NSNumber(value: bitsPerSecond), | ||||
|         ] | ||||
|       } | ||||
|  | ||||
|       // get pixel format (420f, 420v, x420) | ||||
|       let pixelFormat = CMFormatDescriptionGetMediaSubType(videoInput.device.activeFormat.formatDescription) | ||||
|       recordingSession.initializeVideoWriter(withSettings: videoSettings, | ||||
|                                              pixelFormat: pixelFormat) | ||||
|  | ||||
|       // Init Audio (optional) | ||||
|       if enableAudio { | ||||
|         // Activate Audio Session asynchronously | ||||
|         CameraQueues.audioQueue.async { | ||||
|           self.activateAudioSession() | ||||
|         } | ||||
|  | ||||
|         if let audioOutput = self.audioOutput, | ||||
|            let audioSettings = audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: fileType) { | ||||
|           recordingSession.initializeAudioWriter(withSettings: audioSettings) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // start recording session with or without audio. | ||||
|       do { | ||||
|         try recordingSession.startAssetWriter() | ||||
|       } catch let error as NSError { | ||||
|         callback.reject(error: .capture(.createRecorderError(message: "RecordingSession failed to start asset writer.")), cause: error) | ||||
|         return | ||||
|       } | ||||
|       self.isRecording = true | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   func stopRecording(promise: Promise) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       self.isRecording = false | ||||
|  | ||||
|       withPromise(promise) { | ||||
|         guard let recordingSession = self.recordingSession else { | ||||
|           throw CameraError.capture(.noRecordingInProgress) | ||||
|         } | ||||
|         recordingSession.finish() | ||||
|         return nil | ||||
|       } | ||||
|     } | ||||
|     cameraSession.stopRecording(promise: promise) | ||||
|   } | ||||
|  | ||||
|   func pauseRecording(promise: Promise) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       withPromise(promise) { | ||||
|         guard self.recordingSession != nil else { | ||||
|           // there's no active recording! | ||||
|           throw CameraError.capture(.noRecordingInProgress) | ||||
|         } | ||||
|         self.isRecording = false | ||||
|         return nil | ||||
|       } | ||||
|     } | ||||
|     cameraSession.pauseRecording(promise: promise) | ||||
|   } | ||||
|  | ||||
|   func resumeRecording(promise: Promise) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       withPromise(promise) { | ||||
|         guard self.recordingSession != nil else { | ||||
|           // there's no active recording! | ||||
|           throw CameraError.capture(.noRecordingInProgress) | ||||
|         } | ||||
|         self.isRecording = true | ||||
|         return nil | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) { | ||||
|     #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS | ||||
|       if captureOutput is AVCaptureVideoDataOutput { | ||||
|         if let frameProcessor = frameProcessor { | ||||
|           // Call Frame Processor | ||||
|           let frame = Frame(buffer: sampleBuffer, orientation: bufferOrientation) | ||||
|           frameProcessor.call(frame) | ||||
|         } | ||||
|       } | ||||
|     #endif | ||||
|  | ||||
|     // Record Video Frame/Audio Sample to File | ||||
|     if isRecording { | ||||
|       guard let recordingSession = recordingSession else { | ||||
|         invokeOnError(.capture(.unknown(message: "isRecording was true but the RecordingSession was null!"))) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       switch captureOutput { | ||||
|       case is AVCaptureVideoDataOutput: | ||||
|         recordingSession.appendBuffer(sampleBuffer, type: .video, timestamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) | ||||
|       case is AVCaptureAudioDataOutput: | ||||
|         let timestamp = CMSyncConvertTime(CMSampleBufferGetPresentationTimeStamp(sampleBuffer), | ||||
|                                           from: audioCaptureSession.masterClock ?? CMClockGetHostTimeClock(), | ||||
|                                           to: captureSession.masterClock ?? CMClockGetHostTimeClock()) | ||||
|         recordingSession.appendBuffer(sampleBuffer, type: .audio, timestamp: timestamp) | ||||
|       default: | ||||
|         break | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     #if DEBUG | ||||
|       if captureOutput is AVCaptureVideoDataOutput { | ||||
|         // Update FPS Graph per Frame | ||||
|         if let fpsGraph = fpsGraph { | ||||
|           DispatchQueue.main.async { | ||||
|             fpsGraph.onTick(CACurrentMediaTime()) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     #endif | ||||
|   } | ||||
|  | ||||
|   private func recommendedVideoSettings(videoOutput: AVCaptureVideoDataOutput, | ||||
|                                         fileType: AVFileType, | ||||
|                                         videoCodec: AVVideoCodecType?) -> [String: Any]? { | ||||
|     if videoCodec != nil { | ||||
|       return videoOutput.recommendedVideoSettings(forVideoCodecType: videoCodec!, assetWriterOutputFileType: fileType) | ||||
|     } else { | ||||
|       return videoOutput.recommendedVideoSettingsForAssetWriter(writingTo: fileType) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Gets the orientation of the CameraView's images (CMSampleBuffers). | ||||
|    */ | ||||
|   private var bufferOrientation: UIImage.Orientation { | ||||
|     guard let cameraPosition = videoDeviceInput?.device.position else { | ||||
|       return .up | ||||
|     } | ||||
|  | ||||
|     switch outputOrientation { | ||||
|     case .portrait: | ||||
|       return cameraPosition == .front ? .leftMirrored : .right | ||||
|     case .landscapeLeft: | ||||
|       return cameraPosition == .front ? .downMirrored : .up | ||||
|     case .portraitUpsideDown: | ||||
|       return cameraPosition == .front ? .rightMirrored : .left | ||||
|     case .landscapeRight: | ||||
|       return cameraPosition == .front ? .upMirrored : .down | ||||
|     case .unknown: | ||||
|       return .up | ||||
|     @unknown default: | ||||
|       return .up | ||||
|     } | ||||
|     cameraSession.resumeRecording(promise: promise) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -10,83 +10,6 @@ import AVFoundation | ||||
|  | ||||
| extension CameraView { | ||||
|   func takePhoto(options: NSDictionary, promise: Promise) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       guard let photoOutput = self.photoOutput, | ||||
|             let videoDeviceInput = self.videoDeviceInput else { | ||||
|         if self.photo?.boolValue == true { | ||||
|           promise.reject(error: .session(.cameraNotReady)) | ||||
|           return | ||||
|         } else { | ||||
|           promise.reject(error: .capture(.photoNotEnabled)) | ||||
|           return | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       ReactLogger.log(level: .info, message: "Capturing photo...") | ||||
|  | ||||
|       // Create photo settings | ||||
|       let photoSettings = AVCapturePhotoSettings() | ||||
|  | ||||
|       // default, overridable settings if high quality capture was enabled | ||||
|       if self.enableHighQualityPhotos?.boolValue == true { | ||||
|         // 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 | ||||
|       } | ||||
|  | ||||
|       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) | ||||
|     } | ||||
|     cameraSession.takePhoto(options: options, promise: promise) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,51 +0,0 @@ | ||||
| // | ||||
| //  CameraView+Torch.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 20.07.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension CameraView { | ||||
|   final func setTorchMode(_ torchMode: String) { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       invokeOnError(.session(.cameraNotReady)) | ||||
|       return | ||||
|     } | ||||
|     guard var torchMode = AVCaptureDevice.TorchMode(withString: torchMode) else { | ||||
|       invokeOnError(.parameter(.invalid(unionName: "TorchMode", receivedValue: torch))) | ||||
|       return | ||||
|     } | ||||
|     if !captureSession.isRunning { | ||||
|       torchMode = .off | ||||
|     } | ||||
|     if device.torchMode == torchMode { | ||||
|       // no need to run the whole lock/unlock bs | ||||
|       return | ||||
|     } | ||||
|     if !device.hasTorch || !device.isTorchAvailable { | ||||
|       if torchMode == .off { | ||||
|         // ignore it, when it's off and not supported, it's off. | ||||
|         return | ||||
|       } else { | ||||
|         // torch mode is .auto or .on, but no torch is available. | ||||
|         invokeOnError(.device(.flashUnavailable)) | ||||
|         return | ||||
|       } | ||||
|     } | ||||
|     do { | ||||
|       try device.lockForConfiguration() | ||||
|       device.torchMode = torchMode | ||||
|       if torchMode == .on { | ||||
|         try device.setTorchModeOn(level: 1.0) | ||||
|       } | ||||
|       device.unlockForConfiguration() | ||||
|     } catch let error as NSError { | ||||
|       invokeOnError(.device(.configureError), cause: error) | ||||
|       return | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -7,34 +7,20 @@ | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import UIKit | ||||
|  | ||||
| extension CameraView { | ||||
|   var minAvailableZoom: CGFloat { | ||||
|     return videoDeviceInput?.device.minAvailableVideoZoomFactor ?? 1 | ||||
|   } | ||||
|  | ||||
|   var maxAvailableZoom: CGFloat { | ||||
|     return videoDeviceInput?.device.activeFormat.videoMaxZoomFactor ?? 1 | ||||
|   } | ||||
|  | ||||
|   @objc | ||||
|   final func onPinch(_ gesture: UIPinchGestureRecognizer) { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     let scale = max(min(gesture.scale * pinchScaleOffset, device.activeFormat.videoMaxZoomFactor), CGFloat(1.0)) | ||||
|     let scale = max(min(gesture.scale * pinchScaleOffset, cameraSession.maxZoom), CGFloat(1.0)) | ||||
|     if gesture.state == .ended { | ||||
|       pinchScaleOffset = scale | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     do { | ||||
|       try device.lockForConfiguration() | ||||
|       device.videoZoomFactor = scale | ||||
|       device.unlockForConfiguration() | ||||
|     } catch { | ||||
|       invokeOnError(.device(.configureError)) | ||||
|     // Update zoom on Camera | ||||
|     cameraSession.configure { configuration in | ||||
|       configuration.zoom = scale | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -50,24 +36,4 @@ extension CameraView { | ||||
|       self.pinchGestureRecognizer = nil | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @objc | ||||
|   final func zoom(factor: CGFloat, animated: Bool) { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     do { | ||||
|       try device.lockForConfiguration() | ||||
|       let clamped = max(min(factor, device.activeFormat.videoMaxZoomFactor), CGFloat(1.0)) | ||||
|       if animated { | ||||
|         device.ramp(toVideoZoomFactor: clamped, withRate: 1) | ||||
|       } else { | ||||
|         device.videoZoomFactor = clamped | ||||
|       } | ||||
|       device.unlockForConfiguration() | ||||
|     } catch { | ||||
|       invokeOnError(.device(.configureError)) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -10,50 +10,36 @@ import AVFoundation | ||||
| import Foundation | ||||
| import UIKit | ||||
|  | ||||
| // | ||||
| // TODOs for the CameraView which are currently too hard to implement either because of AVFoundation's limitations, or my brain capacity | ||||
| // | ||||
| // CameraView+RecordVideo | ||||
| // TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI) | ||||
|  | ||||
| // | ||||
| // CameraView+TakePhoto | ||||
| // TODO: Photo HDR | ||||
|  | ||||
| private let propsThatRequireReconfiguration = ["cameraId", | ||||
|                                                "enableDepthData", | ||||
|                                                "enableHighQualityPhotos", | ||||
|                                                "enablePortraitEffectsMatteDelivery", | ||||
|                                                "photo", | ||||
|                                                "video", | ||||
|                                                "enableFrameProcessor", | ||||
|                                                "hdr", | ||||
|                                                "pixelFormat", | ||||
|                                                "codeScannerOptions"] | ||||
| private let propsThatRequireDeviceReconfiguration = ["fps", | ||||
|                                                      "lowLightBoost"] | ||||
|  | ||||
| // MARK: - CameraView | ||||
|  | ||||
| public final class CameraView: UIView { | ||||
| public final class CameraView: UIView, CameraSessionDelegate { | ||||
|   // pragma MARK: React Properties | ||||
|   // props that require reconfiguring | ||||
|   @objc var cameraId: NSString? | ||||
|   @objc var enableDepthData = false | ||||
|   @objc var enableHighQualityPhotos: NSNumber? // nullable bool | ||||
|   @objc var enableHighQualityPhotos = false | ||||
|   @objc var enablePortraitEffectsMatteDelivery = false | ||||
|   @objc var enableBufferCompression = false | ||||
|   // use cases | ||||
|   @objc var photo: NSNumber? // nullable bool | ||||
|   @objc var video: NSNumber? // nullable bool | ||||
|   @objc var audio: NSNumber? // nullable bool | ||||
|   @objc var photo = false | ||||
|   @objc var video = false | ||||
|   @objc var audio = false | ||||
|   @objc var enableFrameProcessor = false | ||||
|   @objc var codeScannerOptions: NSDictionary? | ||||
|   @objc var pixelFormat: NSString? | ||||
|   // props that require format reconfiguring | ||||
|   @objc var format: NSDictionary? | ||||
|   @objc var fps: NSNumber? | ||||
|   @objc var hdr: NSNumber? // nullable bool | ||||
|   @objc var lowLightBoost: NSNumber? // nullable bool | ||||
|   @objc var hdr = false | ||||
|   @objc var lowLightBoost = false | ||||
|   @objc var orientation: NSString? | ||||
|   // other props | ||||
|   @objc var isActive = false | ||||
| @@ -63,7 +49,8 @@ public final class CameraView: UIView { | ||||
|   @objc var videoStabilizationMode: NSString? | ||||
|   @objc var resizeMode: NSString = "cover" { | ||||
|     didSet { | ||||
|       previewView.resizeMode = ResizeMode(fromTypeScriptUnion: resizeMode as String) | ||||
|       let parsed = try? ResizeMode(jsValue: resizeMode as String) | ||||
|       previewView.resizeMode = parsed ?? .cover | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -84,20 +71,9 @@ public final class CameraView: UIView { | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Internal Properties | ||||
|   var cameraSession: CameraSession | ||||
|   var isMounted = false | ||||
|   var isReady = false | ||||
|   // Capture Session | ||||
|   let captureSession = AVCaptureSession() | ||||
|   let audioCaptureSession = AVCaptureSession() | ||||
|   // Inputs & Outputs | ||||
|   var videoDeviceInput: AVCaptureDeviceInput? | ||||
|   var audioDeviceInput: AVCaptureDeviceInput? | ||||
|   var photoOutput: AVCapturePhotoOutput? | ||||
|   var videoOutput: AVCaptureVideoDataOutput? | ||||
|   var audioOutput: AVCaptureAudioDataOutput? | ||||
|   // CameraView+RecordView (+ Frame Processor) | ||||
|   var isRecording = false | ||||
|   var recordingSession: RecordingSession? | ||||
|   #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS | ||||
|     @objc public var frameProcessor: FrameProcessor? | ||||
|   #endif | ||||
| @@ -110,30 +86,16 @@ public final class CameraView: UIView { | ||||
|     var fpsGraph: RCTFPSGraph? | ||||
|   #endif | ||||
|  | ||||
|   /// Returns whether the AVCaptureSession is currently running (reflected by isActive) | ||||
|   var isRunning: Bool { | ||||
|     return captureSession.isRunning | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Setup | ||||
|  | ||||
|   override public init(frame: CGRect) { | ||||
|     previewView = PreviewView(frame: frame, session: captureSession) | ||||
|     // Create CameraSession | ||||
|     cameraSession = CameraSession() | ||||
|     previewView = cameraSession.createPreviewView(frame: frame) | ||||
|     super.init(frame: frame) | ||||
|     cameraSession.delegate = self | ||||
|  | ||||
|     addSubview(previewView) | ||||
|  | ||||
|     NotificationCenter.default.addObserver(self, | ||||
|                                            selector: #selector(sessionRuntimeError), | ||||
|                                            name: .AVCaptureSessionRuntimeError, | ||||
|                                            object: captureSession) | ||||
|     NotificationCenter.default.addObserver(self, | ||||
|                                            selector: #selector(sessionRuntimeError), | ||||
|                                            name: .AVCaptureSessionRuntimeError, | ||||
|                                            object: audioCaptureSession) | ||||
|     NotificationCenter.default.addObserver(self, | ||||
|                                            selector: #selector(audioSessionInterrupted), | ||||
|                                            name: AVAudioSession.interruptionNotification, | ||||
|                                            object: AVAudioSession.sharedInstance) | ||||
|   } | ||||
|  | ||||
|   @available(*, unavailable) | ||||
| @@ -141,18 +103,6 @@ public final class CameraView: UIView { | ||||
|     fatalError("init(coder:) is not implemented.") | ||||
|   } | ||||
|  | ||||
|   deinit { | ||||
|     NotificationCenter.default.removeObserver(self, | ||||
|                                               name: .AVCaptureSessionRuntimeError, | ||||
|                                               object: captureSession) | ||||
|     NotificationCenter.default.removeObserver(self, | ||||
|                                               name: .AVCaptureSessionRuntimeError, | ||||
|                                               object: audioCaptureSession) | ||||
|     NotificationCenter.default.removeObserver(self, | ||||
|                                               name: AVAudioSession.interruptionNotification, | ||||
|                                               object: AVAudioSession.sharedInstance) | ||||
|   } | ||||
|  | ||||
|   override public func willMove(toSuperview newSuperview: UIView?) { | ||||
|     super.willMove(toSuperview: newSuperview) | ||||
|  | ||||
| @@ -169,89 +119,111 @@ public final class CameraView: UIView { | ||||
|     previewView.bounds = bounds | ||||
|   } | ||||
|  | ||||
|   func getPixelFormat() -> PixelFormat { | ||||
|     // TODO: Use ObjC RCT enum parser for this | ||||
|     if let pixelFormat = pixelFormat as? String { | ||||
|       do { | ||||
|         return try PixelFormat(jsValue: pixelFormat) | ||||
|       } catch { | ||||
|         if let error = error as? CameraError { | ||||
|           onError(error) | ||||
|         } else { | ||||
|           onError(.unknown(message: error.localizedDescription, cause: error as NSError)) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return .native | ||||
|   } | ||||
|  | ||||
|   func getTorch() -> Torch { | ||||
|     // TODO: Use ObjC RCT enum parser for this | ||||
|     if let torch = try? Torch(jsValue: torch) { | ||||
|       return torch | ||||
|     } | ||||
|     return .off | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Props updating | ||||
|   override public final func didSetProps(_ changedProps: [String]!) { | ||||
|     ReactLogger.log(level: .info, message: "Updating \(changedProps.count) prop(s)...") | ||||
|     let shouldReconfigure = changedProps.contains { propsThatRequireReconfiguration.contains($0) } | ||||
|     let shouldReconfigureFormat = shouldReconfigure || changedProps.contains("format") | ||||
|     let shouldReconfigureDevice = shouldReconfigureFormat || changedProps.contains { propsThatRequireDeviceReconfiguration.contains($0) } | ||||
|     let shouldReconfigureAudioSession = changedProps.contains("audio") | ||||
|     ReactLogger.log(level: .info, message: "Updating \(changedProps.count) props: [\(changedProps.joined(separator: ", "))]") | ||||
|  | ||||
|     let willReconfigure = shouldReconfigure || shouldReconfigureFormat || shouldReconfigureDevice | ||||
|     cameraSession.configure { config in | ||||
|       // Input Camera Device | ||||
|       config.cameraId = cameraId as? String | ||||
|  | ||||
|     let shouldCheckActive = willReconfigure || changedProps.contains("isActive") || captureSession.isRunning != isActive | ||||
|     let shouldUpdateTorch = willReconfigure || changedProps.contains("torch") || shouldCheckActive | ||||
|     let shouldUpdateZoom = willReconfigure || changedProps.contains("zoom") || shouldCheckActive | ||||
|     let shouldUpdateVideoStabilization = willReconfigure || changedProps.contains("videoStabilizationMode") | ||||
|     let shouldUpdateOrientation = willReconfigure || changedProps.contains("orientation") | ||||
|       // Photo | ||||
|       if photo { | ||||
|         config.photo = .enabled(config: CameraConfiguration.Photo(enableHighQualityPhotos: enableHighQualityPhotos, | ||||
|                                                                   enableDepthData: enableDepthData, | ||||
|                                                                   enablePortraitEffectsMatte: enablePortraitEffectsMatteDelivery)) | ||||
|       } else { | ||||
|         config.photo = .disabled | ||||
|       } | ||||
|  | ||||
|       // Video/Frame Processor | ||||
|       if video || enableFrameProcessor { | ||||
|         config.video = .enabled(config: CameraConfiguration.Video(pixelFormat: getPixelFormat(), | ||||
|                                                                   enableBufferCompression: enableBufferCompression, | ||||
|                                                                   enableHdr: hdr, | ||||
|                                                                   enableFrameProcessor: enableFrameProcessor)) | ||||
|       } else { | ||||
|         config.video = .disabled | ||||
|       } | ||||
|  | ||||
|       // Audio | ||||
|       if audio { | ||||
|         config.audio = .enabled(config: CameraConfiguration.Audio()) | ||||
|       } else { | ||||
|         config.audio = .disabled | ||||
|       } | ||||
|  | ||||
|       // Code Scanner | ||||
|       if let codeScannerOptions { | ||||
|         let codeScanner = try CodeScanner(fromJsValue: codeScannerOptions) | ||||
|         config.codeScanner = .enabled(config: codeScanner) | ||||
|       } else { | ||||
|         config.codeScanner = .disabled | ||||
|       } | ||||
|  | ||||
|       // Orientation | ||||
|       if let jsOrientation = orientation as? String { | ||||
|         let orientation = try Orientation(jsValue: jsOrientation) | ||||
|         config.orientation = orientation | ||||
|       } else { | ||||
|         config.orientation = .portrait | ||||
|       } | ||||
|  | ||||
|       // Format | ||||
|       if let jsFormat = format { | ||||
|         let format = try CameraDeviceFormat(jsValue: jsFormat) | ||||
|         config.format = format | ||||
|       } else { | ||||
|         config.format = nil | ||||
|       } | ||||
|  | ||||
|       // Side-Props | ||||
|       config.fps = fps?.int32Value | ||||
|       config.enableLowLightBoost = lowLightBoost | ||||
|       config.torch = getTorch() | ||||
|  | ||||
|       // Zoom | ||||
|       config.zoom = zoom.doubleValue | ||||
|  | ||||
|       // isActive | ||||
|       config.isActive = isActive | ||||
|     } | ||||
|  | ||||
|     // Store `zoom` offset for native pinch-gesture | ||||
|     if changedProps.contains("zoom") { | ||||
|       pinchScaleOffset = zoom.doubleValue | ||||
|     } | ||||
|  | ||||
|     // Set up Debug FPS Graph | ||||
|     if changedProps.contains("enableFpsGraph") { | ||||
|       DispatchQueue.main.async { | ||||
|         self.setupFpsGraph() | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if shouldReconfigure || | ||||
|       shouldReconfigureAudioSession || | ||||
|       shouldCheckActive || | ||||
|       shouldUpdateTorch || | ||||
|       shouldUpdateZoom || | ||||
|       shouldReconfigureFormat || | ||||
|       shouldReconfigureDevice || | ||||
|       shouldUpdateVideoStabilization || | ||||
|       shouldUpdateOrientation { | ||||
|       CameraQueues.cameraQueue.async { | ||||
|         // Video Configuration | ||||
|         if shouldReconfigure { | ||||
|           self.configureCaptureSession() | ||||
|         } | ||||
|         if shouldReconfigureFormat { | ||||
|           self.configureFormat() | ||||
|         } | ||||
|         if shouldReconfigureDevice { | ||||
|           self.configureDevice() | ||||
|         } | ||||
|         if shouldUpdateVideoStabilization, let videoStabilizationMode = self.videoStabilizationMode as String? { | ||||
|           self.captureSession.setVideoStabilizationMode(videoStabilizationMode) | ||||
|         } | ||||
|  | ||||
|         if shouldUpdateZoom { | ||||
|           let zoomClamped = max(min(CGFloat(self.zoom.doubleValue), self.maxAvailableZoom), self.minAvailableZoom) | ||||
|           self.zoom(factor: zoomClamped, animated: false) | ||||
|           self.pinchScaleOffset = zoomClamped | ||||
|         } | ||||
|  | ||||
|         if shouldCheckActive && self.captureSession.isRunning != self.isActive { | ||||
|           if self.isActive { | ||||
|             ReactLogger.log(level: .info, message: "Starting Session...") | ||||
|             self.captureSession.startRunning() | ||||
|             ReactLogger.log(level: .info, message: "Started Session!") | ||||
|           } else { | ||||
|             ReactLogger.log(level: .info, message: "Stopping Session...") | ||||
|             self.captureSession.stopRunning() | ||||
|             ReactLogger.log(level: .info, message: "Stopped Session!") | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if shouldUpdateOrientation { | ||||
|           self.updateOrientation() | ||||
|         } | ||||
|  | ||||
|         // This is a wack workaround, but if I immediately set torch mode after `startRunning()`, the session isn't quite ready yet and will ignore torch. | ||||
|         if shouldUpdateTorch { | ||||
|           CameraQueues.cameraQueue.asyncAfter(deadline: .now() + 0.1) { | ||||
|             self.setTorchMode(self.torch) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Audio Configuration | ||||
|       if shouldReconfigureAudioSession { | ||||
|         CameraQueues.audioQueue.async { | ||||
|           self.configureAudioSession() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   func setupFpsGraph() { | ||||
| @@ -269,12 +241,16 @@ public final class CameraView: UIView { | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Event Invokers | ||||
|   final func invokeOnError(_ error: CameraError, cause: NSError? = nil) { | ||||
|  | ||||
|   func onError(_ error: CameraError) { | ||||
|     ReactLogger.log(level: .error, message: "Invoking onError(): \(error.message)") | ||||
|     guard let onError = onError else { return } | ||||
|     guard let onError = onError else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     var causeDictionary: [String: Any]? | ||||
|     if let cause = cause { | ||||
|     if case let .unknown(_, cause) = error, | ||||
|        let cause = cause { | ||||
|       causeDictionary = [ | ||||
|         "code": cause.code, | ||||
|         "domain": cause.domain, | ||||
| @@ -289,9 +265,58 @@ public final class CameraView: UIView { | ||||
|     ]) | ||||
|   } | ||||
|  | ||||
|   final func invokeOnInitialized() { | ||||
|   func onSessionInitialized() { | ||||
|     ReactLogger.log(level: .info, message: "Camera initialized!") | ||||
|     guard let onInitialized = onInitialized else { return } | ||||
|     guard let onInitialized = onInitialized else { | ||||
|       return | ||||
|     } | ||||
|     onInitialized([String: Any]()) | ||||
|   } | ||||
|  | ||||
|   func onFrame(sampleBuffer: CMSampleBuffer) { | ||||
|     #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS | ||||
|       if let frameProcessor = frameProcessor { | ||||
|         // Call Frame Processor | ||||
|         let frame = Frame(buffer: sampleBuffer, orientation: bufferOrientation) | ||||
|         frameProcessor.call(frame) | ||||
|       } | ||||
|     #endif | ||||
|  | ||||
|     #if DEBUG | ||||
|       if let fpsGraph { | ||||
|         fpsGraph.onTick(CACurrentMediaTime()) | ||||
|       } | ||||
|     #endif | ||||
|   } | ||||
|  | ||||
|   func onCodeScanned(codes: [CameraSession.Code]) { | ||||
|     guard let onCodeScanned = onCodeScanned else { | ||||
|       return | ||||
|     } | ||||
|     onCodeScanned([ | ||||
|       "codes": codes.map { $0.toJSValue() }, | ||||
|     ]) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Gets the orientation of the CameraView's images (CMSampleBuffers). | ||||
|    */ | ||||
|   private var bufferOrientation: UIImage.Orientation { | ||||
|     guard let cameraPosition = cameraSession.videoDeviceInput?.device.position else { | ||||
|       return .up | ||||
|     } | ||||
|     let orientation = cameraSession.configuration?.orientation ?? .portrait | ||||
|  | ||||
|     // TODO: I think this is wrong. | ||||
|     switch orientation { | ||||
|     case .portrait: | ||||
|       return cameraPosition == .front ? .leftMirrored : .right | ||||
|     case .landscapeLeft: | ||||
|       return cameraPosition == .front ? .downMirrored : .up | ||||
|     case .portraitUpsideDown: | ||||
|       return cameraPosition == .front ? .rightMirrored : .left | ||||
|     case .landscapeRight: | ||||
|       return cameraPosition == .front ? .upMirrored : .down | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -25,19 +25,19 @@ RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(installFrameProcessorBindings); | ||||
| RCT_EXPORT_VIEW_PROPERTY(isActive, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(cameraId, NSString); | ||||
| RCT_EXPORT_VIEW_PROPERTY(enableDepthData, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(enableHighQualityPhotos, NSNumber); // nullable bool | ||||
| RCT_EXPORT_VIEW_PROPERTY(enableHighQualityPhotos, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(enablePortraitEffectsMatteDelivery, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(enableBufferCompression, BOOL); | ||||
| // use cases | ||||
| RCT_EXPORT_VIEW_PROPERTY(photo, NSNumber); // nullable bool | ||||
| RCT_EXPORT_VIEW_PROPERTY(video, NSNumber); // nullable bool | ||||
| RCT_EXPORT_VIEW_PROPERTY(audio, NSNumber); // nullable bool | ||||
| RCT_EXPORT_VIEW_PROPERTY(photo, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(video, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(audio, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(enableFrameProcessor, BOOL); | ||||
| // device format | ||||
| RCT_EXPORT_VIEW_PROPERTY(format, NSDictionary); | ||||
| RCT_EXPORT_VIEW_PROPERTY(fps, NSNumber); | ||||
| RCT_EXPORT_VIEW_PROPERTY(hdr, NSNumber);           // nullable bool | ||||
| RCT_EXPORT_VIEW_PROPERTY(lowLightBoost, NSNumber); // nullable bool | ||||
| RCT_EXPORT_VIEW_PROPERTY(hdr, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(lowLightBoost, BOOL); | ||||
| RCT_EXPORT_VIEW_PROPERTY(videoStabilizationMode, NSString); | ||||
| RCT_EXPORT_VIEW_PROPERTY(pixelFormat, NSString); | ||||
| // other props | ||||
|   | ||||
| @@ -38,6 +38,10 @@ final class CameraViewManager: RCTViewManager { | ||||
|     #endif | ||||
|   } | ||||
|  | ||||
|   // TODO: The startRecording() func cannot be async because RN doesn't allow | ||||
|   //       both a callback and a Promise in a single function. Wait for TurboModules? | ||||
|   //       This means that any errors that occur in this function have to be delegated through | ||||
|   //       the callback, but I'd prefer for them to throw for the original function instead. | ||||
|   @objc | ||||
|   final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) { | ||||
|     let component = getCameraView(withTag: node) | ||||
|   | ||||
							
								
								
									
										231
									
								
								package/ios/Core/CameraConfiguration.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								package/ios/Core/CameraConfiguration.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| // | ||||
| //  CameraConfiguration.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| // MARK: - CameraConfiguration | ||||
|  | ||||
| class CameraConfiguration { | ||||
|   // pragma MARK: Configuration Props | ||||
|  | ||||
|   // Input | ||||
|   var cameraId: String? | ||||
|  | ||||
|   // Outputs | ||||
|   var photo: OutputConfiguration<Photo> = .disabled | ||||
|   var video: OutputConfiguration<Video> = .disabled | ||||
|   var codeScanner: OutputConfiguration<CodeScanner> = .disabled | ||||
|  | ||||
|   // Orientation | ||||
|   var orientation: Orientation = .portrait | ||||
|  | ||||
|   // Format | ||||
|   var format: CameraDeviceFormat? | ||||
|  | ||||
|   // Side-Props | ||||
|   var fps: Int32? | ||||
|   var enableLowLightBoost = false | ||||
|   var torch: Torch = .off | ||||
|  | ||||
|   // Zoom | ||||
|   var zoom: CGFloat? | ||||
|  | ||||
|   // isActive (Start/Stop) | ||||
|   var isActive = false | ||||
|  | ||||
|   // Audio Session | ||||
|   var audio: OutputConfiguration<Audio> = .disabled | ||||
|  | ||||
|   init(copyOf other: CameraConfiguration?) { | ||||
|     if let other { | ||||
|       // copy over all values | ||||
|       cameraId = other.cameraId | ||||
|       photo = other.photo | ||||
|       video = other.video | ||||
|       codeScanner = other.codeScanner | ||||
|       orientation = other.orientation | ||||
|       format = other.format | ||||
|       fps = other.fps | ||||
|       enableLowLightBoost = other.enableLowLightBoost | ||||
|       torch = other.torch | ||||
|       zoom = other.zoom | ||||
|       isActive = other.isActive | ||||
|       audio = other.audio | ||||
|     } else { | ||||
|       // self will just be initialized with the default values. | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Types | ||||
|  | ||||
|   struct Difference { | ||||
|     let inputChanged: Bool | ||||
|     let outputsChanged: Bool | ||||
|     let orientationChanged: Bool | ||||
|     let formatChanged: Bool | ||||
|     let sidePropsChanged: Bool | ||||
|     let zoomChanged: Bool | ||||
|  | ||||
|     let audioSessionChanged: Bool | ||||
|  | ||||
|     /** | ||||
|      Returns `true` when props that affect the AVCaptureSession configuration (i.e. props that require beginConfiguration()) have changed. | ||||
|      [`inputChanged`, `outputsChanged`, `orientationChanged`] | ||||
|      */ | ||||
|     var isSessionConfigurationDirty: Bool { | ||||
|       return inputChanged || outputsChanged || orientationChanged | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      Returns `true` when props that affect the AVCaptureDevice configuration (i.e. props that require lockForConfiguration()) have changed. | ||||
|      [`formatChanged`, `sidePropsChanged`, `zoomChanged`] | ||||
|      */ | ||||
|     var isDeviceConfigurationDirty: Bool { | ||||
|       return isSessionConfigurationDirty || formatChanged || sidePropsChanged || zoomChanged | ||||
|     } | ||||
|  | ||||
|     init(between left: CameraConfiguration?, and right: CameraConfiguration) { | ||||
|       // cameraId | ||||
|       inputChanged = left?.cameraId != right.cameraId | ||||
|       // photo, video, codeScanner | ||||
|       outputsChanged = inputChanged || left?.photo != right.photo || left?.video != right.video || left?.codeScanner != right.codeScanner | ||||
|       // orientation | ||||
|       orientationChanged = outputsChanged || left?.orientation != right.orientation | ||||
|       // format (depends on cameraId) | ||||
|       formatChanged = inputChanged || left?.format != right.format | ||||
|       // side-props (depends on format) | ||||
|       sidePropsChanged = formatChanged || left?.fps != right.fps || left?.enableLowLightBoost != right.enableLowLightBoost || left?.torch != right.torch | ||||
|       // zoom (depends on format) | ||||
|       zoomChanged = formatChanged || left?.zoom != right.zoom | ||||
|  | ||||
|       // audio session | ||||
|       audioSessionChanged = left?.audio != right.audio | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   enum OutputConfiguration<T: Equatable>: Equatable { | ||||
|     case disabled | ||||
|     case enabled(config: T) | ||||
|  | ||||
|     public static func == (lhs: OutputConfiguration, rhs: OutputConfiguration) -> Bool { | ||||
|       switch (lhs, rhs) { | ||||
|       case (.disabled, .disabled): | ||||
|         return true | ||||
|       case let (.enabled(a), .enabled(b)): | ||||
|         return a == b | ||||
|       default: | ||||
|         return false | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    A Photo Output configuration | ||||
|    */ | ||||
|   struct Photo: Equatable { | ||||
|     var enableHighQualityPhotos = false | ||||
|     var enableDepthData = false | ||||
|     var enablePortraitEffectsMatte = false | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    A Video Output configuration | ||||
|    */ | ||||
|   struct Video: Equatable { | ||||
|     var pixelFormat: PixelFormat = .native | ||||
|     var enableBufferCompression = false | ||||
|     var enableHdr = false | ||||
|     var enableFrameProcessor = false | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    An Audio Output configuration | ||||
|    */ | ||||
|   struct Audio: Equatable { | ||||
|     // no props for audio at the moment | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension CameraConfiguration.Video { | ||||
|   /** | ||||
|    Returns the pixel format that should be used for the given AVCaptureVideoDataOutput. | ||||
|    If HDR is enabled, this will return YUV 4:2:0 10-bit. | ||||
|    If HDR is disabled, this will return whatever the user specified as a pixelFormat, or the most efficient format as a fallback. | ||||
|    */ | ||||
|   func getPixelFormat(for videoOutput: AVCaptureVideoDataOutput) throws -> OSType { | ||||
|     // as per documentation, the first value is always the most efficient format | ||||
|     var defaultFormat = videoOutput.availableVideoPixelFormatTypes.first! | ||||
|     if enableBufferCompression { | ||||
|       // use compressed format instead if we enabled buffer compression | ||||
|       if defaultFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange && | ||||
|         videoOutput.availableVideoPixelFormatTypes.contains(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange) { | ||||
|         // YUV 4:2:0 8-bit (limited video colors; compressed) | ||||
|         defaultFormat = kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange | ||||
|       } | ||||
|       if defaultFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange && | ||||
|         videoOutput.availableVideoPixelFormatTypes.contains(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange) { | ||||
|         // YUV 4:2:0 8-bit (full video colors; compressed) | ||||
|         defaultFormat = kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // If the user enabled HDR, we can only use the YUV 4:2:0 10-bit pixel format. | ||||
|     if enableHdr == true { | ||||
|       guard pixelFormat == .native || pixelFormat == .yuv else { | ||||
|         throw CameraError.format(.incompatiblePixelFormatWithHDR) | ||||
|       } | ||||
|  | ||||
|       var targetFormats = [kCVPixelFormatType_420YpCbCr10BiPlanarFullRange, | ||||
|                            kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange] | ||||
|       if enableBufferCompression { | ||||
|         // If we enable buffer compression, try to use a lossless compressed YUV format first, otherwise fall back to the others. | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr10PackedBiPlanarVideoRange, at: 0) | ||||
|       } | ||||
|  | ||||
|       // Find the best matching format | ||||
|       guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else { | ||||
|         throw CameraError.format(.invalidHdr) | ||||
|       } | ||||
|       // YUV 4:2:0 10-bit (compressed/uncompressed) | ||||
|       return format | ||||
|     } | ||||
|  | ||||
|     // If we don't use HDR, we can use any other custom pixel format. | ||||
|     switch pixelFormat { | ||||
|     case .yuv: | ||||
|       // YUV 4:2:0 8-bit (full/limited video colors; uncompressed) | ||||
|       var targetFormats = [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, | ||||
|                            kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] | ||||
|       if enableBufferCompression { | ||||
|         // YUV 4:2:0 8-bit (full/limited video colors; compressed) | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarVideoRange, at: 0) | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_420YpCbCr8BiPlanarFullRange, at: 0) | ||||
|       } | ||||
|       guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else { | ||||
|         throw CameraError.device(.pixelFormatNotSupported) | ||||
|       } | ||||
|       return format | ||||
|     case .rgb: | ||||
|       // RGBA 8-bit (uncompressed) | ||||
|       var targetFormats = [kCVPixelFormatType_32BGRA] | ||||
|       if enableBufferCompression { | ||||
|         // RGBA 8-bit (compressed) | ||||
|         targetFormats.insert(kCVPixelFormatType_Lossless_32BGRA, at: 0) | ||||
|       } | ||||
|       guard let format = videoOutput.findPixelFormat(firstOf: targetFormats) else { | ||||
|         throw CameraError.device(.pixelFormatNotSupported) | ||||
|       } | ||||
|       return format | ||||
|     case .native: | ||||
|       return defaultFormat | ||||
|     case .unknown: | ||||
|       throw CameraError.parameter(.invalid(unionName: "pixelFormat", receivedValue: "unknown")) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -175,7 +175,7 @@ enum CaptureError { | ||||
|   case recordingInProgress | ||||
|   case noRecordingInProgress | ||||
|   case fileError | ||||
|   case createTempFileError | ||||
|   case createTempFileError(message: String? = nil) | ||||
|   case createRecorderError(message: String? = nil) | ||||
|   case videoNotEnabled | ||||
|   case photoNotEnabled | ||||
| @@ -213,8 +213,8 @@ enum CaptureError { | ||||
|       return "There was no active video recording in progress! Did you call stopRecording() twice?" | ||||
|     case .fileError: | ||||
|       return "An unexpected File IO error occured!" | ||||
|     case .createTempFileError: | ||||
|       return "Failed to create a temporary file!" | ||||
|     case let .createTempFileError(message: message): | ||||
|       return "Failed to create a temporary file! \(message ?? "(no additional message)")" | ||||
|     case let .createRecorderError(message: message): | ||||
|       return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")" | ||||
|     case .videoNotEnabled: | ||||
| @@ -264,7 +264,7 @@ enum CameraError: Error { | ||||
|   case session(_ id: SessionError) | ||||
|   case capture(_ id: CaptureError) | ||||
|   case codeScanner(_ id: CodeScannerError) | ||||
|   case unknown(message: String? = nil) | ||||
|   case unknown(message: String? = nil, cause: NSError? = nil) | ||||
| 
 | ||||
|   var code: String { | ||||
|     switch self { | ||||
| @@ -303,8 +303,17 @@ enum CameraError: Error { | ||||
|       return id.message | ||||
|     case let .codeScanner(id: id): | ||||
|       return id.message | ||||
|     case let .unknown(message: message): | ||||
|       return message ?? "An unexpected error occured." | ||||
|     case let .unknown(message: message, cause: cause): | ||||
|       return message ?? cause?.description ?? "An unexpected error occured." | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   var cause: NSError? { | ||||
|     switch self { | ||||
|     case let .unknown(message: _, cause: cause): | ||||
|       return cause | ||||
|     default: | ||||
|       return nil | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -10,7 +10,7 @@ import Foundation | ||||
| 
 | ||||
| @objc | ||||
| public class CameraQueues: NSObject { | ||||
|   /// The serial execution queue for the camera preview layer (input stream) as well as output processing of photos. | ||||
|   /// The serial execution queue for camera configuration and setup. | ||||
|   @objc public static let cameraQueue = DispatchQueue(label: "mrousavy/VisionCamera.main", | ||||
|                                                       qos: .userInteractive, | ||||
|                                                       attributes: [], | ||||
							
								
								
									
										93
									
								
								package/ios/Core/CameraSession+Audio.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								package/ios/Core/CameraSession+Audio.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| // | ||||
| //  CameraSession+Audio.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension CameraSession { | ||||
|   /** | ||||
|    Configures the Audio session and activates it. If the session was active it will shortly be deactivated before configuration. | ||||
|  | ||||
|    The Audio Session will be configured to allow background music, haptics (vibrations) and system sound playback while recording. | ||||
|    Background audio is allowed to play on speakers or bluetooth speakers. | ||||
|    */ | ||||
|   final func activateAudioSession() throws { | ||||
|     ReactLogger.log(level: .info, message: "Activating Audio Session...") | ||||
|  | ||||
|     do { | ||||
|       let audioSession = AVAudioSession.sharedInstance() | ||||
|  | ||||
|       try audioSession.updateCategory(AVAudioSession.Category.playAndRecord, | ||||
|                                       options: [.mixWithOthers, | ||||
|                                                 .allowBluetoothA2DP, | ||||
|                                                 .defaultToSpeaker, | ||||
|                                                 .allowAirPlay]) | ||||
|  | ||||
|       if #available(iOS 14.5, *) { | ||||
|         // prevents the audio session from being interrupted by a phone call | ||||
|         try audioSession.setPrefersNoInterruptionsFromSystemAlerts(true) | ||||
|       } | ||||
|  | ||||
|       if #available(iOS 13.0, *) { | ||||
|         // allow system sounds (notifications, calls, music) to play while recording | ||||
|         try audioSession.setAllowHapticsAndSystemSoundsDuringRecording(true) | ||||
|       } | ||||
|  | ||||
|       audioCaptureSession.startRunning() | ||||
|     } catch let error as NSError { | ||||
|       ReactLogger.log(level: .error, message: "Failed to activate audio session! Error \(error.code): \(error.description)") | ||||
|       switch error.code { | ||||
|       case 561_017_449: | ||||
|         throw CameraError.session(.audioInUseByOtherApp) | ||||
|       default: | ||||
|         throw CameraError.session(.audioSessionFailedToActivate) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   final func deactivateAudioSession() { | ||||
|     ReactLogger.log(level: .info, message: "Deactivating Audio Session...") | ||||
|  | ||||
|     audioCaptureSession.stopRunning() | ||||
|   } | ||||
|  | ||||
|   @objc | ||||
|   func audioSessionInterrupted(notification: Notification) { | ||||
|     ReactLogger.log(level: .error, message: "Audio Session Interruption Notification!") | ||||
|     guard let userInfo = notification.userInfo, | ||||
|           let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, | ||||
|           let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // TODO: Add JS-Event for Audio Session interruptions? | ||||
|     switch type { | ||||
|     case .began: | ||||
|       // Something interrupted our Audio Session, stop recording audio. | ||||
|       ReactLogger.log(level: .error, message: "The Audio Session was interrupted!") | ||||
|     case .ended: | ||||
|       ReactLogger.log(level: .info, message: "The Audio Session interruption has ended.") | ||||
|       guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } | ||||
|       let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) | ||||
|       if options.contains(.shouldResume) { | ||||
|         // Try resuming if possible | ||||
|         if isRecording { | ||||
|           CameraQueues.audioQueue.async { | ||||
|             ReactLogger.log(level: .info, message: "Resuming interrupted Audio Session...") | ||||
|             // restart audio session because interruption is over | ||||
|             try? self.activateAudioSession() | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         ReactLogger.log(level: .error, message: "Cannot resume interrupted Audio Session!") | ||||
|       } | ||||
|     @unknown default: | ||||
|       () | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										80
									
								
								package/ios/Core/CameraSession+CodeScanner.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								package/ios/Core/CameraSession+CodeScanner.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| // | ||||
| //  CameraSession+CodeScanner.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension CameraSession: AVCaptureMetadataOutputObjectsDelegate { | ||||
|   public func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) { | ||||
|     guard let onCodeScanned = delegate?.onCodeScanned else { | ||||
|       // No delegate callback | ||||
|       return | ||||
|     } | ||||
|     guard !metadataObjects.isEmpty else { | ||||
|       // No codes detected | ||||
|       return | ||||
|     } | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       // No cameraId set | ||||
|       return | ||||
|     } | ||||
|     let size = device.activeFormat.videoDimensions | ||||
|  | ||||
|     // Map codes to JS values | ||||
|     let codes = metadataObjects.map { object in | ||||
|       var value: String? | ||||
|       if let code = object as? AVMetadataMachineReadableCodeObject { | ||||
|         value = code.stringValue | ||||
|       } | ||||
|       let x = object.bounds.origin.x * Double(size.width) | ||||
|       let y = object.bounds.origin.y * Double(size.height) | ||||
|       let w = object.bounds.width * Double(size.width) | ||||
|       let h = object.bounds.height * Double(size.height) | ||||
|       let frame = CGRect(x: x, y: y, width: w, height: h) | ||||
|  | ||||
|       return Code(type: object.type, value: value, frame: frame) | ||||
|     } | ||||
|  | ||||
|     // Call delegate (JS) event | ||||
|     onCodeScanned(codes) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    A scanned QR/Barcode. | ||||
|    */ | ||||
|   struct Code { | ||||
|     /** | ||||
|      Type of the scanned Code | ||||
|      */ | ||||
|     let type: AVMetadataObject.ObjectType | ||||
|     /** | ||||
|      Decoded value of the code | ||||
|      */ | ||||
|     let value: String? | ||||
|     /** | ||||
|      Location of the code on-screen, relative to the video output layer | ||||
|      */ | ||||
|     let frame: CGRect | ||||
|  | ||||
|     /** | ||||
|      Converts this Code to a JS Object (Dictionary) | ||||
|      */ | ||||
|     func toJSValue() -> [String: AnyHashable] { | ||||
|       return [ | ||||
|         "type": type.descriptor, | ||||
|         "value": value, | ||||
|         "frame": [ | ||||
|           "x": frame.origin.x, | ||||
|           "y": frame.origin.y, | ||||
|           "width": frame.size.width, | ||||
|           "height": frame.size.height, | ||||
|         ], | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										323
									
								
								package/ios/Core/CameraSession+Configuration.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								package/ios/Core/CameraSession+Configuration.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,323 @@ | ||||
| // | ||||
| //  CameraSession+Configuration.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 12.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension CameraSession { | ||||
|   // pragma MARK: Input Device | ||||
|  | ||||
|   /** | ||||
|    Configures the Input Device (`cameraId`) | ||||
|    */ | ||||
|   func configureDevice(configuration: CameraConfiguration) throws { | ||||
|     ReactLogger.log(level: .info, message: "Configuring Input Device...") | ||||
|  | ||||
|     // Remove all inputs | ||||
|     captureSession.inputs.forEach { input in | ||||
|       captureSession.removeInput(input) | ||||
|     } | ||||
|     videoDeviceInput = nil | ||||
|  | ||||
|     #if targetEnvironment(simulator) | ||||
|       // iOS Simulators don't have Cameras | ||||
|       throw CameraError.device(.notAvailableOnSimulator) | ||||
|     #endif | ||||
|  | ||||
|     guard let cameraId = configuration.cameraId else { | ||||
|       throw CameraError.device(.noDevice) | ||||
|     } | ||||
|  | ||||
|     ReactLogger.log(level: .info, message: "Configuring Camera \(cameraId)...") | ||||
|     // Video Input (Camera Device/Sensor) | ||||
|     guard let videoDevice = AVCaptureDevice(uniqueID: cameraId) else { | ||||
|       throw CameraError.device(.invalid) | ||||
|     } | ||||
|     let input = try AVCaptureDeviceInput(device: videoDevice) | ||||
|     guard captureSession.canAddInput(input) else { | ||||
|       throw CameraError.parameter(.unsupportedInput(inputDescriptor: "video-input")) | ||||
|     } | ||||
|     captureSession.addInput(input) | ||||
|     videoDeviceInput = input | ||||
|  | ||||
|     ReactLogger.log(level: .info, message: "Successfully configured Input Device!") | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Outputs | ||||
|  | ||||
|   /** | ||||
|    Configures all outputs (`photo` + `video` + `codeScanner`) | ||||
|    */ | ||||
|   func configureOutputs(configuration: CameraConfiguration) throws { | ||||
|     ReactLogger.log(level: .info, message: "Configuring Outputs...") | ||||
|  | ||||
|     // Remove all outputs | ||||
|     captureSession.outputs.forEach { output in | ||||
|       captureSession.removeOutput(output) | ||||
|     } | ||||
|     photoOutput = nil | ||||
|     videoOutput = nil | ||||
|     audioOutput = nil | ||||
|     codeScannerOutput = nil | ||||
|  | ||||
|     // Photo Output | ||||
|     if case let .enabled(photo) = configuration.photo { | ||||
|       ReactLogger.log(level: .info, message: "Adding Photo output...") | ||||
|       let photoOutput = AVCapturePhotoOutput() | ||||
|  | ||||
|       // 1. Configure | ||||
|       if photo.enableHighQualityPhotos { | ||||
|         // TODO: In iOS 16 this will be removed in favor of maxPhotoDimensions. | ||||
|         photoOutput.isHighResolutionCaptureEnabled = true | ||||
|         if #available(iOS 13.0, *) { | ||||
|           // TODO: Test if this actually does any fusion or if this just calls the captureOutput twice. If the latter, remove it. | ||||
|           photoOutput.isVirtualDeviceConstituentPhotoDeliveryEnabled = photoOutput.isVirtualDeviceConstituentPhotoDeliverySupported | ||||
|           photoOutput.maxPhotoQualityPrioritization = .quality | ||||
|         } else { | ||||
|           photoOutput.isDualCameraDualPhotoDeliveryEnabled = photoOutput.isDualCameraDualPhotoDeliverySupported | ||||
|         } | ||||
|       } | ||||
|       // TODO: Enable isResponsiveCaptureEnabled? (iOS 17+) | ||||
|       // TODO: Enable isFastCapturePrioritizationEnabled? (iOS 17+) | ||||
|       if photo.enableDepthData { | ||||
|         photoOutput.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliverySupported | ||||
|       } | ||||
|       if #available(iOS 12.0, *), photo.enablePortraitEffectsMatte { | ||||
|         photoOutput.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliverySupported | ||||
|       } | ||||
|  | ||||
|       // 2. Add | ||||
|       guard captureSession.canAddOutput(photoOutput) else { | ||||
|         throw CameraError.parameter(.unsupportedOutput(outputDescriptor: "photo-output")) | ||||
|       } | ||||
|       captureSession.addOutput(photoOutput) | ||||
|       self.photoOutput = photoOutput | ||||
|     } | ||||
|  | ||||
|     // Video Output + Frame Processor | ||||
|     if case let .enabled(video) = configuration.video { | ||||
|       ReactLogger.log(level: .info, message: "Adding Video Data output...") | ||||
|       let videoOutput = AVCaptureVideoDataOutput() | ||||
|  | ||||
|       // 1. Configure | ||||
|       videoOutput.setSampleBufferDelegate(self, queue: CameraQueues.videoQueue) | ||||
|       videoOutput.alwaysDiscardsLateVideoFrames = true | ||||
|       let pixelFormatType = try video.getPixelFormat(for: videoOutput) | ||||
|       videoOutput.videoSettings = [ | ||||
|         String(kCVPixelBufferPixelFormatTypeKey): pixelFormatType, | ||||
|       ] | ||||
|  | ||||
|       // 2. Add | ||||
|       guard captureSession.canAddOutput(videoOutput) else { | ||||
|         throw CameraError.parameter(.unsupportedOutput(outputDescriptor: "video-output")) | ||||
|       } | ||||
|       captureSession.addOutput(videoOutput) | ||||
|       self.videoOutput = videoOutput | ||||
|     } | ||||
|  | ||||
|     // Code Scanner | ||||
|     if case let .enabled(codeScanner) = configuration.codeScanner { | ||||
|       ReactLogger.log(level: .info, message: "Adding Code Scanner output...") | ||||
|       let codeScannerOutput = AVCaptureMetadataOutput() | ||||
|  | ||||
|       // 1. Configure | ||||
|       try codeScanner.codeTypes.forEach { type in | ||||
|         if !codeScannerOutput.availableMetadataObjectTypes.contains(type) { | ||||
|           throw CameraError.codeScanner(.codeTypeNotSupported(codeType: type.descriptor)) | ||||
|         } | ||||
|       } | ||||
|       codeScannerOutput.setMetadataObjectsDelegate(self, queue: CameraQueues.codeScannerQueue) | ||||
|       codeScannerOutput.metadataObjectTypes = codeScanner.codeTypes | ||||
|       if let rectOfInterest = codeScanner.regionOfInterest { | ||||
|         codeScannerOutput.rectOfInterest = rectOfInterest | ||||
|       } | ||||
|  | ||||
|       // 2. Add | ||||
|       guard captureSession.canAddOutput(codeScannerOutput) else { | ||||
|         throw CameraError.codeScanner(.notCompatibleWithOutputs) | ||||
|       } | ||||
|       captureSession.addOutput(codeScannerOutput) | ||||
|       self.codeScannerOutput = codeScannerOutput | ||||
|     } | ||||
|  | ||||
|     // Done! | ||||
|     ReactLogger.log(level: .info, message: "Successfully configured all outputs!") | ||||
|     delegate?.onSessionInitialized() | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Orientation | ||||
|  | ||||
|   func configureOrientation(configuration: CameraConfiguration) { | ||||
|     // Set up orientation and mirroring for all outputs. | ||||
|     // Note: Photos are only rotated through EXIF tags, and Preview through view transforms | ||||
|     let isMirrored = videoDeviceInput?.device.position == .front | ||||
|     captureSession.outputs.forEach { output in | ||||
|       if isMirrored { | ||||
|         output.mirror() | ||||
|       } | ||||
|       output.setOrientation(configuration.orientation) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Format | ||||
|  | ||||
|   /** | ||||
|    Configures the active format (`format`) | ||||
|    */ | ||||
|   func configureFormat(configuration: CameraConfiguration) throws { | ||||
|     guard let targetFormat = configuration.format else { | ||||
|       // No format was set, just use the default. | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     ReactLogger.log(level: .info, message: "Configuring Format (\(targetFormat))...") | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       throw CameraError.session(.cameraNotReady) | ||||
|     } | ||||
|  | ||||
|     let currentFormat = CameraDeviceFormat(fromFormat: device.activeFormat) | ||||
|     if currentFormat == targetFormat { | ||||
|       ReactLogger.log(level: .info, message: "Already selected active format, no need to configure.") | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Find matching format (JS Dictionary -> strongly typed Swift class) | ||||
|     let format = device.formats.first { targetFormat.isEqualTo(format: $0) } | ||||
|     guard let format else { | ||||
|       throw CameraError.format(.invalidFormat) | ||||
|     } | ||||
|  | ||||
|     // Set new device Format | ||||
|     device.activeFormat = format | ||||
|  | ||||
|     ReactLogger.log(level: .info, message: "Successfully configured Format!") | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Side-Props | ||||
|  | ||||
|   /** | ||||
|    Configures format-dependant "side-props" (`fps`, `lowLightBoost`, `torch`) | ||||
|    */ | ||||
|   func configureSideProps(configuration: CameraConfiguration) throws { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       throw CameraError.session(.cameraNotReady) | ||||
|     } | ||||
|  | ||||
|     // Configure FPS | ||||
|     if let fps = configuration.fps { | ||||
|       let supportsGivenFps = device.activeFormat.videoSupportedFrameRateRanges.contains { range in | ||||
|         return range.includes(fps: Double(fps)) | ||||
|       } | ||||
|       if !supportsGivenFps { | ||||
|         throw CameraError.format(.invalidFps(fps: Int(fps))) | ||||
|       } | ||||
|  | ||||
|       let duration = CMTimeMake(value: 1, timescale: fps) | ||||
|       device.activeVideoMinFrameDuration = duration | ||||
|       device.activeVideoMaxFrameDuration = duration | ||||
|     } else { | ||||
|       device.activeVideoMinFrameDuration = CMTime.invalid | ||||
|       device.activeVideoMaxFrameDuration = CMTime.invalid | ||||
|     } | ||||
|  | ||||
|     // Configure Low-Light-Boost | ||||
|     if configuration.enableLowLightBoost { | ||||
|       let isDifferent = configuration.enableLowLightBoost != device.automaticallyEnablesLowLightBoostWhenAvailable | ||||
|       if isDifferent && !device.isLowLightBoostSupported { | ||||
|         throw CameraError.device(.lowLightBoostNotSupported) | ||||
|       } | ||||
|       device.automaticallyEnablesLowLightBoostWhenAvailable = configuration.enableLowLightBoost | ||||
|     } | ||||
|  | ||||
|     // Configure Torch | ||||
|     if configuration.torch != .off { | ||||
|       guard device.hasTorch else { | ||||
|         throw CameraError.device(.flashUnavailable) | ||||
|       } | ||||
|  | ||||
|       device.torchMode = configuration.torch.toTorchMode() | ||||
|       try device.setTorchModeOn(level: 1.0) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Zoom | ||||
|  | ||||
|   /** | ||||
|    Configures zoom (`zoom`) | ||||
|    */ | ||||
|   func configureZoom(configuration: CameraConfiguration) throws { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       throw CameraError.session(.cameraNotReady) | ||||
|     } | ||||
|     guard let zoom = configuration.zoom else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     let clamped = max(min(zoom, device.activeFormat.videoMaxZoomFactor), device.minAvailableVideoZoomFactor) | ||||
|     device.videoZoomFactor = clamped | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Audio | ||||
|  | ||||
|   /** | ||||
|    Configures the Audio Capture Session with an audio input and audio data output. | ||||
|    */ | ||||
|   func configureAudioSession(configuration: CameraConfiguration) throws { | ||||
|     ReactLogger.log(level: .info, message: "Configuring Audio Session...") | ||||
|  | ||||
|     // Prevent iOS from automatically configuring the Audio Session for us | ||||
|     audioCaptureSession.automaticallyConfiguresApplicationAudioSession = false | ||||
|     let enableAudio = configuration.audio != .disabled | ||||
|  | ||||
|     // Check microphone permission | ||||
|     if enableAudio { | ||||
|       let audioPermissionStatus = AVCaptureDevice.authorizationStatus(for: .audio) | ||||
|       if audioPermissionStatus != .authorized { | ||||
|         throw CameraError.permission(.microphone) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Remove all current inputs | ||||
|     audioCaptureSession.inputs.forEach { input in | ||||
|       audioCaptureSession.removeInput(input) | ||||
|     } | ||||
|     audioDeviceInput = nil | ||||
|  | ||||
|     // Audio Input (Microphone) | ||||
|     if enableAudio { | ||||
|       ReactLogger.log(level: .info, message: "Adding Audio input...") | ||||
|       guard let microphone = AVCaptureDevice.default(for: .audio) else { | ||||
|         throw CameraError.device(.microphoneUnavailable) | ||||
|       } | ||||
|       let input = try AVCaptureDeviceInput(device: microphone) | ||||
|       guard audioCaptureSession.canAddInput(input) else { | ||||
|         throw CameraError.parameter(.unsupportedInput(inputDescriptor: "audio-input")) | ||||
|       } | ||||
|       audioCaptureSession.addInput(input) | ||||
|       audioDeviceInput = input | ||||
|     } | ||||
|  | ||||
|     // Remove all current outputs | ||||
|     audioCaptureSession.outputs.forEach { output in | ||||
|       audioCaptureSession.removeOutput(output) | ||||
|     } | ||||
|     audioOutput = nil | ||||
|  | ||||
|     // Audio Output | ||||
|     if enableAudio { | ||||
|       ReactLogger.log(level: .info, message: "Adding Audio Data output...") | ||||
|       let output = AVCaptureAudioDataOutput() | ||||
|       guard audioCaptureSession.canAddOutput(output) else { | ||||
|         throw CameraError.parameter(.unsupportedOutput(outputDescriptor: "audio-output")) | ||||
|       } | ||||
|       output.setSampleBufferDelegate(self, queue: CameraQueues.audioQueue) | ||||
|       audioCaptureSession.addOutput(output) | ||||
|       audioOutput = output | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										82
									
								
								package/ios/Core/CameraSession+Focus.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								package/ios/Core/CameraSession+Focus.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| // | ||||
| //  CameraSession+Focus.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension CameraSession { | ||||
|   /** | ||||
|    Focuses the Camera to the specified point. The point must be in the Camera coordinate system, so {0...1} on both axis. | ||||
|    */ | ||||
|   func focus(point: CGPoint) throws { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       throw CameraError.session(SessionError.cameraNotReady) | ||||
|     } | ||||
|     if !device.isFocusPointOfInterestSupported { | ||||
|       throw CameraError.device(DeviceError.focusNotSupported) | ||||
|     } | ||||
|  | ||||
|     do { | ||||
|       try device.lockForConfiguration() | ||||
|       defer { | ||||
|         device.unlockForConfiguration() | ||||
|       } | ||||
|  | ||||
|       // Set Focus | ||||
|       if device.isFocusPointOfInterestSupported { | ||||
|         device.focusPointOfInterest = point | ||||
|         device.focusMode = .autoFocus | ||||
|       } | ||||
|  | ||||
|       // Set Exposure | ||||
|       if device.isExposurePointOfInterestSupported { | ||||
|         device.exposurePointOfInterest = point | ||||
|         device.exposureMode = .autoExpose | ||||
|       } | ||||
|  | ||||
|       // Remove any existing listeners | ||||
|       NotificationCenter.default.removeObserver(self, | ||||
|                                                 name: NSNotification.Name.AVCaptureDeviceSubjectAreaDidChange, | ||||
|                                                 object: nil) | ||||
|  | ||||
|       // Listen for focus completion | ||||
|       device.isSubjectAreaChangeMonitoringEnabled = true | ||||
|       NotificationCenter.default.addObserver(self, | ||||
|                                              selector: #selector(subjectAreaDidChange), | ||||
|                                              name: NSNotification.Name.AVCaptureDeviceSubjectAreaDidChange, | ||||
|                                              object: nil) | ||||
|     } catch { | ||||
|       throw CameraError.device(DeviceError.configureError) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @objc | ||||
|   func subjectAreaDidChange(notification _: NSNotification) { | ||||
|     guard let device = videoDeviceInput?.device else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     try? device.lockForConfiguration() | ||||
|     defer { | ||||
|       device.unlockForConfiguration() | ||||
|     } | ||||
|  | ||||
|     // Reset Focus to continuous/auto | ||||
|     if device.isFocusPointOfInterestSupported { | ||||
|       device.focusMode = .continuousAutoFocus | ||||
|     } | ||||
|  | ||||
|     // Reset Exposure to continuous/auto | ||||
|     if device.isExposurePointOfInterestSupported { | ||||
|       device.exposureMode = .continuousAutoExposure | ||||
|     } | ||||
|  | ||||
|     // Disable listeners | ||||
|     device.isSubjectAreaChangeMonitoringEnabled = false | ||||
|   } | ||||
| } | ||||
							
								
								
									
										107
									
								
								package/ios/Core/CameraSession+Photo.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								package/ios/Core/CameraSession+Photo.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| // | ||||
| //  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) | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										221
									
								
								package/ios/Core/CameraSession+Video.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								package/ios/Core/CameraSession+Video.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | ||||
| // | ||||
| //  CameraSession+Video.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
| import UIKit | ||||
|  | ||||
| extension CameraSession { | ||||
|   /** | ||||
|    Starts a video + audio recording with a custom Asset Writer. | ||||
|    */ | ||||
|   func startRecording(options: RecordVideoOptions, | ||||
|                       onVideoRecorded: @escaping (_ video: Video) -> Void, | ||||
|                       onError: @escaping (_ error: CameraError) -> Void) { | ||||
|     // Run on Camera Queue | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       ReactLogger.log(level: .info, message: "Starting Video recording...") | ||||
|  | ||||
|       if options.flash != .off { | ||||
|         // use the torch as the video's flash | ||||
|         self.configure { config in | ||||
|           config.torch = options.flash | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Get Video Output | ||||
|       guard let videoOutput = self.videoOutput else { | ||||
|         if self.configuration?.video == .disabled { | ||||
|           onError(.capture(.videoNotEnabled)) | ||||
|         } else { | ||||
|           onError(.session(.cameraNotReady)) | ||||
|         } | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       let enableAudio = self.configuration?.audio != .disabled | ||||
|  | ||||
|       // Callback for when the recording ends | ||||
|       let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in | ||||
|         defer { | ||||
|           // Disable Audio Session again | ||||
|           if enableAudio { | ||||
|             CameraQueues.audioQueue.async { | ||||
|               self.deactivateAudioSession() | ||||
|             } | ||||
|           } | ||||
|           // Reset flash | ||||
|           if options.flash != .off { | ||||
|             // Set torch mode back to what it was before if we used it for the video flash. | ||||
|             self.configure { config in | ||||
|               let torch = self.configuration?.torch ?? .off | ||||
|               config.torch = torch | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         self.recordingSession = nil | ||||
|         self.isRecording = false | ||||
|         ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).") | ||||
|  | ||||
|         if let error = error as NSError? { | ||||
|           ReactLogger.log(level: .error, message: "RecordingSession Error \(error.code): \(error.description)") | ||||
|           // Something went wrong, we have an error | ||||
|           if error.domain == "capture/aborted" { | ||||
|             onError(.capture(.aborted)) | ||||
|           } else { | ||||
|             onError(.capture(.unknown(message: "An unknown recording error occured! \(error.code) \(error.description)"))) | ||||
|           } | ||||
|         } else { | ||||
|           if status == .completed { | ||||
|             // Recording was successfully saved | ||||
|             let video = Video(path: recordingSession.url.absoluteString, | ||||
|                               duration: recordingSession.duration) | ||||
|             onVideoRecorded(video) | ||||
|           } else { | ||||
|             // Recording wasn't saved and we don't have an error either. | ||||
|             onError(.unknown(message: "AVAssetWriter completed with status: \(status.descriptor)")) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Create temporary file | ||||
|       let errorPointer = ErrorPointer(nilLiteral: ()) | ||||
|       let fileExtension = options.fileType.descriptor ?? "mov" | ||||
|       guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else { | ||||
|         let message = errorPointer?.pointee?.description | ||||
|         onError(.capture(.createTempFileError(message: message))) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       ReactLogger.log(level: .info, message: "File path: \(tempFilePath)") | ||||
|       let tempURL = URL(string: "file://\(tempFilePath)")! | ||||
|  | ||||
|       let recordingSession: RecordingSession | ||||
|       do { | ||||
|         recordingSession = try RecordingSession(url: tempURL, | ||||
|                                                 fileType: options.fileType, | ||||
|                                                 completion: onFinish) | ||||
|       } catch let error as NSError { | ||||
|         onError(.capture(.createRecorderError(message: error.description))) | ||||
|         return | ||||
|       } | ||||
|       self.recordingSession = recordingSession | ||||
|  | ||||
|       // Init Video | ||||
|       guard var videoSettings = self.recommendedVideoSettings(videoOutput: videoOutput, | ||||
|                                                               fileType: options.fileType, | ||||
|                                                               videoCodec: options.codec), | ||||
|         !videoSettings.isEmpty else { | ||||
|         onError(.capture(.createRecorderError(message: "Failed to get video settings!"))) | ||||
|         return | ||||
|       } | ||||
|       ReactLogger.log(level: .trace, message: "Recommended Video Settings: \(videoSettings.description)") | ||||
|  | ||||
|       // Custom Video Bit Rate | ||||
|       if let videoBitRate = options.bitRate { | ||||
|         // Convert from Mbps -> bps | ||||
|         let bitsPerSecond = videoBitRate * 1_000_000 | ||||
|         videoSettings[AVVideoCompressionPropertiesKey] = [ | ||||
|           AVVideoAverageBitRateKey: NSNumber(value: bitsPerSecond), | ||||
|         ] | ||||
|       } | ||||
|  | ||||
|       // get pixel format (420f, 420v, x420) | ||||
|       let pixelFormat = videoOutput.pixelFormat | ||||
|       recordingSession.initializeVideoWriter(withSettings: videoSettings, | ||||
|                                              pixelFormat: pixelFormat) | ||||
|  | ||||
|       // Enable/Activate Audio Session (optional) | ||||
|       if enableAudio { | ||||
|         if let audioOutput = self.audioOutput { | ||||
|           // Activate Audio Session asynchronously | ||||
|           CameraQueues.audioQueue.async { | ||||
|             do { | ||||
|               try self.activateAudioSession() | ||||
|             } catch { | ||||
|               self.onConfigureError(error) | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           // Initialize audio asset writer | ||||
|           let audioSettings = audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: options.fileType) | ||||
|           recordingSession.initializeAudioWriter(withSettings: audioSettings) | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // start recording session with or without audio. | ||||
|       do { | ||||
|         try recordingSession.startAssetWriter() | ||||
|         self.isRecording = true | ||||
|       } catch let error as NSError { | ||||
|         onError(.capture(.createRecorderError(message: "RecordingSession failed to start asset writer. \(error.description)"))) | ||||
|         return | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Stops an active recording. | ||||
|    */ | ||||
|   func stopRecording(promise: Promise) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       self.isRecording = false | ||||
|  | ||||
|       withPromise(promise) { | ||||
|         guard let recordingSession = self.recordingSession else { | ||||
|           throw CameraError.capture(.noRecordingInProgress) | ||||
|         } | ||||
|         recordingSession.finish() | ||||
|         return nil | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Pauses an active recording. | ||||
|    */ | ||||
|   func pauseRecording(promise: Promise) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       withPromise(promise) { | ||||
|         guard self.recordingSession != nil else { | ||||
|           // there's no active recording! | ||||
|           throw CameraError.capture(.noRecordingInProgress) | ||||
|         } | ||||
|         self.isRecording = false | ||||
|         return nil | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Resumes an active, but paused recording. | ||||
|    */ | ||||
|   func resumeRecording(promise: Promise) { | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       withPromise(promise) { | ||||
|         guard self.recordingSession != nil else { | ||||
|           // there's no active recording! | ||||
|           throw CameraError.capture(.noRecordingInProgress) | ||||
|         } | ||||
|         self.isRecording = true | ||||
|         return nil | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private func recommendedVideoSettings(videoOutput: AVCaptureVideoDataOutput, | ||||
|                                         fileType: AVFileType, | ||||
|                                         videoCodec: AVVideoCodecType?) -> [String: Any]? { | ||||
|     if videoCodec != nil { | ||||
|       return videoOutput.recommendedVideoSettings(forVideoCodecType: videoCodec!, assetWriterOutputFileType: fileType) | ||||
|     } else { | ||||
|       return videoOutput.recommendedVideoSettingsForAssetWriter(writingTo: fileType) | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										260
									
								
								package/ios/Core/CameraSession.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								package/ios/Core/CameraSession.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,260 @@ | ||||
| // | ||||
| //  CameraSession.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  A fully-featured Camera Session supporting preview, video, photo, frame processing, and code scanning outputs. | ||||
|  All changes to the session have to be controlled via the `configure` function. | ||||
|  */ | ||||
| class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { | ||||
|   // Configuration | ||||
|   var configuration: CameraConfiguration? | ||||
|   // Capture Session | ||||
|   let captureSession = AVCaptureSession() | ||||
|   let audioCaptureSession = AVCaptureSession() | ||||
|   // Inputs & Outputs | ||||
|   var videoDeviceInput: AVCaptureDeviceInput? | ||||
|   var audioDeviceInput: AVCaptureDeviceInput? | ||||
|   var photoOutput: AVCapturePhotoOutput? | ||||
|   var videoOutput: AVCaptureVideoDataOutput? | ||||
|   var audioOutput: AVCaptureAudioDataOutput? | ||||
|   var codeScannerOutput: AVCaptureMetadataOutput? | ||||
|   // State | ||||
|   var recordingSession: RecordingSession? | ||||
|   var isRecording = false | ||||
|  | ||||
|   // Callbacks | ||||
|   weak var delegate: CameraSessionDelegate? | ||||
|  | ||||
|   // Public accessors | ||||
|   var maxZoom: Double { | ||||
|     if let device = videoDeviceInput?.device { | ||||
|       return device.maxAvailableVideoZoomFactor | ||||
|     } | ||||
|     return 1.0 | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Create a new instance of the `CameraSession`. | ||||
|    The `onError` callback is used for any runtime errors. | ||||
|    */ | ||||
|   override init() { | ||||
|     super.init() | ||||
|  | ||||
|     NotificationCenter.default.addObserver(self, | ||||
|                                            selector: #selector(sessionRuntimeError), | ||||
|                                            name: .AVCaptureSessionRuntimeError, | ||||
|                                            object: captureSession) | ||||
|     NotificationCenter.default.addObserver(self, | ||||
|                                            selector: #selector(sessionRuntimeError), | ||||
|                                            name: .AVCaptureSessionRuntimeError, | ||||
|                                            object: audioCaptureSession) | ||||
|     NotificationCenter.default.addObserver(self, | ||||
|                                            selector: #selector(audioSessionInterrupted), | ||||
|                                            name: AVAudioSession.interruptionNotification, | ||||
|                                            object: AVAudioSession.sharedInstance) | ||||
|   } | ||||
|  | ||||
|   deinit { | ||||
|     NotificationCenter.default.removeObserver(self, | ||||
|                                               name: .AVCaptureSessionRuntimeError, | ||||
|                                               object: captureSession) | ||||
|     NotificationCenter.default.removeObserver(self, | ||||
|                                               name: .AVCaptureSessionRuntimeError, | ||||
|                                               object: audioCaptureSession) | ||||
|     NotificationCenter.default.removeObserver(self, | ||||
|                                               name: AVAudioSession.interruptionNotification, | ||||
|                                               object: AVAudioSession.sharedInstance) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Creates a PreviewView for the current Capture Session | ||||
|    */ | ||||
|   func createPreviewView(frame: CGRect) -> PreviewView { | ||||
|     return PreviewView(frame: frame, session: captureSession) | ||||
|   } | ||||
|  | ||||
|   func onConfigureError(_ error: Error) { | ||||
|     if let error = error as? CameraError { | ||||
|       // It's a typed Error | ||||
|       delegate?.onError(error) | ||||
|     } else { | ||||
|       // It's any kind of unknown error | ||||
|       let cameraError = CameraError.unknown(message: error.localizedDescription) | ||||
|       delegate?.onError(cameraError) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Update the session configuration. | ||||
|    Any changes in here will be re-configured only if required, and under a lock. | ||||
|    The `configuration` object is a copy of the currently active configuration that can be modified by the caller in the lambda. | ||||
|    */ | ||||
|   func configure(_ lambda: (_ configuration: CameraConfiguration) throws -> Void) { | ||||
|     ReactLogger.log(level: .info, message: "Updating Session Configuration...") | ||||
|  | ||||
|     // Let caller configure a new configuration for the Camera. | ||||
|     let config = CameraConfiguration(copyOf: configuration) | ||||
|     do { | ||||
|       try lambda(config) | ||||
|     } catch { | ||||
|       onConfigureError(error) | ||||
|     } | ||||
|     let difference = CameraConfiguration.Difference(between: configuration, and: config) | ||||
|  | ||||
|     // Set up Camera (Video) Capture Session (on camera queue) | ||||
|     CameraQueues.cameraQueue.async { | ||||
|       do { | ||||
|         // If needed, configure the AVCaptureSession (inputs, outputs) | ||||
|         if difference.isSessionConfigurationDirty { | ||||
|           // Lock Capture Session for configuration | ||||
|           ReactLogger.log(level: .info, message: "Beginning CameraSession configuration...") | ||||
|           self.captureSession.beginConfiguration() | ||||
|  | ||||
|           // 1. Update input device | ||||
|           if difference.inputChanged { | ||||
|             try self.configureDevice(configuration: config) | ||||
|           } | ||||
|           // 2. Update outputs | ||||
|           if difference.outputsChanged { | ||||
|             try self.configureOutputs(configuration: config) | ||||
|           } | ||||
|           // 3. Update output orientation | ||||
|           if difference.orientationChanged { | ||||
|             self.configureOrientation(configuration: config) | ||||
|           } | ||||
|  | ||||
|           // Unlock Capture Session again and submit configuration to Hardware | ||||
|           self.captureSession.commitConfiguration() | ||||
|           ReactLogger.log(level: .info, message: "Committed CameraSession configuration!") | ||||
|         } | ||||
|  | ||||
|         // If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..) | ||||
|         if difference.isDeviceConfigurationDirty { | ||||
|           guard let device = self.videoDeviceInput?.device else { | ||||
|             throw CameraError.session(.cameraNotReady) | ||||
|           } | ||||
|           ReactLogger.log(level: .info, message: "Beginning CaptureDevice configuration...") | ||||
|           try device.lockForConfiguration() | ||||
|  | ||||
|           // 4. Configure format | ||||
|           if difference.formatChanged { | ||||
|             try self.configureFormat(configuration: config) | ||||
|           } | ||||
|           // 5. Configure side-props (fps, lowLightBoost) | ||||
|           if difference.sidePropsChanged { | ||||
|             try self.configureSideProps(configuration: config) | ||||
|           } | ||||
|           // 6. Configure zoom | ||||
|           if difference.zoomChanged { | ||||
|             try self.configureZoom(configuration: config) | ||||
|           } | ||||
|  | ||||
|           device.unlockForConfiguration() | ||||
|           ReactLogger.log(level: .info, message: "Committed CaptureDevice configuration!") | ||||
|         } | ||||
|  | ||||
|         // 6. Start or stop the session if needed | ||||
|         self.checkIsActive(configuration: config) | ||||
|  | ||||
|         // Update successful, set the new configuration! | ||||
|         self.configuration = config | ||||
|       } catch { | ||||
|         self.onConfigureError(error) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Set up Audio Capture Session (on audio queue) | ||||
|     if difference.audioSessionChanged { | ||||
|       CameraQueues.audioQueue.async { | ||||
|         do { | ||||
|           // Lock Capture Session for configuration | ||||
|           ReactLogger.log(level: .info, message: "Beginning AudioSession configuration...") | ||||
|           self.audioCaptureSession.beginConfiguration() | ||||
|  | ||||
|           try self.configureAudioSession(configuration: config) | ||||
|  | ||||
|           // Unlock Capture Session again and submit configuration to Hardware | ||||
|           self.audioCaptureSession.commitConfiguration() | ||||
|           ReactLogger.log(level: .info, message: "Committed AudioSession configuration!") | ||||
|         } catch { | ||||
|           self.onConfigureError(error) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Starts or stops the CaptureSession if needed (`isActive`) | ||||
|    */ | ||||
|   private func checkIsActive(configuration: CameraConfiguration) { | ||||
|     if configuration.isActive == captureSession.isRunning { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Start/Stop session | ||||
|     if configuration.isActive { | ||||
|       captureSession.startRunning() | ||||
|     } else { | ||||
|       captureSession.stopRunning() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Called for every new Frame in the Video output | ||||
|    */ | ||||
|   public final func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) { | ||||
|     // Call Frame Processor (delegate) for every Video Frame | ||||
|     if captureOutput is AVCaptureVideoDataOutput { | ||||
|       delegate?.onFrame(sampleBuffer: sampleBuffer) | ||||
|     } | ||||
|  | ||||
|     // Record Video Frame/Audio Sample to File in custom `RecordingSession` (AVAssetWriter) | ||||
|     if isRecording { | ||||
|       guard let recordingSession = recordingSession else { | ||||
|         delegate?.onError(.capture(.unknown(message: "isRecording was true but the RecordingSession was null!"))) | ||||
|         return | ||||
|       } | ||||
|  | ||||
|       switch captureOutput { | ||||
|       case is AVCaptureVideoDataOutput: | ||||
|         recordingSession.appendBuffer(sampleBuffer, type: .video, timestamp: CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) | ||||
|       case is AVCaptureAudioDataOutput: | ||||
|         let timestamp = CMSyncConvertTime(CMSampleBufferGetPresentationTimeStamp(sampleBuffer), | ||||
|                                           from: audioCaptureSession.masterClock ?? CMClockGetHostTimeClock(), | ||||
|                                           to: captureSession.masterClock ?? CMClockGetHostTimeClock()) | ||||
|         recordingSession.appendBuffer(sampleBuffer, type: .audio, timestamp: timestamp) | ||||
|       default: | ||||
|         break | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // pragma MARK: Notifications | ||||
|  | ||||
|   @objc | ||||
|   func sessionRuntimeError(notification: Notification) { | ||||
|     ReactLogger.log(level: .error, message: "Unexpected Camera Runtime Error occured!") | ||||
|     guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     // Notify consumer about runtime error | ||||
|     delegate?.onError(.unknown(message: error._nsError.description, cause: error._nsError)) | ||||
|  | ||||
|     let shouldRestart = configuration?.isActive == true | ||||
|     if shouldRestart { | ||||
|       // restart capture session after an error occured | ||||
|       CameraQueues.cameraQueue.async { | ||||
|         self.captureSession.startRunning() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										32
									
								
								package/ios/Core/CameraSessionDelegate.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								package/ios/Core/CameraSessionDelegate.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // | ||||
| //  CameraSessionDelegate.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  A listener for [CameraSession] events | ||||
|  */ | ||||
| protocol CameraSessionDelegate: AnyObject { | ||||
|   /** | ||||
|    Called when there is a Runtime Error in the [CameraSession] | ||||
|    */ | ||||
|   func onError(_ error: CameraError) | ||||
|   /** | ||||
|    Called when the [CameraSession] successfully initializes | ||||
|    */ | ||||
|   func onSessionInitialized() | ||||
|   /** | ||||
|    Called for every frame (if video or frameProcessor is enabled) | ||||
|    */ | ||||
|   func onFrame(sampleBuffer: CMSampleBuffer) | ||||
|   /** | ||||
|    Called whenever a QR/Barcode has been scanned. Only if the CodeScanner Output is enabled | ||||
|    */ | ||||
|   func onCodeScanned(codes: [CameraSession.Code]) | ||||
| } | ||||
| @@ -8,6 +8,7 @@ | ||||
| 
 | ||||
| import AVFoundation | ||||
| 
 | ||||
| // Keeps a strong reference on delegates, as the AVCapturePhotoOutput only holds a weak reference. | ||||
| private var delegatesReferences: [NSObject] = [] | ||||
| 
 | ||||
| // MARK: - PhotoCaptureDelegate | ||||
| @@ -42,7 +43,8 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate { | ||||
|     let error = ErrorPointer(nilLiteral: ()) | ||||
|     guard let tempFilePath = RCTTempFilePath("jpeg", error) | ||||
|     else { | ||||
|       promise.reject(error: .capture(.createTempFileError), cause: error?.pointee) | ||||
|       let message = error?.pointee?.description | ||||
|       promise.reject(error: .capture(.createTempFileError(message: message)), cause: error?.pointee) | ||||
|       return | ||||
|     } | ||||
|     let url = URL(string: "file://\(tempFilePath)")! | ||||
| @@ -93,11 +93,7 @@ class RecordingSession { | ||||
|   /** | ||||
|    Initializes an AssetWriter for audio frames (CMSampleBuffers). | ||||
|    */ | ||||
|   func initializeAudioWriter(withSettings settings: [String: Any]) { | ||||
|     guard !settings.isEmpty else { | ||||
|       ReactLogger.log(level: .error, message: "Tried to initialize Audio Writer with empty settings!") | ||||
|       return | ||||
|     } | ||||
|   func initializeAudioWriter(withSettings settings: [String: Any]?) { | ||||
|     guard audioWriter == nil else { | ||||
|       ReactLogger.log(level: .error, message: "Tried to add Audio Writer twice!") | ||||
|       return | ||||
| @@ -1,23 +0,0 @@ | ||||
| // | ||||
| //  AVAudioSession+trySetAllowHaptics.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 26.03.21. | ||||
| //  Copyright © 2021 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension AVAudioSession { | ||||
|   /** | ||||
|    Tries to set allowHapticsAndSystemSoundsDuringRecording and ignore errors. | ||||
|    */ | ||||
|   func trySetAllowHaptics(_ allowHaptics: Bool) { | ||||
|     if #available(iOS 13.0, *) { | ||||
|       if !self.allowHapticsAndSystemSoundsDuringRecording { | ||||
|         try? self.setAllowHapticsAndSystemSoundsDuringRecording(allowHaptics) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -8,6 +8,7 @@ | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
| import UIKit | ||||
|  | ||||
| extension AVCaptureConnection { | ||||
|   /** | ||||
|   | ||||
| @@ -10,6 +10,8 @@ import AVFoundation | ||||
|  | ||||
| extension AVCaptureDevice { | ||||
|   func toDictionary() -> [String: Any] { | ||||
|     let formats = formats.map { CameraDeviceFormat(fromFormat: $0) } | ||||
|  | ||||
|     return [ | ||||
|       "id": uniqueID, | ||||
|       "physicalDevices": physicalDevices.map(\.deviceType.physicalDeviceDescriptor), | ||||
| @@ -25,10 +27,8 @@ extension AVCaptureDevice { | ||||
|       "supportsLowLightBoost": isLowLightBoostSupported, | ||||
|       "supportsFocus": isFocusPointOfInterestSupported, | ||||
|       "hardwareLevel": "full", | ||||
|       "sensorOrientation": "portrait", // TODO: Sensor Orientation? | ||||
|       "formats": formats.map { format -> [String: Any] in | ||||
|         format.toDictionary() | ||||
|       }, | ||||
|       "sensorOrientation": Orientation.landscapeLeft.jsValue, | ||||
|       "formats": formats.map { $0.toJSValue() }, | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -8,27 +8,20 @@ | ||||
|  | ||||
| import AVFoundation | ||||
|  | ||||
| private func getAllVideoStabilizationModes() -> [AVCaptureVideoStabilizationMode] { | ||||
|   var modes: [AVCaptureVideoStabilizationMode] = [.auto, .cinematic, .off, .standard] | ||||
|   if #available(iOS 13, *) { | ||||
|     modes.append(.cinematicExtended) | ||||
|   } | ||||
|   return modes | ||||
| } | ||||
|  | ||||
| extension AVCaptureDevice.Format { | ||||
|   var videoStabilizationModes: [AVCaptureVideoStabilizationMode] { | ||||
|     return getAllVideoStabilizationModes().filter { self.isVideoStabilizationModeSupported($0) } | ||||
|     let allModes = AVCaptureDevice.Format.getAllVideoStabilizationModes() | ||||
|     return allModes.filter { self.isVideoStabilizationModeSupported($0) } | ||||
|   } | ||||
|  | ||||
|   var minFrameRate: Float64 { | ||||
|   var minFps: Float64 { | ||||
|     let maxRange = videoSupportedFrameRateRanges.max { l, r in | ||||
|       return l.maxFrameRate < r.maxFrameRate | ||||
|     } | ||||
|     return maxRange?.maxFrameRate ?? 0 | ||||
|   } | ||||
|  | ||||
|   var maxFrameRate: Float64 { | ||||
|   var maxFps: Float64 { | ||||
|     let maxRange = videoSupportedFrameRateRanges.max { l, r in | ||||
|       return l.maxFrameRate < r.maxFrameRate | ||||
|     } | ||||
| @@ -45,52 +38,20 @@ extension AVCaptureDevice.Format { | ||||
|     return hdrFormats.contains(pixelFormat) | ||||
|   } | ||||
|  | ||||
|   func toDictionary() -> [String: AnyHashable] { | ||||
|     let availablePixelFormats = AVCaptureVideoDataOutput().availableVideoPixelFormatTypes | ||||
|     let pixelFormats = availablePixelFormats.map { format in PixelFormat(mediaSubType: format) } | ||||
|  | ||||
|     return [ | ||||
|       "videoStabilizationModes": videoStabilizationModes.map(\.descriptor), | ||||
|       "autoFocusSystem": autoFocusSystem.descriptor, | ||||
|       "photoHeight": photoDimensions.height, | ||||
|       "photoWidth": photoDimensions.width, | ||||
|       "videoHeight": videoDimensions.height, | ||||
|       "videoWidth": videoDimensions.width, | ||||
|       "maxISO": maxISO, | ||||
|       "minISO": minISO, | ||||
|       "fieldOfView": videoFieldOfView, | ||||
|       "maxZoom": videoMaxZoomFactor, | ||||
|       "supportsVideoHDR": supportsVideoHDR, | ||||
|       "supportsPhotoHDR": false, | ||||
|       "minFps": minFrameRate, | ||||
|       "maxFps": maxFrameRate, | ||||
|       "pixelFormats": pixelFormats.map(\.unionValue), | ||||
|       "supportsDepthCapture": !supportedDepthDataFormats.isEmpty, | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Compares this format to the given JS `CameraDeviceFormat`. | ||||
|    Only the most important properties (such as dimensions and FPS) are taken into consideration, | ||||
|    so this is not an exact equals, but more like a "matches filter" comparison. | ||||
|    */ | ||||
|   func isEqualTo(jsFormat dict: NSDictionary) -> Bool { | ||||
|     guard dict["photoWidth"] as? Int32 == photoDimensions.width && dict["photoHeight"] as? Int32 == photoDimensions.height else { | ||||
|   var supportsPhotoHDR: Bool { | ||||
|     // TODO: Supports Photo HDR on iOS? | ||||
|     return false | ||||
|   } | ||||
|  | ||||
|     guard dict["videoWidth"] as? Int32 == videoDimensions.width && dict["videoHeight"] as? Int32 == videoDimensions.height else { | ||||
|       return false | ||||
|   var supportsDepthCapture: Bool { | ||||
|     return !supportedDepthDataFormats.isEmpty | ||||
|   } | ||||
|  | ||||
|     guard dict["minFps"] as? Float64 == minFrameRate && dict["maxFps"] as? Float64 == maxFrameRate else { | ||||
|       return false | ||||
|   private static func getAllVideoStabilizationModes() -> [AVCaptureVideoStabilizationMode] { | ||||
|     var modes: [AVCaptureVideoStabilizationMode] = [.auto, .cinematic, .off, .standard] | ||||
|     if #available(iOS 13, *) { | ||||
|       modes.append(.cinematicExtended) | ||||
|     } | ||||
|  | ||||
|     guard dict["supportsVideoHDR"] as? Bool == supportsVideoHDR else { | ||||
|       return false | ||||
|     } | ||||
|  | ||||
|     return true | ||||
|     return modes | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										52
									
								
								package/ios/Extensions/AVCaptureOutput+mirror.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								package/ios/Extensions/AVCaptureOutput+mirror.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // | ||||
| //  AVCaptureOutput+mirror.swift | ||||
| //  mrousavy | ||||
| // | ||||
| //  Created by Marc Rousavy on 18.01.21. | ||||
| //  Copyright © 2021 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
|  | ||||
| extension AVCaptureOutput { | ||||
|   /** | ||||
|    Mirrors the video output if possible. | ||||
|    */ | ||||
|   func mirror() { | ||||
|     connections.forEach { connection in | ||||
|       if connection.isVideoMirroringSupported { | ||||
|         connection.automaticallyAdjustsVideoMirroring = false | ||||
|         connection.isVideoMirrored = true | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Sets the target orientation of the video output. | ||||
|    This does not always physically rotate image buffers. | ||||
|  | ||||
|    - For Preview, an orientation hint is used to rotate the layer/view itself. | ||||
|    - For Photos, an EXIF tag is used. | ||||
|    - For Videos, the buffers are physically rotated if available, since we use an AVCaptureVideoDataOutput instead of an AVCaptureMovieFileOutput. | ||||
|    */ | ||||
|   func setOrientation(_ orientation: Orientation) { | ||||
|     // Camera Sensors are always in 90deg rotation. | ||||
|     // We are setting the target rotation here, so we need to rotate by 90deg once. | ||||
|     let cameraOrientation = orientation.rotateRight() | ||||
|  | ||||
|     // Set orientation for each connection | ||||
|     connections.forEach { connection in | ||||
|       // TODO: Use this once Xcode 15 is rolled out | ||||
|       // if #available(iOS 17.0, *) { | ||||
|       //   let degrees = cameraOrientation.toDegrees() | ||||
|       //   if connection.isVideoRotationAngleSupported(degrees) { | ||||
|       //     connection.videoRotationAngle = degrees | ||||
|       //   } | ||||
|       // } else { | ||||
|       if connection.isVideoOrientationSupported { | ||||
|         connection.videoOrientation = cameraOrientation.toAVCaptureVideoOrientation() | ||||
|       } | ||||
|       // } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| // | ||||
| //  AVCapturePhotoOutput+mirror.swift | ||||
| //  mrousavy | ||||
| // | ||||
| //  Created by Marc Rousavy on 18.01.21. | ||||
| //  Copyright © 2021 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
|  | ||||
| extension AVCapturePhotoOutput { | ||||
|   func mirror() { | ||||
|     connections.forEach { connection in | ||||
|       if connection.isVideoMirroringSupported { | ||||
|         connection.automaticallyAdjustsVideoMirroring = false | ||||
|         connection.isVideoMirrored = true | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| // | ||||
| //  AVCaptureVideoDataOutput+pixelFormat.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 12.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| extension AVCaptureVideoDataOutput { | ||||
|   /** | ||||
|    Gets or sets the PixelFormat this output streams in. | ||||
|    By default, the first item in `availableVideoPixelFormatTypes` is chosen. | ||||
|    */ | ||||
|   var pixelFormat: OSType { | ||||
|     get { | ||||
|       let current = videoSettings[String(kCVPixelBufferPixelFormatTypeKey)] as? OSType | ||||
|       return current ?? availableVideoPixelFormatTypes.first! | ||||
|     } | ||||
|     set { | ||||
|       videoSettings[String(kCVPixelBufferPixelFormatTypeKey)] = newValue | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| // | ||||
| //  UIInterfaceOrientation+descriptor.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 04.01.22. | ||||
| //  Copyright © 2022 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import UIKit | ||||
|  | ||||
| extension UIInterfaceOrientation { | ||||
|   init(withString string: String) throws { | ||||
|     switch string { | ||||
|     case "portrait": | ||||
|       self = .portrait | ||||
|       return | ||||
|     case "portrait-upside-down": | ||||
|       self = .portraitUpsideDown | ||||
|       return | ||||
|     case "landscape-left": | ||||
|       self = .landscapeLeft | ||||
|       return | ||||
|     case "landscape-right": | ||||
|       self = .landscapeRight | ||||
|       return | ||||
|     default: | ||||
|       throw EnumParserError.invalidValue | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										41
									
								
								package/ios/Types/AutoFocusSystem.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								package/ios/Types/AutoFocusSystem.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| // | ||||
| //  AutoFocusSystem.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 13.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| enum AutoFocusSystem: String, JSUnionValue { | ||||
|   case contrastDetection = "contrast-detection" | ||||
|   case phaseDetection | ||||
|   case none | ||||
|  | ||||
|   init(jsValue: String) throws { | ||||
|     if let parsed = AutoFocusSystem(rawValue: jsValue) { | ||||
|       self = parsed | ||||
|     } else { | ||||
|       throw CameraError.parameter(.invalid(unionName: "autoFocusSystem", receivedValue: jsValue)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   init(fromFocusSystem focusSystem: AVCaptureDevice.Format.AutoFocusSystem) { | ||||
|     switch focusSystem { | ||||
|     case .none: | ||||
|       self = .none | ||||
|     case .contrastDetection: | ||||
|       self = .contrastDetection | ||||
|     case .phaseDetection: | ||||
|       self = .phaseDetection | ||||
|     @unknown default: | ||||
|       self = .none | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   var jsValue: String { | ||||
|     return rawValue | ||||
|   } | ||||
| } | ||||
							
								
								
									
										119
									
								
								package/ios/Types/CameraDeviceFormat.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								package/ios/Types/CameraDeviceFormat.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| // | ||||
| //  CameraDeviceFormat.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 13.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  A serializable representation of [AVCaptureDevice.Format] | ||||
|  */ | ||||
| struct CameraDeviceFormat: Equatable, CustomStringConvertible { | ||||
|   let videoWidth: Int | ||||
|   let videoHeight: Int | ||||
|  | ||||
|   let photoWidth: Int | ||||
|   let photoHeight: Int | ||||
|  | ||||
|   let minFps: Double | ||||
|   let maxFps: Double | ||||
|  | ||||
|   let minISO: Float | ||||
|   let maxISO: Float | ||||
|  | ||||
|   let fieldOfView: Float | ||||
|   let maxZoom: Double | ||||
|  | ||||
|   let videoStabilizationModes: [VideoStabilizationMode] | ||||
|   let autoFocusSystem: AutoFocusSystem | ||||
|  | ||||
|   let supportsVideoHDR: Bool | ||||
|   let supportsPhotoHDR: Bool | ||||
|  | ||||
|   let pixelFormats: [PixelFormat] | ||||
|  | ||||
|   let supportsDepthCapture: Bool | ||||
|  | ||||
|   init(fromFormat format: AVCaptureDevice.Format) { | ||||
|     videoWidth = Int(format.videoDimensions.width) | ||||
|     videoHeight = Int(format.videoDimensions.height) | ||||
|     photoWidth = Int(format.photoDimensions.width) | ||||
|     photoHeight = Int(format.photoDimensions.height) | ||||
|     minFps = format.minFps | ||||
|     maxFps = format.maxFps | ||||
|     minISO = format.minISO | ||||
|     maxISO = format.maxISO | ||||
|     fieldOfView = format.videoFieldOfView | ||||
|     maxZoom = format.videoMaxZoomFactor | ||||
|     videoStabilizationModes = format.videoStabilizationModes.map { VideoStabilizationMode(from: $0) } | ||||
|     autoFocusSystem = AutoFocusSystem(fromFocusSystem: format.autoFocusSystem) | ||||
|     supportsVideoHDR = format.supportsVideoHDR | ||||
|     supportsPhotoHDR = format.supportsPhotoHDR | ||||
|     pixelFormats = CameraDeviceFormat.getAllPixelFormats() | ||||
|     supportsDepthCapture = format.supportsDepthCapture | ||||
|   } | ||||
|  | ||||
|   init(jsValue: NSDictionary) throws { | ||||
|     // swiftlint:disable force_cast | ||||
|     videoWidth = jsValue["videoWidth"] as! Int | ||||
|     videoHeight = jsValue["videoHeight"] as! Int | ||||
|     photoWidth = jsValue["photoWidth"] as! Int | ||||
|     photoHeight = jsValue["photoHeight"] as! Int | ||||
|     minFps = jsValue["minFps"] as! Double | ||||
|     maxFps = jsValue["maxFps"] as! Double | ||||
|     minISO = jsValue["minISO"] as! Float | ||||
|     maxISO = jsValue["maxISO"] as! Float | ||||
|     fieldOfView = jsValue["fieldOfView"] as! Float | ||||
|     maxZoom = jsValue["maxZoom"] as! Double | ||||
|     let jsVideoStabilizationModes = jsValue["videoStabilizationModes"] as! [String] | ||||
|     videoStabilizationModes = try jsVideoStabilizationModes.map { try VideoStabilizationMode(jsValue: $0) } | ||||
|     let jsAutoFocusSystem = jsValue["autoFocusSystem"] as! String | ||||
|     autoFocusSystem = try AutoFocusSystem(jsValue: jsAutoFocusSystem) | ||||
|     supportsVideoHDR = jsValue["supportsVideoHDR"] as! Bool | ||||
|     supportsPhotoHDR = jsValue["supportsPhotoHDR"] as! Bool | ||||
|     let jsPixelFormats = jsValue["pixelFormats"] as! [String] | ||||
|     pixelFormats = try jsPixelFormats.map { try PixelFormat(jsValue: $0) } | ||||
|     supportsDepthCapture = jsValue["supportsDepthCapture"] as! Bool | ||||
|     // swiftlint:enable force_cast | ||||
|   } | ||||
|  | ||||
|   func isEqualTo(format other: AVCaptureDevice.Format) -> Bool { | ||||
|     let other = CameraDeviceFormat(fromFormat: other) | ||||
|     return self == other | ||||
|   } | ||||
|  | ||||
|   func toJSValue() -> NSDictionary { | ||||
|     return [ | ||||
|       "videoStabilizationModes": videoStabilizationModes.map(\.jsValue), | ||||
|       "autoFocusSystem": autoFocusSystem.jsValue, | ||||
|       "photoHeight": photoHeight, | ||||
|       "photoWidth": photoWidth, | ||||
|       "videoHeight": videoHeight, | ||||
|       "videoWidth": videoWidth, | ||||
|       "maxISO": maxISO, | ||||
|       "minISO": minISO, | ||||
|       "fieldOfView": fieldOfView, | ||||
|       "maxZoom": maxZoom, | ||||
|       "supportsVideoHDR": supportsVideoHDR, | ||||
|       "supportsPhotoHDR": supportsPhotoHDR, | ||||
|       "minFps": minFps, | ||||
|       "maxFps": maxFps, | ||||
|       "pixelFormats": pixelFormats.map(\.jsValue), | ||||
|       "supportsDepthCapture": supportsDepthCapture, | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   var description: String { | ||||
|     return "\(photoWidth)x\(photoHeight) | \(videoWidth)x\(videoHeight)@\(maxFps) (ISO: \(minISO)..\(maxISO), Pixel Formats: \(pixelFormats))" | ||||
|   } | ||||
|  | ||||
|   // On iOS, all PixelFormats are always supported for every format (it can convert natively) | ||||
|   private static func getAllPixelFormats() -> [PixelFormat] { | ||||
|     let availablePixelFormats = AVCaptureVideoDataOutput().availableVideoPixelFormatTypes | ||||
|     return availablePixelFormats.map { format in PixelFormat(mediaSubType: format) } | ||||
|   } | ||||
| } | ||||
| @@ -9,7 +9,7 @@ | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| class CodeScanner { | ||||
| struct CodeScanner: Equatable { | ||||
|   let codeTypes: [AVMetadataObject.ObjectType] | ||||
|   let interval: Int | ||||
|   let regionOfInterest: CGRect? | ||||
|   | ||||
							
								
								
									
										15
									
								
								package/ios/Types/JSUnionValue.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								package/ios/Types/JSUnionValue.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| // | ||||
| //  JSUnionValue.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 13.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| protocol JSUnionValue { | ||||
|   init(jsValue: String) throws | ||||
|  | ||||
|   var jsValue: String { get } | ||||
| } | ||||
							
								
								
									
										83
									
								
								package/ios/Types/Orientation.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								package/ios/Types/Orientation.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| // | ||||
| //  Orientation.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  The Orientation used for the Preview, Photo, Video and Frame Processor outputs. | ||||
|  */ | ||||
| enum Orientation: String, JSUnionValue { | ||||
|   /** | ||||
|    Phone is in upright portrait mode, home button/indicator is at the bottom | ||||
|    */ | ||||
|   case portrait | ||||
|   /** | ||||
|    Phone is in landscape mode, home button/indicator is on the left | ||||
|    */ | ||||
|   case landscapeLeft = "landscape-left" | ||||
|   /** | ||||
|    Phone is in upside-down portrait mode, home button/indicator is at the top | ||||
|    */ | ||||
|   case portraitUpsideDown = "portrait-upside-down" | ||||
|   /** | ||||
|    Phone is in landscape mode, home button/indicator is on the right | ||||
|    */ | ||||
|   case landscapeRight = "landscape-right" | ||||
|  | ||||
|   init(jsValue: String) throws { | ||||
|     if let parsed = Orientation(rawValue: jsValue) { | ||||
|       self = parsed | ||||
|     } else { | ||||
|       throw CameraError.parameter(.invalid(unionName: "orientation", receivedValue: jsValue)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   var jsValue: String { | ||||
|     return rawValue | ||||
|   } | ||||
|  | ||||
|   func toAVCaptureVideoOrientation() -> AVCaptureVideoOrientation { | ||||
|     switch self { | ||||
|     case .portrait: | ||||
|       return .portrait | ||||
|     case .landscapeLeft: | ||||
|       return .landscapeLeft | ||||
|     case .portraitUpsideDown: | ||||
|       return .portraitUpsideDown | ||||
|     case .landscapeRight: | ||||
|       return .landscapeRight | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   func toDegrees() -> Double { | ||||
|     switch self { | ||||
|     case .portrait: | ||||
|       return 0 | ||||
|     case .landscapeLeft: | ||||
|       return 90 | ||||
|     case .portraitUpsideDown: | ||||
|       return 180 | ||||
|     case .landscapeRight: | ||||
|       return 270 | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   func rotateRight() -> Orientation { | ||||
|     switch self { | ||||
|     case .portrait: | ||||
|       return .landscapeLeft | ||||
|     case .landscapeLeft: | ||||
|       return .portraitUpsideDown | ||||
|     case .portraitUpsideDown: | ||||
|       return .landscapeRight | ||||
|     case .landscapeRight: | ||||
|       return .portrait | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -9,38 +9,22 @@ | ||||
| import AVFoundation | ||||
| import Foundation | ||||
| 
 | ||||
| enum PixelFormat { | ||||
| enum PixelFormat: String, JSUnionValue { | ||||
|   case yuv | ||||
|   case rgb | ||||
|   case native | ||||
|   case unknown | ||||
| 
 | ||||
|   var unionValue: String { | ||||
|     switch self { | ||||
|     case .yuv: | ||||
|       return "yuv" | ||||
|     case .rgb: | ||||
|       return "rgb" | ||||
|     case .native: | ||||
|       return "native" | ||||
|     case .unknown: | ||||
|       return "unknown" | ||||
|   init(jsValue: String) throws { | ||||
|     if let parsed = PixelFormat(rawValue: jsValue) { | ||||
|       self = parsed | ||||
|     } else { | ||||
|       throw CameraError.parameter(.invalid(unionName: "pixelFormat", receivedValue: jsValue)) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   init(unionValue: String) throws { | ||||
|     switch unionValue { | ||||
|     case "yuv": | ||||
|       self = .yuv | ||||
|     case "rgb": | ||||
|       self = .rgb | ||||
|     case "native": | ||||
|       self = .native | ||||
|     case "unknown": | ||||
|       self = .unknown | ||||
|     default: | ||||
|       throw CameraError.parameter(.invalid(unionName: "pixelFormat", receivedValue: unionValue)) | ||||
|     } | ||||
|   var jsValue: String { | ||||
|     return rawValue | ||||
|   } | ||||
| 
 | ||||
|   init(mediaSubType: OSType) { | ||||
							
								
								
									
										30
									
								
								package/ios/Types/RecordVideoOptions.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								package/ios/Types/RecordVideoOptions.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| // | ||||
| //  RecordVideoOptions.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 12.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| struct RecordVideoOptions { | ||||
|   var fileType: AVFileType = .mov | ||||
|   var flash: Torch = .off | ||||
|   var codec: AVVideoCodecType? | ||||
|   /** | ||||
|    Bit-Rate of the Video, in Megabits per second (Mbps) | ||||
|    */ | ||||
|   var bitRate: Double? | ||||
|  | ||||
|   init(fromJSValue dictionary: NSDictionary) throws { | ||||
|     // File Type (.mov or .mp4) | ||||
|     if let fileTypeOption = dictionary["fileType"] as? String { | ||||
|       guard let parsed = try? AVFileType(withString: fileTypeOption) else { | ||||
|         throw CameraError.parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption)) | ||||
|       } | ||||
|       fileType = parsed | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -6,12 +6,13 @@ | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  A ResizeMode used for the PreviewView. | ||||
|  */ | ||||
| enum ResizeMode { | ||||
| enum ResizeMode: String, JSUnionValue { | ||||
|   /** | ||||
|    Keep aspect ratio, but fill entire parent view (centered). | ||||
|    */ | ||||
| @@ -21,15 +22,15 @@ enum ResizeMode { | ||||
|    */ | ||||
|   case contain | ||||
|  | ||||
|   init(fromTypeScriptUnion union: String) { | ||||
|     switch union { | ||||
|     case "cover": | ||||
|       self = .cover | ||||
|     case "contain": | ||||
|       self = .contain | ||||
|     default: | ||||
|       // TODO: Use the onError event for safer error handling! | ||||
|       fatalError("Invalid value passed for resizeMode! (\(union))") | ||||
|   init(jsValue: String) throws { | ||||
|     if let parsed = ResizeMode(rawValue: jsValue) { | ||||
|       self = parsed | ||||
|     } else { | ||||
|       throw CameraError.parameter(.invalid(unionName: "resizeMode", receivedValue: jsValue)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   var jsValue: String { | ||||
|     return rawValue | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										45
									
								
								package/ios/Types/Torch.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								package/ios/Types/Torch.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| // | ||||
| //  Torch.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 11.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| /** | ||||
|  A Torch used for permanent flash. | ||||
|  */ | ||||
| enum Torch: String, JSUnionValue { | ||||
|   /** | ||||
|    Torch (flash unit) is always off. | ||||
|    */ | ||||
|   case off | ||||
|   /** | ||||
|    Torch (flash unit) is always on. | ||||
|    */ | ||||
|   case on | ||||
|  | ||||
|   init(jsValue: String) throws { | ||||
|     if let parsed = Torch(rawValue: jsValue) { | ||||
|       self = parsed | ||||
|     } else { | ||||
|       throw CameraError.parameter(.invalid(unionName: "torch", receivedValue: jsValue)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   var jsValue: String { | ||||
|     return rawValue | ||||
|   } | ||||
|  | ||||
|   func toTorchMode() -> AVCaptureDevice.TorchMode { | ||||
|     switch self { | ||||
|     case .on: | ||||
|       return .on | ||||
|     case .off: | ||||
|       return .off | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										28
									
								
								package/ios/Types/Video.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								package/ios/Types/Video.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  Video.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 12.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| struct Video { | ||||
|   /** | ||||
|    Path to the temporary video file | ||||
|    */ | ||||
|   var path: String | ||||
|   /** | ||||
|    Duration of the recorded video (in seconds) | ||||
|    */ | ||||
|   var duration: Double | ||||
|  | ||||
|   func toJSValue() -> NSDictionary { | ||||
|     return [ | ||||
|       "path": path, | ||||
|       "duration": duration, | ||||
|     ] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										47
									
								
								package/ios/Types/VideoStabilizationMode.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								package/ios/Types/VideoStabilizationMode.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| // | ||||
| //  VideoStabilizationMode.swift | ||||
| //  VisionCamera | ||||
| // | ||||
| //  Created by Marc Rousavy on 13.10.23. | ||||
| //  Copyright © 2023 mrousavy. All rights reserved. | ||||
| // | ||||
|  | ||||
| import AVFoundation | ||||
| import Foundation | ||||
|  | ||||
| enum VideoStabilizationMode: String, JSUnionValue { | ||||
|   case off | ||||
|   case standard | ||||
|   case cinematic | ||||
|   case cinematicExtended = "cinematic-extended" | ||||
|   case auto | ||||
|  | ||||
|   init(jsValue: String) throws { | ||||
|     if let parsed = VideoStabilizationMode(rawValue: jsValue) { | ||||
|       self = parsed | ||||
|     } else { | ||||
|       throw CameraError.parameter(.invalid(unionName: "videoStabilizationMode", receivedValue: jsValue)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   init(from mode: AVCaptureVideoStabilizationMode) { | ||||
|     switch mode { | ||||
|     case .off: | ||||
|       self = .off | ||||
|     case .standard: | ||||
|       self = .standard | ||||
|     case .cinematic: | ||||
|       self = .cinematic | ||||
|     case .cinematicExtended: | ||||
|       self = .cinematicExtended | ||||
|     case .auto: | ||||
|       self = .auto | ||||
|     default: | ||||
|       self = .off | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   var jsValue: String { | ||||
|     return rawValue | ||||
|   } | ||||
| } | ||||
| @@ -16,16 +16,25 @@ | ||||
| 		B8446E502ABA14C900E56077 /* CameraDevicesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */; }; | ||||
| 		B84760A62608EE7C004C3180 /* FrameHostObject.mm in Sources */ = {isa = PBXBuildFile; fileRef = B84760A52608EE7C004C3180 /* FrameHostObject.mm */; }; | ||||
| 		B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84760DE2608F57D004C3180 /* CameraQueues.swift */; }; | ||||
| 		B85882322AD966FC00317161 /* CameraDeviceFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882312AD966FC00317161 /* CameraDeviceFormat.swift */; }; | ||||
| 		B85882342AD969E000317161 /* VideoStabilizationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882332AD969E000317161 /* VideoStabilizationMode.swift */; }; | ||||
| 		B85882362AD96AFF00317161 /* AutoFocusSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882352AD96AFF00317161 /* AutoFocusSystem.swift */; }; | ||||
| 		B85882382AD96B4400317161 /* JSUnionValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85882372AD96B4400317161 /* JSUnionValue.swift */; }; | ||||
| 		B85F7AE92A77BB680089C539 /* FrameProcessorPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = B85F7AE82A77BB680089C539 /* FrameProcessorPlugin.m */; }; | ||||
| 		B864005027849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B864004F27849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift */; }; | ||||
| 		B86400522784A23400E9D2CA /* CameraView+Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86400512784A23400E9D2CA /* CameraView+Orientation.swift */; }; | ||||
| 		B86DC971260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */; }; | ||||
| 		B86DC974260E310600FB17B2 /* CameraView+AVAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */; }; | ||||
| 		B86DC977260E315100FB17B2 /* CameraView+AVCaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */; }; | ||||
| 		B87B11BF2A8E63B700732EBF /* PixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87B11BE2A8E63B700732EBF /* PixelFormat.swift */; }; | ||||
| 		B88103DB2AD6F0A00087F063 /* CameraSession+Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DA2AD6F0A00087F063 /* CameraSession+Audio.swift */; }; | ||||
| 		B88103DD2AD6F62C0087F063 /* CameraSession+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DC2AD6F62C0087F063 /* CameraSession+Focus.swift */; }; | ||||
| 		B88103DF2AD6FB230087F063 /* Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103DE2AD6FB230087F063 /* Orientation.swift */; }; | ||||
| 		B88103E12AD7046E0087F063 /* Torch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103E02AD7046E0087F063 /* Torch.swift */; }; | ||||
| 		B88103E32AD7065C0087F063 /* CameraSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */; }; | ||||
| 		B881D35E2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B881D35D2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift */; }; | ||||
| 		B881D3602ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */; }; | ||||
| 		B882721026AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */; }; | ||||
| 		B88685E52AD68D9300E93869 /* CameraSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E42AD68D9300E93869 /* CameraSession.swift */; }; | ||||
| 		B88685E72AD698DF00E93869 /* CameraConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E62AD698DF00E93869 /* CameraConfiguration.swift */; }; | ||||
| 		B88685E92AD6A5D600E93869 /* CameraSession+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685E82AD6A5D600E93869 /* CameraSession+Video.swift */; }; | ||||
| 		B88685EB2AD6A5DE00E93869 /* CameraSession+Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685EA2AD6A5DE00E93869 /* CameraSession+Photo.swift */; }; | ||||
| 		B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88685EC2AD6A5E600E93869 /* CameraSession+CodeScanner.swift */; }; | ||||
| 		B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */; }; | ||||
| 		B887518625E0102000DB86D6 /* CameraView+RecordVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */; }; | ||||
| 		B887518725E0102000DB86D6 /* CameraViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B887515F25E0102000DB86D6 /* CameraViewManager.m */; }; | ||||
| @@ -34,7 +43,7 @@ | ||||
| 		B887518C25E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */; }; | ||||
| 		B887518D25E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516625E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift */; }; | ||||
| 		B887518E25E0102000DB86D6 /* AVFrameRateRange+includes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516725E0102000DB86D6 /* AVFrameRateRange+includes.swift */; }; | ||||
| 		B887518F25E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516825E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift */; }; | ||||
| 		B887518F25E0102000DB86D6 /* AVCaptureOutput+mirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516825E0102000DB86D6 /* AVCaptureOutput+mirror.swift */; }; | ||||
| 		B887519125E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */; }; | ||||
| 		B887519425E0102000DB86D6 /* MakeReactError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516E25E0102000DB86D6 /* MakeReactError.swift */; }; | ||||
| 		B887519525E0102000DB86D6 /* ReactLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516F25E0102000DB86D6 /* ReactLogger.swift */; }; | ||||
| @@ -50,21 +59,23 @@ | ||||
| 		B88751A125E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517C25E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift */; }; | ||||
| 		B88751A325E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517E25E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift */; }; | ||||
| 		B88751A425E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517F25E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift */; }; | ||||
| 		B88751A525E0102000DB86D6 /* CameraView+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518025E0102000DB86D6 /* CameraView+Focus.swift */; }; | ||||
| 		B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518125E0102000DB86D6 /* CameraViewManager.swift */; }; | ||||
| 		B88751A725E0102000DB86D6 /* CameraView+Zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518225E0102000DB86D6 /* CameraView+Zoom.swift */; }; | ||||
| 		B88751A825E0102000DB86D6 /* CameraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518325E0102000DB86D6 /* CameraError.swift */; }; | ||||
| 		B88751A925E0102000DB86D6 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518425E0102000DB86D6 /* CameraView.swift */; }; | ||||
| 		B88B47472667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */; }; | ||||
| 		B8994E6C263F03E100069589 /* JSINSObjectConversion.mm in Sources */ = {isa = PBXBuildFile; fileRef = B8994E6B263F03E100069589 /* JSINSObjectConversion.mm */; }; | ||||
| 		B8A1AEC42AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */; }; | ||||
| 		B8A1AEC62AD7F08E00169C0D /* CameraView+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC52AD7F08E00169C0D /* CameraView+Focus.swift */; }; | ||||
| 		B8A1AEC82AD8005400169C0D /* CameraSession+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC72AD8005400169C0D /* CameraSession+Configuration.swift */; }; | ||||
| 		B8A1AECA2AD8034E00169C0D /* RecordVideoOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AEC92AD8034E00169C0D /* RecordVideoOptions.swift */; }; | ||||
| 		B8A1AECC2AD803B200169C0D /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A1AECB2AD803B200169C0D /* Video.swift */; }; | ||||
| 		B8BD3BA2266E22D2006C80A2 /* Callback.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD3BA1266E22D2006C80A2 /* Callback.swift */; }; | ||||
| 		B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */; }; | ||||
| 		B8DB3BC8263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC7263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift */; }; | ||||
| 		B8DB3BCA263DC4D8004C18D7 /* RecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */; }; | ||||
| 		B8DB3BCC263DC97E004C18D7 /* AVFileType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */; }; | ||||
| 		B8E957D02A693AD2008F5480 /* CameraView+Torch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E957CF2A693AD2008F5480 /* CameraView+Torch.swift */; }; | ||||
| 		B8F127D02ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */; }; | ||||
| 		B8FF60AC2ACC93EF009D612F /* CameraView+CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60AB2ACC93EF009D612F /* CameraView+CodeScanner.swift */; }; | ||||
| 		B8FF60AE2ACC9731009D612F /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60AD2ACC9731009D612F /* CodeScanner.swift */; }; | ||||
| 		B8FF60B12ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */; }; | ||||
| /* End PBXBuildFile section */ | ||||
| @@ -98,16 +109,25 @@ | ||||
| 		B84760A22608EE38004C3180 /* FrameHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameHostObject.h; sourceTree = "<group>"; }; | ||||
| 		B84760A52608EE7C004C3180 /* FrameHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameHostObject.mm; sourceTree = "<group>"; }; | ||||
| 		B84760DE2608F57D004C3180 /* CameraQueues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraQueues.swift; sourceTree = "<group>"; }; | ||||
| 		B85882312AD966FC00317161 /* CameraDeviceFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraDeviceFormat.swift; sourceTree = "<group>"; }; | ||||
| 		B85882332AD969E000317161 /* VideoStabilizationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoStabilizationMode.swift; sourceTree = "<group>"; }; | ||||
| 		B85882352AD96AFF00317161 /* AutoFocusSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoFocusSystem.swift; sourceTree = "<group>"; }; | ||||
| 		B85882372AD96B4400317161 /* JSUnionValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSUnionValue.swift; sourceTree = "<group>"; }; | ||||
| 		B85F7AE82A77BB680089C539 /* FrameProcessorPlugin.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FrameProcessorPlugin.m; sourceTree = "<group>"; }; | ||||
| 		B864004F27849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIInterfaceOrientation+descriptor.swift"; sourceTree = "<group>"; }; | ||||
| 		B86400512784A23400E9D2CA /* CameraView+Orientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+Orientation.swift"; sourceTree = "<group>"; }; | ||||
| 		B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioSession+trySetAllowHaptics.swift"; sourceTree = "<group>"; }; | ||||
| 		B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+AVAudioSession.swift"; sourceTree = "<group>"; }; | ||||
| 		B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+AVCaptureSession.swift"; sourceTree = "<group>"; }; | ||||
| 		B87B11BE2A8E63B700732EBF /* PixelFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFormat.swift; sourceTree = "<group>"; }; | ||||
| 		B88103DA2AD6F0A00087F063 /* CameraSession+Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraSession+Audio.swift"; sourceTree = "<group>"; }; | ||||
| 		B88103DC2AD6F62C0087F063 /* CameraSession+Focus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraSession+Focus.swift"; sourceTree = "<group>"; }; | ||||
| 		B88103DE2AD6FB230087F063 /* Orientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Orientation.swift; sourceTree = "<group>"; }; | ||||
| 		B88103E02AD7046E0087F063 /* Torch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Torch.swift; sourceTree = "<group>"; }; | ||||
| 		B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraSessionDelegate.swift; sourceTree = "<group>"; }; | ||||
| 		B881D35D2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice+toDictionary.swift"; sourceTree = "<group>"; }; | ||||
| 		B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureVideoDataOutput+findPixelFormat.swift"; sourceTree = "<group>"; }; | ||||
| 		B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureConnection+setInterfaceOrientation.swift"; sourceTree = "<group>"; }; | ||||
| 		B88685E42AD68D9300E93869 /* CameraSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraSession.swift; sourceTree = "<group>"; }; | ||||
| 		B88685E62AD698DF00E93869 /* CameraConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraConfiguration.swift; sourceTree = "<group>"; }; | ||||
| 		B88685E82AD6A5D600E93869 /* CameraSession+Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraSession+Video.swift"; sourceTree = "<group>"; }; | ||||
| 		B88685EA2AD6A5DE00E93869 /* CameraSession+Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraSession+Photo.swift"; sourceTree = "<group>"; }; | ||||
| 		B88685EC2AD6A5E600E93869 /* CameraSession+CodeScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraSession+CodeScanner.swift"; sourceTree = "<group>"; }; | ||||
| 		B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDelegate.swift; sourceTree = "<group>"; }; | ||||
| 		B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraView+RecordVideo.swift"; sourceTree = "<group>"; }; | ||||
| 		B887515E25E0102000DB86D6 /* CameraBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CameraBridge.h; sourceTree = "<group>"; }; | ||||
| @@ -117,7 +137,7 @@ | ||||
| 		B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice+isMultiCam.swift"; sourceTree = "<group>"; }; | ||||
| 		B887516625E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice+physicalDevices.swift"; sourceTree = "<group>"; }; | ||||
| 		B887516725E0102000DB86D6 /* AVFrameRateRange+includes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVFrameRateRange+includes.swift"; sourceTree = "<group>"; }; | ||||
| 		B887516825E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCapturePhotoOutput+mirror.swift"; sourceTree = "<group>"; }; | ||||
| 		B887516825E0102000DB86D6 /* AVCaptureOutput+mirror.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureOutput+mirror.swift"; sourceTree = "<group>"; }; | ||||
| 		B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format+toDictionary.swift"; sourceTree = "<group>"; }; | ||||
| 		B887516E25E0102000DB86D6 /* MakeReactError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MakeReactError.swift; sourceTree = "<group>"; }; | ||||
| 		B887516F25E0102000DB86D6 /* ReactLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactLogger.swift; sourceTree = "<group>"; }; | ||||
| @@ -133,7 +153,6 @@ | ||||
| 		B887517C25E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Position+descriptor.swift"; sourceTree = "<group>"; }; | ||||
| 		B887517E25E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.FlashMode+descriptor.swift"; sourceTree = "<group>"; }; | ||||
| 		B887517F25E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift"; sourceTree = "<group>"; }; | ||||
| 		B887518025E0102000DB86D6 /* CameraView+Focus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraView+Focus.swift"; sourceTree = "<group>"; }; | ||||
| 		B887518125E0102000DB86D6 /* CameraViewManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraViewManager.swift; sourceTree = "<group>"; }; | ||||
| 		B887518225E0102000DB86D6 /* CameraView+Zoom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraView+Zoom.swift"; sourceTree = "<group>"; }; | ||||
| 		B887518325E0102000DB86D6 /* CameraError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraError.swift; sourceTree = "<group>"; }; | ||||
| @@ -141,6 +160,11 @@ | ||||
| 		B88873E5263D46C7008B1D0E /* FrameProcessorPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPlugin.h; sourceTree = "<group>"; }; | ||||
| 		B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureSession+setVideoStabilizationMode.swift"; sourceTree = "<group>"; }; | ||||
| 		B8994E6B263F03E100069589 /* JSINSObjectConversion.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = JSINSObjectConversion.mm; sourceTree = "<group>"; }; | ||||
| 		B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureVideoDataOutput+pixelFormat.swift"; sourceTree = "<group>"; }; | ||||
| 		B8A1AEC52AD7F08E00169C0D /* CameraView+Focus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+Focus.swift"; sourceTree = "<group>"; }; | ||||
| 		B8A1AEC72AD8005400169C0D /* CameraSession+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraSession+Configuration.swift"; sourceTree = "<group>"; }; | ||||
| 		B8A1AEC92AD8034E00169C0D /* RecordVideoOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordVideoOptions.swift; sourceTree = "<group>"; }; | ||||
| 		B8A1AECB2AD803B200169C0D /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = "<group>"; }; | ||||
| 		B8BD3BA1266E22D2006C80A2 /* Callback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Callback.swift; sourceTree = "<group>"; }; | ||||
| 		B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift"; sourceTree = "<group>"; }; | ||||
| 		B8DB3BC7263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAssetWriter.Status+descriptor.swift"; sourceTree = "<group>"; }; | ||||
| @@ -148,12 +172,10 @@ | ||||
| 		B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVFileType+descriptor.swift"; sourceTree = "<group>"; }; | ||||
| 		B8E8467D2A696F44000D6A11 /* VisionCameraProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VisionCameraProxy.h; sourceTree = "<group>"; }; | ||||
| 		B8E8467E2A696F4D000D6A11 /* VisionCameraProxy.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VisionCameraProxy.mm; sourceTree = "<group>"; }; | ||||
| 		B8E957CF2A693AD2008F5480 /* CameraView+Torch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+Torch.swift"; sourceTree = "<group>"; }; | ||||
| 		B8F0825E2A6046FC00C17EB6 /* FrameProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessor.h; sourceTree = "<group>"; }; | ||||
| 		B8F0825F2A60491900C17EB6 /* FrameProcessor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessor.mm; sourceTree = "<group>"; }; | ||||
| 		B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMVideoDimensions+toCGSize.swift"; sourceTree = "<group>"; }; | ||||
| 		B8F7DDD1266F715D00120533 /* Frame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Frame.m; sourceTree = "<group>"; }; | ||||
| 		B8FF60AB2ACC93EF009D612F /* CameraView+CodeScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+CodeScanner.swift"; sourceTree = "<group>"; }; | ||||
| 		B8FF60AD2ACC9731009D612F /* CodeScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = "<group>"; }; | ||||
| 		B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVMetadataObject.ObjectType+descriptor.swift"; sourceTree = "<group>"; }; | ||||
| /* End PBXFileReference section */ | ||||
| @@ -180,28 +202,19 @@ | ||||
| 		58B511D21A9E6C8500147676 = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				B88685E32AD68D8A00E93869 /* Core */, | ||||
| 				B80175EA2ABDEBBB00E7DE90 /* Types */, | ||||
| 				B8DCF2D725EA940700EA5C72 /* Frame Processor */, | ||||
| 				B887515E25E0102000DB86D6 /* CameraBridge.h */, | ||||
| 				B84760DE2608F57D004C3180 /* CameraQueues.swift */, | ||||
| 				B887518325E0102000DB86D6 /* CameraError.swift */, | ||||
| 				B887518425E0102000DB86D6 /* CameraView.swift */, | ||||
| 				B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */, | ||||
| 				B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */, | ||||
| 				B8E957CF2A693AD2008F5480 /* CameraView+Torch.swift */, | ||||
| 				B887518025E0102000DB86D6 /* CameraView+Focus.swift */, | ||||
| 				B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */, | ||||
| 				B887517125E0102000DB86D6 /* CameraView+TakePhoto.swift */, | ||||
| 				B8FF60AB2ACC93EF009D612F /* CameraView+CodeScanner.swift */, | ||||
| 				B887518225E0102000DB86D6 /* CameraView+Zoom.swift */, | ||||
| 				B86400512784A23400E9D2CA /* CameraView+Orientation.swift */, | ||||
| 				B8A1AEC52AD7F08E00169C0D /* CameraView+Focus.swift */, | ||||
| 				B887515F25E0102000DB86D6 /* CameraViewManager.m */, | ||||
| 				B887518125E0102000DB86D6 /* CameraViewManager.swift */, | ||||
| 				B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */, | ||||
| 				B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */, | ||||
| 				B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */, | ||||
| 				B83D5EE629377117000AFD2F /* PreviewView.swift */, | ||||
| 				B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */, | ||||
| 				B887516125E0102000DB86D6 /* Extensions */, | ||||
| 				B887517225E0102000DB86D6 /* Parsers */, | ||||
| 				B887516D25E0102000DB86D6 /* React Utils */, | ||||
| @@ -214,15 +227,44 @@ | ||||
| 			children = ( | ||||
| 				B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */, | ||||
| 				B8FF60AD2ACC9731009D612F /* CodeScanner.swift */, | ||||
| 				B88103DE2AD6FB230087F063 /* Orientation.swift */, | ||||
| 				B88103E02AD7046E0087F063 /* Torch.swift */, | ||||
| 				B8A1AEC92AD8034E00169C0D /* RecordVideoOptions.swift */, | ||||
| 				B8A1AECB2AD803B200169C0D /* Video.swift */, | ||||
| 				B85882312AD966FC00317161 /* CameraDeviceFormat.swift */, | ||||
| 				B85882332AD969E000317161 /* VideoStabilizationMode.swift */, | ||||
| 				B85882352AD96AFF00317161 /* AutoFocusSystem.swift */, | ||||
| 				B87B11BE2A8E63B700732EBF /* PixelFormat.swift */, | ||||
| 				B85882372AD96B4400317161 /* JSUnionValue.swift */, | ||||
| 			); | ||||
| 			path = Types; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		B88685E32AD68D8A00E93869 /* Core */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				B88685E42AD68D9300E93869 /* CameraSession.swift */, | ||||
| 				B8A1AEC72AD8005400169C0D /* CameraSession+Configuration.swift */, | ||||
| 				B88685E82AD6A5D600E93869 /* CameraSession+Video.swift */, | ||||
| 				B88685EA2AD6A5DE00E93869 /* CameraSession+Photo.swift */, | ||||
| 				B88685EC2AD6A5E600E93869 /* CameraSession+CodeScanner.swift */, | ||||
| 				B88103DA2AD6F0A00087F063 /* CameraSession+Audio.swift */, | ||||
| 				B88103DC2AD6F62C0087F063 /* CameraSession+Focus.swift */, | ||||
| 				B88685E62AD698DF00E93869 /* CameraConfiguration.swift */, | ||||
| 				B88103E22AD7065C0087F063 /* CameraSessionDelegate.swift */, | ||||
| 				B83D5EE629377117000AFD2F /* PreviewView.swift */, | ||||
| 				B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */, | ||||
| 				B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */, | ||||
| 				B84760DE2608F57D004C3180 /* CameraQueues.swift */, | ||||
| 				B887518325E0102000DB86D6 /* CameraError.swift */, | ||||
| 			); | ||||
| 			path = Core; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| 		B887516125E0102000DB86D6 /* Extensions */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */, | ||||
| 				B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */, | ||||
| 				B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */, | ||||
| 				B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */, | ||||
| 				B887516325E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift */, | ||||
| @@ -230,13 +272,14 @@ | ||||
| 				B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */, | ||||
| 				B887516625E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift */, | ||||
| 				B887516725E0102000DB86D6 /* AVFrameRateRange+includes.swift */, | ||||
| 				B887516825E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift */, | ||||
| 				B887516825E0102000DB86D6 /* AVCaptureOutput+mirror.swift */, | ||||
| 				B881D35D2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift */, | ||||
| 				B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */, | ||||
| 				B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */, | ||||
| 				B887516225E0102000DB86D6 /* Collection+safe.swift */, | ||||
| 				B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */, | ||||
| 				B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */, | ||||
| 				B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */, | ||||
| 			); | ||||
| 			path = Extensions; | ||||
| 			sourceTree = "<group>"; | ||||
| @@ -267,8 +310,6 @@ | ||||
| 				B887517E25E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift */, | ||||
| 				B887517F25E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift */, | ||||
| 				B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */, | ||||
| 				B864004F27849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift */, | ||||
| 				B87B11BE2A8E63B700732EBF /* PixelFormat.swift */, | ||||
| 				B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */, | ||||
| 			); | ||||
| 			path = Parsers; | ||||
| @@ -395,65 +436,76 @@ | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				B86DC974260E310600FB17B2 /* CameraView+AVAudioSession.swift in Sources */, | ||||
| 				B887518625E0102000DB86D6 /* CameraView+RecordVideo.swift in Sources */, | ||||
| 				B8A1AECA2AD8034E00169C0D /* RecordVideoOptions.swift in Sources */, | ||||
| 				B85882322AD966FC00317161 /* CameraDeviceFormat.swift in Sources */, | ||||
| 				B81BE1BF26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift in Sources */, | ||||
| 				B8A1AEC82AD8005400169C0D /* CameraSession+Configuration.swift in Sources */, | ||||
| 				B88685E92AD6A5D600E93869 /* CameraSession+Video.swift in Sources */, | ||||
| 				B8DB3BCA263DC4D8004C18D7 /* RecordingSession.swift in Sources */, | ||||
| 				B83D5EE729377117000AFD2F /* PreviewView.swift in Sources */, | ||||
| 				B887518925E0102000DB86D6 /* Collection+safe.swift in Sources */, | ||||
| 				B8A1AEC42AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift in Sources */, | ||||
| 				B887519125E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift in Sources */, | ||||
| 				B887519725E0102000DB86D6 /* CameraView+TakePhoto.swift in Sources */, | ||||
| 				B887519825E0102000DB86D6 /* EnumParserError.swift in Sources */, | ||||
| 				B887518C25E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift in Sources */, | ||||
| 				B887518D25E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift in Sources */, | ||||
| 				B887519625E0102000DB86D6 /* Promise.swift in Sources */, | ||||
| 				B88103DD2AD6F62C0087F063 /* CameraSession+Focus.swift in Sources */, | ||||
| 				B8DB3BC8263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift in Sources */, | ||||
| 				B88685E72AD698DF00E93869 /* CameraConfiguration.swift in Sources */, | ||||
| 				B887518725E0102000DB86D6 /* CameraViewManager.m in Sources */, | ||||
| 				B88751A925E0102000DB86D6 /* CameraView.swift in Sources */, | ||||
| 				B887519925E0102000DB86D6 /* AVCaptureVideoStabilizationMode+descriptor.swift in Sources */, | ||||
| 				B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */, | ||||
| 				B887519425E0102000DB86D6 /* MakeReactError.swift in Sources */, | ||||
| 				B887519525E0102000DB86D6 /* ReactLogger.swift in Sources */, | ||||
| 				B86400522784A23400E9D2CA /* CameraView+Orientation.swift in Sources */, | ||||
| 				B88685EB2AD6A5DE00E93869 /* CameraSession+Photo.swift in Sources */, | ||||
| 				B88751A725E0102000DB86D6 /* CameraView+Zoom.swift in Sources */, | ||||
| 				B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */, | ||||
| 				B8BD3BA2266E22D2006C80A2 /* Callback.swift in Sources */, | ||||
| 				B8FF60B12ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift in Sources */, | ||||
| 				B84760A62608EE7C004C3180 /* FrameHostObject.mm in Sources */, | ||||
| 				B864005027849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift in Sources */, | ||||
| 				B85882382AD96B4400317161 /* JSUnionValue.swift in Sources */, | ||||
| 				B887518E25E0102000DB86D6 /* AVFrameRateRange+includes.swift in Sources */, | ||||
| 				B88751A125E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift in Sources */, | ||||
| 				B882721026AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift in Sources */, | ||||
| 				B8E957D02A693AD2008F5480 /* CameraView+Torch.swift in Sources */, | ||||
| 				B881D3602ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift in Sources */, | ||||
| 				B86DC977260E315100FB17B2 /* CameraView+AVCaptureSession.swift in Sources */, | ||||
| 				B88103DB2AD6F0A00087F063 /* CameraSession+Audio.swift in Sources */, | ||||
| 				B887518A25E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift in Sources */, | ||||
| 				B85882362AD96AFF00317161 /* AutoFocusSystem.swift in Sources */, | ||||
| 				B88751A325E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */, | ||||
| 				B887519A25E0102000DB86D6 /* AVVideoCodecType+descriptor.swift in Sources */, | ||||
| 				B88751A825E0102000DB86D6 /* CameraError.swift in Sources */, | ||||
| 				B85F7AE92A77BB680089C539 /* FrameProcessorPlugin.m in Sources */, | ||||
| 				B88685E52AD68D9300E93869 /* CameraSession.swift in Sources */, | ||||
| 				B881D35E2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift in Sources */, | ||||
| 				B87B11BF2A8E63B700732EBF /* PixelFormat.swift in Sources */, | ||||
| 				B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */, | ||||
| 				B8FF60AC2ACC93EF009D612F /* CameraView+CodeScanner.swift in Sources */, | ||||
| 				B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */, | ||||
| 				B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */, | ||||
| 				B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */, | ||||
| 				B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */, | ||||
| 				B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */, | ||||
| 				B8FF60AE2ACC9731009D612F /* CodeScanner.swift in Sources */, | ||||
| 				B8446E502ABA14C900E56077 /* CameraDevicesManager.m in Sources */, | ||||
| 				B887518F25E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift in Sources */, | ||||
| 				B887518F25E0102000DB86D6 /* AVCaptureOutput+mirror.swift in Sources */, | ||||
| 				B88751A425E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift in Sources */, | ||||
| 				B8DB3BCC263DC97E004C18D7 /* AVFileType+descriptor.swift in Sources */, | ||||
| 				B85882342AD969E000317161 /* VideoStabilizationMode.swift in Sources */, | ||||
| 				B88751A025E0102000DB86D6 /* AVAuthorizationStatus+descriptor.swift in Sources */, | ||||
| 				B88103E12AD7046E0087F063 /* Torch.swift in Sources */, | ||||
| 				B8446E4D2ABA147C00E56077 /* CameraDevicesManager.swift in Sources */, | ||||
| 				B8A1AECC2AD803B200169C0D /* Video.swift in Sources */, | ||||
| 				B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */, | ||||
| 				B887519C25E0102000DB86D6 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */, | ||||
| 				B88103DF2AD6FB230087F063 /* Orientation.swift in Sources */, | ||||
| 				B8F127D02ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift in Sources */, | ||||
| 				B8994E6C263F03E100069589 /* JSINSObjectConversion.mm in Sources */, | ||||
| 				B88751A525E0102000DB86D6 /* CameraView+Focus.swift in Sources */, | ||||
| 				B86DC971260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift in Sources */, | ||||
| 				B8A1AEC62AD7F08E00169C0D /* CameraView+Focus.swift in Sources */, | ||||
| 				B88B47472667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift in Sources */, | ||||
| 				B88103E32AD7065C0087F063 /* CameraSessionDelegate.swift in Sources */, | ||||
| 				B887519E25E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user