Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions addon/src/main/model/CameraInfo.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class_name CameraInfo extends RefCounted
const DATA_CAMERA_ID_PROPERTY := &"camera_id"
const DATA_IS_FRONT_FACING_PROPERTY := &"is_front_facing"
const DATA_OUTPUT_SIZES_PROPERTY := &"output_sizes"
const DATA_SENSOR_ORIENTATION_PROPERTY := &"sensor_orientation"

var _data: Dictionary

Expand All @@ -30,3 +31,11 @@ func get_output_sizes() -> Array[FrameSize]:
__sizes.append(FrameSize.new(__size_dict))

return __sizes


## Returns the clockwise angle in degrees (0, 90, 180, or 270) that the camera
## sensor image must be rotated to be upright when the device is held in its
## natural (portrait) orientation. Defaults to [code]0[/code] when the value
## is unavailable.
func get_sensor_orientation() -> int:
return _data[DATA_SENSOR_ORIENTATION_PROPERTY] if _data.has(DATA_SENSOR_ORIENTATION_PROPERTY) else 0
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class CameraInfo {
private static final String DATA_CAMERA_ID_PROPERTY = "camera_id";
private static final String DATA_IS_FRONT_FACING_PROPERTY = "is_front_facing";
private static final String DATA_OUTPUT_SIZES_PROPERTY = "output_sizes";
private static final String DATA_SENSOR_ORIENTATION_PROPERTY = "sensor_orientation";

private String cameraId;
private CameraCharacteristics characteristics;
Expand Down Expand Up @@ -56,6 +57,13 @@ public Dictionary buildRawData() {

dict.put(DATA_OUTPUT_SIZES_PROPERTY, dictList.toArray());

// SENSOR_ORIENTATION is the clockwise angle (0, 90, 180, or 270 degrees) that
// the camera image must be rotated to be upright when the device is held in its
// natural (portrait) orientation. Null-safe: defaults to 0 when the value is
// unavailable (e.g. on emulators or in unit tests).
Integer sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
dict.put(DATA_SENSOR_ORIENTATION_PROPERTY, sensorOrientation != null ? sensorOrientation : 0);

return dict;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
* Unit tests for {@link CameraInfo}.
*
* <p>All Android framework classes are mocked with Mockito — no real Android runtime is required.
*
* <p>{@link CameraCharacteristics#get} is called three times in {@link CameraInfo#buildRawData}:
* <ol>
* <li>{@link CameraCharacteristics#LENS_FACING}</li>
* <li>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP}</li>
* <li>{@link CameraCharacteristics#SENSOR_ORIENTATION}</li>
* </ol>
* Each mock helper stubs these three return values in order via chained {@code thenReturn} calls.
*/
@ExtendWith(MockitoExtension.class)
public class CameraInfoTest {
Expand All @@ -40,41 +48,76 @@ private static Size size(int w, int h) {
return s;
}

/**
* Builds a {@link CameraCharacteristics} mock whose three sequential {@code get()} calls
* return {@code facing}, {@code map}, and {@code sensorOrientation} respectively.
*/
@SuppressWarnings("unchecked")
private CameraCharacteristics mockCharacteristics(int facing, Size[] outputSizes) {
private CameraCharacteristics mockCharacteristics(int facing, Size[] outputSizes,
int sensorOrientation) {
CameraCharacteristics chars = mock(CameraCharacteristics.class);
StreamConfigurationMap map = mock(StreamConfigurationMap.class);

// Using any() avoids the raw class literal warning,
// and SuppressWarnings handles the generic return stubbing.
when(chars.get(any()))
.thenReturn(facing)
.thenReturn(map);
.thenReturn(map)
.thenReturn(sensorOrientation);

when(map.getOutputSizes(ImageFormat.YUV_420_888)).thenReturn(outputSizes);

return chars;
}

/**
* Variant with no {@link StreamConfigurationMap} (second {@code get()} returns null).
*/
@SuppressWarnings("unchecked")
private CameraCharacteristics mockCharacteristicsNoMap(int facing) {
private CameraCharacteristics mockCharacteristicsNoMap(int facing, int sensorOrientation) {
CameraCharacteristics chars = mock(CameraCharacteristics.class);

when(chars.get(any()))
.thenReturn(facing)
.thenReturn(null); // no map
.thenReturn(null) // no map
.thenReturn(sensorOrientation);

return chars;
}

/**
* Variant with a null lens-facing value (first {@code get()} returns null).
*/
@SuppressWarnings("unchecked")
private CameraCharacteristics mockCharacteristicsNullFacing(Size[] outputSizes,
int sensorOrientation) {
CameraCharacteristics chars = mock(CameraCharacteristics.class);
StreamConfigurationMap map = mock(StreamConfigurationMap.class);

when(chars.get(any()))
.thenReturn(null) // null facing
.thenReturn(map)
.thenReturn(sensorOrientation);

when(map.getOutputSizes(ImageFormat.YUV_420_888)).thenReturn(outputSizes);

return chars;
}

/**
* Variant where {@link CameraCharacteristics#SENSOR_ORIENTATION} is null (third
* {@code get()} returns null), exercising the null-safety default of 0.
*/
@SuppressWarnings("unchecked")
private CameraCharacteristics mockCharacteristicsNullFacing(Size[] outputSizes) {
private CameraCharacteristics mockCharacteristicsNullSensorOrientation(int facing,
Size[] outputSizes) {
CameraCharacteristics chars = mock(CameraCharacteristics.class);
StreamConfigurationMap map = mock(StreamConfigurationMap.class);

when(chars.get(any()))
.thenReturn(null) // null facing
.thenReturn(map);
.thenReturn(facing)
.thenReturn(map)
.thenReturn(null); // null sensor orientation

when(map.getOutputSizes(ImageFormat.YUV_420_888)).thenReturn(outputSizes);

Expand All @@ -86,14 +129,14 @@ private CameraCharacteristics mockCharacteristicsNullFacing(Size[] outputSizes)
@Test
public void buildRawData_cameraIdBackCamera() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0]));
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0], 90));
assertEquals("0", info.buildRawData().get("camera_id"));
}

@Test
public void buildRawData_cameraIdFrontCamera() {
CameraInfo info = new CameraInfo("1",
mockCharacteristics(CameraCharacteristics.LENS_FACING_FRONT, new Size[0]));
mockCharacteristics(CameraCharacteristics.LENS_FACING_FRONT, new Size[0], 270));
assertEquals("1", info.buildRawData().get("camera_id"));
}

Expand All @@ -102,21 +145,21 @@ public void buildRawData_cameraIdFrontCamera() {
@Test
public void buildRawData_backCamera_isFrontFacingFalse() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0]));
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0], 90));
assertFalse((Boolean) info.buildRawData().get("is_front_facing"));
}

