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..5899c08 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 @@ -42,12 +42,14 @@ class FragmentedRecordingManager( val cameraOrientationDegrees = cameraOrientation.toDegrees() val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).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 camera orientation, same as ChunkedRecordingManager + 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}, " + + Log.d(TAG, "Input size: ${size.width}x${size.height}, encoder size: ${width}x${height}, " + "cameraOrientation: $cameraOrientation ($cameraOrientationDegrees°), " + "recordingOrientation: $recordingOrientationDegrees°") 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..a72cca7 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,91 @@ 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 - writeRotationMatrix(dos) + // Rotation matrix based on orientationDegrees + writeRotationMatrix(dos, width, height) - // 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) + // For 90° and 270° rotations, the display dimensions are swapped + // The tkhd width/height represent the final display size after rotation + val (displayWidth, displayHeight) = when (orientationDegrees) { + 90, 270 -> Pair(height, width) + else -> Pair(width, 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: encoder=${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. + * The matrix is applied to rotate the video content for correct display. + * + * Matrix format in tkhd box (all values in fixed-point): + * | a b u | where a,b,c,d are 16.16 fixed-point + * | c d v | and u,v are 2.30 fixed-point (always 0) + * | x y w | x,y are 16.16, w is 2.30 (always 1.0) + * + * For rotation by θ: a=cos(θ), b=sin(θ), c=-sin(θ), d=cos(θ) + * Translation (x,y) keeps the rotated video in the visible area. */ - private fun writeRotationMatrix(dos: DataOutputStream) { + private fun writeRotationMatrix(dos: DataOutputStream, width: Int, height: Int) { // Fixed-point constants val one = 0x00010000 // 1.0 in 16.16 + val negOne = -0x00010000 // -1.0 in 16.16 (will be written as unsigned) 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 - dos.writeInt(0) // u = 0 - dos.writeInt(0) // c = 0 - dos.writeInt(one) // d = 1 - dos.writeInt(0) // v = 0 - dos.writeInt(0) // x = 0 - dos.writeInt(0) // y = 0 - dos.writeInt(w) // w = 1 + when (orientationDegrees) { + 90 -> { + // 90° rotation: x' = y, y' = -x + width + dos.writeInt(0) // a = 0 + dos.writeInt(negOne) // b = -1 + dos.writeInt(0) // u = 0 + dos.writeInt(one) // c = 1 + dos.writeInt(0) // d = 0 + dos.writeInt(0) // v = 0 + dos.writeInt(0) // x = 0 + dos.writeInt(width shl 16) // y = width (translation) + dos.writeInt(w) // w = 1 + } + 180 -> { + // 180° rotation + dos.writeInt(negOne) // a = -1 + dos.writeInt(0) // b = 0 + dos.writeInt(0) // u = 0 + dos.writeInt(0) // c = 0 + dos.writeInt(negOne) // d = -1 + dos.writeInt(0) // v = 0 + dos.writeInt(width shl 16) // x = width (translation) + dos.writeInt(height shl 16) // y = height (translation) + dos.writeInt(w) // w = 1 + } + 270 -> { + // 270° rotation: x' = -y + height, y' = x + dos.writeInt(0) // a = 0 + dos.writeInt(one) // b = 1 + dos.writeInt(0) // u = 0 + dos.writeInt(negOne) // c = -1 + dos.writeInt(0) // d = 0 + dos.writeInt(0) // v = 0 + dos.writeInt(height shl 16) // x = height (translation) + dos.writeInt(0) // y = 0 + dos.writeInt(w) // w = 1 + } + else -> { + // 0° or unknown: identity matrix + dos.writeInt(one) // a = 1 + dos.writeInt(0) // b = 0 + dos.writeInt(0) // u = 0 + dos.writeInt(0) // c = 0 + dos.writeInt(one) // d = 1 + dos.writeInt(0) // v = 0 + dos.writeInt(0) // x = 0 + dos.writeInt(0) // y = 0 + dos.writeInt(w) // w = 1 + } + } } private fun buildMdiaBox(width: Int, height: Int, sps: ByteArray, pps: ByteArray): ByteArray {