Browse Source

Android: Stabilize camera lifecycle handling

- Pause camera feeds during lifecycle transitions to avoid crashes
- Refresh camera metadata after rotation to keep orientation accurate
KOGA Mitsuhiro 6 days ago
parent
commit
4483871cd3

+ 211 - 28
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::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;
+	}
+
+	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() {
-	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;
+	if (!metadata) {
+		print_verbose(vformat("Camera %s: Metadata is null in _set_rotation, attempting refresh.", camera_id));
+		refresh_camera_metadata();
 	}
 
-	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)));
+	float image_rotation = 0.0f;
+	std::optional<int> 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<float>(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<CameraFeedAndroid *>(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<uint8_t> data_y = feed->data_y;
 	Vector<uint8_t> data_uv = feed->data_uv;
 	Ref<Image> 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<CameraFeedAndroid> feed = feeds[i];
+		if (feed.is_valid()) {
+			feed->handle_pause();
+		}
+	}
+}
+
+void CameraAndroid::handle_resume() {
+	for (int i = 0; i < feeds.size(); i++) {
+		Ref<CameraFeedAndroid> feed = feeds[i];
+		if (feed.is_valid()) {
+			feed->handle_resume();
+		}
+	}
+}
+
+void CameraAndroid::handle_rotation_change() {
+	for (int i = 0; i < feeds.size(); i++) {
+		Ref<CameraFeedAndroid> feed = feeds[i];
+		if (feed.is_valid()) {
+			feed->handle_rotation_change();
+		}
+	}
+}
+
 CameraAndroid::~CameraAndroid() {
 	remove_all_feeds();
 }
+
+std::optional<int> 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();
+	}
+}

+ 25 - 0
modules/camera/camera_android.h

@@ -38,6 +38,18 @@
 #include <camera/NdkCameraManager.h>
 #include <camera/NdkCameraMetadataTags.h>
 #include <media/NdkImageReader.h>
+#include <optional>
+
+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<int> 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();
 };

+ 8 - 0
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
 	}
 
 	/**

+ 5 - 0
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.

+ 33 - 0
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<CameraAndroid>(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<CameraAndroid>(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<CameraAndroid>(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) {

+ 1 - 0
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);