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 = [ |   s.source_files = [ | ||||||
|     # Core |     # Core | ||||||
|     "ios/*.{m,mm,swift}", |     "ios/*.{m,mm,swift}", | ||||||
|  |     "ios/Core/*.{m,mm,swift}", | ||||||
|     "ios/Extensions/*.{m,mm,swift}", |     "ios/Extensions/*.{m,mm,swift}", | ||||||
|     "ios/Parsers/*.{m,mm,swift}", |     "ios/Parsers/*.{m,mm,swift}", | ||||||
|     "ios/React Utils/*.{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.bridge.WritableMap | ||||||
| import com.facebook.react.uimanager.events.RCTEventEmitter | import com.facebook.react.uimanager.events.RCTEventEmitter | ||||||
| import com.google.mlkit.vision.barcode.common.Barcode | 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 | import com.mrousavy.camera.parsers.CodeType | ||||||
|  |  | ||||||
| fun CameraView.invokeOnInitialized() { | fun CameraView.invokeOnInitialized() { | ||||||
|   | |||||||
| @@ -5,7 +5,10 @@ import android.annotation.SuppressLint | |||||||
| import android.content.pm.PackageManager | import android.content.pm.PackageManager | ||||||
| import androidx.core.content.ContextCompat | import androidx.core.content.ContextCompat | ||||||
| import com.facebook.react.bridge.* | 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.RecordingSession | ||||||
|  | import com.mrousavy.camera.core.code | ||||||
| import com.mrousavy.camera.parsers.Torch | import com.mrousavy.camera.parsers.Torch | ||||||
| import com.mrousavy.camera.parsers.VideoCodec | import com.mrousavy.camera.parsers.VideoCodec | ||||||
| import com.mrousavy.camera.parsers.VideoFileType | import com.mrousavy.camera.parsers.VideoFileType | ||||||
|   | |||||||
| @@ -13,7 +13,10 @@ import android.view.Surface | |||||||
| import android.widget.FrameLayout | import android.widget.FrameLayout | ||||||
| import androidx.core.content.ContextCompat | import androidx.core.content.ContextCompat | ||||||
| import com.facebook.react.bridge.ReadableMap | 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.CameraSession | ||||||
|  | import com.mrousavy.camera.core.NoCameraDeviceError | ||||||
| import com.mrousavy.camera.core.PreviewView | import com.mrousavy.camera.core.PreviewView | ||||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | import com.mrousavy.camera.core.outputs.CameraOutputs | ||||||
| import com.mrousavy.camera.extensions.bigger | 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.PermissionAwareActivity | ||||||
| import com.facebook.react.modules.core.PermissionListener | import com.facebook.react.modules.core.PermissionListener | ||||||
| import com.facebook.react.uimanager.UIManagerHelper | 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.VisionCameraInstaller | ||||||
| import com.mrousavy.camera.frameprocessor.VisionCameraProxy | import com.mrousavy.camera.frameprocessor.VisionCameraProxy | ||||||
| import com.mrousavy.camera.parsers.* | 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.core.outputs.CameraOutputs | ||||||
| import com.mrousavy.camera.parsers.CameraDeviceError | 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.Handler | ||||||
| import android.os.HandlerThread | import android.os.HandlerThread | ||||||
| @@ -16,15 +16,7 @@ import android.os.Build | |||||||
| import android.util.Log | import android.util.Log | ||||||
| import android.util.Range | import android.util.Range | ||||||
| import android.util.Size | import android.util.Size | ||||||
| import com.mrousavy.camera.CameraNotReadyError |  | ||||||
| import com.mrousavy.camera.CameraQueues |  | ||||||
| import com.mrousavy.camera.CameraView | 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.core.outputs.CameraOutputs | ||||||
| import com.mrousavy.camera.extensions.capture | import com.mrousavy.camera.extensions.capture | ||||||
| import com.mrousavy.camera.extensions.createCaptureSession | 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.BarcodeScannerOptions | ||||||
| import com.google.mlkit.vision.barcode.BarcodeScanning | import com.google.mlkit.vision.barcode.BarcodeScanning | ||||||
| import com.google.mlkit.vision.common.InputImage | import com.google.mlkit.vision.common.InputImage | ||||||
| import com.mrousavy.camera.CameraQueues |  | ||||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | import com.mrousavy.camera.core.outputs.CameraOutputs | ||||||
| import com.mrousavy.camera.parsers.Orientation | import com.mrousavy.camera.parsers.Orientation | ||||||
| import java.io.Closeable | import java.io.Closeable | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import android.os.Build | |||||||
| import android.util.Log | import android.util.Log | ||||||
| import android.util.Size | import android.util.Size | ||||||
| import android.view.Surface | import android.view.Surface | ||||||
| import com.mrousavy.camera.RecorderError |  | ||||||
| import com.mrousavy.camera.parsers.Orientation | import com.mrousavy.camera.parsers.Orientation | ||||||
| import com.mrousavy.camera.parsers.VideoCodec | import com.mrousavy.camera.parsers.VideoCodec | ||||||
| import com.mrousavy.camera.parsers.VideoFileType | import com.mrousavy.camera.parsers.VideoFileType | ||||||
|   | |||||||
| @@ -9,8 +9,6 @@ import android.os.Build | |||||||
| import android.util.Log | import android.util.Log | ||||||
| import android.view.Surface | import android.view.Surface | ||||||
| import com.facebook.jni.HybridData | 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.Frame | ||||||
| import com.mrousavy.camera.frameprocessor.FrameProcessor | import com.mrousavy.camera.frameprocessor.FrameProcessor | ||||||
| import com.mrousavy.camera.parsers.Orientation | import com.mrousavy.camera.parsers.Orientation | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import android.util.Log | |||||||
| import android.util.Size | import android.util.Size | ||||||
| import android.view.Surface | import android.view.Surface | ||||||
| import com.google.mlkit.vision.barcode.common.Barcode | 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.CodeScannerPipeline | ||||||
| import com.mrousavy.camera.core.VideoPipeline | import com.mrousavy.camera.core.VideoPipeline | ||||||
| import com.mrousavy.camera.extensions.bigger | import com.mrousavy.camera.extensions.bigger | ||||||
|   | |||||||
| @@ -5,9 +5,9 @@ import android.hardware.camera2.CaptureFailure | |||||||
| import android.hardware.camera2.CaptureRequest | import android.hardware.camera2.CaptureRequest | ||||||
| import android.hardware.camera2.TotalCaptureResult | import android.hardware.camera2.TotalCaptureResult | ||||||
| import android.media.MediaActionSound | import android.media.MediaActionSound | ||||||
| import com.mrousavy.camera.CameraQueues | import com.mrousavy.camera.core.CameraQueues | ||||||
| import com.mrousavy.camera.CaptureAbortedError | import com.mrousavy.camera.core.CaptureAbortedError | ||||||
| import com.mrousavy.camera.UnknownCaptureError | import com.mrousavy.camera.core.UnknownCaptureError | ||||||
| import kotlin.coroutines.resume | import kotlin.coroutines.resume | ||||||
| import kotlin.coroutines.resumeWithException | import kotlin.coroutines.resumeWithException | ||||||
| import kotlin.coroutines.suspendCoroutine | import kotlin.coroutines.suspendCoroutine | ||||||
|   | |||||||
| @@ -8,8 +8,8 @@ import android.hardware.camera2.params.OutputConfiguration | |||||||
| import android.hardware.camera2.params.SessionConfiguration | import android.hardware.camera2.params.SessionConfiguration | ||||||
| import android.os.Build | import android.os.Build | ||||||
| import android.util.Log | import android.util.Log | ||||||
| import com.mrousavy.camera.CameraQueues | import com.mrousavy.camera.core.CameraQueues | ||||||
| import com.mrousavy.camera.CameraSessionCannotBeConfiguredError | import com.mrousavy.camera.core.CameraSessionCannotBeConfiguredError | ||||||
| import com.mrousavy.camera.core.outputs.CameraOutputs | import com.mrousavy.camera.core.outputs.CameraOutputs | ||||||
| import kotlin.coroutines.resume | import kotlin.coroutines.resume | ||||||
| import kotlin.coroutines.resumeWithException | import kotlin.coroutines.resumeWithException | ||||||
|   | |||||||
| @@ -5,9 +5,9 @@ import android.hardware.camera2.CameraDevice | |||||||
| import android.hardware.camera2.CameraManager | import android.hardware.camera2.CameraManager | ||||||
| import android.os.Build | import android.os.Build | ||||||
| import android.util.Log | import android.util.Log | ||||||
| import com.mrousavy.camera.CameraCannotBeOpenedError | import com.mrousavy.camera.core.CameraCannotBeOpenedError | ||||||
| import com.mrousavy.camera.CameraDisconnectedError | import com.mrousavy.camera.core.CameraDisconnectedError | ||||||
| import com.mrousavy.camera.CameraQueues | import com.mrousavy.camera.core.CameraQueues | ||||||
| import com.mrousavy.camera.parsers.CameraDeviceError | import com.mrousavy.camera.parsers.CameraDeviceError | ||||||
| import kotlin.coroutines.resume | import kotlin.coroutines.resume | ||||||
| import kotlin.coroutines.resumeWithException | import kotlin.coroutines.resumeWithException | ||||||
|   | |||||||
| @@ -4,12 +4,10 @@ import android.hardware.HardwareBuffer; | |||||||
| import android.media.Image; | import android.media.Image; | ||||||
| import android.os.Build; | import android.os.Build; | ||||||
| import com.facebook.proguard.annotations.DoNotStrip; | 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.PixelFormat; | ||||||
| import com.mrousavy.camera.parsers.Orientation; | import com.mrousavy.camera.parsers.Orientation; | ||||||
|  |  | ||||||
| import java.nio.ByteBuffer; |  | ||||||
|  |  | ||||||
| public class Frame { | public class Frame { | ||||||
|     private final Image image; |     private final Image image; | ||||||
|     private final boolean isMirrored; |     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.turbomodule.core.CallInvokerHolderImpl | ||||||
| import com.facebook.react.uimanager.UIManagerHelper | import com.facebook.react.uimanager.UIManagerHelper | ||||||
| import com.mrousavy.camera.CameraView | import com.mrousavy.camera.CameraView | ||||||
| import com.mrousavy.camera.ViewNotFoundError | import com.mrousavy.camera.core.ViewNotFoundError | ||||||
| import java.lang.ref.WeakReference | import java.lang.ref.WeakReference | ||||||
|  |  | ||||||
| @Suppress("KotlinJniMissingFunction") // we use fbjni. | @Suppress("KotlinJniMissingFunction") // we use fbjni. | ||||||
|   | |||||||
| @@ -2,9 +2,7 @@ package com.mrousavy.camera.frameprocessor; | |||||||
|  |  | ||||||
| import com.facebook.jni.HybridData; | import com.facebook.jni.HybridData; | ||||||
| import com.facebook.proguard.annotations.DoNotStrip; | import com.facebook.proguard.annotations.DoNotStrip; | ||||||
| import com.mrousavy.camera.CameraQueues; | import com.mrousavy.camera.core.CameraQueues; | ||||||
|  |  | ||||||
| import java.util.concurrent.ExecutorService; |  | ||||||
|  |  | ||||||
| @SuppressWarnings("JavaJniMissingFunction") // using fbjni here | @SuppressWarnings("JavaJniMissingFunction") // using fbjni here | ||||||
| public class VisionCameraScheduler { | public class VisionCameraScheduler { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| package com.mrousavy.camera.parsers | package com.mrousavy.camera.parsers | ||||||
|  |  | ||||||
| import com.facebook.react.bridge.ReadableMap | import com.facebook.react.bridge.ReadableMap | ||||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||||
|  |  | ||||||
| class CodeScanner(map: ReadableMap) { | class CodeScanner(map: ReadableMap) { | ||||||
|   val codeTypes: List<CodeType> |   val codeTypes: List<CodeType> | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| package com.mrousavy.camera.parsers | package com.mrousavy.camera.parsers | ||||||
|  |  | ||||||
| import com.google.mlkit.vision.barcode.common.Barcode | import com.google.mlkit.vision.barcode.common.Barcode | ||||||
| import com.mrousavy.camera.CodeTypeNotSupportedError | import com.mrousavy.camera.core.CodeTypeNotSupportedError | ||||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||||
|  |  | ||||||
| enum class CodeType(override val unionValue: String) : JSUnionValue { | enum class CodeType(override val unionValue: String) : JSUnionValue { | ||||||
|   CODE_128("code-128"), |   CODE_128("code-128"), | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| package com.mrousavy.camera.parsers | package com.mrousavy.camera.parsers | ||||||
|  |  | ||||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||||
|  |  | ||||||
| enum class Flash(override val unionValue: String) : JSUnionValue { | enum class Flash(override val unionValue: String) : JSUnionValue { | ||||||
|   OFF("off"), |   OFF("off"), | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| package com.mrousavy.camera.parsers | package com.mrousavy.camera.parsers | ||||||
|  |  | ||||||
| import android.graphics.ImageFormat | import android.graphics.ImageFormat | ||||||
| import com.mrousavy.camera.PixelFormatNotSupportedError | import com.mrousavy.camera.core.PixelFormatNotSupportedError | ||||||
|  |  | ||||||
| enum class PixelFormat(override val unionValue: String) : JSUnionValue { | enum class PixelFormat(override val unionValue: String) : JSUnionValue { | ||||||
|   YUV("yuv"), |   YUV("yuv"), | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| package com.mrousavy.camera.parsers | package com.mrousavy.camera.parsers | ||||||
|  |  | ||||||
| import com.mrousavy.camera.InvalidTypeScriptUnionError | import com.mrousavy.camera.core.InvalidTypeScriptUnionError | ||||||
|  |  | ||||||
| enum class VideoFileType(override val unionValue: String) : JSUnionValue { | enum class VideoFileType(override val unionValue: String) : JSUnionValue { | ||||||
|   MOV("mov"), |   MOV("mov"), | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| package com.mrousavy.camera.utils | package com.mrousavy.camera.utils | ||||||
|  |  | ||||||
| import com.facebook.react.bridge.Promise | import com.facebook.react.bridge.Promise | ||||||
| import com.mrousavy.camera.CameraError | import com.mrousavy.camera.core.CameraError | ||||||
| import com.mrousavy.camera.UnknownCameraError | import com.mrousavy.camera.core.UnknownCameraError | ||||||
|  |  | ||||||
| inline fun withPromise(promise: Promise, closure: () -> Any?) { | inline fun withPromise(promise: Promise, closure: () -> Any?) { | ||||||
|   try { |   try { | ||||||
|   | |||||||
| @@ -747,7 +747,7 @@ SPEC CHECKSUMS: | |||||||
|   SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d |   SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d | ||||||
|   SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d |   SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d | ||||||
|   SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 |   SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 | ||||||
|   VisionCamera: f649cd0c0fa6266f1cd5e0787a7c9583ca143b3a |   VisionCamera: f386aee60abb07d979c506ea9e6d4831e596cafe | ||||||
|   Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce |   Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce | ||||||
|  |  | ||||||
| PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb | PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb | ||||||
|   | |||||||
| @@ -47,7 +47,12 @@ export function CameraPage({ navigation }: Props): React.ReactElement { | |||||||
|  |  | ||||||
|   // camera device settings |   // camera device settings | ||||||
|   const [preferredDevice] = usePreferredCameraDevice() |   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) |   const [targetFps, setTargetFps] = useState(60) | ||||||
|  |  | ||||||
| @@ -172,7 +177,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { | |||||||
|               <ReanimatedCamera |               <ReanimatedCamera | ||||||
|                 ref={camera} |                 ref={camera} | ||||||
|                 style={StyleSheet.absoluteFill} |                 style={StyleSheet.absoluteFill} | ||||||
|                 device={preferredDevice ?? device} |                 device={device} | ||||||
|                 format={format} |                 format={format} | ||||||
|                 fps={fps} |                 fps={fps} | ||||||
|                 hdr={enableHdr} |                 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 | //  CameraView+Focus.swift | ||||||
| //  mrousavy | //  VisionCamera | ||||||
| // | // | ||||||
| //  Created by Marc Rousavy on 19.02.21. | //  Created by Marc Rousavy on 12.10.23. | ||||||
| //  Copyright © 2021 mrousavy. All rights reserved. | //  Copyright © 2023 mrousavy. All rights reserved. | ||||||
| // | // | ||||||
|  |  | ||||||
|  | import AVFoundation | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| extension CameraView { | extension CameraView { | ||||||
|   private func convertPreviewCoordinatesToCameraCoordinates(_ point: CGPoint) -> CGPoint { |  | ||||||
|     return previewView.captureDevicePointConverted(fromLayerPoint: point) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   func focus(point: CGPoint, promise: Promise) { |   func focus(point: CGPoint, promise: Promise) { | ||||||
|     withPromise(promise) { |     withPromise(promise) { | ||||||
|       guard let device = self.videoDeviceInput?.device else { |       try cameraSession.focus(point: point) | ||||||
|         throw CameraError.session(SessionError.cameraNotReady) |       return nil | ||||||
|       } |  | ||||||
|       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) |  | ||||||
|         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 | // MARK: - CameraView + AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate | ||||||
|  |  | ||||||
| extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { | extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { | ||||||
|   /** |   func startRecording(options: NSDictionary, callback jsCallback: @escaping RCTResponseSenderBlock) { | ||||||
|    Starts a video + audio recording with a custom Asset Writer. |     // Type-safety | ||||||
|    */ |     let callback = Callback(jsCallback) | ||||||
|   func startRecording(options: NSDictionary, callback jsCallbackFunc: @escaping RCTResponseSenderBlock) { |  | ||||||
|     CameraQueues.cameraQueue.async { |  | ||||||
|       ReactLogger.log(level: .info, message: "Starting Video recording...") |  | ||||||
|       let callback = Callback(jsCallbackFunc) |  | ||||||
|  |  | ||||||
|       var fileType = AVFileType.mov |     do { | ||||||
|       if let fileTypeOption = options["fileType"] as? String { |       let options = try RecordVideoOptions(fromJSValue: options) | ||||||
|         guard let parsed = try? AVFileType(withString: fileTypeOption) else { |  | ||||||
|           callback.reject(error: .parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption))) |       // Start Recording with success and error callbacks | ||||||
|           return |       cameraSession.startRecording( | ||||||
|  |         options: options, | ||||||
|  |         onVideoRecorded: { video in | ||||||
|  |           callback.resolve(video.toJSValue()) | ||||||
|  |         }, | ||||||
|  |         onError: { error in | ||||||
|  |           callback.reject(error: error) | ||||||
|         } |         } | ||||||
|         fileType = parsed |       ) | ||||||
|  |     } 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) | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       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 |  | ||||||
|  |  | ||||||
|       var videoCodec: AVVideoCodecType? |  | ||||||
|       if let codecString = options["videoCodec"] as? String { |  | ||||||
|         videoCodec = AVVideoCodecType(withString: codecString) |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // 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 |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // 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) { |   func stopRecording(promise: Promise) { | ||||||
|     CameraQueues.cameraQueue.async { |     cameraSession.stopRecording(promise: promise) | ||||||
|       self.isRecording = false |  | ||||||
|  |  | ||||||
|       withPromise(promise) { |  | ||||||
|         guard let recordingSession = self.recordingSession else { |  | ||||||
|           throw CameraError.capture(.noRecordingInProgress) |  | ||||||
|         } |  | ||||||
|         recordingSession.finish() |  | ||||||
|         return nil |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   func pauseRecording(promise: Promise) { |   func pauseRecording(promise: Promise) { | ||||||
|     CameraQueues.cameraQueue.async { |     cameraSession.pauseRecording(promise: promise) | ||||||
|       withPromise(promise) { |  | ||||||
|         guard self.recordingSession != nil else { |  | ||||||
|           // there's no active recording! |  | ||||||
|           throw CameraError.capture(.noRecordingInProgress) |  | ||||||
|         } |  | ||||||
|         self.isRecording = false |  | ||||||
|         return nil |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   func resumeRecording(promise: Promise) { |   func resumeRecording(promise: Promise) { | ||||||
|     CameraQueues.cameraQueue.async { |     cameraSession.resumeRecording(promise: promise) | ||||||
|       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 |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,83 +10,6 @@ import AVFoundation | |||||||
|  |  | ||||||
| extension CameraView { | extension CameraView { | ||||||
|   func takePhoto(options: NSDictionary, promise: Promise) { |   func takePhoto(options: NSDictionary, promise: Promise) { | ||||||
|     CameraQueues.cameraQueue.async { |     cameraSession.takePhoto(options: options, promise: promise) | ||||||
|       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) |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 Foundation | ||||||
|  | import UIKit | ||||||
|  |  | ||||||
| extension CameraView { | extension CameraView { | ||||||
|   var minAvailableZoom: CGFloat { |  | ||||||
|     return videoDeviceInput?.device.minAvailableVideoZoomFactor ?? 1 |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   var maxAvailableZoom: CGFloat { |  | ||||||
|     return videoDeviceInput?.device.activeFormat.videoMaxZoomFactor ?? 1 |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @objc |   @objc | ||||||
|   final func onPinch(_ gesture: UIPinchGestureRecognizer) { |   final func onPinch(_ gesture: UIPinchGestureRecognizer) { | ||||||
|     guard let device = videoDeviceInput?.device else { |     let scale = max(min(gesture.scale * pinchScaleOffset, cameraSession.maxZoom), CGFloat(1.0)) | ||||||
|       return |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let scale = max(min(gesture.scale * pinchScaleOffset, device.activeFormat.videoMaxZoomFactor), CGFloat(1.0)) |  | ||||||
|     if gesture.state == .ended { |     if gesture.state == .ended { | ||||||
|       pinchScaleOffset = scale |       pinchScaleOffset = scale | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     do { |     // Update zoom on Camera | ||||||
|       try device.lockForConfiguration() |     cameraSession.configure { configuration in | ||||||
|       device.videoZoomFactor = scale |       configuration.zoom = scale | ||||||
|       device.unlockForConfiguration() |  | ||||||
|     } catch { |  | ||||||
|       invokeOnError(.device(.configureError)) |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -50,24 +36,4 @@ extension CameraView { | |||||||
|       self.pinchGestureRecognizer = nil |       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 Foundation | ||||||
| import UIKit | import UIKit | ||||||
|  |  | ||||||
| // |  | ||||||
| // TODOs for the CameraView which are currently too hard to implement either because of AVFoundation's limitations, or my brain capacity | // TODOs for the CameraView which are currently too hard to implement either because of AVFoundation's limitations, or my brain capacity | ||||||
| // | // | ||||||
| // CameraView+RecordVideo | // CameraView+RecordVideo | ||||||
| // TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI) | // TODO: Better startRecording()/stopRecording() (promise + callback, wait for TurboModules/JSI) | ||||||
|  | // | ||||||
| // CameraView+TakePhoto | // CameraView+TakePhoto | ||||||
| // TODO: Photo HDR | // TODO: Photo HDR | ||||||
|  |  | ||||||
| private let propsThatRequireReconfiguration = ["cameraId", |  | ||||||
|                                                "enableDepthData", |  | ||||||
|                                                "enableHighQualityPhotos", |  | ||||||
|                                                "enablePortraitEffectsMatteDelivery", |  | ||||||
|                                                "photo", |  | ||||||
|                                                "video", |  | ||||||
|                                                "enableFrameProcessor", |  | ||||||
|                                                "hdr", |  | ||||||
|                                                "pixelFormat", |  | ||||||
|                                                "codeScannerOptions"] |  | ||||||
| private let propsThatRequireDeviceReconfiguration = ["fps", |  | ||||||
|                                                      "lowLightBoost"] |  | ||||||
|  |  | ||||||
| // MARK: - CameraView | // MARK: - CameraView | ||||||
|  |  | ||||||
| public final class CameraView: UIView { | public final class CameraView: UIView, CameraSessionDelegate { | ||||||
|   // pragma MARK: React Properties |   // pragma MARK: React Properties | ||||||
|   // props that require reconfiguring |   // props that require reconfiguring | ||||||
|   @objc var cameraId: NSString? |   @objc var cameraId: NSString? | ||||||
|   @objc var enableDepthData = false |   @objc var enableDepthData = false | ||||||
|   @objc var enableHighQualityPhotos: NSNumber? // nullable bool |   @objc var enableHighQualityPhotos = false | ||||||
|   @objc var enablePortraitEffectsMatteDelivery = false |   @objc var enablePortraitEffectsMatteDelivery = false | ||||||
|   @objc var enableBufferCompression = false |   @objc var enableBufferCompression = false | ||||||
|   // use cases |   // use cases | ||||||
|   @objc var photo: NSNumber? // nullable bool |   @objc var photo = false | ||||||
|   @objc var video: NSNumber? // nullable bool |   @objc var video = false | ||||||
|   @objc var audio: NSNumber? // nullable bool |   @objc var audio = false | ||||||
|   @objc var enableFrameProcessor = false |   @objc var enableFrameProcessor = false | ||||||
|   @objc var codeScannerOptions: NSDictionary? |   @objc var codeScannerOptions: NSDictionary? | ||||||
|   @objc var pixelFormat: NSString? |   @objc var pixelFormat: NSString? | ||||||
|   // props that require format reconfiguring |   // props that require format reconfiguring | ||||||
|   @objc var format: NSDictionary? |   @objc var format: NSDictionary? | ||||||
|   @objc var fps: NSNumber? |   @objc var fps: NSNumber? | ||||||
|   @objc var hdr: NSNumber? // nullable bool |   @objc var hdr = false | ||||||
|   @objc var lowLightBoost: NSNumber? // nullable bool |   @objc var lowLightBoost = false | ||||||
|   @objc var orientation: NSString? |   @objc var orientation: NSString? | ||||||
|   // other props |   // other props | ||||||
|   @objc var isActive = false |   @objc var isActive = false | ||||||
| @@ -63,7 +49,8 @@ public final class CameraView: UIView { | |||||||
|   @objc var videoStabilizationMode: NSString? |   @objc var videoStabilizationMode: NSString? | ||||||
|   @objc var resizeMode: NSString = "cover" { |   @objc var resizeMode: NSString = "cover" { | ||||||
|     didSet { |     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 |   // pragma MARK: Internal Properties | ||||||
|  |   var cameraSession: CameraSession | ||||||
|   var isMounted = false |   var isMounted = false | ||||||
|   var isReady = 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 |   #if VISION_CAMERA_ENABLE_FRAME_PROCESSORS | ||||||
|     @objc public var frameProcessor: FrameProcessor? |     @objc public var frameProcessor: FrameProcessor? | ||||||
|   #endif |   #endif | ||||||
| @@ -110,30 +86,16 @@ public final class CameraView: UIView { | |||||||
|     var fpsGraph: RCTFPSGraph? |     var fpsGraph: RCTFPSGraph? | ||||||
|   #endif |   #endif | ||||||
|  |  | ||||||
|   /// Returns whether the AVCaptureSession is currently running (reflected by isActive) |  | ||||||
|   var isRunning: Bool { |  | ||||||
|     return captureSession.isRunning |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // pragma MARK: Setup |   // pragma MARK: Setup | ||||||
|  |  | ||||||
|   override public init(frame: CGRect) { |   override public init(frame: CGRect) { | ||||||
|     previewView = PreviewView(frame: frame, session: captureSession) |     // Create CameraSession | ||||||
|  |     cameraSession = CameraSession() | ||||||
|  |     previewView = cameraSession.createPreviewView(frame: frame) | ||||||
|     super.init(frame: frame) |     super.init(frame: frame) | ||||||
|  |     cameraSession.delegate = self | ||||||
|  |  | ||||||
|     addSubview(previewView) |     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) |   @available(*, unavailable) | ||||||
| @@ -141,18 +103,6 @@ public final class CameraView: UIView { | |||||||
|     fatalError("init(coder:) is not implemented.") |     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?) { |   override public func willMove(toSuperview newSuperview: UIView?) { | ||||||
|     super.willMove(toSuperview: newSuperview) |     super.willMove(toSuperview: newSuperview) | ||||||
|  |  | ||||||
| @@ -169,89 +119,111 @@ public final class CameraView: UIView { | |||||||
|     previewView.bounds = bounds |     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 |   // pragma MARK: Props updating | ||||||
|   override public final func didSetProps(_ changedProps: [String]!) { |   override public final func didSetProps(_ changedProps: [String]!) { | ||||||
|     ReactLogger.log(level: .info, message: "Updating \(changedProps.count) prop(s)...") |     ReactLogger.log(level: .info, message: "Updating \(changedProps.count) props: [\(changedProps.joined(separator: ", "))]") | ||||||
|     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") |  | ||||||
|  |  | ||||||
|     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 |       // Photo | ||||||
|     let shouldUpdateTorch = willReconfigure || changedProps.contains("torch") || shouldCheckActive |       if photo { | ||||||
|     let shouldUpdateZoom = willReconfigure || changedProps.contains("zoom") || shouldCheckActive |         config.photo = .enabled(config: CameraConfiguration.Photo(enableHighQualityPhotos: enableHighQualityPhotos, | ||||||
|     let shouldUpdateVideoStabilization = willReconfigure || changedProps.contains("videoStabilizationMode") |                                                                   enableDepthData: enableDepthData, | ||||||
|     let shouldUpdateOrientation = willReconfigure || changedProps.contains("orientation") |                                                                   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") { |     if changedProps.contains("enableFpsGraph") { | ||||||
|       DispatchQueue.main.async { |       DispatchQueue.main.async { | ||||||
|         self.setupFpsGraph() |         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() { |   func setupFpsGraph() { | ||||||
| @@ -269,12 +241,16 @@ public final class CameraView: UIView { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // pragma MARK: Event Invokers |   // 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)") |     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]? |     var causeDictionary: [String: Any]? | ||||||
|     if let cause = cause { |     if case let .unknown(_, cause) = error, | ||||||
|  |        let cause = cause { | ||||||
|       causeDictionary = [ |       causeDictionary = [ | ||||||
|         "code": cause.code, |         "code": cause.code, | ||||||
|         "domain": cause.domain, |         "domain": cause.domain, | ||||||
| @@ -289,9 +265,58 @@ public final class CameraView: UIView { | |||||||
|     ]) |     ]) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final func invokeOnInitialized() { |   func onSessionInitialized() { | ||||||
|     ReactLogger.log(level: .info, message: "Camera initialized!") |     ReactLogger.log(level: .info, message: "Camera initialized!") | ||||||
|     guard let onInitialized = onInitialized else { return } |     guard let onInitialized = onInitialized else { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|     onInitialized([String: Any]()) |     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(isActive, BOOL); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(cameraId, NSString); | RCT_EXPORT_VIEW_PROPERTY(cameraId, NSString); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(enableDepthData, BOOL); | 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(enablePortraitEffectsMatteDelivery, BOOL); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(enableBufferCompression, BOOL); | RCT_EXPORT_VIEW_PROPERTY(enableBufferCompression, BOOL); | ||||||
| // use cases | // use cases | ||||||
| RCT_EXPORT_VIEW_PROPERTY(photo, NSNumber); // nullable bool | RCT_EXPORT_VIEW_PROPERTY(photo, BOOL); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(video, NSNumber); // nullable bool | RCT_EXPORT_VIEW_PROPERTY(video, BOOL); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(audio, NSNumber); // nullable bool | RCT_EXPORT_VIEW_PROPERTY(audio, BOOL); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(enableFrameProcessor, BOOL); | RCT_EXPORT_VIEW_PROPERTY(enableFrameProcessor, BOOL); | ||||||
| // device format | // device format | ||||||
| RCT_EXPORT_VIEW_PROPERTY(format, NSDictionary); | RCT_EXPORT_VIEW_PROPERTY(format, NSDictionary); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(fps, NSNumber); | RCT_EXPORT_VIEW_PROPERTY(fps, NSNumber); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(hdr, NSNumber);           // nullable bool | RCT_EXPORT_VIEW_PROPERTY(hdr, BOOL); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(lowLightBoost, NSNumber); // nullable bool | RCT_EXPORT_VIEW_PROPERTY(lowLightBoost, BOOL); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(videoStabilizationMode, NSString); | RCT_EXPORT_VIEW_PROPERTY(videoStabilizationMode, NSString); | ||||||
| RCT_EXPORT_VIEW_PROPERTY(pixelFormat, NSString); | RCT_EXPORT_VIEW_PROPERTY(pixelFormat, NSString); | ||||||
| // other props | // other props | ||||||
|   | |||||||
| @@ -38,6 +38,10 @@ final class CameraViewManager: RCTViewManager { | |||||||
|     #endif |     #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 |   @objc | ||||||
|   final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) { |   final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) { | ||||||
|     let component = getCameraView(withTag: node) |     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 recordingInProgress | ||||||
|   case noRecordingInProgress |   case noRecordingInProgress | ||||||
|   case fileError |   case fileError | ||||||
|   case createTempFileError |   case createTempFileError(message: String? = nil) | ||||||
|   case createRecorderError(message: String? = nil) |   case createRecorderError(message: String? = nil) | ||||||
|   case videoNotEnabled |   case videoNotEnabled | ||||||
|   case photoNotEnabled |   case photoNotEnabled | ||||||
| @@ -213,8 +213,8 @@ enum CaptureError { | |||||||
|       return "There was no active video recording in progress! Did you call stopRecording() twice?" |       return "There was no active video recording in progress! Did you call stopRecording() twice?" | ||||||
|     case .fileError: |     case .fileError: | ||||||
|       return "An unexpected File IO error occured!" |       return "An unexpected File IO error occured!" | ||||||
|     case .createTempFileError: |     case let .createTempFileError(message: message): | ||||||
|       return "Failed to create a temporary file!" |       return "Failed to create a temporary file! \(message ?? "(no additional message)")" | ||||||
|     case let .createRecorderError(message: message): |     case let .createRecorderError(message: message): | ||||||
|       return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")" |       return "Failed to create the AVAssetWriter (Recorder)! \(message ?? "(no additional message)")" | ||||||
|     case .videoNotEnabled: |     case .videoNotEnabled: | ||||||
| @@ -264,7 +264,7 @@ enum CameraError: Error { | |||||||
|   case session(_ id: SessionError) |   case session(_ id: SessionError) | ||||||
|   case capture(_ id: CaptureError) |   case capture(_ id: CaptureError) | ||||||
|   case codeScanner(_ id: CodeScannerError) |   case codeScanner(_ id: CodeScannerError) | ||||||
|   case unknown(message: String? = nil) |   case unknown(message: String? = nil, cause: NSError? = nil) | ||||||
| 
 | 
 | ||||||
|   var code: String { |   var code: String { | ||||||
|     switch self { |     switch self { | ||||||
| @@ -303,8 +303,17 @@ enum CameraError: Error { | |||||||
|       return id.message |       return id.message | ||||||
|     case let .codeScanner(id: id): |     case let .codeScanner(id: id): | ||||||
|       return id.message |       return id.message | ||||||
|     case let .unknown(message: message): |     case let .unknown(message: message, cause: cause): | ||||||
|       return message ?? "An unexpected error occured." |       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 | @objc | ||||||
| public class CameraQueues: NSObject { | 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", |   @objc public static let cameraQueue = DispatchQueue(label: "mrousavy/VisionCamera.main", | ||||||
|                                                       qos: .userInteractive, |                                                       qos: .userInteractive, | ||||||
|                                                       attributes: [], |                                                       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 | import AVFoundation | ||||||
| 
 | 
 | ||||||
|  | // Keeps a strong reference on delegates, as the AVCapturePhotoOutput only holds a weak reference. | ||||||
| private var delegatesReferences: [NSObject] = [] | private var delegatesReferences: [NSObject] = [] | ||||||
| 
 | 
 | ||||||
| // MARK: - PhotoCaptureDelegate | // MARK: - PhotoCaptureDelegate | ||||||
| @@ -42,7 +43,8 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate { | |||||||
|     let error = ErrorPointer(nilLiteral: ()) |     let error = ErrorPointer(nilLiteral: ()) | ||||||
|     guard let tempFilePath = RCTTempFilePath("jpeg", error) |     guard let tempFilePath = RCTTempFilePath("jpeg", error) | ||||||
|     else { |     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 |       return | ||||||
|     } |     } | ||||||
|     let url = URL(string: "file://\(tempFilePath)")! |     let url = URL(string: "file://\(tempFilePath)")! | ||||||
| @@ -93,11 +93,7 @@ class RecordingSession { | |||||||
|   /** |   /** | ||||||
|    Initializes an AssetWriter for audio frames (CMSampleBuffers). |    Initializes an AssetWriter for audio frames (CMSampleBuffers). | ||||||
|    */ |    */ | ||||||
|   func initializeAudioWriter(withSettings settings: [String: Any]) { |   func initializeAudioWriter(withSettings settings: [String: Any]?) { | ||||||
|     guard !settings.isEmpty else { |  | ||||||
|       ReactLogger.log(level: .error, message: "Tried to initialize Audio Writer with empty settings!") |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
|     guard audioWriter == nil else { |     guard audioWriter == nil else { | ||||||
|       ReactLogger.log(level: .error, message: "Tried to add Audio Writer twice!") |       ReactLogger.log(level: .error, message: "Tried to add Audio Writer twice!") | ||||||
|       return |       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 AVFoundation | ||||||
| import Foundation | import Foundation | ||||||
|  | import UIKit | ||||||
|  |  | ||||||
| extension AVCaptureConnection { | extension AVCaptureConnection { | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -10,6 +10,8 @@ import AVFoundation | |||||||
|  |  | ||||||
| extension AVCaptureDevice { | extension AVCaptureDevice { | ||||||
|   func toDictionary() -> [String: Any] { |   func toDictionary() -> [String: Any] { | ||||||
|  |     let formats = formats.map { CameraDeviceFormat(fromFormat: $0) } | ||||||
|  |  | ||||||
|     return [ |     return [ | ||||||
|       "id": uniqueID, |       "id": uniqueID, | ||||||
|       "physicalDevices": physicalDevices.map(\.deviceType.physicalDeviceDescriptor), |       "physicalDevices": physicalDevices.map(\.deviceType.physicalDeviceDescriptor), | ||||||
| @@ -25,10 +27,8 @@ extension AVCaptureDevice { | |||||||
|       "supportsLowLightBoost": isLowLightBoostSupported, |       "supportsLowLightBoost": isLowLightBoostSupported, | ||||||
|       "supportsFocus": isFocusPointOfInterestSupported, |       "supportsFocus": isFocusPointOfInterestSupported, | ||||||
|       "hardwareLevel": "full", |       "hardwareLevel": "full", | ||||||
|       "sensorOrientation": "portrait", // TODO: Sensor Orientation? |       "sensorOrientation": Orientation.landscapeLeft.jsValue, | ||||||
|       "formats": formats.map { format -> [String: Any] in |       "formats": formats.map { $0.toJSValue() }, | ||||||
|         format.toDictionary() |  | ||||||
|       }, |  | ||||||
|     ] |     ] | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,27 +8,20 @@ | |||||||
|  |  | ||||||
| import AVFoundation | 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 { | extension AVCaptureDevice.Format { | ||||||
|   var videoStabilizationModes: [AVCaptureVideoStabilizationMode] { |   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 |     let maxRange = videoSupportedFrameRateRanges.max { l, r in | ||||||
|       return l.maxFrameRate < r.maxFrameRate |       return l.maxFrameRate < r.maxFrameRate | ||||||
|     } |     } | ||||||
|     return maxRange?.maxFrameRate ?? 0 |     return maxRange?.maxFrameRate ?? 0 | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   var maxFrameRate: Float64 { |   var maxFps: Float64 { | ||||||
|     let maxRange = videoSupportedFrameRateRanges.max { l, r in |     let maxRange = videoSupportedFrameRateRanges.max { l, r in | ||||||
|       return l.maxFrameRate < r.maxFrameRate |       return l.maxFrameRate < r.maxFrameRate | ||||||
|     } |     } | ||||||
| @@ -45,52 +38,20 @@ extension AVCaptureDevice.Format { | |||||||
|     return hdrFormats.contains(pixelFormat) |     return hdrFormats.contains(pixelFormat) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   func toDictionary() -> [String: AnyHashable] { |   var supportsPhotoHDR: Bool { | ||||||
|     let availablePixelFormats = AVCaptureVideoDataOutput().availableVideoPixelFormatTypes |     // TODO: Supports Photo HDR on iOS? | ||||||
|     let pixelFormats = availablePixelFormats.map { format in PixelFormat(mediaSubType: format) } |     return false | ||||||
|  |  | ||||||
|     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, |  | ||||||
|     ] |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   var supportsDepthCapture: Bool { | ||||||
|    Compares this format to the given JS `CameraDeviceFormat`. |     return !supportedDepthDataFormats.isEmpty | ||||||
|    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 { |  | ||||||
|       return false |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     guard dict["videoWidth"] as? Int32 == videoDimensions.width && dict["videoHeight"] as? Int32 == videoDimensions.height else { |   private static func getAllVideoStabilizationModes() -> [AVCaptureVideoStabilizationMode] { | ||||||
|       return false |     var modes: [AVCaptureVideoStabilizationMode] = [.auto, .cinematic, .off, .standard] | ||||||
|  |     if #available(iOS 13, *) { | ||||||
|  |       modes.append(.cinematicExtended) | ||||||
|     } |     } | ||||||
|  |     return modes | ||||||
|     guard dict["minFps"] as? Float64 == minFrameRate && dict["maxFps"] as? Float64 == maxFrameRate else { |  | ||||||
|       return false |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     guard dict["supportsVideoHDR"] as? Bool == supportsVideoHDR else { |  | ||||||
|       return false |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return true |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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 AVFoundation | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| class CodeScanner { | struct CodeScanner: Equatable { | ||||||
|   let codeTypes: [AVMetadataObject.ObjectType] |   let codeTypes: [AVMetadataObject.ObjectType] | ||||||
|   let interval: Int |   let interval: Int | ||||||
|   let regionOfInterest: CGRect? |   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 AVFoundation | ||||||
| import Foundation | import Foundation | ||||||
| 
 | 
 | ||||||
| enum PixelFormat { | enum PixelFormat: String, JSUnionValue { | ||||||
|   case yuv |   case yuv | ||||||
|   case rgb |   case rgb | ||||||
|   case native |   case native | ||||||
|   case unknown |   case unknown | ||||||
| 
 | 
 | ||||||
|   var unionValue: String { |   init(jsValue: String) throws { | ||||||
|     switch self { |     if let parsed = PixelFormat(rawValue: jsValue) { | ||||||
|     case .yuv: |       self = parsed | ||||||
|       return "yuv" |     } else { | ||||||
|     case .rgb: |       throw CameraError.parameter(.invalid(unionName: "pixelFormat", receivedValue: jsValue)) | ||||||
|       return "rgb" |  | ||||||
|     case .native: |  | ||||||
|       return "native" |  | ||||||
|     case .unknown: |  | ||||||
|       return "unknown" |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   init(unionValue: String) throws { |   var jsValue: String { | ||||||
|     switch unionValue { |     return rawValue | ||||||
|     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)) |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   init(mediaSubType: OSType) { |   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. | //  Copyright © 2023 mrousavy. All rights reserved. | ||||||
| // | // | ||||||
|  |  | ||||||
|  | import AVFoundation | ||||||
| import Foundation | import Foundation | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  A ResizeMode used for the PreviewView. |  A ResizeMode used for the PreviewView. | ||||||
|  */ |  */ | ||||||
| enum ResizeMode { | enum ResizeMode: String, JSUnionValue { | ||||||
|   /** |   /** | ||||||
|    Keep aspect ratio, but fill entire parent view (centered). |    Keep aspect ratio, but fill entire parent view (centered). | ||||||
|    */ |    */ | ||||||
| @@ -21,15 +22,15 @@ enum ResizeMode { | |||||||
|    */ |    */ | ||||||
|   case contain |   case contain | ||||||
|  |  | ||||||
|   init(fromTypeScriptUnion union: String) { |   init(jsValue: String) throws { | ||||||
|     switch union { |     if let parsed = ResizeMode(rawValue: jsValue) { | ||||||
|     case "cover": |       self = parsed | ||||||
|       self = .cover |     } else { | ||||||
|     case "contain": |       throw CameraError.parameter(.invalid(unionName: "resizeMode", receivedValue: jsValue)) | ||||||
|       self = .contain |  | ||||||
|     default: |  | ||||||
|       // TODO: Use the onError event for safer error handling! |  | ||||||
|       fatalError("Invalid value passed for resizeMode! (\(union))") |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   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 */; }; | 		B8446E502ABA14C900E56077 /* CameraDevicesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */; }; | ||||||
| 		B84760A62608EE7C004C3180 /* FrameHostObject.mm in Sources */ = {isa = PBXBuildFile; fileRef = B84760A52608EE7C004C3180 /* FrameHostObject.mm */; }; | 		B84760A62608EE7C004C3180 /* FrameHostObject.mm in Sources */ = {isa = PBXBuildFile; fileRef = B84760A52608EE7C004C3180 /* FrameHostObject.mm */; }; | ||||||
| 		B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84760DE2608F57D004C3180 /* CameraQueues.swift */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */; }; | ||||||
| 		B887518625E0102000DB86D6 /* CameraView+RecordVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515D25E0102000DB86D6 /* CameraView+RecordVideo.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 */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		B887519425E0102000DB86D6 /* MakeReactError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516E25E0102000DB86D6 /* MakeReactError.swift */; }; | ||||||
| 		B887519525E0102000DB86D6 /* ReactLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516F25E0102000DB86D6 /* ReactLogger.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 */; }; | 		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 */; }; | 		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 */; }; | 		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 */; }; | 		B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518125E0102000DB86D6 /* CameraViewManager.swift */; }; | ||||||
| 		B88751A725E0102000DB86D6 /* CameraView+Zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518225E0102000DB86D6 /* CameraView+Zoom.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 */; }; | 		B88751A825E0102000DB86D6 /* CameraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518325E0102000DB86D6 /* CameraError.swift */; }; | ||||||
| 		B88751A925E0102000DB86D6 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887518425E0102000DB86D6 /* CameraView.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 */; }; | 		B88B47472667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */; }; | ||||||
| 		B8994E6C263F03E100069589 /* JSINSObjectConversion.mm in Sources */ = {isa = PBXBuildFile; fileRef = B8994E6B263F03E100069589 /* JSINSObjectConversion.mm */; }; | 		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 */; }; | 		B8BD3BA2266E22D2006C80A2 /* Callback.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BD3BA1266E22D2006C80A2 /* Callback.swift */; }; | ||||||
| 		B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.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 */; }; | 		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 */; }; | 		B8DB3BCA263DC4D8004C18D7 /* RecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */; }; | ||||||
| 		B8DB3BCC263DC97E004C18D7 /* AVFileType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.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 */; }; | 		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 */; }; | 		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 */; }; | 		B8FF60B12ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */; }; | ||||||
| /* End PBXBuildFile section */ | /* End PBXBuildFile section */ | ||||||
| @@ -98,16 +109,25 @@ | |||||||
| 		B84760A22608EE38004C3180 /* FrameHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameHostObject.h; sourceTree = "<group>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		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>"; }; | 		B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVMetadataObject.ObjectType+descriptor.swift"; sourceTree = "<group>"; }; | ||||||
| /* End PBXFileReference section */ | /* End PBXFileReference section */ | ||||||
| @@ -180,28 +202,19 @@ | |||||||
| 		58B511D21A9E6C8500147676 = { | 		58B511D21A9E6C8500147676 = { | ||||||
| 			isa = PBXGroup; | 			isa = PBXGroup; | ||||||
| 			children = ( | 			children = ( | ||||||
|  | 				B88685E32AD68D8A00E93869 /* Core */, | ||||||
| 				B80175EA2ABDEBBB00E7DE90 /* Types */, | 				B80175EA2ABDEBBB00E7DE90 /* Types */, | ||||||
| 				B8DCF2D725EA940700EA5C72 /* Frame Processor */, | 				B8DCF2D725EA940700EA5C72 /* Frame Processor */, | ||||||
| 				B887515E25E0102000DB86D6 /* CameraBridge.h */, | 				B887515E25E0102000DB86D6 /* CameraBridge.h */, | ||||||
| 				B84760DE2608F57D004C3180 /* CameraQueues.swift */, |  | ||||||
| 				B887518325E0102000DB86D6 /* CameraError.swift */, |  | ||||||
| 				B887518425E0102000DB86D6 /* CameraView.swift */, | 				B887518425E0102000DB86D6 /* CameraView.swift */, | ||||||
| 				B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */, |  | ||||||
| 				B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */, |  | ||||||
| 				B8E957CF2A693AD2008F5480 /* CameraView+Torch.swift */, |  | ||||||
| 				B887518025E0102000DB86D6 /* CameraView+Focus.swift */, |  | ||||||
| 				B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */, | 				B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */, | ||||||
| 				B887517125E0102000DB86D6 /* CameraView+TakePhoto.swift */, | 				B887517125E0102000DB86D6 /* CameraView+TakePhoto.swift */, | ||||||
| 				B8FF60AB2ACC93EF009D612F /* CameraView+CodeScanner.swift */, |  | ||||||
| 				B887518225E0102000DB86D6 /* CameraView+Zoom.swift */, | 				B887518225E0102000DB86D6 /* CameraView+Zoom.swift */, | ||||||
| 				B86400512784A23400E9D2CA /* CameraView+Orientation.swift */, | 				B8A1AEC52AD7F08E00169C0D /* CameraView+Focus.swift */, | ||||||
| 				B887515F25E0102000DB86D6 /* CameraViewManager.m */, | 				B887515F25E0102000DB86D6 /* CameraViewManager.m */, | ||||||
| 				B887518125E0102000DB86D6 /* CameraViewManager.swift */, | 				B887518125E0102000DB86D6 /* CameraViewManager.swift */, | ||||||
| 				B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */, | 				B8446E4F2ABA14C900E56077 /* CameraDevicesManager.m */, | ||||||
| 				B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */, | 				B8446E4C2ABA147C00E56077 /* CameraDevicesManager.swift */, | ||||||
| 				B8DB3BC9263DC4D8004C18D7 /* RecordingSession.swift */, |  | ||||||
| 				B83D5EE629377117000AFD2F /* PreviewView.swift */, |  | ||||||
| 				B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */, |  | ||||||
| 				B887516125E0102000DB86D6 /* Extensions */, | 				B887516125E0102000DB86D6 /* Extensions */, | ||||||
| 				B887517225E0102000DB86D6 /* Parsers */, | 				B887517225E0102000DB86D6 /* Parsers */, | ||||||
| 				B887516D25E0102000DB86D6 /* React Utils */, | 				B887516D25E0102000DB86D6 /* React Utils */, | ||||||
| @@ -214,15 +227,44 @@ | |||||||
| 			children = ( | 			children = ( | ||||||
| 				B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */, | 				B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */, | ||||||
| 				B8FF60AD2ACC9731009D612F /* CodeScanner.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; | 			path = Types; | ||||||
| 			sourceTree = "<group>"; | 			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 */ = { | 		B887516125E0102000DB86D6 /* Extensions */ = { | ||||||
| 			isa = PBXGroup; | 			isa = PBXGroup; | ||||||
| 			children = ( | 			children = ( | ||||||
| 				B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */, | 				B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */, | ||||||
| 				B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */, |  | ||||||
| 				B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */, | 				B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */, | ||||||
| 				B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */, | 				B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */, | ||||||
| 				B887516325E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift */, | 				B887516325E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift */, | ||||||
| @@ -230,13 +272,14 @@ | |||||||
| 				B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */, | 				B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */, | ||||||
| 				B887516625E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift */, | 				B887516625E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift */, | ||||||
| 				B887516725E0102000DB86D6 /* AVFrameRateRange+includes.swift */, | 				B887516725E0102000DB86D6 /* AVFrameRateRange+includes.swift */, | ||||||
| 				B887516825E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift */, | 				B887516825E0102000DB86D6 /* AVCaptureOutput+mirror.swift */, | ||||||
| 				B881D35D2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift */, | 				B881D35D2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift */, | ||||||
| 				B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */, | 				B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */, | ||||||
| 				B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */, | 				B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */, | ||||||
| 				B887516225E0102000DB86D6 /* Collection+safe.swift */, | 				B887516225E0102000DB86D6 /* Collection+safe.swift */, | ||||||
| 				B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */, | 				B881D35F2ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.swift */, | ||||||
| 				B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */, | 				B8F127CF2ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift */, | ||||||
|  | 				B8A1AEC32AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift */, | ||||||
| 			); | 			); | ||||||
| 			path = Extensions; | 			path = Extensions; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -267,8 +310,6 @@ | |||||||
| 				B887517E25E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift */, | 				B887517E25E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift */, | ||||||
| 				B887517F25E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift */, | 				B887517F25E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift */, | ||||||
| 				B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */, | 				B8DB3BCB263DC97E004C18D7 /* AVFileType+descriptor.swift */, | ||||||
| 				B864004F27849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift */, |  | ||||||
| 				B87B11BE2A8E63B700732EBF /* PixelFormat.swift */, |  | ||||||
| 				B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */, | 				B8FF60B02ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift */, | ||||||
| 			); | 			); | ||||||
| 			path = Parsers; | 			path = Parsers; | ||||||
| @@ -395,65 +436,76 @@ | |||||||
| 			isa = PBXSourcesBuildPhase; | 			isa = PBXSourcesBuildPhase; | ||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| 			files = ( | 			files = ( | ||||||
| 				B86DC974260E310600FB17B2 /* CameraView+AVAudioSession.swift in Sources */, |  | ||||||
| 				B887518625E0102000DB86D6 /* CameraView+RecordVideo.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 */, | 				B81BE1BF26B936FF002696CC /* AVCaptureDevice.Format+dimensions.swift in Sources */, | ||||||
|  | 				B8A1AEC82AD8005400169C0D /* CameraSession+Configuration.swift in Sources */, | ||||||
|  | 				B88685E92AD6A5D600E93869 /* CameraSession+Video.swift in Sources */, | ||||||
| 				B8DB3BCA263DC4D8004C18D7 /* RecordingSession.swift in Sources */, | 				B8DB3BCA263DC4D8004C18D7 /* RecordingSession.swift in Sources */, | ||||||
| 				B83D5EE729377117000AFD2F /* PreviewView.swift in Sources */, | 				B83D5EE729377117000AFD2F /* PreviewView.swift in Sources */, | ||||||
| 				B887518925E0102000DB86D6 /* Collection+safe.swift in Sources */, | 				B887518925E0102000DB86D6 /* Collection+safe.swift in Sources */, | ||||||
|  | 				B8A1AEC42AD7EDE800169C0D /* AVCaptureVideoDataOutput+pixelFormat.swift in Sources */, | ||||||
| 				B887519125E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift in Sources */, | 				B887519125E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift in Sources */, | ||||||
| 				B887519725E0102000DB86D6 /* CameraView+TakePhoto.swift in Sources */, | 				B887519725E0102000DB86D6 /* CameraView+TakePhoto.swift in Sources */, | ||||||
| 				B887519825E0102000DB86D6 /* EnumParserError.swift in Sources */, | 				B887519825E0102000DB86D6 /* EnumParserError.swift in Sources */, | ||||||
| 				B887518C25E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift in Sources */, | 				B887518C25E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift in Sources */, | ||||||
| 				B887518D25E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift in Sources */, | 				B887518D25E0102000DB86D6 /* AVCaptureDevice+physicalDevices.swift in Sources */, | ||||||
| 				B887519625E0102000DB86D6 /* Promise.swift in Sources */, | 				B887519625E0102000DB86D6 /* Promise.swift in Sources */, | ||||||
|  | 				B88103DD2AD6F62C0087F063 /* CameraSession+Focus.swift in Sources */, | ||||||
| 				B8DB3BC8263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift in Sources */, | 				B8DB3BC8263DC28C004C18D7 /* AVAssetWriter.Status+descriptor.swift in Sources */, | ||||||
|  | 				B88685E72AD698DF00E93869 /* CameraConfiguration.swift in Sources */, | ||||||
| 				B887518725E0102000DB86D6 /* CameraViewManager.m in Sources */, | 				B887518725E0102000DB86D6 /* CameraViewManager.m in Sources */, | ||||||
| 				B88751A925E0102000DB86D6 /* CameraView.swift in Sources */, | 				B88751A925E0102000DB86D6 /* CameraView.swift in Sources */, | ||||||
| 				B887519925E0102000DB86D6 /* AVCaptureVideoStabilizationMode+descriptor.swift in Sources */, | 				B887519925E0102000DB86D6 /* AVCaptureVideoStabilizationMode+descriptor.swift in Sources */, | ||||||
| 				B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */, | 				B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */, | ||||||
| 				B887519425E0102000DB86D6 /* MakeReactError.swift in Sources */, | 				B887519425E0102000DB86D6 /* MakeReactError.swift in Sources */, | ||||||
| 				B887519525E0102000DB86D6 /* ReactLogger.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 */, | 				B88751A725E0102000DB86D6 /* CameraView+Zoom.swift in Sources */, | ||||||
| 				B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */, | 				B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */, | ||||||
| 				B8BD3BA2266E22D2006C80A2 /* Callback.swift in Sources */, | 				B8BD3BA2266E22D2006C80A2 /* Callback.swift in Sources */, | ||||||
| 				B8FF60B12ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift in Sources */, | 				B8FF60B12ACC981B009D612F /* AVMetadataObject.ObjectType+descriptor.swift in Sources */, | ||||||
| 				B84760A62608EE7C004C3180 /* FrameHostObject.mm in Sources */, | 				B84760A62608EE7C004C3180 /* FrameHostObject.mm in Sources */, | ||||||
| 				B864005027849A2400E9D2CA /* UIInterfaceOrientation+descriptor.swift in Sources */, | 				B85882382AD96B4400317161 /* JSUnionValue.swift in Sources */, | ||||||
| 				B887518E25E0102000DB86D6 /* AVFrameRateRange+includes.swift in Sources */, | 				B887518E25E0102000DB86D6 /* AVFrameRateRange+includes.swift in Sources */, | ||||||
| 				B88751A125E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift in Sources */, | 				B88751A125E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift in Sources */, | ||||||
| 				B882721026AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift in Sources */, | 				B882721026AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift in Sources */, | ||||||
| 				B8E957D02A693AD2008F5480 /* CameraView+Torch.swift in Sources */, |  | ||||||
| 				B881D3602ABC8E4E009B21C8 /* AVCaptureVideoDataOutput+findPixelFormat.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 */, | 				B887518A25E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift in Sources */, | ||||||
|  | 				B85882362AD96AFF00317161 /* AutoFocusSystem.swift in Sources */, | ||||||
| 				B88751A325E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */, | 				B88751A325E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */, | ||||||
| 				B887519A25E0102000DB86D6 /* AVVideoCodecType+descriptor.swift in Sources */, | 				B887519A25E0102000DB86D6 /* AVVideoCodecType+descriptor.swift in Sources */, | ||||||
| 				B88751A825E0102000DB86D6 /* CameraError.swift in Sources */, | 				B88751A825E0102000DB86D6 /* CameraError.swift in Sources */, | ||||||
| 				B85F7AE92A77BB680089C539 /* FrameProcessorPlugin.m in Sources */, | 				B85F7AE92A77BB680089C539 /* FrameProcessorPlugin.m in Sources */, | ||||||
|  | 				B88685E52AD68D9300E93869 /* CameraSession.swift in Sources */, | ||||||
| 				B881D35E2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift in Sources */, | 				B881D35E2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift in Sources */, | ||||||
| 				B87B11BF2A8E63B700732EBF /* PixelFormat.swift in Sources */, | 				B87B11BF2A8E63B700732EBF /* PixelFormat.swift in Sources */, | ||||||
| 				B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */, | 				B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */, | ||||||
| 				B8FF60AC2ACC93EF009D612F /* CameraView+CodeScanner.swift in Sources */, |  | ||||||
| 				B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */, | 				B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */, | ||||||
| 				B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */, | 				B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */, | ||||||
|  | 				B88685ED2AD6A5E600E93869 /* CameraSession+CodeScanner.swift in Sources */, | ||||||
| 				B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */, | 				B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */, | ||||||
| 				B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */, | 				B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */, | ||||||
| 				B8FF60AE2ACC9731009D612F /* CodeScanner.swift in Sources */, | 				B8FF60AE2ACC9731009D612F /* CodeScanner.swift in Sources */, | ||||||
| 				B8446E502ABA14C900E56077 /* CameraDevicesManager.m 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 */, | 				B88751A425E0102000DB86D6 /* AVCaptureDevice.Format.AutoFocusSystem+descriptor.swift in Sources */, | ||||||
| 				B8DB3BCC263DC97E004C18D7 /* AVFileType+descriptor.swift in Sources */, | 				B8DB3BCC263DC97E004C18D7 /* AVFileType+descriptor.swift in Sources */, | ||||||
|  | 				B85882342AD969E000317161 /* VideoStabilizationMode.swift in Sources */, | ||||||
| 				B88751A025E0102000DB86D6 /* AVAuthorizationStatus+descriptor.swift in Sources */, | 				B88751A025E0102000DB86D6 /* AVAuthorizationStatus+descriptor.swift in Sources */, | ||||||
|  | 				B88103E12AD7046E0087F063 /* Torch.swift in Sources */, | ||||||
| 				B8446E4D2ABA147C00E56077 /* CameraDevicesManager.swift in Sources */, | 				B8446E4D2ABA147C00E56077 /* CameraDevicesManager.swift in Sources */, | ||||||
|  | 				B8A1AECC2AD803B200169C0D /* Video.swift in Sources */, | ||||||
| 				B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */, | 				B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */, | ||||||
| 				B887519C25E0102000DB86D6 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */, | 				B887519C25E0102000DB86D6 /* AVCaptureDevice.TorchMode+descriptor.swift in Sources */, | ||||||
|  | 				B88103DF2AD6FB230087F063 /* Orientation.swift in Sources */, | ||||||
| 				B8F127D02ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift in Sources */, | 				B8F127D02ACF054A00B39EA3 /* CMVideoDimensions+toCGSize.swift in Sources */, | ||||||
| 				B8994E6C263F03E100069589 /* JSINSObjectConversion.mm in Sources */, | 				B8994E6C263F03E100069589 /* JSINSObjectConversion.mm in Sources */, | ||||||
| 				B88751A525E0102000DB86D6 /* CameraView+Focus.swift in Sources */, | 				B8A1AEC62AD7F08E00169C0D /* CameraView+Focus.swift in Sources */, | ||||||
| 				B86DC971260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift in Sources */, |  | ||||||
| 				B88B47472667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift in Sources */, | 				B88B47472667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift in Sources */, | ||||||
|  | 				B88103E32AD7065C0087F063 /* CameraSessionDelegate.swift in Sources */, | ||||||
| 				B887519E25E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */, | 				B887519E25E0102000DB86D6 /* AVCapturePhotoOutput.QualityPrioritization+descriptor.swift in Sources */, | ||||||
| 			); | 			); | ||||||
| 			runOnlyForDeploymentPostprocessing = 0; | 			runOnlyForDeploymentPostprocessing = 0; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user