| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  | // | 
					
						
							|  |  |  |  | //  RecordingSession.swift | 
					
						
							|  |  |  |  | //  VisionCamera | 
					
						
							|  |  |  |  | // | 
					
						
							|  |  |  |  | //  Created by Marc Rousavy on 01.05.21. | 
					
						
							| 
									
										
										
										
											2021-06-01 13:07:57 +02:00
										 |  |  |  | //  Copyright © 2021 mrousavy. All rights reserved. | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  | // | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | import AVFoundation | 
					
						
							|  |  |  |  | import Foundation | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | // MARK: - BufferType | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-07 13:08:40 +02:00
										 |  |  |  | enum BufferType { | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |   case audio | 
					
						
							|  |  |  |  |   case video | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | // MARK: - RecordingSession | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  | /**
 | 
					
						
							|  |  |  |  |  A [RecordingSession] class that can record video and audio [CMSampleBuffers] from [AVCaptureVideoDataOutput] and | 
					
						
							|  |  |  |  |  [AVCaptureAudioDataOutput] into a .mov/.mp4 file using [AVAssetWriter]. | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |  It also synchronizes buffers to the CMTime by the CaptureSession so that late frames are removed from the beginning and added | 
					
						
							|  |  |  |  |  towards the end (useful e.g. for videoStabilization). | 
					
						
							|  |  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  | class RecordingSession { | 
					
						
							|  |  |  |  |   private let assetWriter: AVAssetWriter | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |   private var audioWriter: AVAssetWriterInput? | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |   private var videoWriter: AVAssetWriterInput? | 
					
						
							| 
									
										
										
										
											2024-07-12 16:51:09 +01:00
										 |  |  |  |   private let recorder: ChunkedRecorder | 
					
						
							| 
									
										
										
										
											2022-04-15 09:48:32 +02:00
										 |  |  |  |   private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |   private var startTimestamp: CMTime? | 
					
						
							|  |  |  |  |   private var stopTimestamp: CMTime? | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   private var lastWrittenTimestamp: CMTime? | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-23 14:27:25 +02:00
										 |  |  |  |   private var isFinishing = false | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |   private var hasWrittenLastVideoFrame = false | 
					
						
							|  |  |  |  |   private var hasWrittenLastAudioFrame = false | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   private let lock = DispatchSemaphore(value: 1) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // If we are waiting for late frames and none actually arrive, we force stop the session after the given timeout. | 
					
						
							|  |  |  |  |   private let automaticallyStopTimeoutSeconds = 4.0 | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-18 18:04:58 +02:00
										 |  |  |  |   /**
 | 
					
						
							|  |  |  |  |    Gets the file URL of the recorded video. | 
					
						
							|  |  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |  |   var outputDiretory: URL { | 
					
						
							| 
									
										
										
										
											2024-07-12 16:51:09 +01:00
										 |  |  |  |     return recorder.outputURL | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-12 16:43:57 +01:00
										 |  |  |  |   /**
 | 
					
						
							|  |  |  |  |    Gets the size of the recorded video, in pixels. | 
					
						
							|  |  |  |  |    */ | 
					
						
							|  |  |  |  |   var size: CGSize? { | 
					
						
							|  |  |  |  |     return videoWriter?.naturalSize | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-18 18:04:58 +02:00
										 |  |  |  |   /**
 | 
					
						
							|  |  |  |  |    Get the duration (in seconds) of the recorded video. | 
					
						
							|  |  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |   var duration: Double { | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     guard let lastWrittenTimestamp = lastWrittenTimestamp, | 
					
						
							|  |  |  |  |           let startTimestamp = startTimestamp else { | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |       return 0.0 | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     return (lastWrittenTimestamp - startTimestamp).seconds | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |  |   init(outputDiretory: String, | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |        fileType: AVFileType, | 
					
						
							| 
									
										
										
										
											2024-07-15 09:50:39 +01:00
										 |  |  |  |        onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void), | 
					
						
							| 
									
										
										
										
											2022-04-15 09:48:32 +02:00
										 |  |  |  |        completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws { | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |     completionHandler = completion | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |     do { | 
					
						
							| 
									
										
										
										
											2024-07-16 09:50:21 +01:00
										 |  |  |  |       let outputURL = URL(fileURLWithPath: outputDiretory) | 
					
						
							|  |  |  |  |       recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady) | 
					
						
							| 
									
										
										
										
											2024-07-12 16:51:09 +01:00
										 |  |  |  |       assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!) | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |       assetWriter.shouldOptimizeForNetworkUse = false | 
					
						
							| 
									
										
										
										
											2024-07-12 16:51:09 +01:00
										 |  |  |  |       assetWriter.outputFileTypeProfile = .mpeg4AppleHLS | 
					
						
							|  |  |  |  |       assetWriter.preferredOutputSegmentInterval = CMTime(seconds: 6, preferredTimescale: 1) | 
					
						
							|  |  |  |  |        | 
					
						
							|  |  |  |  |       /*
 | 
					
						
							|  |  |  |  |         Apple HLS fMP4 does not have an Edit List Box ('elst') in an initialization segment to remove | 
					
						
							|  |  |  |  |         audio priming duration which advanced audio formats like AAC have, since the sample tables | 
					
						
							|  |  |  |  |         are empty.  As a result, if the output PTS of the first non-fully trimmed audio sample buffer is | 
					
						
							|  |  |  |  |         kCMTimeZero, the audio samples’ presentation time in segment files may be pushed forward by the | 
					
						
							|  |  |  |  |         audio priming duration.  This may cause audio and video to be out of sync.  You should add a time | 
					
						
							|  |  |  |  |         offset to all samples to avoid this situation. | 
					
						
							|  |  |  |  |       */ | 
					
						
							|  |  |  |  |       let startTimeOffset = CMTime(value: 10, timescale: 1) | 
					
						
							|  |  |  |  |       assetWriter.initialSegmentStartTime = startTimeOffset | 
					
						
							|  |  |  |  |        | 
					
						
							|  |  |  |  |       assetWriter.delegate = recorder | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |     } catch let error as NSError { | 
					
						
							|  |  |  |  |       throw CameraError.capture(.createRecorderError(message: error.description)) | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   deinit { | 
					
						
							|  |  |  |  |     if assetWriter.status == .writing { | 
					
						
							| 
									
										
										
										
											2021-09-06 16:27:16 +02:00
										 |  |  |  |       ReactLogger.log(level: .info, message: "Cancelling AssetWriter...") | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |       assetWriter.cancelWriting() | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |   /**
 | 
					
						
							|  |  |  |  |    Initializes an AssetWriter for video frames (CMSampleBuffers). | 
					
						
							|  |  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |   func initializeVideoWriter(withSettings settings: [String: Any]) { | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |     guard !settings.isEmpty else { | 
					
						
							| 
									
										
										
										
											2021-09-06 16:27:16 +02:00
										 |  |  |  |       ReactLogger.log(level: .error, message: "Tried to initialize Video Writer with empty settings!") | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |     guard videoWriter == nil else { | 
					
						
							| 
									
										
										
										
											2021-09-06 16:27:16 +02:00
										 |  |  |  |       ReactLogger.log(level: .error, message: "Tried to add Video Writer twice!") | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |     ReactLogger.log(level: .info, message: "Initializing Video AssetWriter with settings: \(settings.description)") | 
					
						
							|  |  |  |  |     videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: settings) | 
					
						
							|  |  |  |  |     videoWriter!.expectsMediaDataInRealTime = true | 
					
						
							|  |  |  |  |     assetWriter.add(videoWriter!) | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |     ReactLogger.log(level: .info, message: "Initialized Video AssetWriter.") | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |   /**
 | 
					
						
							|  |  |  |  |    Initializes an AssetWriter for audio frames (CMSampleBuffers). | 
					
						
							|  |  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |   func initializeAudioWriter(withSettings settings: [String: Any]?, format: CMFormatDescription) { | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |     guard audioWriter == nil else { | 
					
						
							| 
									
										
										
										
											2021-09-06 16:27:16 +02:00
										 |  |  |  |       ReactLogger.log(level: .error, message: "Tried to add Audio Writer twice!") | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |     if let settings = settings { | 
					
						
							|  |  |  |  |       ReactLogger.log(level: .info, message: "Initializing Audio AssetWriter with settings: \(settings.description)") | 
					
						
							|  |  |  |  |     } else { | 
					
						
							|  |  |  |  |       ReactLogger.log(level: .info, message: "Initializing Audio AssetWriter default settings...") | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     audioWriter = AVAssetWriterInput(mediaType: .audio, outputSettings: settings, sourceFormatHint: format) | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |     audioWriter!.expectsMediaDataInRealTime = true | 
					
						
							|  |  |  |  |     assetWriter.add(audioWriter!) | 
					
						
							|  |  |  |  |     ReactLogger.log(level: .info, message: "Initialized Audio AssetWriter.") | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |   /**
 | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |    Start the RecordingSession using the current time of the provided synchronization clock. | 
					
						
							|  |  |  |  |    All buffers passed to [append] must be synchronized to this Clock. | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |   func start(clock: CMClock) throws { | 
					
						
							|  |  |  |  |     lock.wait() | 
					
						
							|  |  |  |  |     defer { | 
					
						
							|  |  |  |  |       lock.signal() | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |     ReactLogger.log(level: .info, message: "Starting Asset Writer(s)...") | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     let success = assetWriter.startWriting() | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     guard success else { | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |       ReactLogger.log(level: .error, message: "Failed to start Asset Writer(s)!") | 
					
						
							| 
									
										
										
										
											2023-11-22 17:53:10 +01:00
										 |  |  |  |       throw CameraError.capture(.createRecorderError(message: "Failed to start Asset Writer(s)!")) | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     ReactLogger.log(level: .info, message: "Asset Writer(s) started!") | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Get the current time of the AVCaptureSession. | 
					
						
							|  |  |  |  |     // Note: The current time might be more advanced than this buffer's timestamp, for example if the video | 
					
						
							|  |  |  |  |     // pipeline had some additional delay in processing the buffer (aka it is late) - eg because of Video Stabilization (~1s delay). | 
					
						
							|  |  |  |  |     let currentTime = CMClockGetTime(clock) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Start the sesssion at the given time. Frames with earlier timestamps (e.g. late frames) will be dropped. | 
					
						
							|  |  |  |  |     assetWriter.startSession(atSourceTime: currentTime) | 
					
						
							|  |  |  |  |     startTimestamp = currentTime | 
					
						
							|  |  |  |  |     ReactLogger.log(level: .info, message: "Started RecordingSession at time: \(currentTime.seconds)") | 
					
						
							| 
									
										
										
										
											2023-11-27 14:43:48 +01:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     if audioWriter == nil { | 
					
						
							| 
									
										
										
										
											2023-11-28 20:23:28 +01:00
										 |  |  |  |       // Audio was disabled, mark the Audio track as finished so we won't wait for it. | 
					
						
							| 
									
										
										
										
											2023-11-27 14:43:48 +01:00
										 |  |  |  |       hasWrittenLastAudioFrame = true | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |   /**
 | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |    Requests the RecordingSession to stop writing frames at the current time of the provided synchronization clock. | 
					
						
							|  |  |  |  |    The RecordingSession will continue to write video frames and audio frames for a little longer if there was a delay | 
					
						
							|  |  |  |  |    in the video pipeline (e.g. caused by video stabilization) to avoid the video cutting off late frames. | 
					
						
							|  |  |  |  |    Once all late frames have been captured (or an artificial abort timeout has been triggered), the [completionHandler] will be called. | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |   func stop(clock: CMClock) { | 
					
						
							|  |  |  |  |     lock.wait() | 
					
						
							|  |  |  |  |     defer { | 
					
						
							|  |  |  |  |       lock.signal() | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Current time of the synchronization clock (e.g. from [AVCaptureSession]) - this marks the end of the video. | 
					
						
							|  |  |  |  |     let currentTime = CMClockGetTime(clock) | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Request a stop at the given time. Frames with later timestamps (e.g. early frames, while we are waiting for late frames) will be dropped. | 
					
						
							|  |  |  |  |     stopTimestamp = currentTime | 
					
						
							|  |  |  |  |     ReactLogger.log(level: .info, | 
					
						
							|  |  |  |  |                     message: "Requesting stop at \(currentTime.seconds) seconds for AssetWriter with status \"\(assetWriter.status.descriptor)\"...") | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // Start a timeout that will force-stop the session if none of the late frames actually arrive | 
					
						
							|  |  |  |  |     CameraQueues.cameraQueue.asyncAfter(deadline: .now() + automaticallyStopTimeoutSeconds) { | 
					
						
							|  |  |  |  |       if !self.isFinishing { | 
					
						
							|  |  |  |  |         ReactLogger.log(level: .error, message: "Waited \(self.automaticallyStopTimeoutSeconds) seconds but no late Frames came in, aborting capture...") | 
					
						
							|  |  |  |  |         self.finish() | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   /**
 | 
					
						
							|  |  |  |  |    Appends a new CMSampleBuffer to the Asset Writer. | 
					
						
							|  |  |  |  |    - Use clock to specify the CMClock instance this CMSampleBuffer uses for relative time | 
					
						
							|  |  |  |  |    - Use bufferType to specify if this is a video or audio frame. | 
					
						
							|  |  |  |  |    */ | 
					
						
							|  |  |  |  |   func appendBuffer(_ buffer: CMSampleBuffer, clock _: CMClock, type bufferType: BufferType) { | 
					
						
							| 
									
										
										
										
											2023-11-28 12:10:21 +01:00
										 |  |  |  |     // 1. Prepare the data | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     guard !isFinishing else { | 
					
						
							|  |  |  |  |       // Session is already finishing, can't write anything more | 
					
						
							|  |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |     guard assetWriter.status == .writing else { | 
					
						
							|  |  |  |  |       ReactLogger.log(level: .error, message: "Frame arrived, but AssetWriter status is \(assetWriter.status.descriptor)!") | 
					
						
							|  |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |     if !CMSampleBufferDataIsReady(buffer) { | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |       ReactLogger.log(level: .error, message: "Frame arrived, but sample buffer is not ready!") | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-28 12:10:21 +01:00
										 |  |  |  |     // 2. Check the timing of the buffer and make sure it's not after we requested a session stop | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) | 
					
						
							|  |  |  |  |     if let stopTimestamp = stopTimestamp, | 
					
						
							|  |  |  |  |        timestamp >= stopTimestamp { | 
					
						
							|  |  |  |  |       // This Frame is exactly at, or after the point in time when RecordingSession.stop() has been called. | 
					
						
							|  |  |  |  |       // Consider this the last Frame we write | 
					
						
							|  |  |  |  |       switch bufferType { | 
					
						
							|  |  |  |  |       case .video: | 
					
						
							|  |  |  |  |         if hasWrittenLastVideoFrame { | 
					
						
							|  |  |  |  |           // already wrote last Video Frame before, so skip this one. | 
					
						
							|  |  |  |  |           return | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-11-28 12:10:21 +01:00
										 |  |  |  |         hasWrittenLastVideoFrame = true // flip to true, then fallthrough & write it | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |       case .audio: | 
					
						
							|  |  |  |  |         if hasWrittenLastAudioFrame { | 
					
						
							|  |  |  |  |           // already wrote last Audio Frame before, so skip this one. | 
					
						
							|  |  |  |  |           return | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-11-28 12:10:21 +01:00
										 |  |  |  |         hasWrittenLastAudioFrame = true // flip to true, then fallthrough & write it | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |       } | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     // 3. Actually write the Buffer to the AssetWriter | 
					
						
							|  |  |  |  |     let writer = getAssetWriter(forType: bufferType) | 
					
						
							|  |  |  |  |     guard writer.isReadyForMoreMediaData else { | 
					
						
							|  |  |  |  |       ReactLogger.log(level: .warning, message: "\(bufferType) AssetWriter is not ready for more data, dropping this Frame...") | 
					
						
							|  |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     writer.append(buffer) | 
					
						
							|  |  |  |  |     lastWrittenTimestamp = timestamp | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 4. If we failed to write the frames, stop the Recording | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |     if assetWriter.status == .failed { | 
					
						
							| 
									
										
										
										
											2021-06-03 14:16:02 +02:00
										 |  |  |  |       ReactLogger.log(level: .error, | 
					
						
							| 
									
										
										
										
											2021-09-06 16:27:16 +02:00
										 |  |  |  |                       message: "AssetWriter failed to write buffer! Error: \(assetWriter.error?.localizedDescription ?? "none")") | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |       finish() | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 5. If we finished writing both the last video and audio buffers, finish the recording | 
					
						
							|  |  |  |  |     if hasWrittenLastAudioFrame && hasWrittenLastVideoFrame { | 
					
						
							|  |  |  |  |       ReactLogger.log(level: .info, message: "Successfully appended last \(bufferType) Buffer (at \(timestamp.seconds) seconds), finishing RecordingSession...") | 
					
						
							|  |  |  |  |       finish() | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   private func getAssetWriter(forType type: BufferType) -> AVAssetWriterInput { | 
					
						
							|  |  |  |  |     switch type { | 
					
						
							|  |  |  |  |     case .video: | 
					
						
							|  |  |  |  |       guard let videoWriter = videoWriter else { | 
					
						
							|  |  |  |  |         fatalError("Tried to append to a Video Buffer, which was nil!") | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       return videoWriter | 
					
						
							|  |  |  |  |     case .audio: | 
					
						
							|  |  |  |  |       guard let audioWriter = audioWriter else { | 
					
						
							|  |  |  |  |         fatalError("Tried to append to a Audio Buffer, which was nil!") | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       return audioWriter | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |   /**
 | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |    Stops the AssetWriters and calls the completion callback. | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |   private func finish() { | 
					
						
							|  |  |  |  |     lock.wait() | 
					
						
							|  |  |  |  |     defer { | 
					
						
							|  |  |  |  |       lock.signal() | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     ReactLogger.log(level: .info, message: "Stopping AssetWriter with status \"\(assetWriter.status.descriptor)\"...") | 
					
						
							| 
									
										
										
										
											2021-06-09 14:56:56 +02:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     guard !isFinishing else { | 
					
						
							| 
									
										
										
										
											2022-05-23 14:27:25 +02:00
										 |  |  |  |       ReactLogger.log(level: .warning, message: "Tried calling finish() twice while AssetWriter is still writing!") | 
					
						
							|  |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |     guard assetWriter.status == .writing else { | 
					
						
							| 
									
										
										
										
											2022-04-15 09:48:32 +02:00
										 |  |  |  |       completionHandler(self, assetWriter.status, assetWriter.error) | 
					
						
							| 
									
										
										
										
											2023-11-23 18:17:15 +01:00
										 |  |  |  |       return | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     isFinishing = true | 
					
						
							|  |  |  |  |     videoWriter?.markAsFinished() | 
					
						
							|  |  |  |  |     audioWriter?.markAsFinished() | 
					
						
							|  |  |  |  |     assetWriter.finishWriting { | 
					
						
							|  |  |  |  |       self.completionHandler(self, self.assetWriter.status, self.assetWriter.error) | 
					
						
							| 
									
										
										
										
											2021-05-06 14:11:55 +02:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | } |