From 67708f91100e901c309a62beb4a39c8f6d434c9a Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Fri, 22 Dec 2023 01:23:49 -0500 Subject: [PATCH] camera: Emscripten support! This also adds code to deal with waiting for the user to approve camera access, reworks testcameraminimal to use main callbacks, etc. --- CMakeLists.txt | 6 + include/SDL3/SDL_camera.h | 65 ++++- include/SDL3/SDL_events.h | 4 +- include/build_config/SDL_build_config.h.cmake | 1 + .../SDL_build_config_emscripten.h | 4 +- src/camera/SDL_camera.c | 118 ++++++-- src/camera/SDL_syscamera.h | 7 + src/camera/emscripten/SDL_camera_emscripten.c | 269 ++++++++++++++++++ src/camera/v4l2/SDL_camera_v4l2.c | 3 + src/dynapi/SDL_dynapi.sym | 1 + src/dynapi/SDL_dynapi_overrides.h | 1 + src/dynapi/SDL_dynapi_procs.h | 1 + src/events/SDL_events.c | 6 + test/testcameraminimal.c | 218 +++++++------- 14 files changed, 559 insertions(+), 145 deletions(-) create mode 100644 src/camera/emscripten/SDL_camera_emscripten.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b6eefb296..74ed3a10d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1423,6 +1423,12 @@ elseif(EMSCRIPTEN) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/emscripten/*.c") set(HAVE_SDL_FILESYSTEM TRUE) + if(SDL_CAMERA) + set(SDL_CAMERA_DRIVER_EMSCRIPTEN 1) + set(HAVE_CAMERA TRUE) + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/emscripten/*.c") + endif() + if(SDL_JOYSTICK) set(SDL_JOYSTICK_EMSCRIPTEN 1) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/joystick/emscripten/*.c") diff --git a/include/SDL3/SDL_camera.h b/include/SDL3/SDL_camera.h index 0204fe9bd..a57a66a06 100644 --- a/include/SDL3/SDL_camera.h +++ b/include/SDL3/SDL_camera.h @@ -171,6 +171,13 @@ extern DECLSPEC SDL_CameraDeviceID *SDLCALL SDL_GetCameraDevices(int *count); * The returned list is owned by the caller, and should be released with * SDL_free() when no longer needed. * + * Note that it's legal for a camera to supply a list with only the zeroed + * final element and `*count` set to zero; this is what will happen on + * Emscripten builds, since that platform won't tell _anything_ about + * available cameras until you've opened one, and won't even tell if there + * _is_ a camera until the user has given you permission to check through + * a scary warning popup. + * * \param devid the camera device instance ID to query. * \param count a pointer filled in with the number of elements in the list. Can be NULL. * \returns a 0 terminated array of SDL_CameraSpecs, which should be @@ -224,6 +231,16 @@ extern DECLSPEC char * SDLCALL SDL_GetCameraDeviceName(SDL_CameraDeviceID instan * SDL_GetCameraFormat() to see the actual framerate of the opened the device, * and check your timestamps if this is crucial to your app! * + * Note that the camera is not usable until the user approves its use! On + * some platforms, the operating system will prompt the user to permit access + * to the camera, and they can choose Yes or No at that point. Until they do, + * the camera will not be usable. The app should either wait for an + * SDL_EVENT_CAMERA_DEVICE_APPROVED (or SDL_EVENT_CAMERA_DEVICE_DENIED) event, + * or poll SDL_IsCameraApproved() occasionally until it returns non-zero. On + * platforms that don't require explicit user approval (and perhaps in places + * where the user previously permitted access), the approval event might come + * immediately, but it might come seconds, minutes, or hours later! + * * \param instance_id the camera device instance ID * \param spec The desired format for data the device will provide. Can be NULL. * \returns device, or NULL on failure; call SDL_GetError() for more @@ -238,6 +255,38 @@ extern DECLSPEC char * SDLCALL SDL_GetCameraDeviceName(SDL_CameraDeviceID instan */ extern DECLSPEC SDL_Camera *SDLCALL SDL_OpenCameraDevice(SDL_CameraDeviceID instance_id, const SDL_CameraSpec *spec); +/** + * Query if camera access has been approved by the user. + * + * Cameras will not function between when the device is opened by the app + * and when the user permits access to the hardware. On some platforms, this + * presents as a popup dialog where the user has to explicitly approve access; + * on others the approval might be implicit and not alert the user at all. + * + * This function can be used to check the status of that approval. It will + * return 0 if still waiting for user response, 1 if the camera is approved + * for use, and -1 if the user denied access. + * + * Instead of polling with this function, you can wait for a + * SDL_EVENT_CAMERA_DEVICE_APPROVED (or SDL_EVENT_CAMERA_DEVICE_DENIED) event + * in the standard SDL event loop, which is guaranteed to be sent once when + * permission to use the camera is decided. + * + * If a camera is declined, there's nothing to be done but call + * SDL_CloseCamera() to dispose of it. + * + * \param camera the opened camera device to query + * \returns -1 if user denied access to the camera, 1 if user approved access, 0 if no decision has been made yet. + * + * \threadsafety It is safe to call this function from any thread. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_OpenCameraDevice + * \sa SDL_CloseCamera + */ +extern DECLSPEC int SDLCALL SDL_GetCameraPermissionState(SDL_Camera *camera); + /** * Get the instance ID of an opened camera. * @@ -275,6 +324,12 @@ extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetCameraProperties(SDL_Camera *cam * Note that this might not be the native format of the hardware, as SDL * might be converting to this format behind the scenes. * + * If the system is waiting for the user to approve access to the camera, as + * some platforms require, this will return -1, but this isn't necessarily a + * fatal error; you should either wait for an SDL_EVENT_CAMERA_DEVICE_APPROVED + * (or SDL_EVENT_CAMERA_DEVICE_DENIED) event, or poll SDL_IsCameraApproved() + * occasionally until it returns non-zero. + * * \param camera opened camera device * \param spec The SDL_CameraSpec to be initialized by this function. * \returns 0 on success or a negative error code on failure; call @@ -305,13 +360,17 @@ extern DECLSPEC int SDLCALL SDL_GetCameraFormat(SDL_Camera *camera, SDL_CameraSp * failure here is almost always an out of memory condition. * * After use, the frame should be released with SDL_ReleaseCameraFrame(). If you - * don't do this, the system may stop providing more video! If the hardware is - * using DMA to write directly into memory, frames held too long may be overwritten - * with new data. + * don't do this, the system may stop providing more video! * * Do not call SDL_FreeSurface() on the returned surface! It must be given back * to the camera subsystem with SDL_ReleaseCameraFrame! * + * If the system is waiting for the user to approve access to the camera, as + * some platforms require, this will return NULL (no frames available); you should + * either wait for an SDL_EVENT_CAMERA_DEVICE_APPROVED (or + * SDL_EVENT_CAMERA_DEVICE_DENIED) event, or poll SDL_IsCameraApproved() + * occasionally until it returns non-zero. + * * \param camera opened camera device * \param timestampNS a pointer filled in with the frame's timestamp, or 0 on error. Can be NULL. * \returns A new frame of video on success, NULL if none is currently available. diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 74a39267c..cbd1f2a8a 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -208,6 +208,8 @@ typedef enum /* Camera hotplug events */ SDL_EVENT_CAMERA_DEVICE_ADDED = 0x1400, /**< A new camera device is available */ SDL_EVENT_CAMERA_DEVICE_REMOVED, /**< A camera device has been removed. */ + SDL_EVENT_CAMERA_DEVICE_APPROVED, /**< A camera device has been approved for use by the user. */ + SDL_EVENT_CAMERA_DEVICE_DENIED, /**< A camera device has been denied for use by the user. */ /* Render events */ SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */ @@ -535,7 +537,7 @@ typedef struct SDL_AudioDeviceEvent */ typedef struct SDL_CameraDeviceEvent { - Uint32 type; /**< ::SDL_EVENT_CAMERA_DEVICE_ADDED, or ::SDL_EVENT_CAMERA_DEVICE_REMOVED */ + Uint32 type; /**< ::SDL_EVENT_CAMERA_DEVICE_ADDED, ::SDL_EVENT_CAMERA_DEVICE_REMOVED, ::SDL_EVENT_CAMERA_DEVICE_APPROVED, ::SDL_EVENT_CAMERA_DEVICE_DENIED */ Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ SDL_CameraDeviceID which; /**< SDL_CameraDeviceID for the device being added or removed or changing */ Uint8 padding1; diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake index 7985065dc..9f4302a79 100644 --- a/include/build_config/SDL_build_config.h.cmake +++ b/include/build_config/SDL_build_config.h.cmake @@ -472,6 +472,7 @@ #cmakedefine SDL_CAMERA_DRIVER_V4L2 @SDL_CAMERA_DRIVER_V4L2@ #cmakedefine SDL_CAMERA_DRIVER_COREMEDIA @SDL_CAMERA_DRIVER_COREMEDIA@ #cmakedefine SDL_CAMERA_DRIVER_ANDROID @SDL_CAMERA_DRIVER_ANDROID@ +#cmakedefine SDL_CAMERA_DRIVER_EMSCRIPTEN @SDL_CAMERA_DRIVER_EMSCRIPTEND@ /* Enable misc subsystem */ #cmakedefine SDL_MISC_DUMMY @SDL_MISC_DUMMY@ diff --git a/include/build_config/SDL_build_config_emscripten.h b/include/build_config/SDL_build_config_emscripten.h index 07a94d615..9e4ad6aa2 100644 --- a/include/build_config/SDL_build_config_emscripten.h +++ b/include/build_config/SDL_build_config_emscripten.h @@ -209,7 +209,7 @@ /* Enable system filesystem support */ #define SDL_FILESYSTEM_EMSCRIPTEN 1 -/* Enable the camera driver (src/camera/dummy/\*.c) */ /* !!! FIXME */ -#define SDL_CAMERA_DRIVER_DUMMY 1 +/* Enable the camera driver */ +#define SDL_CAMERA_DRIVER_EMSCRIPTEN 1 #endif /* SDL_build_config_emscripten_h */ diff --git a/src/camera/SDL_camera.c b/src/camera/SDL_camera.c index c8af9f0e2..01179dc44 100644 --- a/src/camera/SDL_camera.c +++ b/src/camera/SDL_camera.c @@ -40,6 +40,9 @@ static const CameraBootStrap *const bootstrap[] = { #ifdef SDL_CAMERA_DRIVER_ANDROID &ANDROIDCAMERA_bootstrap, #endif +#ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN + &EMSCRIPTENCAMERA_bootstrap, +#endif #ifdef SDL_CAMERA_DRIVER_DUMMY &DUMMYCAMERA_bootstrap, #endif @@ -247,8 +250,8 @@ static int SDLCALL CameraSpecCmp(const void *vpa, const void *vpb) SDL_CameraDevice *SDL_AddCameraDevice(const char *name, int num_specs, const SDL_CameraSpec *specs, void *handle) { SDL_assert(name != NULL); - SDL_assert(num_specs > 0); - SDL_assert(specs != NULL); + SDL_assert(num_specs >= 0); + SDL_assert((specs != NULL) == (num_specs > 0)); SDL_assert(handle != NULL); SDL_LockRWLockForReading(camera_driver.device_hash_lock); @@ -284,22 +287,24 @@ SDL_CameraDevice *SDL_AddCameraDevice(const char *name, int num_specs, const SDL return NULL; } - SDL_memcpy(device->all_specs, specs, sizeof (*specs) * num_specs); - SDL_qsort(device->all_specs, num_specs, sizeof (*specs), CameraSpecCmp); + if (num_specs > 0) { + SDL_memcpy(device->all_specs, specs, sizeof (*specs) * num_specs); + SDL_qsort(device->all_specs, num_specs, sizeof (*specs), CameraSpecCmp); - // weed out duplicates, just in case. - for (int i = 0; i < num_specs; i++) { - SDL_CameraSpec *a = &device->all_specs[i]; - SDL_CameraSpec *b = &device->all_specs[i + 1]; - if (SDL_memcmp(a, b, sizeof (*a)) == 0) { - SDL_memmove(a, b, sizeof (*specs) * (num_specs - i)); - i--; - num_specs--; + // weed out duplicates, just in case. + for (int i = 0; i < num_specs; i++) { + SDL_CameraSpec *a = &device->all_specs[i]; + SDL_CameraSpec *b = &device->all_specs[i + 1]; + if (SDL_memcmp(a, b, sizeof (*a)) == 0) { + SDL_memmove(a, b, sizeof (*specs) * (num_specs - i)); + i--; + num_specs--; + } } } #if DEBUG_CAMERA - SDL_Log("CAMERA: Adding device ('%s') with %d spec%s:", name, num_specs, (num_specs == 1) ? "" : "s"); + SDL_Log("CAMERA: Adding device ('%s') with %d spec%s%s", name, num_specs, (num_specs == 1) ? "" : "s", (num_specs == 0) ? "" : ":"); for (int i = 0; i < num_specs; i++) { const SDL_CameraSpec *spec = &device->all_specs[i]; SDL_Log("CAMERA: - fmt=%s, w=%d, h=%d, numerator=%d, denominator=%d", SDL_GetPixelFormatName(spec->format), spec->width, spec->height, spec->interval_numerator, spec->interval_denominator); @@ -398,6 +403,42 @@ sdfsdf } } +void SDL_CameraDevicePermissionOutcome(SDL_CameraDevice *device, SDL_bool approved) +{ + if (!device) { + return; + } + + SDL_PendingCameraDeviceEvent pending; + pending.next = NULL; + SDL_PendingCameraDeviceEvent *pending_tail = &pending; + + const int permission = approved ? 1 : -1; + + ObtainPhysicalCameraDeviceObj(device); + if (device->permission != permission) { + device->permission = permission; + SDL_PendingCameraDeviceEvent *p = (SDL_PendingCameraDeviceEvent *) SDL_malloc(sizeof (SDL_PendingCameraDeviceEvent)); + if (p) { // if this failed, no event for you, but you have deeper problems anyhow. + p->type = approved ? SDL_EVENT_CAMERA_DEVICE_APPROVED : SDL_EVENT_CAMERA_DEVICE_DENIED; + p->devid = device->instance_id; + p->next = NULL; + pending_tail->next = p; + pending_tail = p; + } + } + + ReleaseCameraDevice(device); + + SDL_LockRWLockForWriting(camera_driver.device_hash_lock); + SDL_assert(camera_driver.pending_events_tail != NULL); + SDL_assert(camera_driver.pending_events_tail->next == NULL); + camera_driver.pending_events_tail->next = pending.next; + camera_driver.pending_events_tail = pending_tail; + SDL_UnlockRWLock(camera_driver.device_hash_lock); +} + + SDL_CameraDevice *SDL_FindPhysicalCameraDeviceByCallback(SDL_bool (*callback)(SDL_CameraDevice *device, void *userdata), void *userdata) { if (!SDL_GetCurrentCameraDriver()) { @@ -439,7 +480,14 @@ int SDL_GetCameraFormat(SDL_Camera *camera, SDL_CameraSpec *spec) } SDL_CameraDevice *device = (SDL_CameraDevice *) camera; // currently there's no separation between physical and logical device. - SDL_copyp(spec, &device->spec); + ObtainPhysicalCameraDeviceObj(device); + const int retval = (device->permission > 0) ? 0 : SDL_SetError("Camera permission has not been granted"); + if (retval == 0) { + SDL_copyp(spec, &device->spec); + } else { + SDL_zerop(spec); + } + ReleaseCameraDevice(device); return 0; } @@ -545,7 +593,11 @@ SDL_bool SDL_CameraThreadIterate(SDL_CameraDevice *device) return SDL_FALSE; // we're done, shut it down. } - // !!! FIXME: this should block elsewhere without holding the lock until a frame is available, like the audio subsystem does. + const int permission = device->permission; + if (permission <= 0) { + SDL_UnlockMutex(device->lock); + return (permission < 0) ? SDL_FALSE : SDL_TRUE; // if permission was denied, shut it down. if undecided, we're done for now. + } SDL_bool failed = SDL_FALSE; // set to true if disaster worthy of treating the device as lost has happened. SDL_Surface *acquired = NULL; @@ -661,7 +713,7 @@ static int SDLCALL CameraThread(void *devicep) SDL_CameraDevice *device = (SDL_CameraDevice *) devicep; #if DEBUG_CAMERA - SDL_Log("CAMERA: Start thread 'SDL_CameraThread'"); + SDL_Log("CAMERA: dev[%p] Start thread 'CameraThread'", devicep); #endif SDL_assert(device != NULL); @@ -676,7 +728,7 @@ static int SDLCALL CameraThread(void *devicep) SDL_CameraThreadShutdown(device); #if DEBUG_CAMERA - SDL_Log("CAMERA: dev[%p] End thread 'SDL_CameraThread'", (void *)device); + SDL_Log("CAMERA: dev[%p] End thread 'CameraThread'", devicep); #endif return 0; @@ -697,9 +749,13 @@ static void ChooseBestCameraSpec(SDL_CameraDevice *device, const SDL_CameraSpec SDL_zerop(closest); SDL_assert(((Uint32) SDL_PIXELFORMAT_UNKNOWN) == 0); // since we SDL_zerop'd to this value. - if (!spec) { // nothing specifically requested, get the best format we can... + if (device->num_specs == 0) { // device listed no specs! You get whatever you want! + if (spec) { + SDL_copyp(closest, spec); + } + return; + } else if (!spec) { // nothing specifically requested, get the best format we can... // we sorted this into the "best" format order when adding the camera. - SDL_assert(device->num_specs > 0); SDL_copyp(closest, &device->all_specs[0]); } else { // specific thing requested, try to get as close to that as possible... const int num_specs = device->num_specs; @@ -924,6 +980,12 @@ SDL_Surface *SDL_AcquireCameraFrame(SDL_Camera *camera, Uint64 *timestampNS) ObtainPhysicalCameraDeviceObj(device); + if (device->permission <= 0) { + ReleaseCameraDevice(device); + SDL_SetError("Camera permission has not been granted"); + return NULL; + } + SDL_Surface *retval = NULL; // frames are in this list from newest to oldest, so find the end of the list... @@ -996,8 +1058,6 @@ int SDL_ReleaseCameraFrame(SDL_Camera *camera, SDL_Surface *frame) return 0; } -// !!! FIXME: add a way to "pause" camera output. - SDL_CameraDeviceID SDL_GetCameraInstanceID(SDL_Camera *camera) { SDL_CameraDeviceID retval = 0; @@ -1031,6 +1091,22 @@ SDL_PropertiesID SDL_GetCameraProperties(SDL_Camera *camera) return retval; } +int SDL_GetCameraPermissionState(SDL_Camera *camera) +{ + int retval; + if (!camera) { + retval = SDL_InvalidParamError("camera"); + } else { + SDL_CameraDevice *device = (SDL_CameraDevice *) camera; // currently there's no separation between physical and logical device. + ObtainPhysicalCameraDeviceObj(device); + retval = device->permission; + ReleaseCameraDevice(device); + } + + return retval; +} + + static void CompleteCameraEntryPoints(void) { // this doesn't currently fill in stub implementations, it just asserts the backend filled them all in. diff --git a/src/camera/SDL_syscamera.h b/src/camera/SDL_syscamera.h index 27672890b..e6b204771 100644 --- a/src/camera/SDL_syscamera.h +++ b/src/camera/SDL_syscamera.h @@ -50,6 +50,9 @@ extern void SDL_CameraDeviceDisconnected(SDL_CameraDevice *device); // Find an SDL_CameraDevice, selected by a callback. NULL if not found. DOES NOT LOCK THE DEVICE. extern SDL_CameraDevice *SDL_FindPhysicalCameraDeviceByCallback(SDL_bool (*callback)(SDL_CameraDevice *device, void *userdata), void *userdata); +// Backends should call this when the user has approved/denied access to a camera. +extern void SDL_CameraDevicePermissionOutcome(SDL_CameraDevice *device, SDL_bool approved); + // These functions are the heart of the camera threads. Backends can call them directly if they aren't using the SDL-provided thread. extern void SDL_CameraThreadSetup(SDL_CameraDevice *device); extern SDL_bool SDL_CameraThreadIterate(SDL_CameraDevice *device); @@ -129,6 +132,9 @@ struct SDL_CameraDevice // Optional properties. SDL_PropertiesID props; + // -1: user denied permission, 0: waiting for user response, 1: user approved permission. + int permission; + // Data private to this driver, used when device is opened and running. struct SDL_PrivateCameraData *hidden; }; @@ -182,5 +188,6 @@ extern CameraBootStrap DUMMYCAMERA_bootstrap; extern CameraBootStrap V4L2_bootstrap; extern CameraBootStrap COREMEDIA_bootstrap; extern CameraBootStrap ANDROIDCAMERA_bootstrap; +extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap; #endif // SDL_syscamera_h_ diff --git a/src/camera/emscripten/SDL_camera_emscripten.c b/src/camera/emscripten/SDL_camera_emscripten.c new file mode 100644 index 000000000..a04a8e454 --- /dev/null +++ b/src/camera/emscripten/SDL_camera_emscripten.c @@ -0,0 +1,269 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 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 + +// 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_CameraDevice *device) +{ + SDL_assert(!"This shouldn't be called"); // we aren't using SDL's internal thread. + return -1; +} + +static int EMSCRIPTENCAMERA_AcquireFrame(SDL_CameraDevice *device, SDL_Surface *frame, Uint64 *timestampNS) +{ + void *rgba = SDL_malloc(device->actual_spec.width * device->actual_spec.height * 4); + if (!rgba) { + return SDL_OutOfMemory(); + } + + *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_CameraDevice *device, SDL_Surface *frame) +{ + SDL_free(frame->pixels); + frame->pixels = NULL; + frame->pitch = 0; +} + +static void EMSCRIPTENCAMERA_CloseDevice(SDL_CameraDevice *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. + _SDL_free(SDL3.camera.rgba); + SDL3.camera = {}; // dump our references to everything. + }); + SDL_free(device->hidden); + device->hidden = NULL; + } +} + +static void SDLEmscriptenCameraDevicePermissionOutcome(SDL_CameraDevice *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.interval_numerator = device->actual_spec.interval_numerator = 1; + device->spec.interval_denominator = device->actual_spec.interval_denominator = fps; + SDL_CameraDevicePermissionOutcome(device, approved ? SDL_TRUE : SDL_FALSE); +} + +static int EMSCRIPTENCAMERA_OpenDevice(SDL_CameraDevice *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 interval_numerator = $3; + const interval_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 ((interval_numerator > 0) && (interval_denominator > 0)) { + var fps = interval_denominator / interval_numerator; + 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.rgba = 0; + 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->interval_numerator, spec->interval_denominator, SDLEmscriptenCameraDevicePermissionOutcome, SDL_CameraThreadIterate); + + return 0; // the real work waits until the user approves a camera. +} + +static void EMSCRIPTENCAMERA_FreeDeviceHandle(SDL_CameraDevice *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_AddCameraDevice("Web browser's camera", 0, NULL, (void *) (size_t) 0x1); + } +} + +static SDL_bool EMSCRIPTENCAMERA_Init(SDL_CameraDriverImpl *impl) +{ +SDL_Log("EMSCRIPTENCAMERA_Init, %s:%d", __FILE__, __LINE__); + MAIN_THREAD_EM_ASM({ + if (typeof(Module['SDL3']) === 'undefined') { + Module['SDL3'] = {}; + } + Module['SDL3'].camera = {}; + }); + +SDL_Log("EMSCRIPTENCAMERA_Init, %s:%d", __FILE__, __LINE__); + 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 + diff --git a/src/camera/v4l2/SDL_camera_v4l2.c b/src/camera/v4l2/SDL_camera_v4l2.c index fd76ad4f6..9f3b1e08b 100644 --- a/src/camera/v4l2/SDL_camera_v4l2.c +++ b/src/camera/v4l2/SDL_camera_v4l2.c @@ -619,6 +619,9 @@ static int V4L2_OpenDevice(SDL_CameraDevice *device, const SDL_CameraSpec *spec) } } + // Currently there is no user permission prompt for camera access, but maybe there will be a D-Bus portal interface at some point. + SDL_CameraDevicePermissionOutcome(device, SDL_TRUE); + return 0; } diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index a098f2709..c97153b87 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -970,6 +970,7 @@ SDL3_0.0.0 { SDL_AcquireCameraFrame; SDL_ReleaseCameraFrame; SDL_CloseCamera; + SDL_GetCameraPermissionState; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index db16355d7..9ee787ef8 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -995,3 +995,4 @@ #define SDL_AcquireCameraFrame SDL_AcquireCameraFrame_REAL #define SDL_ReleaseCameraFrame SDL_ReleaseCameraFrame_REAL #define SDL_CloseCamera SDL_CloseCamera_REAL +#define SDL_GetCameraPermissionState SDL_GetCameraPermissionState_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 49d5e1a75..bcff07cee 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1020,3 +1020,4 @@ SDL_DYNAPI_PROC(int,SDL_GetCameraFormat,(SDL_Camera *a, SDL_CameraSpec *b),(a,b) SDL_DYNAPI_PROC(SDL_Surface*,SDL_AcquireCameraFrame,(SDL_Camera *a, Uint64 *b),(a,b),return) SDL_DYNAPI_PROC(int,SDL_ReleaseCameraFrame,(SDL_Camera *a, SDL_Surface *b),(a,b),return) SDL_DYNAPI_PROC(void,SDL_CloseCamera,(SDL_Camera *a),(a),) +SDL_DYNAPI_PROC(int,SDL_GetCameraPermissionState,(SDL_Camera *a),(a),return) diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c index 04a54953a..baaff2c99 100644 --- a/src/events/SDL_events.c +++ b/src/events/SDL_events.c @@ -562,6 +562,12 @@ static void SDL_LogEvent(const SDL_Event *event) SDL_EVENT_CASE(SDL_EVENT_CAMERA_DEVICE_REMOVED) PRINT_CAMERADEV_EVENT(event); break; + SDL_EVENT_CASE(SDL_EVENT_CAMERA_DEVICE_APPROVED) + PRINT_CAMERADEV_EVENT(event); + break; + SDL_EVENT_CASE(SDL_EVENT_CAMERA_DEVICE_DENIED) + PRINT_CAMERADEV_EVENT(event); + break; #undef PRINT_CAMERADEV_EVENT SDL_EVENT_CASE(SDL_EVENT_SENSOR_UPDATE) diff --git a/test/testcameraminimal.c b/test/testcameraminimal.c index df8a08b17..6eb4f7968 100644 --- a/test/testcameraminimal.c +++ b/test/testcameraminimal.c @@ -9,38 +9,27 @@ including commercial applications, and to alter it and redistribute it freely. */ -#include "SDL3/SDL_main.h" -#include "SDL3/SDL.h" -#include "SDL3/SDL_test.h" -#include "SDL3/SDL_camera.h" -#ifdef SDL_PLATFORM_EMSCRIPTEN -#include -#endif +#define SDL_MAIN_USE_CALLBACKS 1 +#include +#include +#include -#include +static SDL_Window *window = NULL; +static SDL_Renderer *renderer = NULL; +static SDLTest_CommonState *state = NULL; +static SDL_Camera *camera = NULL; +static SDL_CameraSpec spec; +static SDL_Texture *texture = NULL; +static SDL_bool texture_updated = SDL_FALSE; +static SDL_Surface *frame_current = NULL; -int main(int argc, char **argv) +int SDL_AppInit(int argc, char *argv[]) { - SDL_Window *window = NULL; - SDL_Renderer *renderer = NULL; - SDL_Event evt; - int quit = 0; - SDLTest_CommonState *state = NULL; - - SDL_Camera *camera = NULL; - SDL_CameraSpec spec; - SDL_Texture *texture = NULL; - int texture_updated = 0; - SDL_Surface *frame_current = NULL; - - SDL_zero(evt); - SDL_zero(spec); - /* Initialize test framework */ state = SDLTest_CommonCreateState(argv, 0); if (state == NULL) { - return 1; + return -1; } /* Enable standard application logging */ @@ -49,13 +38,13 @@ int main(int argc, char **argv) /* Load the SDL library */ if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_CAMERA) < 0) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s", SDL_GetError()); - return 1; + return -1; } window = SDL_CreateWindow("Local Video", 1000, 800, 0); if (window == NULL) { SDL_Log("Couldn't create window: %s", SDL_GetError()); - return 1; + return -1; } SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); @@ -63,13 +52,13 @@ int main(int argc, char **argv) renderer = SDL_CreateRenderer(window, NULL, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); if (renderer == NULL) { /* SDL_Log("Couldn't create renderer: %s", SDL_GetError()); */ - return 1; + return -1; } SDL_CameraDeviceID *devices = SDL_GetCameraDevices(NULL); if (!devices) { SDL_Log("SDL_GetCameraDevices failed: %s", SDL_GetError()); - return 1; + return -1; } const SDL_CameraDeviceID devid = devices[0]; /* just take the first one. */ @@ -77,7 +66,7 @@ int main(int argc, char **argv) if (!devid) { SDL_Log("No cameras available? %s", SDL_GetError()); - return 1; + return -1; } SDL_CameraSpec *pspec = NULL; @@ -91,115 +80,108 @@ int main(int argc, char **argv) camera = SDL_OpenCameraDevice(devid, pspec); if (!camera) { SDL_Log("Failed to open camera device: %s", SDL_GetError()); - return 1; + return -1; } - if (SDL_GetCameraFormat(camera, &spec) < 0) { - SDL_Log("Couldn't get camera spec: %s", SDL_GetError()); - return 1; - } - - /* Create texture with appropriate format */ - if (texture == NULL) { - texture = SDL_CreateTexture(renderer, spec.format, SDL_TEXTUREACCESS_STATIC, spec.width, spec.height); - if (texture == NULL) { - SDL_Log("Couldn't create texture: %s", SDL_GetError()); - return 1; - } - } - - while (!quit) { - while (SDL_PollEvent(&evt)) { - int sym = 0; - switch (evt.type) - { - case SDL_EVENT_KEY_DOWN: - { - sym = evt.key.keysym.sym; - break; - } - - case SDL_EVENT_QUIT: - { - quit = 1; - SDL_Log("Ctlr+C : Quit!"); - } - } + return 0; /* start the main app loop. */ +} +int SDL_AppEvent(const SDL_Event *event) +{ + switch (event->type) { + case SDL_EVENT_KEY_DOWN: { + const SDL_Keycode sym = event->key.keysym.sym; if (sym == SDLK_ESCAPE || sym == SDLK_AC_BACK) { - quit = 1; SDL_Log("Key : Escape!"); + return 1; } + break; } - { - Uint64 timestampNS = 0; - SDL_Surface *frame_next = SDL_AcquireCameraFrame(camera, ×tampNS); + case SDL_EVENT_QUIT: + SDL_Log("Ctlr+C : Quit!"); + return 1; -#if 0 - if (frame_next) { - SDL_Log("frame: %p at %" SDL_PRIu64, (void*)frame_next->pixels, timestampNS); + case SDL_EVENT_CAMERA_DEVICE_APPROVED: + if (SDL_GetCameraFormat(camera, &spec) < 0) { + SDL_Log("Couldn't get camera spec: %s", SDL_GetError()); + return -1; } -#endif - if (frame_next) { - if (frame_current) { - if (SDL_ReleaseCameraFrame(camera, frame_current) < 0) { - SDL_Log("err SDL_ReleaseCameraFrame: %s", SDL_GetError()); - } + /* Create texture with appropriate format */ + texture = SDL_CreateTexture(renderer, spec.format, SDL_TEXTUREACCESS_STATIC, spec.width, spec.height); + if (texture == NULL) { + SDL_Log("Couldn't create texture: %s", SDL_GetError()); + return -1; + } + break; + + case SDL_EVENT_CAMERA_DEVICE_DENIED: + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Camera permission denied!", "User denied access to the camera!", window); + return -1; + } + + return SDLTest_CommonEventMainCallbacks(state, event); +} + +int SDL_AppIterate(void) +{ + SDL_SetRenderDrawColor(renderer, 0x99, 0x99, 0x99, 255); + SDL_RenderClear(renderer); + + if (texture != NULL) { /* if not NULL, camera is ready to go. */ + int win_w, win_h, tw, th; + SDL_FRect d; + Uint64 timestampNS = 0; + SDL_Surface *frame_next = SDL_AcquireCameraFrame(camera, ×tampNS); + + #if 0 + if (frame_next) { + SDL_Log("frame: %p at %" SDL_PRIu64, (void*)frame_next->pixels, timestampNS); + } + #endif + + if (frame_next) { + if (frame_current) { + if (SDL_ReleaseCameraFrame(camera, frame_current) < 0) { + SDL_Log("err SDL_ReleaseCameraFrame: %s", SDL_GetError()); } - - /* It's not needed to keep the frame once updated the texture is updated. - * But in case of 0-copy, it's needed to have the frame while using the texture. - */ - frame_current = frame_next; - texture_updated = 0; } + + /* It's not needed to keep the frame once updated the texture is updated. + * But in case of 0-copy, it's needed to have the frame while using the texture. + */ + frame_current = frame_next; + texture_updated = SDL_FALSE; } /* Update SDL_Texture with last video frame (only once per new frame) */ - if (frame_current && texture_updated == 0) { + if (frame_current && !texture_updated) { SDL_UpdateTexture(texture, NULL, frame_current->pixels, frame_current->pitch); - texture_updated = 1; + texture_updated = SDL_TRUE; } - SDL_SetRenderDrawColor(renderer, 0x99, 0x99, 0x99, 255); - SDL_RenderClear(renderer); - { - int win_w, win_h, tw, th, w; - SDL_FRect d; - SDL_QueryTexture(texture, NULL, NULL, &tw, &th); - SDL_GetRenderOutputSize(renderer, &win_w, &win_h); - w = win_w; - if (tw > w - 20) { - float scale = (float) (w - 20) / (float) tw; - tw = w - 20; - th = (int)((float) th * scale); - } - d.x = (float)(10 ); - d.y = ((float)(win_h - th)) / 2.0f; - d.w = (float)tw; - d.h = (float)(th - 10); - SDL_RenderTexture(renderer, texture, NULL, &d); - } - SDL_Delay(10); - SDL_RenderPresent(renderer); + SDL_QueryTexture(texture, NULL, NULL, &tw, &th); + SDL_GetRenderOutputSize(renderer, &win_w, &win_h); + d.x = (float) ((win_w - tw) / 2); + d.y = (float) ((win_h - th) / 2); + d.w = (float) tw; + d.h = (float) th; + SDL_RenderTexture(renderer, texture, NULL, &d); } - if (frame_current) { - SDL_ReleaseCameraFrame(camera, frame_current); - } - SDL_CloseCamera(camera); + SDL_RenderPresent(renderer); - if (texture) { - SDL_DestroyTexture(texture); - } - - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - SDLTest_CommonDestroyState(state); - - return 0; + return 0; /* keep iterating. */ +} + +void SDL_AppQuit(void) +{ + SDL_ReleaseCameraFrame(camera, frame_current); + SDL_CloseCamera(camera); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDLTest_CommonDestroyState(state); }