diff options
| author | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
| commit | 5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch) | |
| tree | 8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/src/camera/emscripten | |
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/src/camera/emscripten')
| -rw-r--r-- | contrib/SDL-3.2.8/src/camera/emscripten/SDL_camera_emscripten.c | 275 |
1 files changed, 275 insertions, 0 deletions
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 @@ | |||
| 1 | /* | ||
| 2 | Simple DirectMedia Layer | ||
| 3 | Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> | ||
| 4 | |||
| 5 | This software is provided 'as-is', without any express or implied | ||
| 6 | warranty. In no event will the authors be held liable for any damages | ||
| 7 | arising from the use of this software. | ||
| 8 | |||
| 9 | Permission is granted to anyone to use this software for any purpose, | ||
| 10 | including commercial applications, and to alter it and redistribute it | ||
| 11 | freely, subject to the following restrictions: | ||
| 12 | |||
| 13 | 1. The origin of this software must not be misrepresented; you must not | ||
| 14 | claim that you wrote the original software. If you use this software | ||
| 15 | in a product, an acknowledgment in the product documentation would be | ||
| 16 | appreciated but is not required. | ||
| 17 | 2. Altered source versions must be plainly marked as such, and must not be | ||
| 18 | misrepresented as being the original software. | ||
| 19 | 3. This notice may not be removed or altered from any source distribution. | ||
| 20 | */ | ||
| 21 | #include "SDL_internal.h" | ||
| 22 | |||
| 23 | #ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN | ||
| 24 | |||
| 25 | #include "../SDL_syscamera.h" | ||
| 26 | #include "../SDL_camera_c.h" | ||
| 27 | #include "../../video/SDL_pixels_c.h" | ||
| 28 | #include "../../video/SDL_surface_c.h" | ||
| 29 | |||
| 30 | #include <emscripten/emscripten.h> | ||
| 31 | |||
| 32 | // just turn off clang-format for this whole file, this INDENT_OFF stuff on | ||
| 33 | // each EM_ASM section is ugly. | ||
| 34 | /* *INDENT-OFF* */ // clang-format off | ||
| 35 | |||
| 36 | EM_JS_DEPS(sdlcamera, "$dynCall"); | ||
| 37 | |||
| 38 | static bool EMSCRIPTENCAMERA_WaitDevice(SDL_Camera *device) | ||
| 39 | { | ||
| 40 | SDL_assert(!"This shouldn't be called"); // we aren't using SDL's internal thread. | ||
| 41 | return false; | ||
| 42 | } | ||
| 43 | |||
| 44 | static SDL_CameraFrameResult EMSCRIPTENCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS) | ||
| 45 | { | ||
| 46 | void *rgba = SDL_malloc(device->actual_spec.width * device->actual_spec.height * 4); | ||
| 47 | if (!rgba) { | ||
| 48 | return SDL_CAMERA_FRAME_ERROR; | ||
| 49 | } | ||
| 50 | |||
| 51 | *timestampNS = SDL_GetTicksNS(); // best we can do here. | ||
| 52 | |||
| 53 | const int rc = MAIN_THREAD_EM_ASM_INT({ | ||
| 54 | const w = $0; | ||
| 55 | const h = $1; | ||
| 56 | const rgba = $2; | ||
| 57 | const SDL3 = Module['SDL3']; | ||
| 58 | if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.ctx2d) === 'undefined')) { | ||
| 59 | return 0; // don't have something we need, oh well. | ||
| 60 | } | ||
| 61 | |||
| 62 | SDL3.camera.ctx2d.drawImage(SDL3.camera.video, 0, 0, w, h); | ||
| 63 | const imgrgba = SDL3.camera.ctx2d.getImageData(0, 0, w, h).data; | ||
| 64 | Module.HEAPU8.set(imgrgba, rgba); | ||
| 65 | |||
| 66 | return 1; | ||
| 67 | }, device->actual_spec.width, device->actual_spec.height, rgba); | ||
| 68 | |||
| 69 | if (!rc) { | ||
| 70 | SDL_free(rgba); | ||
| 71 | return SDL_CAMERA_FRAME_ERROR; // something went wrong, maybe shutting down; just don't return a frame. | ||
| 72 | } | ||
| 73 | |||
| 74 | frame->pixels = rgba; | ||
| 75 | frame->pitch = device->actual_spec.width * 4; | ||
| 76 | |||
| 77 | return SDL_CAMERA_FRAME_READY; | ||
| 78 | } | ||
| 79 | |||
| 80 | static void EMSCRIPTENCAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame) | ||
| 81 | { | ||
| 82 | SDL_free(frame->pixels); | ||
| 83 | } | ||
| 84 | |||
| 85 | static void EMSCRIPTENCAMERA_CloseDevice(SDL_Camera *device) | ||
| 86 | { | ||
| 87 | if (device) { | ||
| 88 | MAIN_THREAD_EM_ASM({ | ||
| 89 | const SDL3 = Module['SDL3']; | ||
| 90 | if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.stream) === 'undefined')) { | ||
| 91 | return; // camera was closed and/or subsystem was shut down, we're already done. | ||
| 92 | } | ||
| 93 | SDL3.camera.stream.getTracks().forEach(track => track.stop()); // stop all recording. | ||
| 94 | SDL3.camera = {}; // dump our references to everything. | ||
| 95 | }); | ||
| 96 | SDL_free(device->hidden); | ||
| 97 | device->hidden = NULL; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | static int SDLEmscriptenCameraPermissionOutcome(SDL_Camera *device, int approved, int w, int h, int fps) | ||
| 102 | { | ||
| 103 | if (approved) { | ||
| 104 | device->actual_spec.format = SDL_PIXELFORMAT_RGBA32; | ||
| 105 | device->actual_spec.width = w; | ||
| 106 | device->actual_spec.height = h; | ||
| 107 | device->actual_spec.framerate_numerator = fps; | ||
| 108 | device->actual_spec.framerate_denominator = 1; | ||
| 109 | |||
| 110 | if (!SDL_PrepareCameraSurfaces(device)) { | ||
| 111 | // uhoh, we're in trouble. Probably ran out of memory. | ||
| 112 | SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Camera could not prepare surfaces: %s ... revoking approval!", SDL_GetError()); | ||
| 113 | approved = 0; // disconnecting the SDL camera might not be safe here, just mark it as denied by user. | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | SDL_CameraPermissionOutcome(device, approved ? true : false); | ||
| 118 | return approved; | ||
| 119 | } | ||
| 120 | |||
| 121 | static bool EMSCRIPTENCAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec) | ||
| 122 | { | ||
| 123 | MAIN_THREAD_EM_ASM({ | ||
| 124 | // Since we can't get actual specs until we make a move that prompts the user for | ||
| 125 | // permission, we don't list any specs for the device and wrangle it during device open. | ||
| 126 | const device = $0; | ||
| 127 | const w = $1; | ||
| 128 | const h = $2; | ||
| 129 | const framerate_numerator = $3; | ||
| 130 | const framerate_denominator = $4; | ||
| 131 | const outcome = $5; | ||
| 132 | const iterate = $6; | ||
| 133 | |||
| 134 | const constraints = {}; | ||
| 135 | if ((w <= 0) || (h <= 0)) { | ||
| 136 | constraints.video = true; // didn't ask for anything, let the system choose. | ||
| 137 | } else { | ||
| 138 | constraints.video = {}; // asked for a specific thing: request it as "ideal" but take closest hardware will offer. | ||
| 139 | constraints.video.width = w; | ||
| 140 | constraints.video.height = h; | ||
| 141 | } | ||
| 142 | |||
| 143 | if ((framerate_numerator > 0) && (framerate_denominator > 0)) { | ||
| 144 | var fps = framerate_numerator / framerate_denominator; | ||
| 145 | constraints.video.frameRate = { ideal: fps }; | ||
| 146 | } | ||
| 147 | |||
| 148 | function grabNextCameraFrame() { // !!! FIXME: this (currently) runs as a requestAnimationFrame callback, for lack of a better option. | ||
| 149 | const SDL3 = Module['SDL3']; | ||
| 150 | if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.stream) === 'undefined')) { | ||
| 151 | return; // camera was closed and/or subsystem was shut down, stop iterating here. | ||
| 152 | } | ||
| 153 | |||
| 154 | // time for a new frame from the camera? | ||
| 155 | const nextframems = SDL3.camera.next_frame_time; | ||
| 156 | const now = performance.now(); | ||
| 157 | if (now >= nextframems) { | ||
| 158 | dynCall('vi', iterate, [device]); // calls SDL_CameraThreadIterate, which will call our AcquireFrame implementation. | ||
| 159 | |||
| 160 | // bump ahead but try to stay consistent on timing, in case we dropped frames. | ||
| 161 | while (SDL3.camera.next_frame_time < now) { | ||
| 162 | SDL3.camera.next_frame_time += SDL3.camera.fpsincrms; | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | requestAnimationFrame(grabNextCameraFrame); // run this function again at the display framerate. (!!! FIXME: would this be better as requestIdleCallback?) | ||
| 167 | } | ||
| 168 | |||
| 169 | navigator.mediaDevices.getUserMedia(constraints) | ||
| 170 | .then((stream) => { | ||
| 171 | const settings = stream.getVideoTracks()[0].getSettings(); | ||
| 172 | const actualw = settings.width; | ||
| 173 | const actualh = settings.height; | ||
| 174 | const actualfps = settings.frameRate; | ||
| 175 | console.log("Camera is opened! Actual spec: (" + actualw + "x" + actualh + "), fps=" + actualfps); | ||
| 176 | |||
| 177 | if (dynCall('iiiiii', outcome, [device, 1, actualw, actualh, actualfps])) { | ||
| 178 | const video = document.createElement("video"); | ||
| 179 | video.width = actualw; | ||
| 180 | video.height = actualh; | ||
| 181 | video.style.display = 'none'; // we need to attach this to a hidden video node so we can read it as pixels. | ||
| 182 | video.srcObject = stream; | ||
| 183 | |||
| 184 | const canvas = document.createElement("canvas"); | ||
| 185 | canvas.width = actualw; | ||
| 186 | canvas.height = actualh; | ||
| 187 | canvas.style.display = 'none'; // we need to attach this to a hidden video node so we can read it as pixels. | ||
| 188 | |||
| 189 | const ctx2d = canvas.getContext('2d'); | ||
| 190 | |||
| 191 | const SDL3 = Module['SDL3']; | ||
| 192 | SDL3.camera.width = actualw; | ||
| 193 | SDL3.camera.height = actualh; | ||
| 194 | SDL3.camera.fps = actualfps; | ||
| 195 | SDL3.camera.fpsincrms = 1000.0 / actualfps; | ||
| 196 | SDL3.camera.stream = stream; | ||
| 197 | SDL3.camera.video = video; | ||
| 198 | SDL3.camera.canvas = canvas; | ||
| 199 | SDL3.camera.ctx2d = ctx2d; | ||
| 200 | SDL3.camera.next_frame_time = performance.now(); | ||
| 201 | |||
| 202 | video.play(); | ||
| 203 | video.addEventListener('loadedmetadata', () => { | ||
| 204 | grabNextCameraFrame(); // start this loop going. | ||
| 205 | }); | ||
| 206 | } | ||
| 207 | }) | ||
| 208 | .catch((err) => { | ||
| 209 | console.error("Tried to open camera but it threw an error! " + err.name + ": " + err.message); | ||
| 210 | dynCall('iiiiii', outcome, [device, 0, 0, 0, 0]); // we call this a permission error, because it probably is. | ||
| 211 | }); | ||
| 212 | }, device, spec->width, spec->height, spec->framerate_numerator, spec->framerate_denominator, SDLEmscriptenCameraPermissionOutcome, SDL_CameraThreadIterate); | ||
| 213 | |||
| 214 | return true; // the real work waits until the user approves a camera. | ||
| 215 | } | ||
| 216 | |||
| 217 | static void EMSCRIPTENCAMERA_FreeDeviceHandle(SDL_Camera *device) | ||
| 218 | { | ||
| 219 | // no-op. | ||
| 220 | } | ||
| 221 | |||
| 222 | static void EMSCRIPTENCAMERA_Deinitialize(void) | ||
| 223 | { | ||
| 224 | MAIN_THREAD_EM_ASM({ | ||
| 225 | if (typeof(Module['SDL3']) !== 'undefined') { | ||
| 226 | Module['SDL3'].camera = undefined; | ||
| 227 | } | ||
| 228 | }); | ||
| 229 | } | ||
| 230 | |||
| 231 | static void EMSCRIPTENCAMERA_DetectDevices(void) | ||
| 232 | { | ||
| 233 | // `navigator.mediaDevices` is not defined if unsupported or not in a secure context! | ||
| 234 | const int supported = MAIN_THREAD_EM_ASM_INT({ return (navigator.mediaDevices === undefined) ? 0 : 1; }); | ||
| 235 | |||
| 236 | // if we have support at all, report a single generic camera with no specs. | ||
| 237 | // We'll find out if there really _is_ a camera when we try to open it, but querying it for real here | ||
| 238 | // will pop up a user permission dialog warning them we're trying to access the camera, and we generally | ||
| 239 | // don't want that during SDL_Init(). | ||
| 240 | if (supported) { | ||
| 241 | SDL_AddCamera("Web browser's camera", SDL_CAMERA_POSITION_UNKNOWN, 0, NULL, (void *) (size_t) 0x1); | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | static bool EMSCRIPTENCAMERA_Init(SDL_CameraDriverImpl *impl) | ||
| 246 | { | ||
| 247 | MAIN_THREAD_EM_ASM({ | ||
| 248 | if (typeof(Module['SDL3']) === 'undefined') { | ||
| 249 | Module['SDL3'] = {}; | ||
| 250 | } | ||
| 251 | Module['SDL3'].camera = {}; | ||
| 252 | }); | ||
| 253 | |||
| 254 | impl->DetectDevices = EMSCRIPTENCAMERA_DetectDevices; | ||
| 255 | impl->OpenDevice = EMSCRIPTENCAMERA_OpenDevice; | ||
| 256 | impl->CloseDevice = EMSCRIPTENCAMERA_CloseDevice; | ||
| 257 | impl->WaitDevice = EMSCRIPTENCAMERA_WaitDevice; | ||
| 258 | impl->AcquireFrame = EMSCRIPTENCAMERA_AcquireFrame; | ||
| 259 | impl->ReleaseFrame = EMSCRIPTENCAMERA_ReleaseFrame; | ||
| 260 | impl->FreeDeviceHandle = EMSCRIPTENCAMERA_FreeDeviceHandle; | ||
| 261 | impl->Deinitialize = EMSCRIPTENCAMERA_Deinitialize; | ||
| 262 | |||
| 263 | impl->ProvidesOwnCallbackThread = true; | ||
| 264 | |||
| 265 | return true; | ||
| 266 | } | ||
| 267 | |||
| 268 | CameraBootStrap EMSCRIPTENCAMERA_bootstrap = { | ||
| 269 | "emscripten", "SDL Emscripten MediaStream camera driver", EMSCRIPTENCAMERA_Init, false | ||
| 270 | }; | ||
| 271 | |||
| 272 | /* *INDENT-ON* */ // clang-format on | ||
| 273 | |||
| 274 | #endif // SDL_CAMERA_DRIVER_EMSCRIPTEN | ||
| 275 | |||
