add orientation and aspect ratio handling for landscape recording
This commit is contained in:
@@ -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
|
// 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.
|
// doesn't update when rotating between landscape-left and landscape-right on Android.
|
||||||
// Map device rotation to the correct orientationHint for video recording:
|
// Map device rotation to the correct orientationHint for video recording:
|
||||||
// - Counter-clockwise (ROTATION_90) → 270° hint
|
// - Counter-clockwise (ROTATION_90) → 90° hint
|
||||||
// - Clockwise (ROTATION_270) → 90° hint
|
// - Clockwise (ROTATION_270) → 270° hint
|
||||||
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
val deviceRotation = windowManager.defaultDisplay.rotation
|
val deviceRotation = windowManager.defaultDisplay.rotation
|
||||||
val recordingOrientation = when (deviceRotation) {
|
val recordingOrientation = when (deviceRotation) {
|
||||||
Surface.ROTATION_0 -> Orientation.PORTRAIT
|
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_180 -> Orientation.PORTRAIT_UPSIDE_DOWN
|
||||||
Surface.ROTATION_270 -> Orientation.LANDSCAPE_LEFT
|
Surface.ROTATION_270 -> Orientation.LANDSCAPE_RIGHT
|
||||||
else -> Orientation.PORTRAIT
|
else -> Orientation.PORTRAIT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +448,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
|||||||
enableAudio,
|
enableAudio,
|
||||||
fps,
|
fps,
|
||||||
videoOutput.enableHdr,
|
videoOutput.enableHdr,
|
||||||
orientation,
|
recordingOrientation,
|
||||||
options,
|
options,
|
||||||
filePath,
|
filePath,
|
||||||
callback,
|
callback,
|
||||||
|
|||||||
@@ -39,17 +39,21 @@ class FragmentedRecordingManager(
|
|||||||
segmentDurationSeconds: Int = DEFAULT_SEGMENT_DURATION_SECONDS
|
segmentDurationSeconds: Int = DEFAULT_SEGMENT_DURATION_SECONDS
|
||||||
): FragmentedRecordingManager {
|
): FragmentedRecordingManager {
|
||||||
val mimeType = options.videoCodec.toMimeType()
|
val mimeType = options.videoCodec.toMimeType()
|
||||||
val cameraOrientationDegrees = cameraOrientation.toDegrees()
|
// Use cameraOrientation from Android (computed from device rotation)
|
||||||
val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees()
|
// 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
|
// Swap dimensions based on orientation - same logic as ChunkedRecordingManager
|
||||||
// Don't swap based on orientation here; the camera pipeline handles that
|
// When camera is in landscape orientation, we need to swap width/height for the encoder
|
||||||
val width = size.width
|
val (width, height) = if (cameraOrientation.isLandscape()) {
|
||||||
val height = size.height
|
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}, " +
|
||||||
"cameraOrientation: $cameraOrientation ($cameraOrientationDegrees°), " +
|
"encoder size: ${width}x${height}, " +
|
||||||
"recordingOrientation: $recordingOrientationDegrees°")
|
"orientation: $cameraOrientation ($recordingOrientationDegrees°)")
|
||||||
|
|
||||||
val format = MediaFormat.createVideoFormat(mimeType, width, height)
|
val format = MediaFormat.createVideoFormat(mimeType, width, height)
|
||||||
val codec = MediaCodec.createEncoderByType(mimeType)
|
val codec = MediaCodec.createEncoderByType(mimeType)
|
||||||
|
|||||||
@@ -478,40 +478,56 @@ 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 - use identity and rely on correct dimensions from encoder
|
// Rotation matrix
|
||||||
// The encoder output format already has the correct dimensions for the content
|
|
||||||
writeRotationMatrix(dos)
|
writeRotationMatrix(dos)
|
||||||
|
|
||||||
// Use dimensions as-is from encoder output format
|
// Display dimensions should be post-rotation dimensions
|
||||||
dos.writeInt(width shl 16) // width (16.16 fixed point)
|
// For 90° or 270° rotation, swap width and height
|
||||||
dos.writeInt(height shl 16) // height (16.16 fixed point)
|
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())
|
return wrapBox("tkhd", output.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the 3x3 transformation matrix for video rotation.
|
* Writes the 3x3 transformation matrix for video rotation.
|
||||||
* Uses simple rotation values - the encoder already outputs correctly oriented frames.
|
|
||||||
*/
|
*/
|
||||||
private fun writeRotationMatrix(dos: DataOutputStream) {
|
private fun writeRotationMatrix(dos: DataOutputStream) {
|
||||||
// Fixed-point constants
|
|
||||||
val one = 0x00010000 // 1.0 in 16.16
|
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
|
val w = 0x40000000 // 1.0 in 2.30
|
||||||
|
|
||||||
// Identity matrix - no transformation
|
// For 270° device orientation (landscape-right), apply 90° CW rotation
|
||||||
// Most HLS players handle rotation via the dimensions themselves
|
// For 90° device orientation (landscape-left), apply 270° CW rotation
|
||||||
// or we can add rotation metadata separately if needed
|
val a: Int
|
||||||
dos.writeInt(one) // a = 1
|
val b: Int
|
||||||
dos.writeInt(0) // b = 0
|
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) // u = 0
|
||||||
dos.writeInt(0) // c = 0
|
dos.writeInt(c)
|
||||||
dos.writeInt(one) // d = 1
|
dos.writeInt(d)
|
||||||
dos.writeInt(0) // v = 0
|
dos.writeInt(0) // v = 0
|
||||||
dos.writeInt(0) // x = 0
|
dos.writeInt(0) // tx = 0
|
||||||
dos.writeInt(0) // y = 0
|
dos.writeInt(0) // ty = 0
|
||||||
dos.writeInt(w) // w = 1
|
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 {
|
private fun buildMdiaBox(width: Int, height: Int, sps: ByteArray, pps: ByteArray): ByteArray {
|
||||||
|
|||||||
Reference in New Issue
Block a user