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 <noreply@anthropic.com>
This commit is contained in:
Dean
2026-01-21 07:43:53 -08:00
parent dd26812a9c
commit be800b12c2

View File

@@ -544,80 +544,29 @@ class HlsMuxer(
dos.writeShort(0) // volume (0 for video) dos.writeShort(0) // volume (0 for video)
dos.writeShort(0) // reserved dos.writeShort(0) // reserved
// Rotation matrix based on orientationDegrees // Web players like Shaka don't support the tkhd rotation matrix properly.
writeRotationMatrix(dos, width, height) // 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 // Use coded dimensions (not rotated) for web compatibility
// The tkhd width/height represent the final display size after rotation dos.writeInt(width shl 16) // width (16.16 fixed point)
val (displayWidth, displayHeight) = when (orientationDegrees) { dos.writeInt(height shl 16) // height (16.16 fixed point)
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: 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()) return wrapBox("tkhd", output.toByteArray())
} }
/** /**
* Writes the 3x3 transformation matrix for video rotation. * Writes identity (no rotation) transformation matrix.
* The matrix is applied to rotate the video content for correct display. * Used for web compatibility since players like Shaka don't handle rotation matrices.
*
* 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, width: Int, height: Int) { private fun writeIdentityMatrix(dos: DataOutputStream) {
// Fixed-point constants
val one = 0x00010000 // 1.0 in 16.16 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 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(one) // a = 1
dos.writeInt(0) // b = 0 dos.writeInt(0) // b = 0
dos.writeInt(0) // u = 0 dos.writeInt(0) // u = 0
@@ -628,8 +577,6 @@ class HlsMuxer(
dos.writeInt(0) // y = 0 dos.writeInt(0) // y = 0
dos.writeInt(w) // w = 1 dos.writeInt(w) // w = 1
} }
}
}
private fun buildMdiaBox(width: Int, height: Int, sps: ByteArray, pps: ByteArray): ByteArray { private fun buildMdiaBox(width: Int, height: Int, sps: ByteArray, pps: ByteArray): ByteArray {
val content = ByteArrayOutputStream() val content = ByteArrayOutputStream()