Skip to content

Add CameraFeed support for Web #106784

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion doc/classes/CameraFeed.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<description>
A camera feed gives you access to a single physical camera attached to your device. When enabled, Godot will start capturing frames from the camera which can then be used. See also [CameraServer].
[b]Note:[/b] Many cameras will return YCbCr images which are split into two textures and need to be combined in a shader. Godot does this automatically for you if you set the environment to show the camera image in the background.
[b]Note:[/b] This class is currently only implemented on Linux, Android, macOS, and iOS. On other platforms no [CameraFeed]s will be available. To get a [CameraFeed] on iOS, the camera plugin from [url=https://github.com/godotengine/godot-ios-plugins]godot-ios-plugins[/url] is required.
[b]Note:[/b] This class is currently only implemented on Linux, Android, Web, macOS, and iOS. On other platforms no [CameraFeed]s will be available. To get a [CameraFeed] on iOS, the camera plugin from [url=https://github.com/godotengine/godot-ios-plugins]godot-ios-plugins[/url] is required.
</description>
<tutorials>
</tutorials>
Expand Down
2 changes: 1 addition & 1 deletion doc/classes/CameraServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<description>
The [CameraServer] keeps track of different cameras accessible in Godot. These are external cameras such as webcams or the cameras on your phone.
It is notably used to provide AR modules with a video feed from the camera.
[b]Note:[/b] This class is currently only implemented on Linux, Android, macOS, and iOS. On other platforms no [CameraFeed]s will be available. To get a [CameraFeed] on iOS, the camera plugin from [url=https://github.com/godotengine/godot-ios-plugins]godot-ios-plugins[/url] is required.
[b]Note:[/b] This class is currently only implemented on Linux, Android, Web, macOS, and iOS. On other platforms no [CameraFeed]s will be available. To get a [CameraFeed] on iOS, the camera plugin from [url=https://github.com/godotengine/godot-ios-plugins]godot-ios-plugins[/url] is required.
</description>
<tutorials>
</tutorials>
Expand Down
5 changes: 4 additions & 1 deletion modules/camera/SCsub
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Import("env_modules")

env_camera = env_modules.Clone()

if env["platform"] in ["windows", "macos", "linuxbsd", "android"]:
if env["platform"] in ["windows", "macos", "linuxbsd", "android", "web"]:
env_camera.add_source_files(env.modules_sources, "register_types.cpp")

if env["platform"] == "windows":
Expand All @@ -23,3 +23,6 @@ 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")
env_camera.add_source_files(env.modules_sources, "buffer_decoder.cpp")

elif env["platform"] == "web":
env_camera.add_source_files(env.modules_sources, "camera_web.cpp")
182 changes: 182 additions & 0 deletions modules/camera/camera_web.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**************************************************************************/
/* camera_web.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_web.h"

#include "core/io/json.h"

void CameraFeedWeb::_on_get_pixeldata(void *context, const uint8_t *rawdata, const int length, const int p_width, const int p_height, const char *error) {
CameraFeedWeb *feed = reinterpret_cast<CameraFeedWeb *>(context);
if (error) {
if (feed->is_active()) {
feed->deactivate_feed();
};
String error_str = String::utf8(error);
ERR_PRINT(vformat("Camera feed error from JS: %s", error_str));
return;
}

if (context == nullptr || rawdata == nullptr || length < 0 || p_width <= 0 || p_height <= 0) {
if (feed->is_active()) {
feed->deactivate_feed();
};
ERR_PRINT("Camera feed error: Invalid pixel data received.");
return;
}

Vector<uint8_t> data = feed->data;
Ref<Image> image = feed->image;

if (length != data.size()) {
int64_t size = Image::get_image_data_size(p_width, p_height, Image::FORMAT_RGBA8, false);
data.resize(length > size ? length : size);
}
memcpy(data.ptrw(), rawdata, length);

image->initialize_data(p_width, p_height, false, Image::FORMAT_RGBA8, data);
feed->set_rgb_image(image);
feed->emit_signal(SNAME("frame_changed"));
}

void CameraFeedWeb::_on_denied_callback(void *context) {
CameraFeedWeb *feed = reinterpret_cast<CameraFeedWeb *>(context);
feed->deactivate_feed();
}

bool CameraFeedWeb::activate_feed() {
ERR_FAIL_COND_V_MSG(selected_format == -1, false, "CameraFeed format needs to be set before activating.");

int width = parameters.get(KEY_WIDTH, 0);
int height = parameters.get(KEY_HEIGHT, 0);
// Firefox ESR (128.11.0esr) does not implement MediaStreamTrack.getCapabilities(), so 'formats' will be empty.
if (formats.size() > selected_format) {
CameraFeed::FeedFormat f = formats[selected_format];
width = width > 0 ? width : f.width;
height = height > 0 ? height : f.height;
}
CameraDriverWeb::get_singleton()->get_pixel_data(this, device_id, width, height, &_on_get_pixeldata, &_on_denied_callback);
return true;
}

void CameraFeedWeb::deactivate_feed() {
CameraDriverWeb::get_singleton()->stop_stream(device_id);
}

bool CameraFeedWeb::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;
parameters = p_parameters;
return true;
}

Array CameraFeedWeb::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 CameraFeedWeb::get_format() const {
CameraFeed::FeedFormat feed_format = {};
return selected_format == -1 ? feed_format : formats[selected_format];
}

CameraFeedWeb::CameraFeedWeb(const CameraInfo &info) {
name = info.label;
device_id = info.device_id;

FeedFormat feed_format;
feed_format.width = info.capability.width;
feed_format.height = info.capability.height;
feed_format.format = String("RGBA");
formats.append(feed_format);

image.instantiate();
}

CameraFeedWeb::~CameraFeedWeb() {
if (is_active()) {
deactivate_feed();
}
}

void CameraWeb::_on_get_cameras_callback(void *context, const Vector<CameraInfo> &camera_info) {
CameraWeb *server = static_cast<CameraWeb *>(context);
for (int i = server->feeds.size() - 1; i >= 0; i--) {
server->remove_feed(server->feeds[i]);
}
for (int i = 0; i < camera_info.size(); i++) {
CameraInfo info = camera_info[i];
Ref<CameraFeedWeb> feed = memnew(CameraFeedWeb(info));
server->add_feed(feed);
}
server->CameraServer::set_monitoring_feeds(true);
server->activating = false;
}

void CameraWeb::_update_feeds() {
activating = true;
camera_driver_web->get_cameras((void *)this, &_on_get_cameras_callback);
}

void CameraWeb::_cleanup() {
if (camera_driver_web != nullptr) {
camera_driver_web->stop_stream();
memdelete(camera_driver_web);
camera_driver_web = nullptr;
}
}

void CameraWeb::set_monitoring_feeds(bool p_monitoring_feeds) {
if (p_monitoring_feeds == monitoring_feeds || activating) {
return;
}

if (p_monitoring_feeds) {
if (camera_driver_web == nullptr) {
camera_driver_web = new CameraDriverWeb();
}
_update_feeds();
} else {
CameraServer::set_monitoring_feeds(p_monitoring_feeds);
_cleanup();
}
}

CameraWeb::~CameraWeb() {
_cleanup();
}
76 changes: 76 additions & 0 deletions modules/camera/camera_web.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**************************************************************************/
/* camera_web.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 "platform/web/camera_driver_web.h"
#include "servers/camera/camera_feed.h"
#include "servers/camera_server.h"

#include <atomic>

class CameraFeedWeb : public CameraFeed {
GDCLASS(CameraFeedWeb, CameraFeed);

private:
String device_id;
Ref<Image> image;
Vector<uint8_t> data;
static void _on_get_pixeldata(void *, const uint8_t *, const int, const int, const int, const char *error);
static void _on_denied_callback(void *context);

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;

CameraFeedWeb(const CameraInfo &info);
~CameraFeedWeb();
};

class CameraWeb : public CameraServer {
GDCLASS(CameraWeb, CameraServer);

private:
CameraDriverWeb *camera_driver_web = nullptr;
std::atomic<bool> activating;
void _cleanup();
void _update_feeds();
static void _on_get_cameras_callback(void *context, const Vector<CameraInfo> &camera_info);

protected:
public:
void set_monitoring_feeds(bool p_monitoring_feeds) override;

~CameraWeb();
};
2 changes: 1 addition & 1 deletion modules/camera/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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" or platform == "android"
return platform in ["macos", "windows", "linuxbsd", "android", "web"]


def configure(env):
Expand Down
6 changes: 6 additions & 0 deletions modules/camera/register_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
#if defined(ANDROID_ENABLED)
#include "camera_android.h"
#endif
#if defined(WEB_ENABLED)
#include "camera_web.h"
#endif

void initialize_camera_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
Expand All @@ -60,6 +63,9 @@ void initialize_camera_module(ModuleInitializationLevel p_level) {
#if defined(ANDROID_ENABLED)
CameraServer::make_default<CameraAndroid>();
#endif
#if defined(WEB_ENABLED)
CameraServer::make_default<CameraWeb>();
#endif
}

void uninitialize_camera_module(ModuleInitializationLevel p_level) {
Expand Down
2 changes: 2 additions & 0 deletions platform/web/SCsub
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ if "serve" in COMMAND_LINE_TARGETS or "run" in COMMAND_LINE_TARGETS:

web_files = [
"audio_driver_web.cpp",
"camera_driver_web.cpp",
"webmidi_driver.cpp",
"display_server_web.cpp",
"http_client_web.cpp",
Expand All @@ -39,6 +40,7 @@ sys_env = env.Clone()
sys_env.AddJSLibraries(
[
"js/libs/library_godot_audio.js",
"js/libs/library_godot_camera.js",
"js/libs/library_godot_display.js",
"js/libs/library_godot_fetch.js",
"js/libs/library_godot_webmidi.js",
Expand Down
Loading