diff --git a/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt b/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt index 64c3389..e88fbf6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt @@ -30,8 +30,15 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu iFrameInterval: Int = 5 ): ChunkedRecordingManager { val mimeType = options.videoCodec.toMimeType() - val cameraOrientationDegrees = cameraOrientation.toDegrees() - val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees(); + // Use cameraOrientation (from WindowManager) for rotation metadata + // The options.orientation from JavaScript is unreliable on Android when rotating between landscape modes + // Note: MediaMuxer.setOrientationHint() uses opposite convention from HlsMuxer's rotation matrix + // We need to invert the rotation: 90 <-> 270, while 0 and 180 stay the same + val orientationDegrees = when (cameraOrientation.toDegrees()) { + 90 -> 270 + 270 -> 90 + else -> cameraOrientation.toDegrees() + } val (width, height) = if (cameraOrientation.isLandscape()) { size.height to size.width } else { @@ -55,12 +62,12 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval) format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate) - Log.d(TAG, "Video Format: $format, camera orientation $cameraOrientationDegrees, recordingOrientation: $recordingOrientationDegrees") + Log.d(TAG, "Video Format: $format, orientation: $orientationDegrees") // Create a MediaCodec encoder, and configure it with our format. Get a Surface // we can use for input and wrap it with a class that handles the EGL work. codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) return ChunkedRecordingManager( - codec, outputDirectory, recordingOrientationDegrees, iFrameInterval, callbacks + codec, outputDirectory, orientationDegrees, iFrameInterval, callbacks ) } } @@ -91,12 +98,13 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu muxer.start() } + fun writeSample(buffer: java.nio.ByteBuffer, bufferInfo: BufferInfo) { + muxer.writeSampleData(videoTrack, buffer, bufferInfo) + } fun finish() { muxer.stop() muxer.release() - // Calculate duration from start time - this is approximate - // The new FragmentedRecordingManager provides accurate duration callbacks.onVideoChunkReady(filepath, chunkIndex, null) } } @@ -170,7 +178,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu encoder.releaseOutputBuffer(index, false) return } - context.muxer.writeSampleData(context.videoTrack, encodedData, bufferInfo) + context.writeSample(encodedData, bufferInfo) encoder.releaseOutputBuffer(index, false) } } 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 3c1b8e4..4784904 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 @@ -8,6 +8,7 @@ import com.facebook.common.statfs.StatFsHelper import com.mrousavy.camera.extensions.getRecommendedBitRate import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.RecordVideoOptions +import com.mrousavy.camera.types.StreamSegmentType import com.mrousavy.camera.utils.FileUtils import java.io.File import android.os.Environment @@ -27,9 +28,7 @@ class RecordingSession( private val filePath: String, private val callback: (video: Video) -> Unit, private val onError: (error: CameraError) -> Unit, - private val allCallbacks: CameraSession.Callback, - // Use FragmentedRecordingManager for HLS-compatible fMP4 output - private val useFragmentedMp4: Boolean = true + private val allCallbacks: CameraSession.Callback ) { companion object { private const val TAG = "RecordingSession" @@ -51,7 +50,7 @@ class RecordingSession( // Use FragmentedRecordingManager for HLS-compatible fMP4 output, // or fall back to ChunkedRecordingManager for regular MP4 chunks - private val recorder: ChunkedRecorderInterface = if (useFragmentedMp4) { + private val recorder: ChunkedRecorderInterface = if (options.streamSegmentType == StreamSegmentType.FRAGMENTED_MP4) { FragmentedRecordingManager.fromParams( allCallbacks, size, @@ -83,7 +82,7 @@ class RecordingSession( fun start() { synchronized(this) { - Log.i(TAG, "Starting RecordingSession..") + Log.i(TAG, "Starting RecordingSession with ${options.streamSegmentType} recorder..") startTime = System.currentTimeMillis() recorder.start() } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/RecordVideoOptions.kt b/package/android/src/main/java/com/mrousavy/camera/types/RecordVideoOptions.kt index 78bfdd2..921f223 100644 --- a/package/android/src/main/java/com/mrousavy/camera/types/RecordVideoOptions.kt +++ b/package/android/src/main/java/com/mrousavy/camera/types/RecordVideoOptions.kt @@ -9,6 +9,7 @@ class RecordVideoOptions(map: ReadableMap) { var videoBitRateOverride: Double? = null var videoBitRateMultiplier: Double? = null var orientation: Orientation? = null + var streamSegmentType: StreamSegmentType = StreamSegmentType.FRAGMENTED_MP4 init { if (map.hasKey("fileType")) { @@ -29,5 +30,8 @@ class RecordVideoOptions(map: ReadableMap) { if (map.hasKey("orientation")) { orientation = Orientation.fromUnionValue(map.getString("orientation")) } + if (map.hasKey("streamSegmentType")) { + streamSegmentType = StreamSegmentType.fromUnionValue(map.getString("streamSegmentType")) + } } } diff --git a/package/android/src/main/java/com/mrousavy/camera/types/StreamSegmentType.kt b/package/android/src/main/java/com/mrousavy/camera/types/StreamSegmentType.kt new file mode 100644 index 0000000..a67dccf --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/types/StreamSegmentType.kt @@ -0,0 +1,15 @@ +package com.mrousavy.camera.types + +enum class StreamSegmentType(override val unionValue: String) : JSUnionValue { + FRAGMENTED_MP4("FRAGMENTED_MP4"), + RB_CHUNKED_MP4("RB_CHUNKED_MP4"); + + companion object : JSUnionValue.Companion { + override fun fromUnionValue(unionValue: String?): StreamSegmentType = + when (unionValue) { + "FRAGMENTED_MP4" -> FRAGMENTED_MP4 + "RB_CHUNKED_MP4" -> RB_CHUNKED_MP4 + else -> FRAGMENTED_MP4 // Default to fMP4 + } + } +} diff --git a/package/src/VideoFile.ts b/package/src/VideoFile.ts index 65d3722..6266106 100644 --- a/package/src/VideoFile.ts +++ b/package/src/VideoFile.ts @@ -41,6 +41,17 @@ export interface RecordVideoOptions { * @default 'normal' */ videoBitRate?: 'extra-low' | 'low' | 'normal' | 'high' | 'extra-high' | number + /** + * The stream segment type for recording on Android. + * - `FRAGMENTED_MP4`: HLS-compatible segments (init.mp4 + numbered segments) + * - `RB_CHUNKED_MP4`: Legacy chunked MP4 format + * + * iOS always uses FRAGMENTED_MP4 regardless of this setting. + * + * @platform android + * @default 'FRAGMENTED_MP4' + */ + streamSegmentType?: 'FRAGMENTED_MP4' | 'RB_CHUNKED_MP4' } /**