add orientation and aspect ratio handling for landscape recording

This commit is contained in:
2025-12-23 21:56:17 -05:00
parent e60c1a4eb1
commit c43f4d3a80
3 changed files with 53 additions and 33 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 {