@Test
public void buildRawData_frontCamera_isFrontFacingTrue() {
CameraInfo info = new CameraInfo("1",
mockCharacteristics(CameraCharacteristics.LENS_FACING_FRONT, new Size[0]));
mockCharacteristics(CameraCharacteristics.LENS_FACING_FRONT, new Size[0], 270));
assertTrue((Boolean) info.buildRawData().get("is_front_facing"));
}

@Test
public void buildRawData_nullFacing_isFrontFacingFalse() {
CameraInfo info = new CameraInfo("2",
mockCharacteristicsNullFacing(new Size[0]));
mockCharacteristicsNullFacing(new Size[0], 90));
assertFalse((Boolean) info.buildRawData().get("is_front_facing"));
}

Expand All @@ -125,7 +168,7 @@ public void buildRawData_nullFacing_isFrontFacingFalse() {
@Test
public void buildRawData_noOutputSizes_returnsEmptyArray() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0]));
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0], 90));
Object[] sizes = (Object[]) info.buildRawData().get("output_sizes");
assertNotNull(sizes);
assertEquals(0, sizes.length);
Expand All @@ -135,7 +178,7 @@ public void buildRawData_noOutputSizes_returnsEmptyArray() {
public void buildRawData_twoOutputSizes_returnsTwoEntries() {
Size[] sizes = {size(1920, 1080), size(1280, 720)};
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, sizes));
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, sizes, 90));
Object[] result = (Object[]) info.buildRawData().get("output_sizes");
assertEquals(2, result.length);
}
Expand All @@ -144,7 +187,7 @@ public void buildRawData_twoOutputSizes_returnsTwoEntries() {
public void buildRawData_outputSizesContainWidthAndHeight() {
Size[] sizes = {size(640, 480)};
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, sizes));
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, sizes, 90));
Object[] result = (Object[]) info.buildRawData().get("output_sizes");
Dictionary sizeDict = (Dictionary) result[0];
assertEquals(640, sizeDict.get("width"));
Expand All @@ -154,7 +197,7 @@ public void buildRawData_outputSizesContainWidthAndHeight() {
@Test
public void buildRawData_nullStreamConfigMap_returnsEmptyOutputSizes() {
CameraInfo info = new CameraInfo("0",
mockCharacteristicsNoMap(CameraCharacteristics.LENS_FACING_BACK));
mockCharacteristicsNoMap(CameraCharacteristics.LENS_FACING_BACK, 90));
Object[] result = (Object[]) info.buildRawData().get("output_sizes");
assertNotNull(result);
assertEquals(0, result.length);
Expand All @@ -163,21 +206,71 @@ public void buildRawData_nullStreamConfigMap_returnsEmptyOutputSizes() {
@Test
public void buildRawData_nullOutputSizesArray_returnsEmptyArray() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, null));
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, null, 90));
Object[] result = (Object[]) info.buildRawData().get("output_sizes");
assertNotNull(result);
assertEquals(0, result.length);
}

// -- sensor_orientation ------------------------------------------------

@Test
public void buildRawData_backCamera_sensorOrientation90() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0], 90));
assertEquals(90, info.buildRawData().get("sensor_orientation"));
}

@Test
public void buildRawData_frontCamera_sensorOrientation270() {
CameraInfo info = new CameraInfo("1",
mockCharacteristics(CameraCharacteristics.LENS_FACING_FRONT, new Size[0], 270));
assertEquals(270, info.buildRawData().get("sensor_orientation"));
}

