From be800b12c2eaf5cef6af1ecc507df2ab85abb86a Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 21 Jan 2026 07:43:53 -0800 Subject: [PATCH] fix: Use identity matrix for web player compatibility Web players like Shaka don't support the tkhd rotation matrix properly. Use identity matrix and coded dimensions instead - the video will appear in the orientation it was encoded. Co-Authored-By: Claude Opus 4.5 --- .../java/com/mrousavy/camera/core/HlsMuxer.kt | 95 ++++--------------- 1 file changed, 21 insertions(+), 74 deletions(-) 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..710ec15 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 @@ -544,91 +544,38 @@ class HlsMuxer( dos.writeShort(0) // volume (0 for video) dos.writeShort(0) // reserved - // Rotation matrix based on orientationDegrees - writeRotationMatrix(dos, width, height) + // Web players like Shaka don't support the tkhd rotation matrix properly. + // Use identity matrix and coded dimensions - the video will appear in the + // orientation it was encoded (usually portrait for mobile recordings). + // TODO: Find a better solution for web rotation (CSS transform, etc.) + writeIdentityMatrix(dos) - // 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) + // Use coded dimensions (not rotated) for web compatibility + dos.writeInt(width shl 16) // width (16.16 fixed point) + dos.writeInt(height shl 16) // height (16.16 fixed point) - Log.d(TAG, "tkhd: encoder=${width}x${height}, display=${displayWidth}x${displayHeight}, rotation=$orientationDegrees") + Log.d(TAG, "tkhd: encoder=${width}x${height}, display=${width}x${height}, rotation=0 (identity matrix for web compatibility)") return wrapBox("tkhd", output.toByteArray()) } /** - * Writes the 3x3 transformation matrix for video rotation. - * 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. + * Writes identity (no rotation) transformation matrix. + * Used for web compatibility since players like Shaka don't handle rotation matrices. */ - private fun writeRotationMatrix(dos: DataOutputStream, width: Int, height: Int) { - // Fixed-point constants + private fun writeIdentityMatrix(dos: DataOutputStream) { 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 - 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 - } - } + 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 {