Fix fMP4 video orientation by using raw sensor frames with Y-flip transform
This commit is contained in:
@@ -26,6 +26,7 @@ OpenGLRenderer::OpenGLRenderer(std::shared_ptr<OpenGLContext> context, ANativeWi
|
|||||||
_outputSurface = surface;
|
_outputSurface = surface;
|
||||||
_width = ANativeWindow_getWidth(surface);
|
_width = ANativeWindow_getWidth(surface);
|
||||||
_height = ANativeWindow_getHeight(surface);
|
_height = ANativeWindow_getHeight(surface);
|
||||||
|
__android_log_print(ANDROID_LOG_INFO, TAG, "ROTATION_DEBUG OpenGLRenderer created with output surface dimensions: %dx%d", _width, _height);
|
||||||
}
|
}
|
||||||
|
|
||||||
OpenGLRenderer::~OpenGLRenderer() {
|
OpenGLRenderer::~OpenGLRenderer() {
|
||||||
|
|||||||
@@ -79,7 +79,17 @@ void VideoPipeline::onFrame(jni::alias_ref<jni::JArrayFloat> transformMatrixPara
|
|||||||
|
|
||||||
if (_recordingSessionOutput) {
|
if (_recordingSessionOutput) {
|
||||||
__android_log_print(ANDROID_LOG_INFO, TAG, "Rendering to RecordingSession..");
|
__android_log_print(ANDROID_LOG_INFO, TAG, "Rendering to RecordingSession..");
|
||||||
_recordingSessionOutput->renderTextureToSurface(texture, transformMatrix);
|
// For recording, use a simple Y-flip matrix instead of the display transform.
|
||||||
|
// The display transform includes rotations for preview which we don't want in recordings.
|
||||||
|
// We'll rely on rotation metadata in the MP4 container for playback orientation.
|
||||||
|
// This matrix flips Y (OpenGL origin is bottom-left, video expects top-left).
|
||||||
|
float recordingMatrix[16] = {
|
||||||
|
1.0f, 0.0f, 0.0f, 0.0f, // row 0
|
||||||
|
0.0f, -1.0f, 0.0f, 0.0f, // row 1 (Y flip)
|
||||||
|
0.0f, 0.0f, 1.0f, 0.0f, // row 2
|
||||||
|
0.0f, 1.0f, 0.0f, 1.0f // row 3 (translate Y by 1 after flip)
|
||||||
|
};
|
||||||
|
_recordingSessionOutput->renderTextureToSurface(texture, recordingMatrix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -428,18 +428,16 @@ 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:
|
|
||||||
// - Counter-clockwise (ROTATION_90) → 270° hint
|
|
||||||
// - Clockwise (ROTATION_270) → 90° 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 // CCW rotation, top to 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 // CW rotation, top to right
|
||||||
else -> Orientation.PORTRAIT
|
else -> Orientation.PORTRAIT
|
||||||
}
|
}
|
||||||
|
Log.i(TAG, "ROTATION_DEBUG: deviceRotation=$deviceRotation, recordingOrientation=$recordingOrientation, options.orientation=${options.orientation}")
|
||||||
|
|
||||||
val recording = RecordingSession(
|
val recording = RecordingSession(
|
||||||
context,
|
context,
|
||||||
@@ -448,7 +446,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,
|
||||||
|
|||||||
@@ -51,14 +51,21 @@ class FragmentedRecordingManager(
|
|||||||
segmentDurationSeconds: Int = 6
|
segmentDurationSeconds: Int = 6
|
||||||
): FragmentedRecordingManager {
|
): FragmentedRecordingManager {
|
||||||
val mimeType = options.videoCodec.toMimeType()
|
val mimeType = options.videoCodec.toMimeType()
|
||||||
val cameraOrientationDegrees = cameraOrientation.toDegrees()
|
// For fragmented MP4: DON'T swap dimensions, use camera's native dimensions.
|
||||||
val recordingOrientationDegrees = (options.orientation ?: Orientation.PORTRAIT).toDegrees()
|
// The C++ VideoPipeline now uses a simple Y-flip matrix (not the display transform).
|
||||||
|
// This gives us raw sensor frames, and we rely on rotation metadata for playback.
|
||||||
val (width, height) = if (cameraOrientation.isLandscape()) {
|
val cameraOrientationDegrees = when (cameraOrientation) {
|
||||||
size.height to size.width
|
Orientation.LANDSCAPE_LEFT -> 0 // CCW landscape - works!
|
||||||
} else {
|
Orientation.LANDSCAPE_RIGHT -> 0 // CW landscape - same as CCW (Y-flip handles it)
|
||||||
size.width to size.height
|
Orientation.PORTRAIT -> 90 // Portrait typically needs 90° on Android
|
||||||
|
Orientation.PORTRAIT_UPSIDE_DOWN -> 270
|
||||||
}
|
}
|
||||||
|
Log.i(TAG, "ROTATION_DEBUG FragmentedRecordingManager: cameraOrientation=$cameraOrientation, cameraOrientationDegrees=$cameraOrientationDegrees, inputSize=${size.width}x${size.height}")
|
||||||
|
|
||||||
|
// Keep original dimensions - don't swap. Let rotation metadata handle orientation.
|
||||||
|
val width = size.width
|
||||||
|
val height = size.height
|
||||||
|
Log.i(TAG, "ROTATION_DEBUG FragmentedRecordingManager: outputDimensions=${width}x${height} (no swap)")
|
||||||
|
|
||||||
val format = MediaFormat.createVideoFormat(mimeType, width, height)
|
val format = MediaFormat.createVideoFormat(mimeType, width, height)
|
||||||
val codec = MediaCodec.createEncoderByType(mimeType)
|
val codec = MediaCodec.createEncoderByType(mimeType)
|
||||||
@@ -74,14 +81,14 @@ class FragmentedRecordingManager(
|
|||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, segmentDurationSeconds)
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, segmentDurationSeconds)
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
|
||||||
|
|
||||||
Log.d(TAG, "Video Format: $format, camera orientation $cameraOrientationDegrees, recordingOrientation: $recordingOrientationDegrees")
|
Log.d(TAG, "Video Format: $format, camera orientation $cameraOrientationDegrees")
|
||||||
|
|
||||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||||
|
|
||||||
return FragmentedRecordingManager(
|
return FragmentedRecordingManager(
|
||||||
codec,
|
codec,
|
||||||
outputDirectory,
|
outputDirectory,
|
||||||
recordingOrientationDegrees,
|
cameraOrientationDegrees,
|
||||||
segmentDurationSeconds * 1_000_000L,
|
segmentDurationSeconds * 1_000_000L,
|
||||||
callbacks
|
callbacks
|
||||||
)
|
)
|
||||||
@@ -317,6 +324,7 @@ class FragmentedRecordingManager(
|
|||||||
initData.add(bytes)
|
initData.add(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "ROTATION_DEBUG convertToMedia3Format: orientationDegrees=$orientationDegrees, width=$width, height=$height")
|
||||||
return Format.Builder()
|
return Format.Builder()
|
||||||
.setSampleMimeType(mimeType)
|
.setSampleMimeType(mimeType)
|
||||||
.setWidth(width)
|
.setWidth(width)
|
||||||
|
|||||||
Reference in New Issue
Block a user