Browse Source

Add CameraFeed support for Android

Co-authored-by: KOGA Mitsuhiro <[email protected]>
Leo de Penning 4 months ago
parent
commit
296ca79b9d

+ 5 - 1
modules/camera/SCsub

@@ -6,7 +6,7 @@ Import("env_modules")
 
 env_camera = env_modules.Clone()
 
-if env["platform"] in ["windows", "macos", "linuxbsd"]:
+if env["platform"] in ["windows", "macos", "linuxbsd", "android"]:
     env_camera.add_source_files(env.modules_sources, "register_types.cpp")
 
 if env["platform"] == "windows":
@@ -15,6 +15,10 @@ if env["platform"] == "windows":
 elif env["platform"] == "macos":
     env_camera.add_source_files(env.modules_sources, "camera_macos.mm")
 
+elif env["platform"] == "android":
+    env_camera.add_source_files(env.modules_sources, "camera_android.cpp")
+    env.Append(LIBS=["camera2ndk", "mediandk"])
+
 elif env["platform"] == "linuxbsd":
     env_camera.add_source_files(env.modules_sources, "camera_linux.cpp")
     env_camera.add_source_files(env.modules_sources, "camera_feed_linux.cpp")

+ 511 - 0
modules/camera/camera_android.cpp

@@ -0,0 +1,511 @@
+/**************************************************************************/
+/*  camera_android.cpp                                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "camera_android.h"
+
+#include "core/os/os.h"
+#include "platform/android/display_server_android.h"
+
+//////////////////////////////////////////////////////////////////////////
+// Helper functions
+//
+// The following code enables you to view the contents of a media type while
+// debugging.
+
+#ifndef IF_EQUAL_RETURN
+#define MAKE_FORMAT_CONST(suffix) AIMAGE_FORMAT_##suffix
+#define IF_EQUAL_RETURN(param, val)      \
+	if (MAKE_FORMAT_CONST(val) == param) \
+	return #val
+#endif
+
+String GetFormatName(const int32_t &format) {
+	IF_EQUAL_RETURN(format, YUV_420_888);
+	IF_EQUAL_RETURN(format, RGB_888);
+	IF_EQUAL_RETURN(format, RGBA_8888);
+
+	return "Unsupported";
+}
+
+//////////////////////////////////////////////////////////////////////////
+// CameraFeedAndroid - Subclass for our camera feed on Android
+
+CameraFeedAndroid::CameraFeedAndroid(ACameraManager *manager, ACameraMetadata *metadata, const char *id,
+		CameraFeed::FeedPosition position, int32_t orientation) :
+		CameraFeed() {
+	this->manager = manager;
+	this->metadata = metadata;
+	this->orientation = orientation;
+	_add_formats();
+	camera_id = id;
+	set_position(position);
+
+	// Position
+	switch (position) {
+		case CameraFeed::FEED_BACK:
+			name = vformat("%s | BACK", id);
+			break;
+		case CameraFeed::FEED_FRONT:
+			name = vformat("%s | FRONT", id);
+			break;
+		default:
+			name = vformat("%s", id);
+			break;
+	}
+
+	image_y.instantiate();
+	image_uv.instantiate();
+}
+
+CameraFeedAndroid::~CameraFeedAndroid() {
+	if (is_active()) {
+		deactivate_feed();
+	}
+	if (metadata != nullptr) {
+		ACameraMetadata_free(metadata);
+	}
+}
+
+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;
+	}
+
+	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)));
+}
+
+void CameraFeedAndroid::_add_formats() {
+	// Get supported formats
+	ACameraMetadata_const_entry formats;
+	camera_status_t status = ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &formats);
+
+	if (status == ACAMERA_OK) {
+		for (uint32_t f = 0; f < formats.count; f += 4) {
+			// Only support output streams
+			int32_t input = formats.data.i32[f + 3];
+			if (input) {
+				continue;
+			}
+
+			// Get format and resolution
+			int32_t format = formats.data.i32[f + 0];
+			if (format == AIMAGE_FORMAT_YUV_420_888 ||
+					format == AIMAGE_FORMAT_RGBA_8888 ||
+					format == AIMAGE_FORMAT_RGB_888) {
+				CameraFeed::FeedFormat feed_format;
+				feed_format.width = formats.data.i32[f + 1];
+				feed_format.height = formats.data.i32[f + 2];
+				feed_format.format = GetFormatName(format);
+				feed_format.pixel_format = format;
+				this->formats.append(feed_format);
+			}
+		}
+	}
+}
+
+bool CameraFeedAndroid::activate_feed() {
+	ERR_FAIL_COND_V_MSG(selected_format == -1, false, "CameraFeed format needs to be set before activating.");
+	if (is_active()) {
+		deactivate_feed();
+	};
+
+	// Request permission
+	if (!OS::get_singleton()->request_permission("CAMERA")) {
+		return false;
+	}
+
+	// Open device
+	static ACameraDevice_stateCallbacks deviceCallbacks = {
+		.context = this,
+		.onDisconnected = onDisconnected,
+		.onError = onError,
+	};
+	camera_status_t c_status = ACameraManager_openCamera(manager, camera_id.utf8().get_data(), &deviceCallbacks, &device);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	// Create image reader
+	const FeedFormat &feed_format = formats[selected_format];
+	media_status_t m_status = AImageReader_new(feed_format.width, feed_format.height, feed_format.pixel_format, 1, &reader);
+	if (m_status != AMEDIA_OK) {
+		onError(this, device, m_status);
+		return false;
+	}
+
+	// Get image listener
+	static AImageReader_ImageListener listener{
+		.context = this,
+		.onImageAvailable = onImage,
+	};
+	m_status = AImageReader_setImageListener(reader, &listener);
+	if (m_status != AMEDIA_OK) {
+		onError(this, device, m_status);
+		return false;
+	}
+
+	// Get image surface
+	ANativeWindow *surface;
+	m_status = AImageReader_getWindow(reader, &surface);
+	if (m_status != AMEDIA_OK) {
+		onError(this, device, m_status);
+		return false;
+	}
+
+	// Prepare session outputs
+	ACaptureSessionOutput *output = nullptr;
+	c_status = ACaptureSessionOutput_create(surface, &output);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	ACaptureSessionOutputContainer *outputs = nullptr;
+	c_status = ACaptureSessionOutputContainer_create(&outputs);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	c_status = ACaptureSessionOutputContainer_add(outputs, output);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	// Create capture session
+	static ACameraCaptureSession_stateCallbacks sessionStateCallbacks{
+		.context = this,
+		.onClosed = onSessionClosed,
+		.onReady = onSessionReady,
+		.onActive = onSessionActive
+	};
+	c_status = ACameraDevice_createCaptureSession(device, outputs, &sessionStateCallbacks, &session);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	// Create capture request
+	c_status = ACameraDevice_createCaptureRequest(device, TEMPLATE_PREVIEW, &request);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	// Set capture target
+	ACameraOutputTarget *imageTarget = nullptr;
+	c_status = ACameraOutputTarget_create(surface, &imageTarget);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	c_status = ACaptureRequest_addTarget(request, imageTarget);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	// Start capture
+	c_status = ACameraCaptureSession_setRepeatingRequest(session, nullptr, 1, &request, nullptr);
+	if (c_status != ACAMERA_OK) {
+		onError(this, device, c_status);
+		return false;
+	}
+
+	return true;
+}
+
+bool CameraFeedAndroid::set_format(int p_index, const Dictionary &p_parameters) {
+	ERR_FAIL_COND_V_MSG(active, false, "Feed is active.");
+	ERR_FAIL_INDEX_V_MSG(p_index, formats.size(), false, "Invalid format index.");
+
+	selected_format = p_index;
+	return true;
+}
+
+Array CameraFeedAndroid::get_formats() const {
+	Array result;
+	for (const FeedFormat &feed_format : formats) {
+		Dictionary dictionary;
+		dictionary["width"] = feed_format.width;
+		dictionary["height"] = feed_format.height;
+		dictionary["format"] = feed_format.format;
+		result.push_back(dictionary);
+	}
+	return result;
+}
+
+CameraFeed::FeedFormat CameraFeedAndroid::get_format() const {
+	CameraFeed::FeedFormat feed_format = {};
+	return selected_format == -1 ? feed_format : formats[selected_format];
+}
+
+void CameraFeedAndroid::onImage(void *context, AImageReader *p_reader) {
+	CameraFeedAndroid *feed = static_cast<CameraFeedAndroid *>(context);
+	Vector<uint8_t> data_y = feed->data_y;
+	Vector<uint8_t> data_uv = feed->data_uv;
+	Ref<Image> image_y = feed->image_y;
+	Ref<Image> image_uv = feed->image_uv;
+
+	// Get image
+	AImage *image = nullptr;
+	media_status_t status = AImageReader_acquireNextImage(p_reader, &image);
+	ERR_FAIL_COND(status != AMEDIA_OK);
+
+	// Get image data
+	uint8_t *data = nullptr;
+	int len = 0;
+	int32_t pixel_stride, row_stride;
+	FeedFormat format = feed->get_format();
+	int width = format.width;
+	int height = format.height;
+	switch (format.pixel_format) {
+		case AIMAGE_FORMAT_YUV_420_888:
+			AImage_getPlaneData(image, 0, &data, &len);
+			if (len <= 0) {
+				return;
+			}
+			if (len != data_y.size()) {
+				int64_t size = Image::get_image_data_size(width, height, Image::FORMAT_R8, false);
+				data_y.resize(len > size ? len : size);
+			}
+			memcpy(data_y.ptrw(), data, len);
+
+			AImage_getPlanePixelStride(image, 1, &pixel_stride);
+			AImage_getPlaneRowStride(image, 1, &row_stride);
+			AImage_getPlaneData(image, 1, &data, &len);
+			if (len <= 0) {
+				return;
+			}
+			if (len != data_uv.size()) {
+				int64_t size = Image::get_image_data_size(width / 2, height / 2, Image::FORMAT_RG8, false);
+				data_uv.resize(len > size ? len : size);
+			}
+			memcpy(data_uv.ptrw(), data, len);
+
+			image_y->initialize_data(width, height, false, Image::FORMAT_R8, data_y);
+			image_uv->initialize_data(width / 2, height / 2, false, Image::FORMAT_RG8, data_uv);
+
+			feed->set_ycbcr_images(image_y, image_uv);
+			break;
+		case AIMAGE_FORMAT_RGBA_8888:
+			AImage_getPlaneData(image, 0, &data, &len);
+			if (len <= 0) {
+				return;
+			}
+			if (len != data_y.size()) {
+				int64_t size = Image::get_image_data_size(width, height, Image::FORMAT_RGBA8, false);
+				data_y.resize(len > size ? len : size);
+			}
+			memcpy(data_y.ptrw(), data, len);
+
+			image_y->initialize_data(width, height, false, Image::FORMAT_RGBA8, data_y);
+
+			feed->set_rgb_image(image_y);
+			break;
+		case AIMAGE_FORMAT_RGB_888:
+			AImage_getPlaneData(image, 0, &data, &len);
+			if (len <= 0) {
+				return;
+			}
+			if (len != data_y.size()) {
+				int64_t size = Image::get_image_data_size(width, height, Image::FORMAT_RGB8, false);
+				data_y.resize(len > size ? len : size);
+			}
+			memcpy(data_y.ptrw(), data, len);
+
+			image_y->initialize_data(width, height, false, Image::FORMAT_RGB8, data_y);
+
+			feed->set_rgb_image(image_y);
+			break;
+		default:
+			return;
+	}
+
+	// Rotation
+	feed->_set_rotation();
+
+	// Release image
+	AImage_delete(image);
+
+	feed->emit_signal(SNAME("frame_changed"));
+}
+
+void CameraFeedAndroid::onSessionReady(void *context, ACameraCaptureSession *session) {
+	print_verbose("Capture session ready");
+}
+
+void CameraFeedAndroid::onSessionActive(void *context, ACameraCaptureSession *session) {
+	print_verbose("Capture session active");
+}
+
+void CameraFeedAndroid::onSessionClosed(void *context, ACameraCaptureSession *session) {
+	print_verbose("Capture session closed");
+}
+
+void CameraFeedAndroid::deactivate_feed() {
+	if (session != nullptr) {
+		ACameraCaptureSession_stopRepeating(session);
+		ACameraCaptureSession_close(session);
+		session = nullptr;
+	}
+
+	if (request != nullptr) {
+		ACaptureRequest_free(request);
+		request = nullptr;
+	}
+
+	if (reader != nullptr) {
+		AImageReader_delete(reader);
+		reader = nullptr;
+	}
+
+	if (device != nullptr) {
+		ACameraDevice_close(device);
+		device = nullptr;
+	}
+}
+
+void CameraFeedAndroid::onError(void *context, ACameraDevice *p_device, int error) {
+	print_error(vformat("Camera error: %d", error));
+	onDisconnected(context, p_device);
+}
+
+void CameraFeedAndroid::onDisconnected(void *context, ACameraDevice *p_device) {
+	print_verbose("Camera disconnected");
+	auto *feed = static_cast<CameraFeedAndroid *>(context);
+	feed->set_active(false);
+}
+
+//////////////////////////////////////////////////////////////////////////
+// CameraAndroid - Subclass for our camera server on Android
+
+void CameraAndroid::update_feeds() {
+	ACameraIdList *cameraIds = nullptr;
+	camera_status_t c_status = ACameraManager_getCameraIdList(cameraManager, &cameraIds);
+	ERR_FAIL_COND(c_status != ACAMERA_OK);
+
+	// remove existing devices
+	for (int i = feeds.size() - 1; i >= 0; i--) {
+		remove_feed(feeds[i]);
+	}
+
+	for (int c = 0; c < cameraIds->numCameras; ++c) {
+		const char *id = cameraIds->cameraIds[c];
+		ACameraMetadata *metadata = nullptr;
+		ACameraManager_getCameraCharacteristics(cameraManager, id, &metadata);
+		if (!metadata) {
+			continue;
+		}
+
+		// Get sensor orientation
+		ACameraMetadata_const_entry orientation;
+		c_status = ACameraMetadata_getConstEntry(metadata, ACAMERA_SENSOR_ORIENTATION, &orientation);
+		int32_t cameraOrientation;
+		if (c_status == ACAMERA_OK) {
+			cameraOrientation = orientation.data.i32[0];
+		} else {
+			cameraOrientation = 0;
+			print_error(vformat("Unable to get sensor orientation: %s", id));
+		}
+
+		// Get position
+		ACameraMetadata_const_entry lensInfo;
+		CameraFeed::FeedPosition position = CameraFeed::FEED_UNSPECIFIED;
+		camera_status_t status;
+		status = ACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &lensInfo);
+		if (status != ACAMERA_OK) {
+			ACameraMetadata_free(metadata);
+			continue;
+		}
+		uint8_t lens_facing = static_cast<acamera_metadata_enum_android_lens_facing_t>(lensInfo.data.u8[0]);
+		if (lens_facing == ACAMERA_LENS_FACING_FRONT) {
+			position = CameraFeed::FEED_FRONT;
+		} else if (lens_facing == ACAMERA_LENS_FACING_BACK) {
+			position = CameraFeed::FEED_BACK;
+		} else {
+			ACameraMetadata_free(metadata);
+			continue;
+		}
+
+		Ref<CameraFeedAndroid> feed = memnew(CameraFeedAndroid(cameraManager, metadata, id, position, cameraOrientation));
+		add_feed(feed);
+	}
+
+	ACameraManager_deleteCameraIdList(cameraIds);
+}
+
+void CameraAndroid::remove_all_feeds() {
+	// remove existing devices
+	for (int i = feeds.size() - 1; i >= 0; i--) {
+		remove_feed(feeds[i]);
+	}
+
+	if (cameraManager != nullptr) {
+		ACameraManager_delete(cameraManager);
+		cameraManager = nullptr;
+	}
+}
+
+void CameraAndroid::set_monitoring_feeds(bool p_monitoring_feeds) {
+	if (p_monitoring_feeds == monitoring_feeds) {
+		return;
+	}
+
+	CameraServer::set_monitoring_feeds(p_monitoring_feeds);
+	if (p_monitoring_feeds) {
+		if (cameraManager == nullptr) {
+			cameraManager = ACameraManager_create();
+		}
+
+		// Update feeds
+		update_feeds();
+	} else {
+		remove_all_feeds();
+	}
+}
+
+CameraAndroid::~CameraAndroid() {
+	remove_all_feeds();
+}

+ 96 - 0
modules/camera/camera_android.h

@@ -0,0 +1,96 @@
+/**************************************************************************/
+/*  camera_android.h                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "servers/camera/camera_feed.h"
+#include "servers/camera_server.h"
+
+#include <camera/NdkCameraDevice.h>
+#include <camera/NdkCameraError.h>
+#include <camera/NdkCameraManager.h>
+#include <camera/NdkCameraMetadataTags.h>
+#include <media/NdkImageReader.h>
+
+class CameraFeedAndroid : public CameraFeed {
+	GDSOFTCLASS(CameraFeedAndroid, CameraFeed);
+
+private:
+	String camera_id;
+	int32_t orientation;
+	Ref<Image> image_y;
+	Ref<Image> image_uv;
+	Vector<uint8_t> data_y;
+	Vector<uint8_t> data_uv;
+
+	ACameraManager *manager = nullptr;
+	ACameraMetadata *metadata = nullptr;
+	ACameraDevice *device = nullptr;
+	AImageReader *reader = nullptr;
+	ACameraCaptureSession *session = nullptr;
+	ACaptureRequest *request = nullptr;
+
+	void _add_formats();
+	void _set_rotation();
+
+	static void onError(void *context, ACameraDevice *p_device, int error);
+	static void onDisconnected(void *context, ACameraDevice *p_device);
+	static void onImage(void *context, AImageReader *p_reader);
+	static void onSessionReady(void *context, ACameraCaptureSession *session);
+	static void onSessionActive(void *context, ACameraCaptureSession *session);
+	static void onSessionClosed(void *context, ACameraCaptureSession *session);
+
+protected:
+public:
+	bool activate_feed() override;
+	void deactivate_feed() override;
+	bool set_format(int p_index, const Dictionary &p_parameters) override;
+	Array get_formats() const override;
+	FeedFormat get_format() const override;
+
+	CameraFeedAndroid(ACameraManager *manager, ACameraMetadata *metadata, const char *id,
+			CameraFeed::FeedPosition position, int32_t orientation);
+	~CameraFeedAndroid() override;
+};
+
+class CameraAndroid : public CameraServer {
+	GDSOFTCLASS(CameraAndroid, CameraServer);
+
+private:
+	ACameraManager *cameraManager = nullptr;
+
+	void update_feeds();
+	void remove_all_feeds();
+
+public:
+	void set_monitoring_feeds(bool p_monitoring_feeds) override;
+
+	~CameraAndroid();
+};

+ 1 - 1
modules/camera/config.py

@@ -3,7 +3,7 @@ def can_build(env, platform):
 
     if sys.platform.startswith("freebsd"):
         return False
-    return platform == "macos" or platform == "windows" or platform == "linuxbsd"
+    return platform == "macos" or platform == "windows" or platform == "linuxbsd" or platform == "android"
 
 
 def configure(env):

+ 6 - 0
modules/camera/register_types.cpp

@@ -39,6 +39,9 @@
 #if defined(MACOS_ENABLED)
 #include "camera_macos.h"
 #endif
+#if defined(ANDROID_ENABLED)
+#include "camera_android.h"
+#endif
 
 void initialize_camera_module(ModuleInitializationLevel p_level) {
 	if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
@@ -54,6 +57,9 @@ void initialize_camera_module(ModuleInitializationLevel p_level) {
 #if defined(MACOS_ENABLED)
 	CameraServer::make_default<CameraMacOS>();
 #endif
+#if defined(ANDROID_ENABLED)
+	CameraServer::make_default<CameraAndroid>();
+#endif
 }
 
 void uninitialize_camera_module(ModuleInitializationLevel p_level) {

+ 7 - 0
platform/android/display_server_android.cpp

@@ -279,6 +279,13 @@ DisplayServer::ScreenOrientation DisplayServerAndroid::screen_get_orientation(in
 	return (ScreenOrientation)orientation;
 }
 
+int DisplayServerAndroid::get_display_rotation() const {
+	GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java();
+	ERR_FAIL_NULL_V(godot_io_java, 0);
+
+	return godot_io_java->get_display_rotation();
+}
+
 int DisplayServerAndroid::get_screen_count() const {
 	return 1;
 }

+ 1 - 0
platform/android/display_server_android.h

@@ -144,6 +144,7 @@ public:
 
 	virtual void screen_set_orientation(ScreenOrientation p_orientation, int p_screen = SCREEN_OF_MAIN_WINDOW) override;
 	virtual ScreenOrientation screen_get_orientation(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+	int get_display_rotation() const;
 
 	virtual int get_screen_count() const override;
 	virtual int get_primary_screen() const override;

+ 6 - 0
platform/android/java/editor/src/horizonos/AndroidManifest.xml

@@ -37,6 +37,12 @@
     <!-- Temp removal of the 'REQUEST_INSTALL_PACKAGES' permission as it's currently forbidden by the Horizon OS store -->
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove" />
 
+    <!-- Passthrough feature -->
+    <uses-feature
+            android:name="android.hardware.camera2.any"
+            android:required="false"/>
+    <uses-permission android:name="horizonos.permission.HEADSET_CAMERA"/>
+
     <application>
 
         <activity

+ 4 - 0
platform/android/java/editor/src/main/AndroidManifest.xml

@@ -12,6 +12,9 @@
     <uses-feature
         android:glEsVersion="0x00030000"
         android:required="true" />
+    <uses-feature
+        android:name="android.hardware.camera"
+        android:required="false" />
 
     <uses-permission
         android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
@@ -26,6 +29,7 @@
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.CAMERA" />
 
     <application
         android:allowBackup="false"

+ 16 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotIO.java

@@ -34,6 +34,7 @@ import org.godotengine.godot.error.Error;
 import org.godotengine.godot.input.GodotEditText;
 
 import android.app.Activity;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
@@ -46,7 +47,9 @@ import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Display;
 import android.view.DisplayCutout;
+import android.view.Surface;
 import android.view.WindowInsets;
+import android.view.WindowManager;
 
 import androidx.core.content.FileProvider;
 
@@ -306,6 +309,19 @@ public class GodotIO {
 		}
 	}
 
+	public int getDisplayRotation() {
+		WindowManager windowManager = (WindowManager)activity.getSystemService(Context.WINDOW_SERVICE);
+		int rotation = windowManager.getDefaultDisplay().getRotation();
+		if (rotation == Surface.ROTATION_90) {
+			return 90;
+		} else if (rotation == Surface.ROTATION_180) {
+			return 180;
+		} else if (rotation == Surface.ROTATION_270) {
+			return 270;
+		}
+		return 0;
+	}
+
 	public void setEdit(GodotEditText _edit) {
 		edit = _edit;
 	}

+ 11 - 0
platform/android/java_godot_io_wrapper.cpp

@@ -67,6 +67,7 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc
 		_set_screen_orientation = p_env->GetMethodID(cls, "setScreenOrientation", "(I)V");
 		_get_screen_orientation = p_env->GetMethodID(cls, "getScreenOrientation", "()I");
 		_get_system_dir = p_env->GetMethodID(cls, "getSystemDir", "(IZ)Ljava/lang/String;");
+		_get_display_rotation = p_env->GetMethodID(cls, "getDisplayRotation", "()I");
 	}
 }
 
@@ -289,6 +290,16 @@ String GodotIOJavaWrapper::get_system_dir(int p_dir, bool p_shared_storage) {
 	}
 }
 
+int GodotIOJavaWrapper::get_display_rotation() {
+	if (_get_display_rotation) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL_V(env, 0);
+		return env->CallIntMethod(godot_io_instance, _get_display_rotation);
+	} else {
+		return 0;
+	}
+}
+
 // SafeNumeric because it can be changed from non-main thread and we need to
 // ensure the change is immediately visible to other threads.
 static SafeNumeric<int> virtual_keyboard_height;

+ 2 - 0
platform/android/java_godot_io_wrapper.h

@@ -49,6 +49,7 @@ private:
 	jmethodID _get_data_dir = 0;
 	jmethodID _get_temp_dir = 0;
 	jmethodID _get_display_cutouts = 0;
+	jmethodID _get_display_rotation = 0;
 	jmethodID _get_display_safe_area = 0;
 	jmethodID _get_locale = 0;
 	jmethodID _get_model = 0;
@@ -90,4 +91,5 @@ public:
 	void set_screen_orientation(int p_orient);
 	int get_screen_orientation();
 	String get_system_dir(int p_dir, bool p_shared_storage);
+	int get_display_rotation();
 };