Add CameraFeed support for Web#106784
Conversation
140bdef to
f939c20
Compare
|
I've tried enabling Asyncify as follows, which I believe should fix it, but the build is still failing. diff --git a/platform/web/SCsub b/platform/web/SCsub
index cf872e65ff..5bb105e1cd 100644
--- a/platform/web/SCsub
+++ b/platform/web/SCsub
@@ -106,6 +106,8 @@ else:
sys_env.Append(LIBS=["idbfs.js"])
build = sys_env.add_program(build_targets, web_files + ["web_runtime.cpp"])
+sys_env.Append(LINKFLAGS=["-sASYNCIFY=1"])
+
sys_env.Depends(build[0], sys_env["JS_LIBS"])
sys_env.Depends(build[0], sys_env["JS_PRE"])
sys_env.Depends(build[0], sys_env["JS_POST"]) |
|
Asyncify makes binaries a lot larger, so we need to be able to use it selectively only for functions that need it. cc @adamscott |
a6e548e to
9354b30
Compare
be89cda to
706e8d0
Compare
|
Ready for review. Demo site: https://shiena.github.io/godot-camerafeed-demo/ |
706e8d0 to
6854b08
Compare
Faless
left a comment
There was a problem hiding this comment.
The demo site requires Chrome 137+ due to its use of JSPI.
I wanted to useASYNCIFY=1in SCsub but ran into errors, so I'm currently usingASYNCIFY=2for JSPI.
This means it can't be merged, we at least want support the latest version of Chrome, Firefox and iOS (and we also try to support the latest version of Firefox ESR, but that has been on and off in the past).
That said, it should be possible to implement the API by relying on callbacks instead of requiring asyncify.
| EM_ASYNC_JS(void, godot_js_camera_get_cameras, (void *context, CameraLibrary_OnGetCamerasCallback p_callback_ptr), { | ||
| await GodotCamera.api.getCameras(context, p_callback_ptr); | ||
| }); | ||
|
|
||
| EM_ASYNC_JS(void, godot_js_camera_get_capabilities, (void *context, const char *p_device_id_ptr, CameraLibrary_OnGetCapabilitiesCallback p_callback_ptr), { | ||
| await GodotCamera.api.getCameraCapabilities(p_device_id_ptr, context, p_callback_ptr); | ||
| }); |
There was a problem hiding this comment.
We shouldn't use the EM_*_JS functions, as they break dynamic linking.
There was a problem hiding this comment.
Sorry for the late reply! This has already been addressed — EM_ASYNC_JS has been replaced with extern declarations in godot_camera.h + library_godot_camera.js, following the same pattern as the other platform libraries.
I'm trying to get a list of However, both of these methods return a JavaScript Promise, which makes using callbacks inherently slow. |
18f57bb to
f5fd45b
Compare
|
@Faless
However, please note the following limitations:
|
f5fd45b to
ed064b2
Compare
1d0df9e to
e347626
Compare
db71d0f to
e63edd8
Compare
2ae2905 to
af6f232
Compare
Benjamin-Dobell
left a comment
There was a problem hiding this comment.
Not a maintainer, but I think there's a few things that will need to be changed. That said, I have tested this and it does indeed work as is, and has unblocked me in the interim, so thank you very much! 🙏
| CameraServer::set_monitoring_feeds(p_monitoring_feeds); | ||
| if (p_monitoring_feeds) { | ||
| if (driver == nullptr) { | ||
| driver = new CameraDriverWeb(); |
There was a problem hiding this comment.
This needs to be swapped out for:
driver = memnew(CameraDriverWeb);
Otherwise, we get errors like:
[Browser Error] memory access out of bounds
RuntimeError: memory access out of bounds
at $dlfree (wasm://wasm/godot.web.template_debug.wasm32.dlink.wasm-0220781e:1:886272)
at $Memory::free_static(void*, bool) (wasm://wasm/godot.side.web.template_debug.wasm32.dlink.wasm-88ec8f42:1:55343982)
at $CameraWeb::_cleanup() (wasm://wasm/godot.side.web.template_debug.wasm32.dlink.wasm-88ec8f42:1:9515425)
at $CameraWeb::~CameraWeb() (wasm://wasm/godot.side.web.template_debug.wasm32.dlink.wasm-88ec8f42:1:9515608)
at $Main::cleanup(bool) (wasm://wasm/godot.side.web.template_debug.wasm32.dlink.wasm-88ec8f42:1:3893745)
There was a problem hiding this comment.
Fixed in 833ccaf38df7fbea00e022ae0c56eaf0d18434fe.
| const frameWidth = videoFrame.displayWidth; | ||
| const frameHeight = videoFrame.displayHeight; | ||
| const bufferSize = frameWidth * frameHeight * 4; | ||
| const pixelBuffer = new Uint8Array(bufferSize); |
There was a problem hiding this comment.
I suspect this will be a blocker to getting this merged. The other camera implementations make use of a fixed number of buffers. Allocating a new buffer every frame will lead to significant GC churn on the main thread.
Admittedly, making this work nicely with transferable objects is probably a bit more complex than just maintaining bunch of shared buffers shared by threads with locking.
It think the best way to do it would be something like maintain 4 buffers. When a frame is copied into a buffer, we post/transfer it to the main thread as long as we have at least 1 buffer left remaining in the worker after the transfer. Otherwise, we don't post the frame (yet).
The main thread receives buffers similar to now, and always keeps minimum 1 buffer on its side as well (except for when the first image is yet to be received).
When the main thread/context has:
- More than 1 buffer on its side.
- Already processed the head buffer (frame callback).
then is posts the head buffer back to the worker.
However, this still leads to GC as wrapper references to those transferred buffers are allocated as the buffers ping pong back and forth. The peak memory usage is constrained though, so that's a pretty significant win on its lonesome.
The ideal implementation would detect availability of SharedArrayBuffer and when available use it in conjunction with Atomics.
There was a problem hiding this comment.
Addressed in 40a3f5e46c43fbac2c575a09ac8af59b9a2475cd. Three changes to reduce per-frame GC pressure:
- Introduced a transferable buffer pool (4 pre-allocated ArrayBuffers) in the worker, ping-ponged back from the main thread after use.
- Cached the Wasm heap pointer for frame data copy to avoid per-frame malloc/free.
- Cached facingMode at stream start instead of calling getVideoTracks() + getSettings() every frame.
With these changes, the JS heap stays flat over 60 seconds at 1024×768 @ 30fps. The SharedArrayBuffer upgrade can be explored as a follow-up.

|
I'm gonna take a look at this PR today or tomorrow. |
|
@shiena Thanks for making those changes. I'd made some similar changes locally but adopted yours about 24 hours ago. Working great 🙏 |
Implement camera capture for the Web platform using WebCodecs API with automatic fallback to Canvas 2D for broad browser compatibility. Worker offloading is used when available to prevent main thread blocking.
|
A note on the camera frame capture strategy. Browser support for the APIs we'd like to use for pulling pixels out of a In order of preference:
The tier is chosen once by feature detection when the stream starts. On top of that, each tier also falls back to a lower tier at runtime if its API unexpectedly throws (e.g. |
fixed: godotengine/godot-proposals#12493
Current Limitation:
Theplatform/web/js/libs/library_godot_camera.jslibrary includes certain functionalities that are inherently asynchronous. We are currently investigating how to execute these functions synchronously, as this is a requirement for our project, but haven't yet found a solution.I was able to build with JSPI and call asynchronous functions synchronously in Chrome. However, JSPI only works in Chrome and Edge 137.https://webassembly.org/features/https://learn.microsoft.com/en-us/microsoft-edge/web-platform/release-notes/137#javascript-promise-integration-jspi-in-webassemblySupported browsers: