| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  | // | 
					
						
							|  |  |  | //  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, | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |                       filePath: String, | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |                       onVideoRecorded: @escaping (_ video: Video) -> Void, | 
					
						
							|  |  |  |                       onError: @escaping (_ error: CameraError) -> Void) { | 
					
						
							|  |  |  |     // Run on Camera Queue | 
					
						
							|  |  |  |     CameraQueues.cameraQueue.async { | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |       let start = DispatchTime.now() | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |       ReactLogger.log(level: .info, message: "Starting Video recording...") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // 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 | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 09:50:39 +01:00
										 |  |  |       // Callback for when new chunks are ready | 
					
						
							|  |  |  |       let onChunkReady: (ChunkedRecorder.Chunk) -> Void = { chunk in | 
					
						
							|  |  |  |         guard let delegate = self.delegate else { | 
					
						
							|  |  |  |           return | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         delegate.onVideoChunkReady(chunk: chunk) | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // 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() | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.isRecording = false | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |         self.recordingSession = nil | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         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)) | 
					
						
							| 
									
										
										
										
											2024-01-24 11:48:38 +01:00
										 |  |  |           } else if error.code == -11807 { | 
					
						
							|  |  |  |             onError(.capture(.insufficientStorage)) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |           } else { | 
					
						
							|  |  |  |             onError(.capture(.unknown(message: "An unknown recording error occured! \(error.code) \(error.description)"))) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           if status == .completed { | 
					
						
							|  |  |  |             // Recording was successfully saved | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |             let video = Video(path: recordingSession.outputDiretory.absoluteString, | 
					
						
							| 
									
										
										
										
											2023-12-12 16:43:57 +01:00
										 |  |  |                               duration: recordingSession.duration, | 
					
						
							|  |  |  |                               size: recordingSession.size ?? CGSize.zero) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |             onVideoRecorded(video) | 
					
						
							|  |  |  |           } else { | 
					
						
							|  |  |  |             // Recording wasn't saved and we don't have an error either. | 
					
						
							|  |  |  |             onError(.unknown(message: "AVAssetWriter completed with status: \(status.descriptor)")) | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |       if !FileManager.default.fileExists(atPath: filePath) { | 
					
						
							|  |  |  |         do { | 
					
						
							|  |  |  |           try FileManager.default.createDirectory(atPath: filePath, withIntermediateDirectories: true) | 
					
						
							|  |  |  |         } catch { | 
					
						
							|  |  |  |           onError(.capture(.createRecordingDirectoryError(message: error.localizedDescription))) | 
					
						
							|  |  |  |           return | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |       ReactLogger.log(level: .info, message: "Will record to temporary file: \(filePath)") | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |       do { | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |         // Create RecordingSession for the temp file | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |         let recordingSession = try RecordingSession(outputDiretory: filePath, | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |                                                     fileType: options.fileType, | 
					
						
							| 
									
										
										
										
											2024-07-15 09:50:39 +01:00
										 |  |  |                                                     onChunkReady: onChunkReady, | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |                                                     completion: onFinish) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Init Audio + Activate Audio Session (optional) | 
					
						
							|  |  |  |         if enableAudio, | 
					
						
							|  |  |  |            let audioOutput = self.audioOutput, | 
					
						
							|  |  |  |            let audioInput = self.audioDeviceInput { | 
					
						
							|  |  |  |           ReactLogger.log(level: .trace, message: "Enabling Audio for Recording...") | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |           // 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) | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |           recordingSession.initializeAudioWriter(withSettings: audioSettings, | 
					
						
							|  |  |  |                                                  format: audioInput.device.activeFormat.formatDescription) | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |         // Init Video | 
					
						
							|  |  |  |         let videoSettings = try videoOutput.recommendedVideoSettings(forOptions: options) | 
					
						
							|  |  |  |         recordingSession.initializeVideoWriter(withSettings: videoSettings) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // start recording session with or without audio. | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |         // Use Video [AVCaptureSession] clock as a timebase - all other sessions (here; audio) have to be synced to that Clock. | 
					
						
							|  |  |  |         try recordingSession.start(clock: self.captureSession.clock) | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |         self.recordingSession = recordingSession | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         self.isRecording = true | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         let end = DispatchTime.now() | 
					
						
							|  |  |  |         ReactLogger.log(level: .info, message: "RecordingSesssion started in \(Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000)ms!") | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |       } catch let error as NSError { | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |         if let error = error as? CameraError { | 
					
						
							|  |  |  |           onError(error) | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           onError(.capture(.createRecorderError(message: "RecordingSession failed with unknown error: \(error.description)"))) | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         return | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /**
 | 
					
						
							|  |  |  |    Stops an active recording. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   func stopRecording(promise: Promise) { | 
					
						
							|  |  |  |     CameraQueues.cameraQueue.async { | 
					
						
							|  |  |  |       withPromise(promise) { | 
					
						
							|  |  |  |         guard let recordingSession = self.recordingSession else { | 
					
						
							|  |  |  |           throw CameraError.capture(.noRecordingInProgress) | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |         // Use Video [AVCaptureSession] clock as a timebase - all other sessions (here; audio) have to be synced to that Clock. | 
					
						
							|  |  |  |         recordingSession.stop(clock: self.captureSession.clock) | 
					
						
							|  |  |  |         // There might be late frames, so maybe we need to still provide more Frames to the RecordingSession. Let's keep isRecording true for now. | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  |         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 | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |   func lockCurrentExposure(promise: Promise) { | 
					
						
							|  |  |  |     CameraQueues.cameraQueue.async { | 
					
						
							|  |  |  |       withPromise(promise) { | 
					
						
							|  |  |  |         guard let captureDevice = AVCaptureDevice.default(for: .video) else { | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |           print("No capture device available") | 
					
						
							|  |  |  |           return | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |         guard captureDevice.isExposureModeSupported(.custom) else { | 
					
						
							|  |  |  |           ReactLogger.log(level: .info, message: "Custom exposure mode not supported") | 
					
						
							|  |  |  |           return | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |         do { | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |           // Lock the device for configuration | 
					
						
							|  |  |  |           try captureDevice.lockForConfiguration() | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |           // Get the current exposure duration and ISO | 
					
						
							|  |  |  |           let currentExposureDuration = captureDevice.exposureDuration | 
					
						
							|  |  |  |           let currentISO = captureDevice.iso | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |           // Check if the device supports custom exposure settings | 
					
						
							|  |  |  |           if captureDevice.isExposureModeSupported(.custom) { | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |             // Lock the current exposure and ISO by setting custom exposure mode | 
					
						
							|  |  |  |             captureDevice.setExposureModeCustom(duration: currentExposureDuration, iso: currentISO, completionHandler: nil) | 
					
						
							|  |  |  |             ReactLogger.log(level: .info, message: "Exposure and ISO locked at current values") | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |           } else { | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |             ReactLogger.log(level: .info, message:"Custom exposure mode not supported") | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |           // Unlock the device after configuration | 
					
						
							|  |  |  |           captureDevice.unlockForConfiguration() | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |         } catch { | 
					
						
							|  |  |  |           ReactLogger.log(level: .warning, message:"Error locking exposure: \(error)") | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |         return nil | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |   func unlockCurrentExposure(promise: Promise) { | 
					
						
							|  |  |  |     CameraQueues.cameraQueue.async { | 
					
						
							|  |  |  |       withPromise(promise) { | 
					
						
							|  |  |  |         guard let captureDevice = AVCaptureDevice.default(for: .video) else { | 
					
						
							|  |  |  |           print("No capture device available") | 
					
						
							|  |  |  |           return | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |         do { | 
					
						
							|  |  |  |           if captureDevice.isExposureModeSupported(.autoExpose) { | 
					
						
							|  |  |  |             try captureDevice.lockForConfiguration() | 
					
						
							|  |  |  |             captureDevice.exposureMode = .continuousAutoExposure | 
					
						
							|  |  |  |             captureDevice.unlockForConfiguration() | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } catch { | 
					
						
							|  |  |  |           ReactLogger.log(level: .warning, message:"Error unlocking exposure: \(error)") | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-10-09 17:59:22 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 15:53:47 +02:00
										 |  |  |         return nil | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-09-27 10:35:29 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-10-13 18:33:20 +02:00
										 |  |  | } |