| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  | // | 
					
						
							|  |  |  | //  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? | 
					
						
							| 
									
										
										
										
											2023-12-13 16:38:02 +01:00
										 |  |  |   var currentConfigureCall: DispatchTime = .now() | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |   // 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. | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2024-01-08 11:41:57 +01:00
										 |  |  |   func configure(_ lambda: @escaping (_ configuration: CameraConfiguration) throws -> Void) { | 
					
						
							|  |  |  |     ReactLogger.log(level: .info, message: "configure { ... }: Waiting for lock...") | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 16:38:02 +01:00
										 |  |  |     // Set up Camera (Video) Capture Session (on camera queue, acts like a lock) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |     CameraQueues.cameraQueue.async { | 
					
						
							| 
									
										
										
										
											2024-01-08 11:41:57 +01:00
										 |  |  |       // Let caller configure a new configuration for the Camera. | 
					
						
							|  |  |  |       let config = CameraConfiguration(copyOf: self.configuration) | 
					
						
							|  |  |  |       do { | 
					
						
							|  |  |  |         try lambda(config) | 
					
						
							|  |  |  |       } catch { | 
					
						
							|  |  |  |         self.onConfigureError(error) | 
					
						
							| 
									
										
										
										
											2024-02-14 13:47:18 +01:00
										 |  |  |         return | 
					
						
							| 
									
										
										
										
											2023-12-13 16:38:02 +01:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2024-01-08 11:41:57 +01:00
										 |  |  |       let difference = CameraConfiguration.Difference(between: self.configuration, and: config) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       ReactLogger.log(level: .info, message: "configure { ... }: Updating CameraSession Configuration... \(difference)") | 
					
						
							| 
									
										
										
										
											2023-12-13 16:38:02 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |       do { | 
					
						
							|  |  |  |         // If needed, configure the AVCaptureSession (inputs, outputs) | 
					
						
							|  |  |  |         if difference.isSessionConfigurationDirty { | 
					
						
							| 
									
										
										
										
											2024-02-13 13:32:11 +01:00
										 |  |  |           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 Video Stabilization | 
					
						
							|  |  |  |           if difference.videoStabilizationChanged { | 
					
						
							|  |  |  |             self.configureVideoStabilization(configuration: config) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           // 4. Update output orientation | 
					
						
							|  |  |  |           if difference.orientationChanged { | 
					
						
							|  |  |  |             self.configureOrientation(configuration: config) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-13 13:32:11 +01:00
										 |  |  |         guard let device = self.videoDeviceInput?.device else { | 
					
						
							|  |  |  |           throw CameraError.device(.noDevice) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         // If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..) | 
					
						
							|  |  |  |         if difference.isDeviceConfigurationDirty { | 
					
						
							| 
									
										
										
										
											2024-02-13 13:32:11 +01:00
										 |  |  |           try device.lockForConfiguration() | 
					
						
							|  |  |  |           defer { | 
					
						
							|  |  |  |             device.unlockForConfiguration() | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // 4. Configure format | 
					
						
							|  |  |  |           if difference.formatChanged { | 
					
						
							|  |  |  |             try self.configureFormat(configuration: config, device: device) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           // 5. After step 2. and 4., we also need to configure the PixelFormat. | 
					
						
							|  |  |  |           //    This needs to be done AFTER we updated the `format`, as this controls the supported PixelFormats. | 
					
						
							|  |  |  |           if difference.outputsChanged || difference.formatChanged { | 
					
						
							|  |  |  |             try self.configurePixelFormat(configuration: config) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           // 6. Configure side-props (fps, lowLightBoost) | 
					
						
							|  |  |  |           if difference.sidePropsChanged { | 
					
						
							|  |  |  |             try self.configureSideProps(configuration: config, device: device) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           // 7. Configure zoom | 
					
						
							|  |  |  |           if difference.zoomChanged { | 
					
						
							|  |  |  |             self.configureZoom(configuration: config, device: device) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2024-02-13 13:32:11 +01:00
										 |  |  |           // 8. Configure exposure bias | 
					
						
							|  |  |  |           if difference.exposureChanged { | 
					
						
							|  |  |  |             self.configureExposure(configuration: config, device: device) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if difference.isSessionConfigurationDirty { | 
					
						
							|  |  |  |           // We commit the session config updates AFTER the device config, | 
					
						
							|  |  |  |           // that way we can also batch those changes into one update instead of doing two updates. | 
					
						
							|  |  |  |           self.captureSession.commitConfiguration() | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-19 15:26:43 +01:00
										 |  |  |         // 9. Start or stop the session if needed | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         self.checkIsActive(configuration: config) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-19 15:26:43 +01:00
										 |  |  |         // 10. Enable or disable the Torch if needed (requires session to be running) | 
					
						
							| 
									
										
										
										
											2023-10-18 18:04:58 +02:00
										 |  |  |         if difference.torchChanged { | 
					
						
							| 
									
										
										
										
											2024-02-13 13:32:11 +01:00
										 |  |  |           try device.lockForConfiguration() | 
					
						
							|  |  |  |           defer { | 
					
						
							|  |  |  |             device.unlockForConfiguration() | 
					
						
							| 
									
										
										
										
											2023-10-18 18:04:58 +02:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2024-02-13 13:32:11 +01:00
										 |  |  |           try self.configureTorch(configuration: config, device: device) | 
					
						
							| 
									
										
										
										
											2023-10-18 18:04:58 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-24 11:59:22 +02:00
										 |  |  |         // Notify about Camera initialization | 
					
						
							|  |  |  |         if difference.inputChanged { | 
					
						
							| 
									
										
										
										
											2023-10-24 13:39:25 +02:00
										 |  |  |           self.delegate?.onSessionInitialized() | 
					
						
							| 
									
										
										
										
											2023-10-24 11:59:22 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-12-13 16:38:02 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // After configuring, set this to the new configuration. | 
					
						
							|  |  |  |         self.configuration = config | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |       } catch { | 
					
						
							|  |  |  |         self.onConfigureError(error) | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 16:38:02 +01:00
										 |  |  |       // 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) | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    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() | 
					
						
							| 
									
										
										
										
											2023-12-09 21:09:55 +03:00
										 |  |  |       delegate?.onCameraStarted() | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |     } else { | 
					
						
							|  |  |  |       captureSession.stopRunning() | 
					
						
							| 
									
										
										
										
											2023-12-09 21:09:55 +03:00
										 |  |  |       delegate?.onCameraStopped() | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    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: | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |         // Write the Video Buffer to the .mov/.mp4 file, this is the first timestamp if nothing has been recorded yet | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |         recordingSession.appendBuffer(sampleBuffer, clock: captureSession.clock, type: .video) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |       case is AVCaptureAudioDataOutput: | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |         // Synchronize the Audio Buffer with the Video Session's time because it's two separate AVCaptureSessions | 
					
						
							|  |  |  |         audioCaptureSession.synchronizeBuffer(sampleBuffer, toSession: captureSession) | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |         recordingSession.appendBuffer(sampleBuffer, clock: audioCaptureSession.clock, type: .audio) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |       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() | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |