Skip to content

πŸ› CameraOrientation.fromDegrees() has LEFT/RIGHT swapped on Android β€” doesn't round-trip its own .degrees getterΒ #4096

Description

@hmelonjp

Prerequisites

Reproduction

https://github.com/Melon-Technologies/vc-camera-orientation-fromdegrees-repro

Steps to reproduce

  1. Clone https://github.com/Melon-Technologies/vc-camera-orientation-fromdegrees-repro
  2. kotlinc camera_orientation_fromdegrees_test.kt -include-runtime -d test.jar
  3. kotlin -classpath test.jar Camera_orientation_fromdegrees_testKt
  4. Observe: LEFT.degrees=270 -> fromDegrees(270)=RIGHT and RIGHT.degrees=90 -> fromDegrees(90)=LEFT

What did you expect to happen?

CameraOrientation.Companion.fromDegrees(degrees) should be the inverse of CameraOrientation.degrees β€” i.e. fromDegrees(o.degrees) == o for every CameraOrientation value.

What actually happened?

It only round-trips for UP and DOWN. For LEFT and RIGHT it returns the wrong value:

val CameraOrientation.degrees: Int
  get() = when (this) {
    UP -> 0; DOWN -> 180; LEFT -> 270; RIGHT -> 90
  }

fun CameraOrientation.Companion.fromDegrees(degrees: Int): CameraOrientation {
  val normalizedDegrees = normalizeDegrees(degrees)
  return when (normalizedDegrees) {
    in 45..135 -> CameraOrientation.LEFT    // should be RIGHT (RIGHT.degrees == 90, inside this bucket)
    in 135..225 -> CameraOrientation.DOWN
    in 225..315 -> CameraOrientation.RIGHT  // should be LEFT (LEFT.degrees == 270, inside this bucket)
    else -> CameraOrientation.UP
  }
}

fromDegrees(90) returns LEFT instead of RIGHT; fromDegrees(270) returns RIGHT instead of LEFT. The LEFT/RIGHT buckets are swapped relative to .degrees' own mapping.

Why it matters: ImageProxy.orientation calls fromDegrees(imageInfo.rotationDegrees) to label every incoming camera frame:

val ImageProxy.orientation: CameraOrientation
  get() {
    val degrees = imageInfo.rotationDegrees
    return CameraOrientation.fromDegrees(degrees)
  }

A front-facing sensor held in portrait commonly reports rotationDegrees = 270. Pre-fix, fromDegrees(270) returns RIGHT instead of the self-consistent LEFT β€” a 180-degree-equivalent mislabeling. That wrong orientation value feeds react-native-vision-camera-resizer's rotationDegrees push constant downstream.

Observed live: on a real Android device (Samsung Galaxy A54 5G, Android 15), a detected face's bounding box was drawn over an unrelated part of the frame instead of the actual face, while the identical JS/native pipeline on iOS (iPhone 12, iOS 16.2) drew it correctly β€” which is what pointed at this Android-only orientation mapping rather than the detector model itself.

Fix

Swap the LEFT/RIGHT buckets to match .degrees:

 fun CameraOrientation.Companion.fromDegrees(degrees: Int): CameraOrientation {
   val normalizedDegrees = normalizeDegrees(degrees)
   return when (normalizedDegrees) {
-    in 45..135 -> CameraOrientation.LEFT
+    in 45..135 -> CameraOrientation.RIGHT
     in 135..225 -> CameraOrientation.DOWN
-    in 225..315 -> CameraOrientation.RIGHT
+    in 225..315 -> CameraOrientation.LEFT
     else -> CameraOrientation.UP
   }
 }

We've been running this as a local patch-package patch to react-native-vision-camera@5.1.1 in production.

Affected platforms

Android (device)

Device(s) affected

Observed on Samsung Galaxy A54 5G (Android 15). Root cause confirmed by direct source inspection and a pure JVM round-trip test β€” the bug is deterministic enum-mapping logic independent of specific hardware.

VisionCamera version

5.1.1

React Native version

0.86

React Native architecture

New Architecture (Fabric / bridgeless)

Features being used

  • Preview
  • Photo capture
  • Video capture
  • Frame Processors (worklets)
  • Skia Frame Processors
  • Code/Barcode Scanner
  • Location metadata
  • Multi-cam
  • Depth data
  • HDR / custom dynamic range
  • Custom format / FPS / resolution

Relevant logs / stack trace

This is a logic/correctness bug, not a crash β€” there is no stack trace. See the reproduction repo's console output:


CameraOrientation.fromDegrees round-trip test (issue: LEFT/RIGHT swapped)

BUGGY fromDegrees (pristine upstream code):
  UP.degrees=0 -> fromDegreesBuggy(0)=UP  OK
  DOWN.degrees=180 -> fromDegreesBuggy(180)=DOWN  OK
  LEFT.degrees=270 -> fromDegreesBuggy(270)=RIGHT  MISMATCH (bug reproduced)
  RIGHT.degrees=90 -> fromDegreesBuggy(90)=LEFT  MISMATCH (bug reproduced)
  -> all orientations round-trip correctly: NO -- bug confirmed

FIXED fromDegrees (LEFT/RIGHT buckets swapped):
  UP.degrees=0 -> fromDegreesFixed(0)=UP  OK
  DOWN.degrees=180 -> fromDegreesFixed(180)=DOWN  OK
  LEFT.degrees=270 -> fromDegreesFixed(270)=LEFT  OK
  RIGHT.degrees=90 -> fromDegreesFixed(90)=RIGHT  OK
  -> all orientations round-trip correctly: yes, bug is fixed

RESULT: PASS -- bug reproduced on unpatched code, confirmed fixed on patched code.

Additional context

No response

Submission

  • The reproduction I linked is either (preferred) a PR against this repo that adds a failing harness test following the harness-tests README, or (fallback) a public repo that reproduces the bug on a fresh clone. I understand the issue will be closed without one.
  • I pasted logs as text (not screenshots).
  • I wrote this report in my own words. I did not paste AI-generated descriptions of the bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions