| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- /*
- Simple DirectMedia Layer
- Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
- 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 <emscripten/emscripten.h>
- // 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 int EMSCRIPTENCAMERA_WaitDevice(SDL_Camera *device)
- {
- SDL_assert(!"This shouldn't be called"); // we aren't using SDL's internal thread.
- return -1;
- }
- static int 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 -1;
- }
- *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 0; // something went wrong, maybe shutting down; just don't return a frame.
- }
- frame->pixels = rgba;
- frame->pitch = device->actual_spec.width * 4;
- return 1;
- }
- 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 void SDLEmscriptenCameraPermissionOutcome(SDL_Camera *device, int approved, int w, int h, int fps)
- {
- device->spec.width = device->actual_spec.width = w;
- device->spec.height = device->actual_spec.height = h;
- device->spec.framerate_numerator = device->actual_spec.framerate_numerator = fps;
- device->spec.framerate_denominator = device->actual_spec.framerate_denominator = 1;
- if (device->acquire_surface) {
- device->acquire_surface->w = w;
- device->acquire_surface->h = h;
- }
- SDL_CameraPermissionOutcome(device, approved ? SDL_TRUE : SDL_FALSE);
- }
- static int 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);
- dynCall('viiiii', 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('viiiii', 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 0; // 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 SDL_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 = SDL_TRUE;
- return SDL_TRUE;
- }
- CameraBootStrap EMSCRIPTENCAMERA_bootstrap = {
- "emscripten", "SDL Emscripten MediaStream camera driver", EMSCRIPTENCAMERA_Init, SDL_FALSE
- };
- /* *INDENT-ON* */ /* clang-format on */
- #endif // SDL_CAMERA_DRIVER_EMSCRIPTEN
|