diff --git a/package/ios/Core/CameraSession+Video.swift b/package/ios/Core/CameraSession+Video.swift index 6c39787..825b432 100644 --- a/package/ios/Core/CameraSession+Video.swift +++ b/package/ios/Core/CameraSession+Video.swift @@ -38,11 +38,27 @@ extension CameraSession { // Callback for when new chunks are ready let onChunkReady: (ChunkedRecorder.Chunk) -> Void = { chunk in guard let delegate = self.delegate else { + ReactLogger.log(level: .warning, message: "Chunk ready but delegate is nil, dropping chunk: \(chunk)") return } delegate.onVideoChunkReady(chunk: chunk) } + // Callback for when a chunk write fails (e.g. init file write failure) + let onChunkError: (Error) -> Void = { error in + ReactLogger.log(level: .error, message: "Chunk write error, stopping recording: \(error.localizedDescription)") + // Stop recording immediately + if let session = self.recordingSession { + session.stop(clock: self.captureSession.clock) + } + // Surface error to RN + if let cameraError = error as? CameraError { + onError(cameraError) + } else { + onError(.capture(.fileError)) + } + } + // Callback for when the recording ends let onFinish = { (recordingSession: RecordingSession, status: AVAssetWriter.Status, error: Error?) in defer { @@ -98,6 +114,7 @@ extension CameraSession { let recordingSession = try RecordingSession(outputDiretory: filePath, fileType: options.fileType, onChunkReady: onChunkReady, + onChunkError: onChunkError, completion: onFinish) // Init Audio + Activate Audio Session (optional) diff --git a/package/ios/Core/ChunkedRecorder.swift b/package/ios/Core/ChunkedRecorder.swift index 91b42dd..3f7c933 100644 --- a/package/ios/Core/ChunkedRecorder.swift +++ b/package/ios/Core/ChunkedRecorder.swift @@ -24,12 +24,14 @@ class ChunkedRecorder: NSObject { let outputURL: URL let onChunkReady: ((Chunk) -> Void) + let onError: ((Error) -> Void)? private var chunkIndex: UInt64 = 0 - init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void)) throws { + init(outputURL: URL, onChunkReady: @escaping ((Chunk) -> Void), onError: ((Error) -> Void)? = nil) throws { self.outputURL = outputURL self.onChunkReady = onChunkReady + self.onError = onError guard FileManager.default.fileExists(atPath: outputURL.path) else { throw CameraError.unknown(message: "output directory does not exist at: \(outputURL.path)", cause: nil) } @@ -56,28 +58,36 @@ extension ChunkedRecorder: AVAssetWriterDelegate { private func saveInitSegment(_ data: Data) { let url = outputURL.appendingPathComponent("init.mp4") - save(data: data, url: url) - onChunkReady(url: url, type: .initialization) + do { + try data.write(to: url) + onChunkReady(url: url, type: .initialization) + } catch { + ReactLogger.log(level: .error, message: "Failed to write init file \(url): \(error.localizedDescription)") + onError?(CameraError.capture(.fileError)) + } } private func saveSegment(_ data: Data, report: AVAssetSegmentReport?) { let name = "\(chunkIndex).mp4" let url = outputURL.appendingPathComponent(name) - save(data: data, url: url) - let duration = report? - .trackReports - .filter { $0.mediaType == .video } - .first? - .duration - onChunkReady(url: url, type: .data(index: chunkIndex, duration: duration)) - chunkIndex += 1 + if save(data: data, url: url) { + let duration = report? + .trackReports + .filter { $0.mediaType == .video } + .first? + .duration + onChunkReady(url: url, type: .data(index: chunkIndex, duration: duration)) + chunkIndex += 1 + } } - private func save(data: Data, url: URL) { + private func save(data: Data, url: URL) -> Bool { do { try data.write(to: url) + return true } catch { ReactLogger.log(level: .error, message: "Unable to write \(url): \(error.localizedDescription)") + return false } } diff --git a/package/ios/Core/RecordingSession.swift b/package/ios/Core/RecordingSession.swift index b02c645..72a2645 100644 --- a/package/ios/Core/RecordingSession.swift +++ b/package/ios/Core/RecordingSession.swift @@ -74,12 +74,13 @@ class RecordingSession { init(outputDiretory: String, fileType: AVFileType, onChunkReady: @escaping ((ChunkedRecorder.Chunk) -> Void), + onChunkError: ((Error) -> Void)? = nil, completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws { completionHandler = completion do { let outputURL = URL(fileURLWithPath: outputDiretory) - recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady) + recorder = try ChunkedRecorder(outputURL: outputURL, onChunkReady: onChunkReady, onError: onChunkError) assetWriter = AVAssetWriter(contentType: UTType(fileType.rawValue)!) assetWriter.shouldOptimizeForNetworkUse = false assetWriter.outputFileTypeProfile = .mpeg4AppleHLS