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.
This commit is contained in:
Ryan C. Gordon 2023-12-22 01:23:49 -05:00
parent 182f707284
commit 67708f9110
14 changed files with 559 additions and 145 deletions

View File

@ -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")

View File

@ -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.

View File

@ -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;

View File

@ -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@

View File

@ -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 */

View File

@ -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.

View File

@ -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_

View File

@ -0,0 +1,269 @@
/*
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_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

View File

@ -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;
}

View File

@ -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: *;
};

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 <emscripten/emscripten.h>
#endif
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL_test.h>
#include <SDL3/SDL_test_common.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
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, &timestampNS);
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, &timestampNS);
#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);
}