@Test
public void buildRawData_sensorOrientation0_returnsZero() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0], 0));
assertEquals(0, info.buildRawData().get("sensor_orientation"));
}

@Test
public void buildRawData_sensorOrientation180_returns180() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0], 180));
assertEquals(180, info.buildRawData().get("sensor_orientation"));
}

@Test
public void buildRawData_nullSensorOrientation_defaultsToZero() {
CameraInfo info = new CameraInfo("0",
mockCharacteristicsNullSensorOrientation(
CameraCharacteristics.LENS_FACING_BACK, new Size[0]));
assertEquals(0, info.buildRawData().get("sensor_orientation"));
}

@Test
public void buildRawData_nullSensorOrientation_doesNotThrow() {
CameraInfo info = new CameraInfo("0",
mockCharacteristicsNullSensorOrientation(
CameraCharacteristics.LENS_FACING_BACK, new Size[0]));
// Must not throw; must contain the key with the default value
Dictionary d = info.buildRawData();
assertTrue(d.containsKey("sensor_orientation"));
assertEquals(0, d.get("sensor_orientation"));
}

// -- key completeness -------------------------------------------------

@Test
public void buildRawData_containsAllExpectedKeys() {
CameraInfo info = new CameraInfo("0",
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0]));
mockCharacteristics(CameraCharacteristics.LENS_FACING_BACK, new Size[0], 90));
Dictionary d = info.buildRawData();
assertTrue(d.containsKey("camera_id"));
assertTrue(d.containsKey("is_front_facing"));
assertTrue(d.containsKey("output_sizes"));
assertTrue(d.containsKey("sensor_orientation"));
}
}
2 changes: 1 addition & 1 deletion common/config/plugin.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
pluginNodeName=NativeCamera
pluginModuleName=native_camera
pluginPackage=org.godotengine.plugin.nativecamera
pluginVersion=2.0
pluginVersion=3.0
14 changes: 12 additions & 2 deletions demo/Main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,19 @@ func _on_get_button_pressed() -> void:
var __cameras_array = camera_node.get_all_cameras()
for __camera_info in __cameras_array:
_cameras[__camera_info.get_camera_id()] = __camera_info
print("Available size:")
_print_to_screen(
(
"Camera %s: front-facing?=%s, sensorOrientation=%d"
% [
__camera_info.get_camera_id(),
str(__camera_info.is_front_facing()),
__camera_info.get_sensor_orientation()
]
)
)
_print_to_screen("Available output sizes:")
for __size: FrameSize in __camera_info.get_output_sizes():
print("[%d,%d]" % [__size.get_width(), __size.get_height()])
_print_to_screen("[%d,%d]" % [__size.get_width(), __size.get_height()])
cameras_option_button.add_item(__camera_info.get_camera_id())

if not __cameras_array.is_empty():
Expand Down
1 change: 1 addition & 0 deletions demo/addons/GMPShared/FrameInfo.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://3nivbhilp5kc
1 change: 1 addition & 0 deletions demo/addons/GMPShared/FrameSize.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://0nvtl37flq1c
2 changes: 1 addition & 1 deletion demo/addons/NativeCameraPlugin/model/CameraInfo.gd.uid
Original file line number Diff line number Diff line change
@@ -1 +1 @@
uid://cpkga3i4ky2d0
uid://cltabpju1wo4a
2 changes: 1 addition & 1 deletion demo/addons/NativeCameraPlugin/model/FeedRequest.gd.uid
Original file line number Diff line number Diff line change
@@ -1 +1 @@
uid://bl58f6bhh4jp5
uid://dy36kbvkhhp7q
1 change: 0 additions & 1 deletion demo/addons/NativeCameraPlugin/model/FrameInfo.gd.uid

This file was deleted.

1 change: 0 additions & 1 deletion demo/addons/NativeCameraPlugin/model/FrameSize.gd.uid

This file was deleted.

1 change: 1 addition & 0 deletions demo/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]

config/name="Native Camera Demo"
config/tags=PackedStringArray("android", "camera", "demo", "ios", "plugin")
run/main_scene="uid://daro5x6n3pd26"
config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://assets/native-camera-android.png"
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ Encapsulates camera metadata provided by the mobile OS.
* `get_camera_id() -> String`
* `is_front_facing() -> bool`
* `get_output_sizes() -> Array[FrameSize]`
* `get_sensor_orientation() -> int`
* Returns the clockwise angle in degrees (`0`, `90`, `180`, or `270`) that the camera sensor image must be rotated to be upright when the device is held in its natural (portrait) orientation. This value is a fixed hardware property of the camera and does not change with device rotation. Defaults to `0` when the value is unavailable (e.g. on emulators). Useful when implementing custom frame processing that needs to account for sensor mounting angle independently of device orientation.

### <img src="https://raw.githubusercontent.com/godot-mobile-plugins/godot-native-camera/main/addon/src/main/icon.png" width="16"> FeedRequest

Expand Down
Loading