Skip to content

Commit a4607f4

Browse files
committed
Merge pull request godotengine#111871 from shiena/fix/camera_android.2
Android: Stabilize camera lifecycle handling
2 parents 0caf805 + 4483871 commit a4607f4

File tree

6 files changed

+283
-28
lines changed

6 files changed

+283
-28
lines changed

modules/camera/camera_android.cpp

Lines changed: 211 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
#include "core/os/os.h"
3434
#include "platform/android/display_server_android.h"
35+
#include "platform/android/java_godot_io_wrapper.h"
36+
#include "platform/android/os_android.h"
3537

3638
//////////////////////////////////////////////////////////////////////////
3739
// Helper functions
@@ -93,23 +95,75 @@ CameraFeedAndroid::~CameraFeedAndroid() {
9395
}
9496
}
9597

98+
void CameraFeedAndroid::refresh_camera_metadata() {
99+
ERR_FAIL_NULL_MSG(manager, vformat("Camera %s: Cannot refresh metadata, manager is null.", camera_id));
100+
101+
if (metadata != nullptr) {
102+
ACameraMetadata_free(metadata);
103+
metadata = nullptr;
104+
}
105+
106+
camera_status_t status = ACameraManager_getCameraCharacteristics(manager, camera_id.utf8().get_data(), &metadata);
107+
if (status != ACAMERA_OK || metadata == nullptr) {
108+
ERR_FAIL_MSG(vformat("Camera %s: Failed to refresh metadata (status: %d).", camera_id, status));
109+
}
110+
111+
ACameraMetadata_const_entry orientation_entry;
112+
status = ACameraMetadata_getConstEntry(metadata, ACAMERA_SENSOR_ORIENTATION, &orientation_entry);
113+
if (status == ACAMERA_OK) {
114+
orientation = orientation_entry.data.i32[0];
115+
print_verbose(vformat("Camera %s: Orientation updated to %d.", camera_id, orientation));
116+
} else {
117+
ERR_PRINT(vformat("Camera %s: Failed to get sensor orientation after refresh (status: %d).", camera_id, status));
118+
}
119+
120+
formats.clear();
121+
_add_formats();
122+
123+
print_verbose(vformat("Camera %s: Metadata refreshed successfully.", camera_id));
124+
}
125+
96126
void CameraFeedAndroid::_set_rotation() {
97-
int display_rotation = DisplayServerAndroid::get_singleton()->get_display_rotation();
98-
// reverse rotation
99-
switch (display_rotation) {
100-
case 90:
101-
display_rotation = 270;
102-
break;
103-
case 270:
104-
display_rotation = 90;
105-
break;
106-
default:
107-
break;
127+
if (!metadata) {
128+
print_verbose(vformat("Camera %s: Metadata is null in _set_rotation, attempting refresh.", camera_id));
129+
refresh_camera_metadata();
108130
}
109131

110-
int sign = position == CameraFeed::FEED_FRONT ? 1 : -1;
111-
float imageRotation = (orientation - display_rotation * sign + 360) % 360;
112-
transform.set_rotation(real_t(Math::deg_to_rad(imageRotation)));
132+
float image_rotation = 0.0f;
133+
std::optional<int> result;
134+
135+
if (metadata) {
136+
CameraRotationParams params;
137+
params.sensor_orientation = orientation;
138+
params.camera_facing = (position == CameraFeed::FEED_FRONT) ? CameraFacing::FRONT : CameraFacing::BACK;
139+
params.display_rotation = get_app_orientation();
140+
141+
result = calculate_rotation(params);
142+
} else {
143+
ERR_PRINT(vformat("Camera %s: Cannot update rotation, metadata unavailable after refresh, using fallback.", camera_id));
144+
}
145+
146+
if (result.has_value()) {
147+
image_rotation = static_cast<float>(result.value());
148+
} else {
149+
int display_rotation = DisplayServerAndroid::get_singleton()->get_display_rotation();
150+
switch (display_rotation) {
151+
case 90:
152+
display_rotation = 270;
153+
break;
154+
case 270:
155+
display_rotation = 90;
156+
break;
157+
default:
158+
break;
159+
}
160+
161+
int sign = position == CameraFeed::FEED_FRONT ? 1 : -1;
162+
image_rotation = (orientation - display_rotation * sign + 360) % 360;
163+
}
164+
165+
transform = Transform2D();
166+
transform = transform.rotated(Math::deg_to_rad(image_rotation));
113167
}
114168

115169
void CameraFeedAndroid::_add_formats() {
@@ -142,7 +196,9 @@ void CameraFeedAndroid::_add_formats() {
142196
}
143197

144198
bool CameraFeedAndroid::activate_feed() {
145-
ERR_FAIL_COND_V_MSG(selected_format == -1, false, "CameraFeed format needs to be set before activating.");
199+
ERR_FAIL_COND_V_MSG(formats.is_empty(), false, "No camera formats available.");
200+
ERR_FAIL_INDEX_V_MSG(selected_format, formats.size(), false,
201+
vformat("CameraFeed format needs to be set before activating. Selected format index: %d (formats size: %d)", selected_format, formats.size()));
146202
if (is_active()) {
147203
deactivate_feed();
148204
};
@@ -278,11 +334,52 @@ Array CameraFeedAndroid::get_formats() const {
278334

279335
CameraFeed::FeedFormat CameraFeedAndroid::get_format() const {
280336
CameraFeed::FeedFormat feed_format = {};
281-
return selected_format == -1 ? feed_format : formats[selected_format];
337+
ERR_FAIL_INDEX_V_MSG(selected_format, formats.size(), feed_format,
338+
vformat("Invalid format index: %d (formats size: %d)", selected_format, formats.size()));
339+
return formats[selected_format];
340+
}
341+
342+
void CameraFeedAndroid::handle_pause() {
343+
if (is_active()) {
344+
was_active_before_pause = true;
345+
print_verbose(vformat("Camera %s: Pausing (was active).", camera_id));
346+
deactivate_feed();
347+
} else {
348+
was_active_before_pause = false;
349+
}
350+
}
351+
352+
void CameraFeedAndroid::handle_resume() {
353+
if (was_active_before_pause) {
354+
print_verbose(vformat("Camera %s: Resuming.", camera_id));
355+
activate_feed();
356+
was_active_before_pause = false;
357+
}
358+
}
359+
360+
void CameraFeedAndroid::handle_rotation_change() {
361+
if (!is_active()) {
362+
return;
363+
}
364+
365+
print_verbose(vformat("Camera %s: Handling rotation change.", camera_id));
366+
refresh_camera_metadata();
367+
_set_rotation();
282368
}
283369

284370
void CameraFeedAndroid::onImage(void *context, AImageReader *p_reader) {
285371
CameraFeedAndroid *feed = static_cast<CameraFeedAndroid *>(context);
372+
373+
MutexLock lock(feed->callback_mutex);
374+
375+
if (!feed->is_active()) {
376+
AImage *pending_image = nullptr;
377+
if (AImageReader_acquireNextImage(p_reader, &pending_image) == AMEDIA_OK) {
378+
AImage_delete(pending_image);
379+
}
380+
return;
381+
}
382+
286383
Vector<uint8_t> data_y = feed->data_y;
287384
Vector<uint8_t> data_uv = feed->data_uv;
288385
Ref<Image> image_y = feed->image_y;
@@ -363,8 +460,17 @@ void CameraFeedAndroid::onImage(void *context, AImageReader *p_reader) {
363460
return;
364461
}
365462

366-
// Rotation
367-
feed->_set_rotation();
463+
if (!feed->formats.is_empty()) {
464+
if (feed->metadata != nullptr) {
465+
feed->_set_rotation();
466+
} else {
467+
print_verbose(vformat("Camera %s: Metadata invalidated in onImage, attempting refresh.", feed->camera_id));
468+
feed->refresh_camera_metadata();
469+
if (feed->metadata != nullptr && !feed->formats.is_empty()) {
470+
feed->_set_rotation();
471+
}
472+
}
473+
}
368474

369475
// Release image
370476
AImage_delete(image);
@@ -389,19 +495,27 @@ void CameraFeedAndroid::deactivate_feed() {
389495
session = nullptr;
390496
}
391497

392-
if (request != nullptr) {
393-
ACaptureRequest_free(request);
394-
request = nullptr;
395-
}
396-
397498
if (reader != nullptr) {
398-
AImageReader_delete(reader);
399-
reader = nullptr;
499+
AImageReader_setImageListener(reader, nullptr);
400500
}
401501

402-
if (device != nullptr) {
403-
ACameraDevice_close(device);
404-
device = nullptr;
502+
{
503+
MutexLock lock(callback_mutex);
504+
505+
if (device != nullptr) {
506+
ACameraDevice_close(device);
507+
device = nullptr;
508+
}
509+
510+
if (reader != nullptr) {
511+
AImageReader_delete(reader);
512+
reader = nullptr;
513+
}
514+
515+
if (request != nullptr) {
516+
ACaptureRequest_free(request);
517+
request = nullptr;
518+
}
405519
}
406520
}
407521

@@ -505,6 +619,75 @@ void CameraAndroid::set_monitoring_feeds(bool p_monitoring_feeds) {
505619
}
506620
}
507621

622+
void CameraAndroid::handle_pause() {
623+
for (int i = 0; i < feeds.size(); i++) {
624+
Ref<CameraFeedAndroid> feed = feeds[i];
625+
if (feed.is_valid()) {
626+
feed->handle_pause();
627+
}
628+
}
629+
}
630+
631+
void CameraAndroid::handle_resume() {
632+
for (int i = 0; i < feeds.size(); i++) {
633+
Ref<CameraFeedAndroid> feed = feeds[i];
634+
if (feed.is_valid()) {
635+
feed->handle_resume();
636+
}
637+
}
638+
}
639+
640+
void CameraAndroid::handle_rotation_change() {
641+
for (int i = 0; i < feeds.size(); i++) {
642+
Ref<CameraFeedAndroid> feed = feeds[i];
643+
if (feed.is_valid()) {
644+
feed->handle_rotation_change();
645+
}
646+
}
647+
}
648+
508649
CameraAndroid::~CameraAndroid() {
509650
remove_all_feeds();
510651
}
652+
653+
std::optional<int> CameraFeedAndroid::calculate_rotation(const CameraRotationParams &p_params) {
654+
if (p_params.sensor_orientation < 0 || p_params.sensor_orientation > 270 || p_params.sensor_orientation % 90 != 0) {
655+
return std::nullopt;
656+
}
657+
658+
int rotation_angle = p_params.sensor_orientation - p_params.display_rotation;
659+
return normalize_angle(rotation_angle);
660+
}
661+
662+
int CameraFeedAndroid::normalize_angle(int p_angle) {
663+
while (p_angle < 0) {
664+
p_angle += 360;
665+
}
666+
return p_angle % 360;
667+
}
668+
669+
int CameraFeedAndroid::get_display_rotation() {
670+
return DisplayServerAndroid::get_singleton()->get_display_rotation();
671+
}
672+
673+
int CameraFeedAndroid::get_app_orientation() {
674+
GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
675+
ERR_FAIL_NULL_V(godot_io_java, 0);
676+
677+
int orientation = godot_io_java->get_screen_orientation();
678+
switch (orientation) {
679+
case 0: // SCREEN_LANDSCAPE
680+
return 90;
681+
case 1: // SCREEN_PORTRAIT
682+
return 0;
683+
case 2: // SCREEN_REVERSE_LANDSCAPE
684+
return 270;
685+
case 3: // SCREEN_REVERSE_PORTRAIT
686+
return 180;
687+
case 4: // SCREEN_SENSOR_LANDSCAPE
688+
case 5: // SCREEN_SENSOR_PORTRAIT
689+
case 6: // SCREEN_SENSOR
690+
default:
691+
return get_display_rotation();
692+
}
693+
}

modules/camera/camera_android.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@
3838
#include <camera/NdkCameraManager.h>
3939
#include <camera/NdkCameraMetadataTags.h>
4040
#include <media/NdkImageReader.h>
41+
#include <optional>
42+
43+
enum class CameraFacing {
44+
BACK = 0,
45+
FRONT = 1,
46+
};
47+
48+
struct CameraRotationParams {
49+
int sensor_orientation;
50+
CameraFacing camera_facing;
51+
int display_rotation;
52+
};
4153

4254
class CameraFeedAndroid : public CameraFeed {
4355
GDSOFTCLASS(CameraFeedAndroid, CameraFeed);
@@ -56,9 +68,16 @@ class CameraFeedAndroid : public CameraFeed {
5668
AImageReader *reader = nullptr;
5769
ACameraCaptureSession *session = nullptr;
5870
ACaptureRequest *request = nullptr;
71+
Mutex callback_mutex;
72+
bool was_active_before_pause = false;
5973

6074
void _add_formats();
6175
void _set_rotation();
76+
void refresh_camera_metadata();
77+
static std::optional<int> calculate_rotation(const CameraRotationParams &p_params);
78+
static int normalize_angle(int p_angle);
79+
static int get_display_rotation();
80+
static int get_app_orientation();
6281

6382
static void onError(void *context, ACameraDevice *p_device, int error);
6483
static void onDisconnected(void *context, ACameraDevice *p_device);
@@ -74,6 +93,9 @@ class CameraFeedAndroid : public CameraFeed {
7493
bool set_format(int p_index, const Dictionary &p_parameters) override;
7594
Array get_formats() const override;
7695
FeedFormat get_format() const override;
96+
void handle_pause();
97+
void handle_resume();
98+
void handle_rotation_change();
7799

78100
CameraFeedAndroid(ACameraManager *manager, ACameraMetadata *metadata, const char *id,
79101
CameraFeed::FeedPosition position, int32_t orientation);
@@ -91,6 +113,9 @@ class CameraAndroid : public CameraServer {
91113

92114
public:
93115
void set_monitoring_feeds(bool p_monitoring_feeds) override;
116+
void handle_pause();
117+
void handle_resume();
118+
void handle_rotation_change();
94119

95120
~CameraAndroid();
96121
};

platform/android/java/lib/src/org/godotengine/godot/Godot.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ class Godot private constructor(val context: Context) {
170170
*/
171171
private var renderViewInitialized = false
172172
private var primaryHost: GodotHost? = null
173+
private var currentConfig = context.resources.configuration
173174

174175
/**
175176
* Tracks whether we're in the RESUMED lifecycle state.
@@ -757,6 +758,13 @@ class Godot private constructor(val context: Context) {
757758
darkMode = newDarkMode
758759
GodotLib.onNightModeChanged()
759760
}
761+
762+
if (currentConfig.orientation != newConfig.orientation) {
763+
runOnRenderThread {
764+
GodotLib.onScreenRotationChange()
765+
}
766+
}
767+
currentConfig = newConfig
760768
}
761769

762770
/**

platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ public static void calldeferred(long p_id, String p_method, Object[] p_params) {
295295
*/
296296
public static native void onRendererPaused();
297297

298+
/**
299+
* Invoked when the screen orientation changes.
300+
*/
301+
static native void onScreenRotationChange();
302+
298303
/**
299304
* @return true if input must be dispatched from the render thread. If false, input is
300305
* dispatched from the UI thread.

0 commit comments

Comments
 (0)