From a1f23e0183cc2e5cddeb0baf41f9883b294b679a Mon Sep 17 00:00:00 2001 From: Loewy Date: Mon, 18 May 2026 16:59:17 -0700 Subject: [PATCH] Surface Android fMP4 writer failures --- .../com/mrousavy/camera/core/CameraError.kt | 2 + .../camera/core/FragmentedRecordingManager.kt | 39 +++++++++++++++---- .../java/com/mrousavy/camera/core/HlsMuxer.kt | 5 ++- .../mrousavy/camera/core/RecordingSession.kt | 1 + 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index 834c190..8484f78 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -110,6 +110,8 @@ class UnknownCaptureError(wasImageCaptured: Boolean) : CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured") class RecorderError(name: String, extra: Int) : CameraError("capture", "recorder-error", "An error occured while recording a video! $name $extra") +class FragmentedRecorderError(message: String, cause: Throwable? = null) : + CameraError("capture", "fragmented-recorder-error", message, cause) class NoRecordingInProgressError : CameraError("capture", "no-recording-in-progress", "There was no active video recording in progress! Did you call stopRecording() twice?") class InsufficientStorageError : CameraError("capture", "insufficient-storage", "There is not enough storage space available.") diff --git a/package/android/src/main/java/com/mrousavy/camera/core/FragmentedRecordingManager.kt b/package/android/src/main/java/com/mrousavy/camera/core/FragmentedRecordingManager.kt index f9713e7..07f4451 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/FragmentedRecordingManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/FragmentedRecordingManager.kt @@ -20,7 +20,8 @@ import java.io.File */ class FragmentedRecordingManager( private val encoder: MediaCodec, - private val muxer: HlsMuxer + private val muxer: HlsMuxer, + private val onError: (CameraError) -> Unit ) : MediaCodec.Callback(), ChunkedRecorderInterface { companion object { @@ -36,6 +37,7 @@ class FragmentedRecordingManager( bitRate: Int, options: RecordVideoOptions, outputDirectory: File, + onError: (CameraError) -> Unit, segmentDurationSeconds: Int = DEFAULT_SEGMENT_DURATION_SECONDS ): FragmentedRecordingManager { val mimeType = options.videoCodec.toMimeType() @@ -81,11 +83,12 @@ class FragmentedRecordingManager( ) muxer.setSegmentDuration(segmentDurationSeconds * 1_000_000L) - return FragmentedRecordingManager(codec, muxer) + return FragmentedRecordingManager(codec, muxer, onError) } } private var recording = false + private var failed = false private var muxerStarted = false private var trackIndex = -1 @@ -100,6 +103,17 @@ class FragmentedRecordingManager( recording = true } + private fun failRecording(message: String, cause: Throwable) { + if (failed) { + return + } + + failed = true + recording = false + Log.e(TAG, message, cause) + onError(FragmentedRecorderError(message, cause)) + } + override fun finish() { synchronized(this) { recording = false @@ -138,7 +152,10 @@ class FragmentedRecordingManager( val buffer = encoder.getOutputBuffer(index) if (buffer == null) { - Log.e(TAG, "getOutputBuffer returned null") + failRecording( + "Failed to write fragmented MP4 sample: output buffer was null", + RuntimeException("getOutputBuffer returned null") + ) encoder.releaseOutputBuffer(index, false) return } @@ -146,7 +163,7 @@ class FragmentedRecordingManager( try { muxer.writeSampleData(trackIndex, buffer, bufferInfo) } catch (e: Exception) { - Log.e(TAG, "Error writing sample", e) + failRecording("Failed to write fragmented MP4 sample", e) } encoder.releaseOutputBuffer(index, false) @@ -154,16 +171,22 @@ class FragmentedRecordingManager( } override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { - Log.e(TAG, "Codec error: ${e.message}") + synchronized(this) { + failRecording("Fragmented MP4 encoder error", e) + } } override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { synchronized(this) { Log.i(TAG, "Output format changed: $format") - trackIndex = muxer.addTrack(format) - muxer.start() - muxerStarted = true + try { + trackIndex = muxer.addTrack(format) + muxer.start() + muxerStarted = true + } catch (e: Exception) { + failRecording("Failed to start fragmented MP4 muxer", e) + } } } } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt b/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt index 891844c..1cc74a9 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/HlsMuxer.kt @@ -273,7 +273,10 @@ class HlsMuxer( // Write init segment val initBytes = buildInitSegment(format) val initFile = File(outputDirectory, "init.mp4") - FileOutputStream(initFile).use { it.write(initBytes) } + FileOutputStream(initFile).use { output -> + output.write(initBytes) + output.fd.sync() + } // Log frame rate metadata for debugging val defaultSampleDuration = timescale / fps diff --git a/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt index a6bf377..c5cdd99 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt @@ -72,6 +72,7 @@ class RecordingSession( bitRate, options, outputPath, + onError, SEGMENT_DURATION_SECONDS ) } else {