diff --git a/modules/camera/camera_android.cpp b/modules/camera/camera_android.cpp index 63c58f61343..1798078c1f4 100644 --- a/modules/camera/camera_android.cpp +++ b/modules/camera/camera_android.cpp @@ -32,6 +32,8 @@ #include "core/os/os.h" #include "platform/android/display_server_android.h" +#include "platform/android/java_godot_io_wrapper.h" +#include "platform/android/os_android.h" ////////////////////////////////////////////////////////////////////////// // Helper functions @@ -93,23 +95,75 @@ CameraFeedAndroid::~CameraFeedAndroid() { } } -void CameraFeedAndroid::_set_rotation() { - int display_rotation = DisplayServerAndroid::get_singleton()->get_display_rotation(); - // reverse rotation - switch (display_rotation) { - case 90: - display_rotation = 270; - break; - case 270: - display_rotation = 90; - break; - default: - break; +void CameraFeedAndroid::refresh_camera_metadata() { + ERR_FAIL_NULL_MSG(manager, vformat("Camera %s: Cannot refresh metadata, manager is null.", camera_id)); + + if (metadata != nullptr) { + ACameraMetadata_free(metadata); + metadata = nullptr; } - int sign = position == CameraFeed::FEED_FRONT ? 1 : -1; - float imageRotation = (orientation - display_rotation * sign + 360) % 360; - transform.set_rotation(real_t(Math::deg_to_rad(imageRotation))); + camera_status_t status = ACameraManager_getCameraCharacteristics(manager, camera_id.utf8().get_data(), &metadata); + if (status != ACAMERA_OK || metadata == nullptr) { + ERR_FAIL_MSG(vformat("Camera %s: Failed to refresh metadata (status: %d).", camera_id, status)); + } + + ACameraMetadata_const_entry orientation_entry; + status = ACameraMetadata_getConstEntry(metadata, ACAMERA_SENSOR_ORIENTATION, &orientation_entry); + if (status == ACAMERA_OK) { + orientation = orientation_entry.data.i32[0]; + print_verbose(vformat("Camera %s: Orientation updated to %d.", camera_id, orientation)); + } else { + ERR_PRINT(vformat("Camera %s: Failed to get sensor orientation after refresh (status: %d).", camera_id, status)); + } + + formats.clear(); + _add_formats(); + + print_verbose(vformat("Camera %s: Metadata refreshed successfully.", camera_id)); +} + +void CameraFeedAndroid::_set_rotation() { + if (!metadata) { + print_verbose(vformat("Camera %s: Metadata is null in _set_rotation, attempting refresh.", camera_id)); + refresh_camera_metadata(); + } + + float image_rotation = 0.0f; + std::optional result; + + if (metadata) { + CameraRotationParams params; + params.sensor_orientation = orientation; + params.camera_facing = (position == CameraFeed::FEED_FRONT) ? CameraFacing::FRONT : CameraFacing::BACK; + params.display_rotation = get_app_orientation(); + + result = calculate_rotation(params); + } else { + ERR_PRINT(vformat("Camera %s: Cannot update rotation, metadata unavailable after refresh, using fallback.", camera_id)); + } + + if (result.has_value()) { + image_rotation = static_cast(result.value()); + } else { + int display_rotation = DisplayServerAndroid::get_singleton()->get_display_rotation(); + switch (display_rotation) { + case 90: + display_rotation = 270; + break; + case 270: + display_rotation = 90; + break; + default: + break; + } + + int sign = position == CameraFeed::FEED_FRONT ? 1 : -1; + image_rotation = (orientation - display_rotation * sign + 360) % 360; + } + + transform = Transform2D(); + transform = transform.rotated(Math::deg_to_rad(image_rotation)); } void CameraFeedAndroid::_add_formats() { @@ -142,7 +196,9 @@ void CameraFeedAndroid::_add_formats() { } bool CameraFeedAndroid::activate_feed() { - ERR_FAIL_COND_V_MSG(selected_format == -1, false, "CameraFeed format needs to be set before activating."); + ERR_FAIL_COND_V_MSG(formats.is_empty(), false, "No camera formats available."); + ERR_FAIL_INDEX_V_MSG(selected_format, formats.size(), false, + vformat("CameraFeed format needs to be set before activating. Selected format index: %d (formats size: %d)", selected_format, formats.size())); if (is_active()) { deactivate_feed(); }; @@ -278,11 +334,52 @@ Array CameraFeedAndroid::get_formats() const { CameraFeed::FeedFormat CameraFeedAndroid::get_format() const { CameraFeed::FeedFormat feed_format = {}; - return selected_format == -1 ? feed_format : formats[selected_format]; + ERR_FAIL_INDEX_V_MSG(selected_format, formats.size(), feed_format, + vformat("Invalid format index: %d (formats size: %d)", selected_format, formats.size())); + return formats[selected_format]; +} + +void CameraFeedAndroid::handle_pause() { + if (is_active()) { + was_active_before_pause = true; + print_verbose(vformat("Camera %s: Pausing (was active).", camera_id)); + deactivate_feed(); + } else { + was_active_before_pause = false; + } +} + +void CameraFeedAndroid::handle_resume() { + if (was_active_before_pause) { + print_verbose(vformat("Camera %s: Resuming.", camera_id)); + activate_feed(); + was_active_before_pause = false; + } +} + +void CameraFeedAndroid::handle_rotation_change() { + if (!is_active()) { + return; + } + + print_verbose(vformat("Camera %s: Handling rotation change.", camera_id)); + refresh_camera_metadata(); + _set_rotation(); } void CameraFeedAndroid::onImage(void *context, AImageReader *p_reader) { CameraFeedAndroid *feed = static_cast(context); + + MutexLock lock(feed->callback_mutex); + + if (!feed->is_active()) { + AImage *pending_image = nullptr; + if (AImageReader_acquireNextImage(p_reader, &pending_image) == AMEDIA_OK) { + AImage_delete(pending_image); + } + return; + } + Vector data_y = feed->data_y; Vector data_uv = feed->data_uv; Ref image_y = feed->image_y; @@ -363,8 +460,17 @@ void CameraFeedAndroid::onImage(void *context, AImageReader *p_reader) { return; } - // Rotation - feed->_set_rotation(); + if (!feed->formats.is_empty()) { + if (feed->metadata != nullptr) { + feed->_set_rotation(); + } else { + print_verbose(vformat("Camera %s: Metadata invalidated in onImage, attempting refresh.", feed->camera_id)); + feed->refresh_camera_metadata(); + if (feed->metadata != nullptr && !feed->formats.is_empty()) { + feed->_set_rotation(); + } + } + } // Release image AImage_delete(image); @@ -389,19 +495,27 @@ void CameraFeedAndroid::deactivate_feed() { session = nullptr; } - if (request != nullptr) { - ACaptureRequest_free(request); - request = nullptr; - } - if (reader != nullptr) { - AImageReader_delete(reader); - reader = nullptr; + AImageReader_setImageListener(reader, nullptr); } - if (device != nullptr) { - ACameraDevice_close(device); - device = nullptr; + { + MutexLock lock(callback_mutex); + + if (device != nullptr) { + ACameraDevice_close(device); + device = nullptr; + } + + if (reader != nullptr) { + AImageReader_delete(reader); + reader = nullptr; + } + + if (request != nullptr) { + ACaptureRequest_free(request); + request = nullptr; + } } } @@ -505,6 +619,75 @@ void CameraAndroid::set_monitoring_feeds(bool p_monitoring_feeds) { } } +void CameraAndroid::handle_pause() { + for (int i = 0; i < feeds.size(); i++) { + Ref feed = feeds[i]; + if (feed.is_valid()) { + feed->handle_pause(); + } + } +} + +void CameraAndroid::handle_resume() { + for (int i = 0; i < feeds.size(); i++) { + Ref feed = feeds[i]; + if (feed.is_valid()) { + feed->handle_resume(); + } + } +} + +void CameraAndroid::handle_rotation_change() { + for (int i = 0; i < feeds.size(); i++) { + Ref feed = feeds[i]; + if (feed.is_valid()) { + feed->handle_rotation_change(); + } + } +} + CameraAndroid::~CameraAndroid() { remove_all_feeds(); } + +std::optional CameraFeedAndroid::calculate_rotation(const CameraRotationParams &p_params) { + if (p_params.sensor_orientation < 0 || p_params.sensor_orientation > 270 || p_params.sensor_orientation % 90 != 0) { + return std::nullopt; + } + + int rotation_angle = p_params.sensor_orientation - p_params.display_rotation; + return normalize_angle(rotation_angle); +} + +int CameraFeedAndroid::normalize_angle(int p_angle) { + while (p_angle < 0) { + p_angle += 360; + } + return p_angle % 360; +} + +int CameraFeedAndroid::get_display_rotation() { + return DisplayServerAndroid::get_singleton()->get_display_rotation(); +} + +int CameraFeedAndroid::get_app_orientation() { + GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); + ERR_FAIL_NULL_V(godot_io_java, 0); + + int orientation = godot_io_java->get_screen_orientation(); + switch (orientation) { + case 0: // SCREEN_LANDSCAPE + return 90; + case 1: // SCREEN_PORTRAIT + return 0; + case 2: // SCREEN_REVERSE_LANDSCAPE + return 270; + case 3: // SCREEN_REVERSE_PORTRAIT + return 180; + case 4: // SCREEN_SENSOR_LANDSCAPE + case 5: // SCREEN_SENSOR_PORTRAIT + case 6: // SCREEN_SENSOR + default: + return get_display_rotation(); + } +} diff --git a/modules/camera/camera_android.h b/modules/camera/camera_android.h index 9417fd0701e..802ce88ec96 100644 --- a/modules/camera/camera_android.h +++ b/modules/camera/camera_android.h @@ -38,6 +38,18 @@ #include #include #include +#include + +enum class CameraFacing { + BACK = 0, + FRONT = 1, +}; + +struct CameraRotationParams { + int sensor_orientation; + CameraFacing camera_facing; + int display_rotation; +}; class CameraFeedAndroid : public CameraFeed { GDSOFTCLASS(CameraFeedAndroid, CameraFeed); @@ -56,9 +68,16 @@ private: AImageReader *reader = nullptr; ACameraCaptureSession *session = nullptr; ACaptureRequest *request = nullptr; + Mutex callback_mutex; + bool was_active_before_pause = false; void _add_formats(); void _set_rotation(); + void refresh_camera_metadata(); + static std::optional calculate_rotation(const CameraRotationParams &p_params); + static int normalize_angle(int p_angle); + static int get_display_rotation(); + static int get_app_orientation(); static void onError(void *context, ACameraDevice *p_device, int error); static void onDisconnected(void *context, ACameraDevice *p_device); @@ -74,6 +93,9 @@ public: bool set_format(int p_index, const Dictionary &p_parameters) override; Array get_formats() const override; FeedFormat get_format() const override; + void handle_pause(); + void handle_resume(); + void handle_rotation_change(); CameraFeedAndroid(ACameraManager *manager, ACameraMetadata *metadata, const char *id, CameraFeed::FeedPosition position, int32_t orientation); @@ -91,6 +113,9 @@ private: public: void set_monitoring_feeds(bool p_monitoring_feeds) override; + void handle_pause(); + void handle_resume(); + void handle_rotation_change(); ~CameraAndroid(); }; diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 7205477dba0..20b0be77881 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -170,6 +170,7 @@ class Godot private constructor(val context: Context) { */ private var renderViewInitialized = false private var primaryHost: GodotHost? = null + private var currentConfig = context.resources.configuration /** * Tracks whether we're in the RESUMED lifecycle state. @@ -757,6 +758,13 @@ class Godot private constructor(val context: Context) { darkMode = newDarkMode GodotLib.onNightModeChanged() } + + if (currentConfig.orientation != newConfig.orientation) { + runOnRenderThread { + GodotLib.onScreenRotationChange() + } + } + currentConfig = newConfig } /** diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index bbc7c1d5614..92a5d450576 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -295,6 +295,11 @@ public class GodotLib { */ public static native void onRendererPaused(); + /** + * Invoked when the screen orientation changes. + */ + static native void onScreenRotationChange(); + /** * @return true if input must be dispatched from the render thread. If false, input is * dispatched from the UI thread. diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 317f484ff3a..fd19541f7cd 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -52,6 +52,13 @@ #include "main/main.h" #include "servers/rendering/rendering_server.h" +#include "modules/modules_enabled.gen.h" // For camera. + +#ifdef MODULE_CAMERA_ENABLED +#include "modules/camera/camera_android.h" +#include "servers/camera/camera_server.h" +#endif + #ifndef XR_DISABLED #include "servers/xr/xr_server.h" #endif // XR_DISABLED @@ -593,6 +600,12 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI // We force redraw to ensure we render at least once when resuming the app. Main::force_redraw(); +#ifdef MODULE_CAMERA_ENABLED + CameraAndroid *camera_android = Object::cast_to(CameraServer::get_singleton()); + if (camera_android) { + camera_android->handle_resume(); + } +#endif // MODULE_CAMERA_ENABLED if (os_android->get_main_loop()) { os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_RESUMED); } @@ -603,11 +616,31 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIE return; } +#ifdef MODULE_CAMERA_ENABLED + CameraAndroid *camera_android = Object::cast_to(CameraServer::get_singleton()); + if (camera_android) { + camera_android->handle_pause(); + } +#endif // MODULE_CAMERA_ENABLED + if (os_android->get_main_loop()) { os_android->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_PAUSED); } } +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onScreenRotationChange(JNIEnv *env, jclass clazz) { + if (step.get() <= STEP_SETUP) { + return; + } + +#ifdef MODULE_CAMERA_ENABLED + CameraAndroid *camera_android = Object::cast_to(CameraServer::get_singleton()); + if (camera_android) { + camera_android->handle_rotation_change(); + } +#endif // MODULE_CAMERA_ENABLED +} + JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz) { Input *input = Input::get_singleton(); if (input) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index fe8c5c7593c..4dac64870aa 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -72,6 +72,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_hardwareKeyboardConne JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_filePickerCallback(JNIEnv *env, jclass clazz, jboolean p_ok, jobjectArray p_selected_paths); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onScreenRotationChange(JNIEnv *env, jclass clazz); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz); JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz);