fix: Make recorder less error-prone (#189)
* Abort recording if failed to start or empty frames * Activate Audio Session on `cameraQueue` * Double-check stop recording in callback * Only call callback once * Format * Add description to `.aborted` error * Update RecordingSession.swift * Update AVAudioSession+updateCategory.swift * Rename serial dispatch queues
This commit is contained in:
		| @@ -16,6 +16,12 @@ enum BufferType { | ||||
|   case video | ||||
| } | ||||
|  | ||||
| // MARK: - RecordingSessionError | ||||
|  | ||||
| enum RecordingSessionError: Error { | ||||
|   case failedToStartSession | ||||
| } | ||||
|  | ||||
| // MARK: - RecordingSession | ||||
|  | ||||
| class RecordingSession { | ||||
| @@ -59,6 +65,9 @@ class RecordingSession { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Initializes an AssetWriter for video frames (CMSampleBuffers). | ||||
|    */ | ||||
|   func initializeVideoWriter(withSettings settings: [String: Any], isVideoMirrored: Bool) { | ||||
|     guard !settings.isEmpty else { | ||||
|       ReactLogger.log(level: .error, message: "Tried to initialize Video Writer with empty settings!", alsoLogToJS: true) | ||||
| @@ -83,6 +92,9 @@ class RecordingSession { | ||||
|     ReactLogger.log(level: .info, message: "Initialized Video AssetWriter.") | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Initializes an AssetWriter for audio frames (CMSampleBuffers). | ||||
|    */ | ||||
|   func initializeAudioWriter(withSettings settings: [String: Any]) { | ||||
|     guard !settings.isEmpty else { | ||||
|       ReactLogger.log(level: .error, message: "Tried to initialize Audio Writer with empty settings!", alsoLogToJS: true) | ||||
| @@ -99,15 +111,34 @@ class RecordingSession { | ||||
|     ReactLogger.log(level: .info, message: "Initialized Audio AssetWriter.") | ||||
|   } | ||||
|  | ||||
|   func start() { | ||||
|     assetWriter.startWriting() | ||||
|   /** | ||||
|    Start the Asset Writer(s). If the AssetWriter failed to start, an error will be thrown. | ||||
|    */ | ||||
|   func start() throws { | ||||
|     ReactLogger.log(level: .info, message: "Starting Asset Writer(s)...") | ||||
|  | ||||
|     let success = assetWriter.startWriting() | ||||
|     if !success { | ||||
|       ReactLogger.log(level: .error, message: "Failed to start Asset Writer(s)!") | ||||
|       throw RecordingSessionError.failedToStartSession | ||||
|     } | ||||
|  | ||||
|     initialTimestamp = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: 1_000_000_000) | ||||
|     assetWriter.startSession(atSourceTime: initialTimestamp!) | ||||
|     ReactLogger.log(level: .info, message: "Started RecordingSession at \(initialTimestamp!.seconds) seconds.") | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Appends a new CMSampleBuffer to the Asset Writer. Use bufferType to specify if this is a video or audio frame. | ||||
|    The timestamp parameter represents the presentation timestamp of the buffer, which should be synchronized across video and audio frames. | ||||
|    */ | ||||
|   func appendBuffer(_ buffer: CMSampleBuffer, type bufferType: BufferType, timestamp: CMTime) { | ||||
|     guard assetWriter.status == .writing else { | ||||
|       ReactLogger.log(level: .error, message: "Frame arrived, but AssetWriter status is \(assetWriter.status.descriptor)!") | ||||
|       return | ||||
|     } | ||||
|     if !CMSampleBufferDataIsReady(buffer) { | ||||
|       ReactLogger.log(level: .error, message: "Frame arrived, but sample buffer is not ready!") | ||||
|       return | ||||
|     } | ||||
|     guard let initialTimestamp = initialTimestamp else { | ||||
| @@ -138,7 +169,7 @@ class RecordingSession { | ||||
|       bufferAdaptor.append(imageBuffer, withPresentationTime: timestamp) | ||||
|       if !hasWrittenFirstVideoFrame { | ||||
|         hasWrittenFirstVideoFrame = true | ||||
|         ReactLogger.log(level: .warning, message: "VideoWriter: First frame arrived \((timestamp - initialTimestamp).seconds) seconds late.") | ||||
|         ReactLogger.log(level: .warning, message: "VideoWriter: First frame arrived \((initialTimestamp - timestamp).seconds) seconds late.") | ||||
|       } | ||||
|     case .audio: | ||||
|       guard let audioWriter = audioWriter else { | ||||
| @@ -156,16 +187,25 @@ class RecordingSession { | ||||
|     } | ||||
|  | ||||
|     if assetWriter.status == .failed { | ||||
|       // TODO: Should I call the completion handler or is this instance still valid? | ||||
|       ReactLogger.log(level: .error, | ||||
|                       message: "AssetWriter failed to write buffer! Error: \(assetWriter.error?.localizedDescription ?? "none")", | ||||
|                       alsoLogToJS: true) | ||||
|       finish() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    Marks the AssetWriters as finished and stops writing frames. The callback will be invoked either with an error or the status "success". | ||||
|    */ | ||||
|   func finish() { | ||||
|     ReactLogger.log(level: .info, message: "Finishing Recording with AssetWriter status \"\(assetWriter.status.descriptor)\"...") | ||||
|     if assetWriter.status == .writing { | ||||
|  | ||||
|     if !hasWrittenFirstVideoFrame { | ||||
|       let error = NSError(domain: "capture/aborted", | ||||
|                           code: 1, | ||||
|                           userInfo: [NSLocalizedDescriptionKey: "Stopped Recording Session too early, no frames have been recorded!"]) | ||||
|       completionHandler(.failed, error) | ||||
|     } else if assetWriter.status == .writing { | ||||
|       bufferAdaptor?.assetWriterInput.markAsFinished() | ||||
|       audioWriter?.markAsFinished() | ||||
|       assetWriter.finishWriting { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user