CameraOrientation.fromDegrees() has LEFT/RIGHT swapped on Android — doesn't round-trip its own .degrees getter

Author: hmelonjpCreated Jul 26, 2026Updated Aug 20, 2026
Labels🐛 bug

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:

kotlin
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:

kotlin
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:

diff
 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 [email protected] 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

bash
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.

Source: mrousavy/react-native-vision-camera