1111import android .graphics .ImageFormat ;
1212import android .hardware .camera2 .CameraAccessException ;
1313import android .hardware .camera2 .CameraCaptureSession ;
14+ import android .hardware .camera2 .CameraCharacteristics ;
1415import android .hardware .camera2 .CameraDevice ;
1516import android .hardware .camera2 .CameraManager ;
1617import android .hardware .camera2 .CaptureRequest ;
2223import android .os .Handler ;
2324import android .os .HandlerThread ;
2425import android .util .Log ;
26+ import android .view .Surface ;
27+ import android .view .WindowManager ;
2528
2629import androidx .core .app .ActivityCompat ;
2730import androidx .core .content .ContextCompat ;
@@ -71,6 +74,29 @@ public class NativeCameraPlugin extends GodotPlugin {
7174 private volatile int scaleWidth ;
7275 /** Target height for post-capture scaling; 0 means disabled. */
7376 private volatile int scaleHeight ;
77+
78+ /**
79+ * When true, the rotation applied to each frame is computed automatically
80+ * from the camera sensor orientation and the live device orientation instead
81+ * of using the fixed {@link #rotation} value supplied by the caller.
82+ */
83+ private volatile boolean autoUpright ;
84+
85+ /**
86+ * Clockwise angle (0 / 90 / 180 / 270) that the camera sensor image must
87+ * be rotated to be upright when the device is in its natural (portrait)
88+ * orientation. Populated from {@link CameraCharacteristics#SENSOR_ORIENTATION}
89+ * each time {@link #start} is called.
90+ */
91+ private volatile int sensorOrientation ;
92+
93+ /**
94+ * True when the active camera is front-facing. Used by
95+ * {@link #computeUprightRotation()} to mirror-compensate the rotation
96+ * formula for selfie cameras.
97+ */
98+ private volatile boolean isFrontFacingCamera ;
99+
74100 private int frameCounter = 0 ;
75101
76102 private volatile boolean running = false ;
@@ -174,6 +200,7 @@ public void start(Dictionary requestDict) {
174200 mirrorVertical = feedRequest .isMirrorVertical ();
175201 scaleWidth = feedRequest .getScaleWidth ();
176202 scaleHeight = feedRequest .getScaleHeight ();
203+ autoUpright = feedRequest .isAutoUpright ();
177204 openCamera (feedRequest );
178205 }
179206
@@ -219,6 +246,21 @@ private void openCamera(FeedRequest request) {
219246
220247 CameraManager manager = (CameraManager ) activity .getSystemService (Context .CAMERA_SERVICE );
221248 try {
249+ // Read sensor orientation and lens facing so that computeUprightRotation()
250+ // has the information it needs without touching the (potentially slow)
251+ // CameraCharacteristics API on every frame.
252+ CameraCharacteristics characteristics = manager .getCameraCharacteristics (request .getCameraId ());
253+
254+ Integer sensorOrientationValue = characteristics .get (CameraCharacteristics .SENSOR_ORIENTATION );
255+ sensorOrientation = (sensorOrientationValue != null ) ? sensorOrientationValue : 0 ;
256+
257+ Integer lensFacing = characteristics .get (CameraCharacteristics .LENS_FACING );
258+ isFrontFacingCamera = (lensFacing != null && lensFacing == CameraCharacteristics .LENS_FACING_FRONT );
259+
260+ Log .d (LOG_TAG , String .format (
261+ "openCamera(): sensorOrientation=%d, isFrontFacing=%b, autoUpright=%b" ,
262+ sensorOrientation , isFrontFacingCamera , autoUpright ));
263+
222264 reader = ImageReader .newInstance (request .getWidth (), request .getHeight (), ImageFormat .YUV_420_888 , 2 );
223265 reader .setOnImageAvailableListener (this ::onImageAvailable , bgHandler );
224266
@@ -228,6 +270,59 @@ private void openCamera(FeedRequest request) {
228270 }
229271 }
230272
273+ /**
274+ * Computes the clockwise rotation (in degrees) required to produce an upright
275+ * image for the currently active camera and the current device orientation.
276+ *
277+ * <p>The algorithm follows the standard Camera2 guidance:
278+ * <ul>
279+ * <li>Back-facing: {@code (sensorOrientation − deviceDegrees + 360) % 360}</li>
280+ * <li>Front-facing: {@code (sensorOrientation + deviceDegrees + 360) % 360}<br>
281+ * The extra compensation accounts for the horizontal mirror inherent to
282+ * front cameras — the sensor rotation and the device rotation add rather
283+ * than subtract.</li>
284+ * </ul>
285+ *
286+ * <p>This method is called once per processed frame and is intentionally
287+ * lightweight: the only dynamic read is {@link android.view.Display#getRotation()}.
288+ */
289+ private int computeUprightRotation () {
290+ Activity activity = getActivity ();
291+
292+ int surfaceRotation ;
293+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
294+ android .view .Display display = activity .getDisplay ();
295+ surfaceRotation = (display != null ) ? display .getRotation () : Surface .ROTATION_0 ;
296+ } else {
297+ WindowManager wm = (WindowManager ) activity .getSystemService (Context .WINDOW_SERVICE );
298+ //noinspection deprecation
299+ surfaceRotation = wm .getDefaultDisplay ().getRotation ();
300+ }
301+
302+ int deviceDegrees ;
303+ switch (surfaceRotation ) {
304+ case Surface .ROTATION_90 : deviceDegrees = 90 ; break ;
305+ case Surface .ROTATION_180 : deviceDegrees = 180 ; break ;
306+ case Surface .ROTATION_270 : deviceDegrees = 270 ; break ;
307+ default : deviceDegrees = 0 ; break ;
308+ }
309+
310+ int uprightRotation ;
311+ if (isFrontFacingCamera ) {
312+ // Front cameras are horizontally mirrored: sensor and device rotations add.
313+ uprightRotation = (sensorOrientation + deviceDegrees + 360 ) % 360 ;
314+ } else {
315+ // Back cameras: sensor rotation minus device rotation.
316+ uprightRotation = (sensorOrientation - deviceDegrees + 360 ) % 360 ;
317+ }
318+
319+ Log .v (LOG_TAG , String .format (
320+ "computeUprightRotation(): deviceDegrees=%d, sensorOrientation=%d, result=%d" ,
321+ deviceDegrees , sensorOrientation , uprightRotation ));
322+
323+ return uprightRotation ;
324+ }
325+
231326 void emitFrame (byte [] buffer , int width , int height , int rotation , boolean isGrayscale ) {
232327 Activity activity = getActivity ();
233328
@@ -410,12 +505,17 @@ private void onImageAvailable(ImageReader reader) {
410505 }
411506 }
412507
413- if (rotation != 0 ) {
508+ // When auto_upright is enabled, derive the required rotation from the
509+ // camera sensor orientation and the live device orientation rather than
510+ // using the fixed value set by the caller.
511+ int effectiveRotation = autoUpright ? computeUprightRotation () : rotation ;
512+
513+ if (effectiveRotation != 0 ) {
414514 RotationResult result ;
415515 if (isGrayscale ) {
416- result = rotateGray (output , width , height , rotation );
516+ result = rotateGray (output , width , height , effectiveRotation );
417517 } else {
418- result = rotateRGBA (output , width , height , rotation );
518+ result = rotateRGBA (output , width , height , effectiveRotation );
419519 }
420520
421521 output = result .buffer ;
@@ -444,7 +544,7 @@ private void onImageAvailable(ImageReader reader) {
444544 }
445545
446546 if (running ) {
447- emitFrame (output , width , height , rotation , isGrayscale );
547+ emitFrame (output , width , height , effectiveRotation , isGrayscale );
448548 }
449549
450550 image .close ();
0 commit comments