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:
parent
54871022f4
commit
cd0b413706
@ -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)
|
|
||||||
}
|
|
||||||
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
|
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
|
|
||||||
if let fileTypeOption = options["fileType"] as? String {
|
|
||||||
guard let parsed = try? AVFileType(withString: fileTypeOption) else {
|
|
||||||
callback.reject(error: .parameter(.invalid(unionName: "fileType", receivedValue: fileTypeOption)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileType = parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorPointer = ErrorPointer(nilLiteral: ())
|
|
||||||
let fileExtension = fileType.descriptor ?? "mov"
|
|
||||||
guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else {
|
|
||||||
callback.reject(error: .capture(.createTempFileError), cause: errorPointer?.pointee)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactLogger.log(level: .info, message: "File path: \(tempFilePath)")
|
|
||||||
let tempURL = URL(string: "file://\(tempFilePath)")!
|
|
||||||
|
|
||||||
if let flashMode = options["flash"] as? String {
|
|
||||||
// use the torch as the video's flash
|
|
||||||
self.setTorchMode(flashMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let videoOutput = self.videoOutput else {
|
|
||||||
if self.video?.boolValue == true {
|
|
||||||
callback.reject(error: .session(.cameraNotReady))
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
callback.reject(error: .capture(.videoNotEnabled))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guard let videoInput = self.videoDeviceInput else {
|
|
||||||
callback.reject(error: .session(.cameraNotReady))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: The startRecording() func cannot be async because RN doesn't allow
|
|
||||||
// both a callback and a Promise in a single function. Wait for TurboModules?
|
|
||||||
// This means that any errors that occur in this function have to be delegated through
|
|
||||||
// the callback, but I'd prefer for them to throw for the original function instead.
|
|
||||||
|
|
||||||
let enableAudio = self.audio?.boolValue == true
|
|
||||||
|
|
||||||
let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in
|
|
||||||
defer {
|
|
||||||
if enableAudio {
|
|
||||||
CameraQueues.audioQueue.async {
|
|
||||||
self.deactivateAudioSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options["flash"] != nil {
|
|
||||||
// Set torch mode back to what it was before if we used it for the video flash.
|
|
||||||
self.setTorchMode(self.torch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.recordingSession = nil
|
|
||||||
self.isRecording = false
|
|
||||||
ReactLogger.log(level: .info, message: "RecordingSession finished with status \(status.descriptor).")
|
|
||||||
|
|
||||||
if let error = error as NSError? {
|
|
||||||
if error.domain == "capture/aborted" {
|
|
||||||
callback.reject(error: .capture(.aborted), cause: error)
|
|
||||||
} else {
|
|
||||||
callback.reject(error: .capture(.unknown(message: "An unknown recording error occured! \(error.description)")), cause: error)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if status == .completed {
|
|
||||||
callback.resolve([
|
|
||||||
"path": recordingSession.url.absoluteString,
|
|
||||||
"duration": recordingSession.duration,
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
callback.reject(error: .unknown(message: "AVAssetWriter completed with status: \(status.descriptor)"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let recordingSession: RecordingSession
|
|
||||||
do {
|
do {
|
||||||
recordingSession = try RecordingSession(url: tempURL,
|
let options = try RecordVideoOptions(fromJSValue: options)
|
||||||
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?
|
// Start Recording with success and error callbacks
|
||||||
if let codecString = options["videoCodec"] as? String {
|
cameraSession.startRecording(
|
||||||
videoCodec = AVVideoCodecType(withString: codecString)
|
options: options,
|
||||||
|
onVideoRecorded: { video in
|
||||||
|
callback.resolve(video.toJSValue())
|
||||||
|
},
|
||||||
|
onError: { error in
|
||||||
|
callback.reject(error: error)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
// Init Video
|
} catch {
|
||||||
guard var videoSettings = self.recommendedVideoSettings(videoOutput: videoOutput, fileType: fileType, videoCodec: videoCodec),
|
// Some error occured while initializing VideoSettings
|
||||||
!videoSettings.isEmpty else {
|
if let error = error as? CameraError {
|
||||||
callback.reject(error: .capture(.createRecorderError(message: "Failed to get video settings!")))
|
callback.reject(error: error)
|
||||||
return
|
} else {
|
||||||
|
callback.reject(error: .capture(.unknown(message: error.localizedDescription)), cause: error as NSError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom Video Bit Rate (Mbps -> bps)
|
|
||||||
if let videoBitRate = options["videoBitRate"] as? NSNumber {
|
|
||||||
let bitsPerSecond = videoBitRate.doubleValue * 1_000_000
|
|
||||||
videoSettings[AVVideoCompressionPropertiesKey] = [
|
|
||||||
AVVideoAverageBitRateKey: NSNumber(value: bitsPerSecond),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// get pixel format (420f, 420v, x420)
|
|
||||||
let pixelFormat = CMFormatDescriptionGetMediaSubType(videoInput.device.activeFormat.formatDescription)
|
|
||||||
recordingSession.initializeVideoWriter(withSettings: videoSettings,
|
|
||||||
pixelFormat: pixelFormat)
|
|
||||||
|
|
||||||
// Init Audio (optional)
|
|
||||||
if enableAudio {
|
|
||||||
// Activate Audio Session asynchronously
|
|
||||||
CameraQueues.audioQueue.async {
|
|
||||||
self.activateAudioSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let audioOutput = self.audioOutput,
|
|
||||||
let audioSettings = audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: fileType) {
|
|
||||||
recordingSession.initializeAudioWriter(withSettings: audioSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start recording session with or without audio.
|
|
||||||
do {
|
|
||||||
try recordingSession.startAssetWriter()
|
|
||||||
} catch let error as NSError {
|
|
||||||
callback.reject(error: .capture(.createRecorderError(message: "RecordingSession failed to start asset writer.")), cause: error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.isRecording = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopRecording(promise: Promise) {
|
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 [
|
|
||||||
"videoStabilizationModes": videoStabilizationModes.map(\.descriptor),
|
|
||||||
"autoFocusSystem": autoFocusSystem.descriptor,
|
|
||||||
"photoHeight": photoDimensions.height,
|
|
||||||
"photoWidth": photoDimensions.width,
|
|
||||||
"videoHeight": videoDimensions.height,
|
|
||||||
"videoWidth": videoDimensions.width,
|
|
||||||
"maxISO": maxISO,
|
|
||||||
"minISO": minISO,
|
|
||||||
"fieldOfView": videoFieldOfView,
|
|
||||||
"maxZoom": videoMaxZoomFactor,
|
|
||||||
"supportsVideoHDR": supportsVideoHDR,
|
|
||||||
"supportsPhotoHDR": false,
|
|
||||||
"minFps": minFrameRate,
|
|
||||||
"maxFps": maxFrameRate,
|
|
||||||
"pixelFormats": pixelFormats.map(\.unionValue),
|
|
||||||
"supportsDepthCapture": !supportedDepthDataFormats.isEmpty,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Compares this format to the given JS `CameraDeviceFormat`.
|
|
||||||
Only the most important properties (such as dimensions and FPS) are taken into consideration,
|
|
||||||
so this is not an exact equals, but more like a "matches filter" comparison.
|
|
||||||
*/
|
|
||||||
func isEqualTo(jsFormat dict: NSDictionary) -> Bool {
|
|
||||||
guard dict["photoWidth"] as? Int32 == photoDimensions.width && dict["photoHeight"] as? Int32 == photoDimensions.height else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
guard dict["videoWidth"] as? Int32 == videoDimensions.width && dict["videoHeight"] as? Int32 == videoDimensions.height else {
|
var supportsDepthCapture: Bool {
|
||||||
return false
|
return !supportedDepthDataFormats.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
guard dict["minFps"] as? Float64 == minFrameRate && dict["maxFps"] as? Float64 == maxFrameRate 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["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;
|
||||||
|
Loading…
Reference in New Issue
Block a user