From c43f4d3a80f48b3d195270137c81b7b4f5b33622 Mon Sep 17 00:00:00 2001 From: Loewy Date: Tue, 23 Dec 2025 21:56:17 -0500 Subject: [PATCH] add orientation and aspect ratio handling for landscape recording --- .../com/mrousavy/camera/core/CameraSession.kt | 10 ++-- .../camera/core/FragmentedRecordingManager.kt | 20 ++++--- .../java/com/mrousavy/camera/core/HlsMuxer.kt | 56 ++++++++++++------- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 770812f..ed8ea69 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -429,15 +429,15 @@ class CameraSession(private val context: Context, private val cameraManager: Cam // Get actual device rotation from WindowManager since the React Native orientation hook // doesn't update when rotating between landscape-left and landscape-right on Android. // Map device rotation to the correct orientationHint for video recording: - // - Counter-clockwise (ROTATION_90) → 270° hint - // - Clockwise (ROTATION_270) → 90° hint + // - Counter-clockwise (ROTATION_90) → 90° hint + // - Clockwise (ROTATION_270) → 270° hint val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val deviceRotation = windowManager.defaultDisplay.rotation val recordingOrientation = when (deviceRotation) { Surface.ROTATION_0 -> Orientation.PORTRAIT - Surface.ROTATION_90 -> Orientation.LANDSCAPE_RIGHT + Surface.ROTATION_90 -> Orientation.LANDSCAPE_LEFT Surface.ROTATION_180 -> Orientation.PORTRAIT_UPSIDE_DOWN - Surface.ROTATION_270 -> Orientation.LANDSCAPE_LEFT + Surface.ROTATION_270 -> Orientation.LANDSCAPE_RIGHT else -> Orientation.PORTRAIT } @@ -448,7 +448,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam enableAudio, fps, videoOutput.enableHdr, - orientation, + recordingOrientation, options, filePath, callback, 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 8ad49d7..09ea131 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 @@ -39,17 +39,21 @@ class FragmentedRecordingManager( segmentDurationSeconds: Int = DEFAULT_SEGMENT_DURATION_SECONDS ): FragmentedRecordingManager { val mimeType = options.videoCodec.toMimeType() - val cameraOrientationDegrees = cameraOrientation.toDegrees() - val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees() + // Use cameraOrientation from Android (computed from device rotation) + // instead of options.orientation from JS which may be stale + val recordingOrientationDegrees = cameraOrientation.toDegrees() - // Use size dimensions directly - the encoder output format will have the actual dimensions - // Don't swap based on orientation here; the camera pipeline handles that - val width = size.width - val height = size.height + // Swap dimensions based on orientation - same logic as ChunkedRecordingManager + // When camera is in landscape orientation, we need to swap width/height for the encoder + val (width, height) = if (cameraOrientation.isLandscape()) { + size.height to size.width + } else { + size.width to size.height + } Log.d(TAG, "Input size: ${size.width}x${size.height}, " + - "cameraOrientation: $cameraOrientation ($cameraOrientationDegrees°), " + - "recordingOrientation: $recordingOrientationDegrees°") + "encoder size: ${width}x${height}, " + + "orientation: $cameraOrientation ($recordingOrientationDegrees°)") val format = MediaFormat.createVideoFormat(mimeType, width, height) val codec = MediaCodec.createEncoderByType(mimeType) 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 5c68668..9c0c3da 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 @@ -478,40 +478,56 @@ class HlsMuxer( dos.writeShort(0) // volume (0 for video) dos.writeShort(0) // reserved - // Rotation matrix - use identity and rely on correct dimensions from encoder - // The encoder output format already has the correct dimensions for the content + // Rotation matrix writeRotationMatrix(dos) - // Use dimensions as-is from encoder output format - dos.writeInt(width shl 16) // width (16.16 fixed point) - dos.writeInt(height shl 16) // height (16.16 fixed point) + // Display dimensions should be post-rotation dimensions + // For 90° or 270° rotation, swap width and height + val (displayWidth, displayHeight) = when (orientationDegrees) { + 90, 270 -> height to width + else -> width to height + } + dos.writeInt(displayWidth shl 16) // width (16.16 fixed point) + dos.writeInt(displayHeight shl 16) // height (16.16 fixed point) - Log.d(TAG, "tkhd: ${width}x${height}, rotation=$orientationDegrees") + Log.d(TAG, "tkhd: encoded=${width}x${height}, display=${displayWidth}x${displayHeight}, rotation=$orientationDegrees") return wrapBox("tkhd", output.toByteArray()) } /** * Writes the 3x3 transformation matrix for video rotation. - * Uses simple rotation values - the encoder already outputs correctly oriented frames. */ private fun writeRotationMatrix(dos: DataOutputStream) { - // Fixed-point constants - val one = 0x00010000 // 1.0 in 16.16 - val w = 0x40000000 // 1.0 in 2.30 + val one = 0x00010000 // 1.0 in 16.16 + val negOne = 0xFFFF0000.toInt() // -1.0 in 16.16 + val w = 0x40000000 // 1.0 in 2.30 - // Identity matrix - no transformation - // Most HLS players handle rotation via the dimensions themselves - // or we can add rotation metadata separately if needed - dos.writeInt(one) // a = 1 - dos.writeInt(0) // b = 0 + // For 270° device orientation (landscape-right), apply 90° CW rotation + // For 90° device orientation (landscape-left), apply 270° CW rotation + val a: Int + val b: Int + val c: Int + val d: Int + + when (orientationDegrees) { + 90 -> { a = 0; b = negOne; c = one; d = 0 } + 180 -> { a = negOne; b = 0; c = 0; d = negOne } + 270 -> { a = 0; b = one; c = negOne; d = 0 } + else -> { a = one; b = 0; c = 0; d = one } + } + + dos.writeInt(a) + dos.writeInt(b) dos.writeInt(0) // u = 0 - dos.writeInt(0) // c = 0 - dos.writeInt(one) // d = 1 + dos.writeInt(c) + dos.writeInt(d) dos.writeInt(0) // v = 0 - dos.writeInt(0) // x = 0 - dos.writeInt(0) // y = 0 - dos.writeInt(w) // w = 1 + dos.writeInt(0) // tx = 0 + dos.writeInt(0) // ty = 0 + dos.writeInt(w) // w = 1.0 + + Log.d(TAG, "Rotation matrix for $orientationDegrees°") } private fun buildMdiaBox(width: Int, height: Int, sps: ByteArray, pps: ByteArray): ByteArray {