From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- .../src/camera/emscripten/SDL_camera_emscripten.c | 275 +++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/camera/emscripten/SDL_camera_emscripten.c (limited to 'contrib/SDL-3.2.8/src/camera/emscripten/SDL_camera_emscripten.c') diff --git a/contrib/SDL-3.2.8/src/camera/emscripten/SDL_camera_emscripten.c b/contrib/SDL-3.2.8/src/camera/emscripten/SDL_camera_emscripten.c new file mode 100644 index 0000000..fa2a511 --- /dev/null +++ b/contrib/SDL-3.2.8/src/camera/emscripten/SDL_camera_emscripten.c @@ -0,0 +1,275 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN + +#include "../SDL_syscamera.h" +#include "../SDL_camera_c.h" +#include "../../video/SDL_pixels_c.h" +#include "../../video/SDL_surface_c.h" + +#include + +// just turn off clang-format for this whole file, this INDENT_OFF stuff on +// each EM_ASM section is ugly. +/* *INDENT-OFF* */ // clang-format off + +EM_JS_DEPS(sdlcamera, "$dynCall"); + +static bool EMSCRIPTENCAMERA_WaitDevice(SDL_Camera *device) +{ + SDL_assert(!"This shouldn't be called"); // we aren't using SDL's internal thread. + return false; +} + +static SDL_CameraFrameResult EMSCRIPTENCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS) +{ + void *rgba = SDL_malloc(device->actual_spec.width * device->actual_spec.height * 4); + if (!rgba) { + return SDL_CAMERA_FRAME_ERROR; + } + + *timestampNS = SDL_GetTicksNS(); // best we can do here. + + const int rc = MAIN_THREAD_EM_ASM_INT({ + const w = $0; + const h = $1; + const rgba = $2; + const SDL3 = Module['SDL3']; + if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.ctx2d) === 'undefined')) { + return 0; // don't have something we need, oh well. + } + + SDL3.camera.ctx2d.drawImage(SDL3.camera.video, 0, 0, w, h); + const imgrgba = SDL3.camera.ctx2d.getImageData(0, 0, w, h).data; + Module.HEAPU8.set(imgrgba, rgba); + + return 1; + }, device->actual_spec.width, device->actual_spec.height, rgba); + + if (!rc) { + SDL_free(rgba); + return SDL_CAMERA_FRAME_ERROR; // something went wrong, maybe shutting down; just don't return a frame. + } + + frame->pixels = rgba; + frame->pitch = device->actual_spec.width * 4; + + return SDL_CAMERA_FRAME_READY; +} + +static void EMSCRIPTENCAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame) +{ + SDL_free(frame->pixels); +} + +static void EMSCRIPTENCAMERA_CloseDevice(SDL_Camera *device) +{ + if (device) { + MAIN_THREAD_EM_ASM({ + const SDL3 = Module['SDL3']; + if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.stream) === 'undefined')) { + return; // camera was closed and/or subsystem was shut down, we're already done. + } + SDL3.camera.stream.getTracks().forEach(track => track.stop()); // stop all recording. + SDL3.camera = {}; // dump our references to everything. + }); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static int SDLEmscriptenCameraPermissionOutcome(SDL_Camera *device, int approved, int w, int h, int fps) +{ + if (approved) { + device->actual_spec.format = SDL_PIXELFORMAT_RGBA32; + device->actual_spec.width = w; + device->actual_spec.height = h; + device->actual_spec.framerate_numerator = fps; + device->actual_spec.framerate_denominator = 1; + + if (!SDL_PrepareCameraSurfaces(device)) { + // uhoh, we're in trouble. Probably ran out of memory. + SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Camera could not prepare surfaces: %s ... revoking approval!", SDL_GetError()); + approved = 0; // disconnecting the SDL camera might not be safe here, just mark it as denied by user. + } + } + + SDL_CameraPermissionOutcome(device, approved ? true : false); + return approved; +} + +static bool EMSCRIPTENCAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec) +{ + MAIN_THREAD_EM_ASM({ + // Since we can't get actual specs until we make a move that prompts the user for + // permission, we don't list any specs for the device and wrangle it during device open. + const device = $0; + const w = $1; + const h = $2; + const framerate_numerator = $3; + const framerate_denominator = $4; + const outcome = $5; + const iterate = $6; + + const constraints = {}; + if ((w <= 0) || (h <= 0)) { + constraints.video = true; // didn't ask for anything, let the system choose. + } else { + constraints.video = {}; // asked for a specific thing: request it as "ideal" but take closest hardware will offer. + constraints.video.width = w; + constraints.video.height = h; + } + + if ((framerate_numerator > 0) && (framerate_denominator > 0)) { + var fps = framerate_numerator / framerate_denominator; + constraints.video.frameRate = { ideal: fps }; + } + + function grabNextCameraFrame() { // !!! FIXME: this (currently) runs as a requestAnimationFrame callback, for lack of a better option. + const SDL3 = Module['SDL3']; + if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.stream) === 'undefined')) { + return; // camera was closed and/or subsystem was shut down, stop iterating here. + } + + // time for a new frame from the camera? + const nextframems = SDL3.camera.next_frame_time; + const now = performance.now(); + if (now >= nextframems) { + dynCall('vi', iterate, [device]); // calls SDL_CameraThreadIterate, which will call our AcquireFrame implementation. + + // bump ahead but try to stay consistent on timing, in case we dropped frames. + while (SDL3.camera.next_frame_time < now) { + SDL3.camera.next_frame_time += SDL3.camera.fpsincrms; + } + } + + requestAnimationFrame(grabNextCameraFrame); // run this function again at the display framerate. (!!! FIXME: would this be better as requestIdleCallback?) + } + + navigator.mediaDevices.getUserMedia(constraints) + .then((stream) => { + const settings = stream.getVideoTracks()[0].getSettings(); + const actualw = settings.width; + const actualh = settings.height; + const actualfps = settings.frameRate; + console.log("Camera is opened! Actual spec: (" + actualw + "x" + actualh + "), fps=" + actualfps); + + if (dynCall('iiiiii', outcome, [device, 1, actualw, actualh, actualfps])) { + const video = document.createElement("video"); + video.width = actualw; + video.height = actualh; + video.style.display = 'none'; // we need to attach this to a hidden video node so we can read it as pixels. + video.srcObject = stream; + + const canvas = document.createElement("canvas"); + canvas.width = actualw; + canvas.height = actualh; + canvas.style.display = 'none'; // we need to attach this to a hidden video node so we can read it as pixels. + + const ctx2d = canvas.getContext('2d'); + + const SDL3 = Module['SDL3']; + SDL3.camera.width = actualw; + SDL3.camera.height = actualh; + SDL3.camera.fps = actualfps; + SDL3.camera.fpsincrms = 1000.0 / actualfps; + SDL3.camera.stream = stream; + SDL3.camera.video = video; + SDL3.camera.canvas = canvas; + SDL3.camera.ctx2d = ctx2d; + SDL3.camera.next_frame_time = performance.now(); + + video.play(); + video.addEventListener('loadedmetadata', () => { + grabNextCameraFrame(); // start this loop going. + }); + } + }) + .catch((err) => { + console.error("Tried to open camera but it threw an error! " + err.name + ": " + err.message); + dynCall('iiiiii', outcome, [device, 0, 0, 0, 0]); // we call this a permission error, because it probably is. + }); + }, device, spec->width, spec->height, spec->framerate_numerator, spec->framerate_denominator, SDLEmscriptenCameraPermissionOutcome, SDL_CameraThreadIterate); + + return true; // the real work waits until the user approves a camera. +} + +static void EMSCRIPTENCAMERA_FreeDeviceHandle(SDL_Camera *device) +{ + // no-op. +} + +static void EMSCRIPTENCAMERA_Deinitialize(void) +{ + MAIN_THREAD_EM_ASM({ + if (typeof(Module['SDL3']) !== 'undefined') { + Module['SDL3'].camera = undefined; + } + }); +} + +static void EMSCRIPTENCAMERA_DetectDevices(void) +{ + // `navigator.mediaDevices` is not defined if unsupported or not in a secure context! + const int supported = MAIN_THREAD_EM_ASM_INT({ return (navigator.mediaDevices === undefined) ? 0 : 1; }); + + // if we have support at all, report a single generic camera with no specs. + // We'll find out if there really _is_ a camera when we try to open it, but querying it for real here + // will pop up a user permission dialog warning them we're trying to access the camera, and we generally + // don't want that during SDL_Init(). + if (supported) { + SDL_AddCamera("Web browser's camera", SDL_CAMERA_POSITION_UNKNOWN, 0, NULL, (void *) (size_t) 0x1); + } +} + +static bool EMSCRIPTENCAMERA_Init(SDL_CameraDriverImpl *impl) +{ + MAIN_THREAD_EM_ASM({ + if (typeof(Module['SDL3']) === 'undefined') { + Module['SDL3'] = {}; + } + Module['SDL3'].camera = {}; + }); + + impl->DetectDevices = EMSCRIPTENCAMERA_DetectDevices; + impl->OpenDevice = EMSCRIPTENCAMERA_OpenDevice; + impl->CloseDevice = EMSCRIPTENCAMERA_CloseDevice; + impl->WaitDevice = EMSCRIPTENCAMERA_WaitDevice; + impl->AcquireFrame = EMSCRIPTENCAMERA_AcquireFrame; + impl->ReleaseFrame = EMSCRIPTENCAMERA_ReleaseFrame; + impl->FreeDeviceHandle = EMSCRIPTENCAMERA_FreeDeviceHandle; + impl->Deinitialize = EMSCRIPTENCAMERA_Deinitialize; + + impl->ProvidesOwnCallbackThread = true; + + return true; +} + +CameraBootStrap EMSCRIPTENCAMERA_bootstrap = { + "emscripten", "SDL Emscripten MediaStream camera driver", EMSCRIPTENCAMERA_Init, false +}; + +/* *INDENT-ON* */ // clang-format on + +#endif // SDL_CAMERA_DRIVER_EMSCRIPTEN + -- cgit v1.2.3