Simple Directmedia Layer
at main 268 lines 11 kB view raw
1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2023 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 36EM_JS_DEPS(sdlcamera, "$dynCall"); 37 38static 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 44static 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 80static void EMSCRIPTENCAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame) 81{ 82 SDL_free(frame->pixels); 83} 84 85static 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 101static void SDLEmscriptenCameraPermissionOutcome(SDL_Camera *device, int approved, int w, int h, int fps) 102{ 103 device->spec.width = device->actual_spec.width = w; 104 device->spec.height = device->actual_spec.height = h; 105 device->spec.framerate_numerator = device->actual_spec.framerate_numerator = fps; 106 device->spec.framerate_denominator = device->actual_spec.framerate_denominator = 1; 107 if (device->acquire_surface) { 108 device->acquire_surface->w = w; 109 device->acquire_surface->h = h; 110 } 111 SDL_CameraPermissionOutcome(device, approved ? true : false); 112} 113 114static bool EMSCRIPTENCAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec) 115{ 116 MAIN_THREAD_EM_ASM({ 117 // Since we can't get actual specs until we make a move that prompts the user for 118 // permission, we don't list any specs for the device and wrangle it during device open. 119 const device = $0; 120 const w = $1; 121 const h = $2; 122 const framerate_numerator = $3; 123 const framerate_denominator = $4; 124 const outcome = $5; 125 const iterate = $6; 126 127 const constraints = {}; 128 if ((w <= 0) || (h <= 0)) { 129 constraints.video = true; // didn't ask for anything, let the system choose. 130 } else { 131 constraints.video = {}; // asked for a specific thing: request it as "ideal" but take closest hardware will offer. 132 constraints.video.width = w; 133 constraints.video.height = h; 134 } 135 136 if ((framerate_numerator > 0) && (framerate_denominator > 0)) { 137 var fps = framerate_numerator / framerate_denominator; 138 constraints.video.frameRate = { ideal: fps }; 139 } 140 141 function grabNextCameraFrame() { // !!! FIXME: this (currently) runs as a requestAnimationFrame callback, for lack of a better option. 142 const SDL3 = Module['SDL3']; 143 if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.stream) === 'undefined')) { 144 return; // camera was closed and/or subsystem was shut down, stop iterating here. 145 } 146 147 // time for a new frame from the camera? 148 const nextframems = SDL3.camera.next_frame_time; 149 const now = performance.now(); 150 if (now >= nextframems) { 151 dynCall('vi', iterate, [device]); // calls SDL_CameraThreadIterate, which will call our AcquireFrame implementation. 152 153 // bump ahead but try to stay consistent on timing, in case we dropped frames. 154 while (SDL3.camera.next_frame_time < now) { 155 SDL3.camera.next_frame_time += SDL3.camera.fpsincrms; 156 } 157 } 158 159 requestAnimationFrame(grabNextCameraFrame); // run this function again at the display framerate. (!!! FIXME: would this be better as requestIdleCallback?) 160 } 161 162 navigator.mediaDevices.getUserMedia(constraints) 163 .then((stream) => { 164 const settings = stream.getVideoTracks()[0].getSettings(); 165 const actualw = settings.width; 166 const actualh = settings.height; 167 const actualfps = settings.frameRate; 168 console.log("Camera is opened! Actual spec: (" + actualw + "x" + actualh + "), fps=" + actualfps); 169 170 dynCall('viiiii', outcome, [device, 1, actualw, actualh, actualfps]); 171 172 const video = document.createElement("video"); 173 video.width = actualw; 174 video.height = actualh; 175 video.style.display = 'none'; // we need to attach this to a hidden video node so we can read it as pixels. 176 video.srcObject = stream; 177 178 const canvas = document.createElement("canvas"); 179 canvas.width = actualw; 180 canvas.height = actualh; 181 canvas.style.display = 'none'; // we need to attach this to a hidden video node so we can read it as pixels. 182 183 const ctx2d = canvas.getContext('2d'); 184 185 const SDL3 = Module['SDL3']; 186 SDL3.camera.width = actualw; 187 SDL3.camera.height = actualh; 188 SDL3.camera.fps = actualfps; 189 SDL3.camera.fpsincrms = 1000.0 / actualfps; 190 SDL3.camera.stream = stream; 191 SDL3.camera.video = video; 192 SDL3.camera.canvas = canvas; 193 SDL3.camera.ctx2d = ctx2d; 194 SDL3.camera.next_frame_time = performance.now(); 195 196 video.play(); 197 video.addEventListener('loadedmetadata', () => { 198 grabNextCameraFrame(); // start this loop going. 199 }); 200 }) 201 .catch((err) => { 202 console.error("Tried to open camera but it threw an error! " + err.name + ": " + err.message); 203 dynCall('viiiii', outcome, [device, 0, 0, 0, 0]); // we call this a permission error, because it probably is. 204 }); 205 }, device, spec->width, spec->height, spec->framerate_numerator, spec->framerate_denominator, SDLEmscriptenCameraPermissionOutcome, SDL_CameraThreadIterate); 206 207 return true; // the real work waits until the user approves a camera. 208} 209 210static void EMSCRIPTENCAMERA_FreeDeviceHandle(SDL_Camera *device) 211{ 212 // no-op. 213} 214 215static void EMSCRIPTENCAMERA_Deinitialize(void) 216{ 217 MAIN_THREAD_EM_ASM({ 218 if (typeof(Module['SDL3']) !== 'undefined') { 219 Module['SDL3'].camera = undefined; 220 } 221 }); 222} 223 224static void EMSCRIPTENCAMERA_DetectDevices(void) 225{ 226 // `navigator.mediaDevices` is not defined if unsupported or not in a secure context! 227 const int supported = MAIN_THREAD_EM_ASM_INT({ return (navigator.mediaDevices === undefined) ? 0 : 1; }); 228 229 // if we have support at all, report a single generic camera with no specs. 230 // We'll find out if there really _is_ a camera when we try to open it, but querying it for real here 231 // will pop up a user permission dialog warning them we're trying to access the camera, and we generally 232 // don't want that during SDL_Init(). 233 if (supported) { 234 SDL_AddCamera("Web browser's camera", SDL_CAMERA_POSITION_UNKNOWN, 0, NULL, (void *) (size_t) 0x1); 235 } 236} 237 238static bool EMSCRIPTENCAMERA_Init(SDL_CameraDriverImpl *impl) 239{ 240 MAIN_THREAD_EM_ASM({ 241 if (typeof(Module['SDL3']) === 'undefined') { 242 Module['SDL3'] = {}; 243 } 244 Module['SDL3'].camera = {}; 245 }); 246 247 impl->DetectDevices = EMSCRIPTENCAMERA_DetectDevices; 248 impl->OpenDevice = EMSCRIPTENCAMERA_OpenDevice; 249 impl->CloseDevice = EMSCRIPTENCAMERA_CloseDevice; 250 impl->WaitDevice = EMSCRIPTENCAMERA_WaitDevice; 251 impl->AcquireFrame = EMSCRIPTENCAMERA_AcquireFrame; 252 impl->ReleaseFrame = EMSCRIPTENCAMERA_ReleaseFrame; 253 impl->FreeDeviceHandle = EMSCRIPTENCAMERA_FreeDeviceHandle; 254 impl->Deinitialize = EMSCRIPTENCAMERA_Deinitialize; 255 256 impl->ProvidesOwnCallbackThread = true; 257 258 return true; 259} 260 261CameraBootStrap EMSCRIPTENCAMERA_bootstrap = { 262 "emscripten", "SDL Emscripten MediaStream camera driver", EMSCRIPTENCAMERA_Init, false 263}; 264 265/* *INDENT-ON* */ // clang-format on 266 267#endif // SDL_CAMERA_DRIVER_EMSCRIPTEN 268