change build system to CMake and use SDL3 for everything
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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"
|
||||
|
||||
#ifndef SDL_camera_c_h_
|
||||
#define SDL_camera_c_h_
|
||||
|
||||
// Initialize the camera subsystem
|
||||
extern bool SDL_CameraInit(const char *driver_name);
|
||||
|
||||
// Shutdown the camera subsystem
|
||||
extern void SDL_QuitCamera(void);
|
||||
|
||||
// "Pump" the event queue.
|
||||
extern void SDL_UpdateCamera(void);
|
||||
|
||||
#endif // SDL_camera_c_h_
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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"
|
||||
|
||||
#ifndef SDL_syscamera_h_
|
||||
#define SDL_syscamera_h_
|
||||
|
||||
#include "../video/SDL_surface_c.h"
|
||||
|
||||
#define DEBUG_CAMERA 0
|
||||
|
||||
/* Backends should call this as devices are added to the system (such as
|
||||
a USB camera being plugged in), and should also be called for
|
||||
for every device found during DetectDevices(). */
|
||||
extern SDL_Camera *SDL_AddCamera(const char *name, SDL_CameraPosition position, int num_specs, const SDL_CameraSpec *specs, void *handle);
|
||||
|
||||
/* Backends should call this if an opened camera device is lost.
|
||||
This can happen due to i/o errors, or a device being unplugged, etc. */
|
||||
extern void SDL_CameraDisconnected(SDL_Camera *device);
|
||||
|
||||
// Find an SDL_Camera, selected by a callback. NULL if not found. DOES NOT LOCK THE DEVICE.
|
||||
extern SDL_Camera *SDL_FindPhysicalCameraByCallback(bool (*callback)(SDL_Camera *device, void *userdata), void *userdata);
|
||||
|
||||
// Backends should call this when the user has approved/denied access to a camera.
|
||||
extern void SDL_CameraPermissionOutcome(SDL_Camera *device, bool approved);
|
||||
|
||||
// Backends can call this to get a standardized name for a thread to power a specific camera device.
|
||||
extern char *SDL_GetCameraThreadName(SDL_Camera *device, char *buf, size_t buflen);
|
||||
|
||||
// Backends can call these to change a device's refcount.
|
||||
extern void RefPhysicalCamera(SDL_Camera *device);
|
||||
extern void UnrefPhysicalCamera(SDL_Camera *device);
|
||||
|
||||
// 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_Camera *device);
|
||||
extern bool SDL_CameraThreadIterate(SDL_Camera *device);
|
||||
extern void SDL_CameraThreadShutdown(SDL_Camera *device);
|
||||
|
||||
// common utility functionality to gather up camera specs. Not required!
|
||||
typedef struct CameraFormatAddData
|
||||
{
|
||||
SDL_CameraSpec *specs;
|
||||
int num_specs;
|
||||
int allocated_specs;
|
||||
} CameraFormatAddData;
|
||||
|
||||
bool SDL_AddCameraFormat(CameraFormatAddData *data, SDL_PixelFormat format, SDL_Colorspace colorspace, int w, int h, int framerate_numerator, int framerate_denominator);
|
||||
|
||||
typedef enum SDL_CameraFrameResult
|
||||
{
|
||||
SDL_CAMERA_FRAME_ERROR,
|
||||
SDL_CAMERA_FRAME_SKIP,
|
||||
SDL_CAMERA_FRAME_READY
|
||||
} SDL_CameraFrameResult;
|
||||
|
||||
typedef struct SurfaceList
|
||||
{
|
||||
SDL_Surface *surface;
|
||||
Uint64 timestampNS;
|
||||
struct SurfaceList *next;
|
||||
} SurfaceList;
|
||||
|
||||
// Define the SDL camera driver structure
|
||||
struct SDL_Camera
|
||||
{
|
||||
// A mutex for locking
|
||||
SDL_Mutex *lock;
|
||||
|
||||
// Human-readable device name.
|
||||
char *name;
|
||||
|
||||
// Position of camera (front-facing, back-facing, etc).
|
||||
SDL_CameraPosition position;
|
||||
|
||||
// When refcount hits zero, we destroy the device object.
|
||||
SDL_AtomicInt refcount;
|
||||
|
||||
// These are, initially, set from camera_driver, but we might swap them out with Zombie versions on disconnect/failure.
|
||||
bool (*WaitDevice)(SDL_Camera *device);
|
||||
SDL_CameraFrameResult (*AcquireFrame)(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS);
|
||||
void (*ReleaseFrame)(SDL_Camera *device, SDL_Surface *frame);
|
||||
|
||||
// All supported formats/dimensions for this device.
|
||||
SDL_CameraSpec *all_specs;
|
||||
|
||||
// Elements in all_specs.
|
||||
int num_specs;
|
||||
|
||||
// The device's actual specification that the camera is outputting, before conversion.
|
||||
SDL_CameraSpec actual_spec;
|
||||
|
||||
// The device's current camera specification, after conversions.
|
||||
SDL_CameraSpec spec;
|
||||
|
||||
// Unique value assigned at creation time.
|
||||
SDL_CameraID instance_id;
|
||||
|
||||
// Driver-specific hardware data on how to open device (`hidden` is driver-specific data _when opened_).
|
||||
void *handle;
|
||||
|
||||
// Dropping the first frame(s) after open seems to help timing on some platforms.
|
||||
int drop_frames;
|
||||
|
||||
// Backend timestamp of first acquired frame, so we can keep these meaningful regardless of epoch.
|
||||
Uint64 base_timestamp;
|
||||
|
||||
// SDL timestamp of first acquired frame, so we can roughly convert to SDL ticks.
|
||||
Uint64 adjust_timestamp;
|
||||
|
||||
// Pixel data flows from the driver into these, then gets converted for the app if necessary.
|
||||
SDL_Surface *acquire_surface;
|
||||
|
||||
// acquire_surface converts or scales to this surface before landing in output_surfaces, if necessary.
|
||||
SDL_Surface *conversion_surface;
|
||||
|
||||
// A queue of surfaces that buffer converted/scaled frames of video until the app claims them.
|
||||
SurfaceList output_surfaces[8];
|
||||
SurfaceList filled_output_surfaces; // this is FIFO
|
||||
SurfaceList empty_output_surfaces; // this is LIFO
|
||||
SurfaceList app_held_output_surfaces;
|
||||
|
||||
// A fake video frame we allocate if the camera fails/disconnects.
|
||||
Uint8 *zombie_pixels;
|
||||
|
||||
// non-zero if acquire_surface needs to be scaled for final output.
|
||||
int needs_scaling; // -1: downscale, 0: no scaling, 1: upscale
|
||||
|
||||
// true if acquire_surface needs to be converted for final output.
|
||||
bool needs_conversion;
|
||||
|
||||
// Current state flags
|
||||
SDL_AtomicInt shutdown;
|
||||
SDL_AtomicInt zombie;
|
||||
|
||||
// A thread to feed the camera device
|
||||
SDL_Thread *thread;
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
typedef struct SDL_CameraDriverImpl
|
||||
{
|
||||
void (*DetectDevices)(void);
|
||||
bool (*OpenDevice)(SDL_Camera *device, const SDL_CameraSpec *spec);
|
||||
void (*CloseDevice)(SDL_Camera *device);
|
||||
bool (*WaitDevice)(SDL_Camera *device);
|
||||
SDL_CameraFrameResult (*AcquireFrame)(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS); // set frame->pixels, frame->pitch, and *timestampNS!
|
||||
void (*ReleaseFrame)(SDL_Camera *device, SDL_Surface *frame); // Reclaim frame->pixels and frame->pitch!
|
||||
void (*FreeDeviceHandle)(SDL_Camera *device); // SDL is done with this device; free the handle from SDL_AddCamera()
|
||||
void (*Deinitialize)(void);
|
||||
|
||||
bool ProvidesOwnCallbackThread;
|
||||
} SDL_CameraDriverImpl;
|
||||
|
||||
typedef struct SDL_PendingCameraEvent
|
||||
{
|
||||
Uint32 type;
|
||||
SDL_CameraID devid;
|
||||
struct SDL_PendingCameraEvent *next;
|
||||
} SDL_PendingCameraEvent;
|
||||
|
||||
typedef struct SDL_CameraDriver
|
||||
{
|
||||
const char *name; // The name of this camera driver
|
||||
const char *desc; // The description of this camera driver
|
||||
SDL_CameraDriverImpl impl; // the backend's interface
|
||||
|
||||
SDL_RWLock *device_hash_lock; // A rwlock that protects `device_hash`
|
||||
SDL_HashTable *device_hash; // the collection of currently-available camera devices
|
||||
SDL_PendingCameraEvent pending_events;
|
||||
SDL_PendingCameraEvent *pending_events_tail;
|
||||
|
||||
SDL_AtomicInt device_count;
|
||||
SDL_AtomicInt shutting_down; // non-zero during SDL_Quit, so we known not to accept any last-minute device hotplugs.
|
||||
} SDL_CameraDriver;
|
||||
|
||||
typedef struct CameraBootStrap
|
||||
{
|
||||
const char *name;
|
||||
const char *desc;
|
||||
bool (*init)(SDL_CameraDriverImpl *impl);
|
||||
bool demand_only; // if true: request explicitly, or it won't be available.
|
||||
} CameraBootStrap;
|
||||
|
||||
// Not all of these are available in a given build. Use #ifdefs, etc.
|
||||
extern CameraBootStrap DUMMYCAMERA_bootstrap;
|
||||
extern CameraBootStrap PIPEWIRECAMERA_bootstrap;
|
||||
extern CameraBootStrap V4L2_bootstrap;
|
||||
extern CameraBootStrap COREMEDIA_bootstrap;
|
||||
extern CameraBootStrap ANDROIDCAMERA_bootstrap;
|
||||
extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap;
|
||||
extern CameraBootStrap MEDIAFOUNDATION_bootstrap;
|
||||
extern CameraBootStrap VITACAMERA_bootstrap;
|
||||
|
||||
#endif // SDL_syscamera_h_
|
||||
@@ -0,0 +1,905 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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"
|
||||
|
||||
#include "../SDL_syscamera.h"
|
||||
#include "../SDL_camera_c.h"
|
||||
#include "../../video/SDL_pixels_c.h"
|
||||
#include "../../video/SDL_surface_c.h"
|
||||
#include "../../thread/SDL_systhread.h"
|
||||
|
||||
#ifdef SDL_CAMERA_DRIVER_ANDROID
|
||||
|
||||
/*
|
||||
* AndroidManifest.xml:
|
||||
* <uses-permission android:name="android.permission.CAMERA"></uses-permission>
|
||||
* <uses-feature android:name="android.hardware.camera" />
|
||||
*
|
||||
* Very likely SDL must be build with YUV support (done by default)
|
||||
*
|
||||
* https://developer.android.com/reference/android/hardware/camera2/CameraManager
|
||||
* "All camera devices intended to be operated concurrently, must be opened using openCamera(String, CameraDevice.StateCallback, Handler),
|
||||
* before configuring sessions on any of the camera devices."
|
||||
*/
|
||||
|
||||
// this is kinda gross, but on older NDK headers all the camera stuff is
|
||||
// gated behind __ANDROID_API__. We'll dlopen() it at runtime, so we'll do
|
||||
// the right thing on pre-Android 7.0 devices, but we still
|
||||
// need the struct declarations and such in those headers.
|
||||
// The other option is to make a massive jump in minimum Android version we
|
||||
// support--going from ancient to merely really old--but this seems less
|
||||
// distasteful and using dlopen matches practices on other SDL platforms.
|
||||
// We'll see if it works out.
|
||||
#if __ANDROID_API__ < 24
|
||||
#undef __ANDROID_API__
|
||||
#define __ANDROID_API__ 24
|
||||
#endif
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <camera/NdkCameraDevice.h>
|
||||
#include <camera/NdkCameraManager.h>
|
||||
#include <media/NdkImage.h>
|
||||
#include <media/NdkImageReader.h>
|
||||
|
||||
#include "../../core/android/SDL_android.h"
|
||||
|
||||
static void *libcamera2ndk = NULL;
|
||||
typedef ACameraManager* (*pfnACameraManager_create)(void);
|
||||
typedef camera_status_t (*pfnACameraManager_registerAvailabilityCallback)(ACameraManager*, const ACameraManager_AvailabilityCallbacks*);
|
||||
typedef camera_status_t (*pfnACameraManager_unregisterAvailabilityCallback)(ACameraManager*, const ACameraManager_AvailabilityCallbacks*);
|
||||
typedef camera_status_t (*pfnACameraManager_getCameraIdList)(ACameraManager*, ACameraIdList**);
|
||||
typedef void (*pfnACameraManager_deleteCameraIdList)(ACameraIdList*);
|
||||
typedef void (*pfnACameraCaptureSession_close)(ACameraCaptureSession*);
|
||||
typedef void (*pfnACaptureRequest_free)(ACaptureRequest*);
|
||||
typedef void (*pfnACameraOutputTarget_free)(ACameraOutputTarget*);
|
||||
typedef camera_status_t (*pfnACameraDevice_close)(ACameraDevice*);
|
||||
typedef void (*pfnACameraManager_delete)(ACameraManager*);
|
||||
typedef void (*pfnACaptureSessionOutputContainer_free)(ACaptureSessionOutputContainer*);
|
||||
typedef void (*pfnACaptureSessionOutput_free)(ACaptureSessionOutput*);
|
||||
typedef camera_status_t (*pfnACameraManager_openCamera)(ACameraManager*, const char*, ACameraDevice_StateCallbacks*, ACameraDevice**);
|
||||
typedef camera_status_t (*pfnACameraDevice_createCaptureRequest)(const ACameraDevice*, ACameraDevice_request_template, ACaptureRequest**);
|
||||
typedef camera_status_t (*pfnACameraDevice_createCaptureSession)(ACameraDevice*, const ACaptureSessionOutputContainer*, const ACameraCaptureSession_stateCallbacks*,ACameraCaptureSession**);
|
||||
typedef camera_status_t (*pfnACameraManager_getCameraCharacteristics)(ACameraManager*, const char*, ACameraMetadata**);
|
||||
typedef void (*pfnACameraMetadata_free)(ACameraMetadata*);
|
||||
typedef camera_status_t (*pfnACameraMetadata_getConstEntry)(const ACameraMetadata*, uint32_t tag, ACameraMetadata_const_entry*);
|
||||
typedef camera_status_t (*pfnACameraCaptureSession_setRepeatingRequest)(ACameraCaptureSession*, ACameraCaptureSession_captureCallbacks*, int numRequests, ACaptureRequest**, int*);
|
||||
typedef camera_status_t (*pfnACameraOutputTarget_create)(ACameraWindowType*,ACameraOutputTarget**);
|
||||
typedef camera_status_t (*pfnACaptureRequest_addTarget)(ACaptureRequest*, const ACameraOutputTarget*);
|
||||
typedef camera_status_t (*pfnACaptureSessionOutputContainer_add)(ACaptureSessionOutputContainer*, const ACaptureSessionOutput*);
|
||||
typedef camera_status_t (*pfnACaptureSessionOutputContainer_create)(ACaptureSessionOutputContainer**);
|
||||
typedef camera_status_t (*pfnACaptureSessionOutput_create)(ACameraWindowType*, ACaptureSessionOutput**);
|
||||
static pfnACameraManager_create pACameraManager_create = NULL;
|
||||
static pfnACameraManager_registerAvailabilityCallback pACameraManager_registerAvailabilityCallback = NULL;
|
||||
static pfnACameraManager_unregisterAvailabilityCallback pACameraManager_unregisterAvailabilityCallback = NULL;
|
||||
static pfnACameraManager_getCameraIdList pACameraManager_getCameraIdList = NULL;
|
||||
static pfnACameraManager_deleteCameraIdList pACameraManager_deleteCameraIdList = NULL;
|
||||
static pfnACameraCaptureSession_close pACameraCaptureSession_close = NULL;
|
||||
static pfnACaptureRequest_free pACaptureRequest_free = NULL;
|
||||
static pfnACameraOutputTarget_free pACameraOutputTarget_free = NULL;
|
||||
static pfnACameraDevice_close pACameraDevice_close = NULL;
|
||||
static pfnACameraManager_delete pACameraManager_delete = NULL;
|
||||
static pfnACaptureSessionOutputContainer_free pACaptureSessionOutputContainer_free = NULL;
|
||||
static pfnACaptureSessionOutput_free pACaptureSessionOutput_free = NULL;
|
||||
static pfnACameraManager_openCamera pACameraManager_openCamera = NULL;
|
||||
static pfnACameraDevice_createCaptureRequest pACameraDevice_createCaptureRequest = NULL;
|
||||
static pfnACameraDevice_createCaptureSession pACameraDevice_createCaptureSession = NULL;
|
||||
static pfnACameraManager_getCameraCharacteristics pACameraManager_getCameraCharacteristics = NULL;
|
||||
static pfnACameraMetadata_free pACameraMetadata_free = NULL;
|
||||
static pfnACameraMetadata_getConstEntry pACameraMetadata_getConstEntry = NULL;
|
||||
static pfnACameraCaptureSession_setRepeatingRequest pACameraCaptureSession_setRepeatingRequest = NULL;
|
||||
static pfnACameraOutputTarget_create pACameraOutputTarget_create = NULL;
|
||||
static pfnACaptureRequest_addTarget pACaptureRequest_addTarget = NULL;
|
||||
static pfnACaptureSessionOutputContainer_add pACaptureSessionOutputContainer_add = NULL;
|
||||
static pfnACaptureSessionOutputContainer_create pACaptureSessionOutputContainer_create = NULL;
|
||||
static pfnACaptureSessionOutput_create pACaptureSessionOutput_create = NULL;
|
||||
|
||||
static void *libmediandk = NULL;
|
||||
typedef void (*pfnAImage_delete)(AImage*);
|
||||
typedef media_status_t (*pfnAImage_getTimestamp)(const AImage*, int64_t*);
|
||||
typedef media_status_t (*pfnAImage_getNumberOfPlanes)(const AImage*, int32_t*);
|
||||
typedef media_status_t (*pfnAImage_getPlaneRowStride)(const AImage*, int, int32_t*);
|
||||
typedef media_status_t (*pfnAImage_getPlaneData)(const AImage*, int, uint8_t**, int*);
|
||||
typedef media_status_t (*pfnAImageReader_acquireNextImage)(AImageReader*, AImage**);
|
||||
typedef void (*pfnAImageReader_delete)(AImageReader*);
|
||||
typedef media_status_t (*pfnAImageReader_setImageListener)(AImageReader*, AImageReader_ImageListener*);
|
||||
typedef media_status_t (*pfnAImageReader_getWindow)(AImageReader*, ANativeWindow**);
|
||||
typedef media_status_t (*pfnAImageReader_new)(int32_t, int32_t, int32_t, int32_t, AImageReader**);
|
||||
static pfnAImage_delete pAImage_delete = NULL;
|
||||
static pfnAImage_getTimestamp pAImage_getTimestamp = NULL;
|
||||
static pfnAImage_getNumberOfPlanes pAImage_getNumberOfPlanes = NULL;
|
||||
static pfnAImage_getPlaneRowStride pAImage_getPlaneRowStride = NULL;
|
||||
static pfnAImage_getPlaneData pAImage_getPlaneData = NULL;
|
||||
static pfnAImageReader_acquireNextImage pAImageReader_acquireNextImage = NULL;
|
||||
static pfnAImageReader_delete pAImageReader_delete = NULL;
|
||||
static pfnAImageReader_setImageListener pAImageReader_setImageListener = NULL;
|
||||
static pfnAImageReader_getWindow pAImageReader_getWindow = NULL;
|
||||
static pfnAImageReader_new pAImageReader_new = NULL;
|
||||
|
||||
typedef media_status_t (*pfnAImage_getWidth)(const AImage*, int32_t*);
|
||||
typedef media_status_t (*pfnAImage_getHeight)(const AImage*, int32_t*);
|
||||
static pfnAImage_getWidth pAImage_getWidth = NULL;
|
||||
static pfnAImage_getHeight pAImage_getHeight = NULL;
|
||||
|
||||
struct SDL_PrivateCameraData
|
||||
{
|
||||
ACameraDevice *device;
|
||||
AImageReader *reader;
|
||||
ANativeWindow *window;
|
||||
ACaptureSessionOutput *sessionOutput;
|
||||
ACaptureSessionOutputContainer *sessionOutputContainer;
|
||||
ACameraOutputTarget *outputTarget;
|
||||
ACaptureRequest *request;
|
||||
ACameraCaptureSession *session;
|
||||
SDL_CameraSpec requested_spec;
|
||||
};
|
||||
|
||||
static bool SetErrorStr(const char *what, const char *errstr, const int rc)
|
||||
{
|
||||
char errbuf[128];
|
||||
if (!errstr) {
|
||||
SDL_snprintf(errbuf, sizeof (errbuf), "Unknown error #%d", rc);
|
||||
errstr = errbuf;
|
||||
}
|
||||
return SDL_SetError("%s: %s", what, errstr);
|
||||
}
|
||||
|
||||
static const char *CameraStatusStr(const camera_status_t rc)
|
||||
{
|
||||
switch (rc) {
|
||||
case ACAMERA_OK: return "no error";
|
||||
case ACAMERA_ERROR_UNKNOWN: return "unknown error";
|
||||
case ACAMERA_ERROR_INVALID_PARAMETER: return "invalid parameter";
|
||||
case ACAMERA_ERROR_CAMERA_DISCONNECTED: return "camera disconnected";
|
||||
case ACAMERA_ERROR_NOT_ENOUGH_MEMORY: return "not enough memory";
|
||||
case ACAMERA_ERROR_METADATA_NOT_FOUND: return "metadata not found";
|
||||
case ACAMERA_ERROR_CAMERA_DEVICE: return "camera device error";
|
||||
case ACAMERA_ERROR_CAMERA_SERVICE: return "camera service error";
|
||||
case ACAMERA_ERROR_SESSION_CLOSED: return "session closed";
|
||||
case ACAMERA_ERROR_INVALID_OPERATION: return "invalid operation";
|
||||
case ACAMERA_ERROR_STREAM_CONFIGURE_FAIL: return "configure failure";
|
||||
case ACAMERA_ERROR_CAMERA_IN_USE: return "camera in use";
|
||||
case ACAMERA_ERROR_MAX_CAMERA_IN_USE: return "max cameras in use";
|
||||
case ACAMERA_ERROR_CAMERA_DISABLED: return "camera disabled";
|
||||
case ACAMERA_ERROR_PERMISSION_DENIED: return "permission denied";
|
||||
case ACAMERA_ERROR_UNSUPPORTED_OPERATION: return "unsupported operation";
|
||||
default: break;
|
||||
}
|
||||
|
||||
return NULL; // unknown error
|
||||
}
|
||||
|
||||
static bool SetCameraError(const char *what, const camera_status_t rc)
|
||||
{
|
||||
return SetErrorStr(what, CameraStatusStr(rc), (int) rc);
|
||||
}
|
||||
|
||||
static const char *MediaStatusStr(const media_status_t rc)
|
||||
{
|
||||
switch (rc) {
|
||||
case AMEDIA_OK: return "no error";
|
||||
case AMEDIACODEC_ERROR_INSUFFICIENT_RESOURCE: return "insufficient resources";
|
||||
case AMEDIACODEC_ERROR_RECLAIMED: return "reclaimed";
|
||||
case AMEDIA_ERROR_UNKNOWN: return "unknown error";
|
||||
case AMEDIA_ERROR_MALFORMED: return "malformed";
|
||||
case AMEDIA_ERROR_UNSUPPORTED: return "unsupported";
|
||||
case AMEDIA_ERROR_INVALID_OBJECT: return "invalid object";
|
||||
case AMEDIA_ERROR_INVALID_PARAMETER: return "invalid parameter";
|
||||
case AMEDIA_ERROR_INVALID_OPERATION: return "invalid operation";
|
||||
case AMEDIA_ERROR_END_OF_STREAM: return "end of stream";
|
||||
case AMEDIA_ERROR_IO: return "i/o error";
|
||||
case AMEDIA_ERROR_WOULD_BLOCK: return "operation would block";
|
||||
case AMEDIA_DRM_NOT_PROVISIONED: return "DRM not provisioned";
|
||||
case AMEDIA_DRM_RESOURCE_BUSY: return "DRM resource busy";
|
||||
case AMEDIA_DRM_DEVICE_REVOKED: return "DRM device revoked";
|
||||
case AMEDIA_DRM_SHORT_BUFFER: return "DRM short buffer";
|
||||
case AMEDIA_DRM_SESSION_NOT_OPENED: return "DRM session not opened";
|
||||
case AMEDIA_DRM_TAMPER_DETECTED: return "DRM tampering detected";
|
||||
case AMEDIA_DRM_VERIFY_FAILED: return "DRM verify failed";
|
||||
case AMEDIA_DRM_NEED_KEY: return "DRM need key";
|
||||
case AMEDIA_DRM_LICENSE_EXPIRED: return "DRM license expired";
|
||||
case AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE: return "no buffer available";
|
||||
case AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED: return "maximum images acquired";
|
||||
case AMEDIA_IMGREADER_CANNOT_LOCK_IMAGE: return "cannot lock image";
|
||||
case AMEDIA_IMGREADER_CANNOT_UNLOCK_IMAGE: return "cannot unlock image";
|
||||
case AMEDIA_IMGREADER_IMAGE_NOT_LOCKED: return "image not locked";
|
||||
default: break;
|
||||
}
|
||||
|
||||
return NULL; // unknown error
|
||||
}
|
||||
|
||||
static bool SetMediaError(const char *what, const media_status_t rc)
|
||||
{
|
||||
return SetErrorStr(what, MediaStatusStr(rc), (int) rc);
|
||||
}
|
||||
|
||||
|
||||
static ACameraManager *cameraMgr = NULL;
|
||||
|
||||
static bool CreateCameraManager(void)
|
||||
{
|
||||
SDL_assert(cameraMgr == NULL);
|
||||
|
||||
cameraMgr = pACameraManager_create();
|
||||
if (!cameraMgr) {
|
||||
return SDL_SetError("Error creating ACameraManager");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void DestroyCameraManager(void)
|
||||
{
|
||||
if (cameraMgr) {
|
||||
pACameraManager_delete(cameraMgr);
|
||||
cameraMgr = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void format_android_to_sdl(Uint32 fmt, SDL_PixelFormat *format, SDL_Colorspace *colorspace)
|
||||
{
|
||||
switch (fmt) {
|
||||
#define CASE(x, y, z) case x: *format = y; *colorspace = z; return
|
||||
CASE(AIMAGE_FORMAT_YUV_420_888, SDL_PIXELFORMAT_NV12, SDL_COLORSPACE_BT709_LIMITED);
|
||||
CASE(AIMAGE_FORMAT_RGB_565, SDL_PIXELFORMAT_RGB565, SDL_COLORSPACE_SRGB);
|
||||
CASE(AIMAGE_FORMAT_RGB_888, SDL_PIXELFORMAT_XRGB8888, SDL_COLORSPACE_SRGB);
|
||||
CASE(AIMAGE_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888, SDL_COLORSPACE_SRGB);
|
||||
CASE(AIMAGE_FORMAT_RGBX_8888, SDL_PIXELFORMAT_RGBX8888, SDL_COLORSPACE_SRGB);
|
||||
CASE(AIMAGE_FORMAT_RGBA_FP16, SDL_PIXELFORMAT_RGBA64_FLOAT, SDL_COLORSPACE_SRGB);
|
||||
#undef CASE
|
||||
default: break;
|
||||
}
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
//SDL_Log("Unknown format AIMAGE_FORMAT '%d'", fmt);
|
||||
#endif
|
||||
|
||||
*format = SDL_PIXELFORMAT_UNKNOWN;
|
||||
*colorspace = SDL_COLORSPACE_UNKNOWN;
|
||||
}
|
||||
|
||||
static Uint32 format_sdl_to_android(SDL_PixelFormat fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
#define CASE(x, y) case y: return x
|
||||
CASE(AIMAGE_FORMAT_YUV_420_888, SDL_PIXELFORMAT_NV12);
|
||||
CASE(AIMAGE_FORMAT_RGB_565, SDL_PIXELFORMAT_RGB565);
|
||||
CASE(AIMAGE_FORMAT_RGB_888, SDL_PIXELFORMAT_XRGB8888);
|
||||
CASE(AIMAGE_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888);
|
||||
CASE(AIMAGE_FORMAT_RGBX_8888, SDL_PIXELFORMAT_RGBX8888);
|
||||
#undef CASE
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ANDROIDCAMERA_WaitDevice(SDL_Camera *device)
|
||||
{
|
||||
return true; // this isn't used atm, since we run our own thread via onImageAvailable callbacks.
|
||||
}
|
||||
|
||||
static SDL_CameraFrameResult ANDROIDCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
|
||||
{
|
||||
SDL_CameraFrameResult result = SDL_CAMERA_FRAME_READY;
|
||||
media_status_t res;
|
||||
AImage *image = NULL;
|
||||
|
||||
res = pAImageReader_acquireNextImage(device->hidden->reader, &image);
|
||||
// We could also use this one:
|
||||
//res = AImageReader_acquireLatestImage(device->hidden->reader, &image);
|
||||
|
||||
SDL_assert(res != AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE); // we should only be here if onImageAvailable was called.
|
||||
|
||||
if (res != AMEDIA_OK) {
|
||||
SetMediaError("Error AImageReader_acquireNextImage", res);
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
|
||||
int64_t atimestamp = 0;
|
||||
if (pAImage_getTimestamp(image, &atimestamp) == AMEDIA_OK) {
|
||||
*timestampNS = (Uint64) atimestamp;
|
||||
} else {
|
||||
*timestampNS = 0;
|
||||
}
|
||||
|
||||
// !!! FIXME: this currently copies the data to the surface (see FIXME about non-contiguous planar surfaces, but in theory we could just keep this locked until ReleaseFrame...
|
||||
int32_t num_planes = 0;
|
||||
pAImage_getNumberOfPlanes(image, &num_planes);
|
||||
|
||||
if ((num_planes == 3) && (device->spec.format == SDL_PIXELFORMAT_NV12)) {
|
||||
num_planes--; // treat the interleaved planes as one.
|
||||
}
|
||||
|
||||
size_t buflen = 0;
|
||||
pAImage_getPlaneRowStride(image, 0, &frame->pitch);
|
||||
for (int i = 0; (i < num_planes) && (i < 3); i++) {
|
||||
int32_t expected;
|
||||
if (i == 0) {
|
||||
expected = frame->pitch * frame->h;
|
||||
} else {
|
||||
expected = frame->pitch * (frame->h + 1) / 2;
|
||||
}
|
||||
buflen += expected;
|
||||
}
|
||||
|
||||
frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), buflen);
|
||||
if (frame->pixels == NULL) {
|
||||
result = SDL_CAMERA_FRAME_ERROR;
|
||||
} else {
|
||||
Uint8 *dst = frame->pixels;
|
||||
|
||||
for (int i = 0; (i < num_planes) && (i < 3); i++) {
|
||||
uint8_t *data = NULL;
|
||||
int32_t datalen = 0;
|
||||
int32_t expected;
|
||||
if (i == 0) {
|
||||
expected = frame->pitch * frame->h;
|
||||
} else {
|
||||
expected = frame->pitch * (frame->h + 1) / 2;
|
||||
}
|
||||
pAImage_getPlaneData(image, i, &data, &datalen);
|
||||
|
||||
int32_t row_stride = 0;
|
||||
pAImage_getPlaneRowStride(image, i, &row_stride);
|
||||
SDL_assert(row_stride == frame->pitch);
|
||||
SDL_memcpy(dst, data, SDL_min(expected, datalen));
|
||||
dst += expected;
|
||||
}
|
||||
}
|
||||
|
||||
pAImage_delete(image);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void ANDROIDCAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
|
||||
{
|
||||
// !!! FIXME: this currently copies the data to the surface, but in theory we could just keep the AImage until ReleaseFrame...
|
||||
SDL_aligned_free(frame->pixels);
|
||||
}
|
||||
|
||||
static void onImageAvailable(void *context, AImageReader *reader)
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onImageAvailable");
|
||||
#endif
|
||||
SDL_Camera *device = (SDL_Camera *) context;
|
||||
SDL_CameraThreadIterate(device);
|
||||
}
|
||||
|
||||
static void onDisconnected(void *context, ACameraDevice *device)
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onDisconnected");
|
||||
#endif
|
||||
SDL_CameraDisconnected((SDL_Camera *) context);
|
||||
}
|
||||
|
||||
static void onError(void *context, ACameraDevice *device, int error)
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onError");
|
||||
#endif
|
||||
SDL_CameraDisconnected((SDL_Camera *) context);
|
||||
}
|
||||
|
||||
static void onClosed(void* context, ACameraCaptureSession *session)
|
||||
{
|
||||
// SDL_Camera *_this = (SDL_Camera *) context;
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onClosed");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void onReady(void* context, ACameraCaptureSession *session)
|
||||
{
|
||||
// SDL_Camera *_this = (SDL_Camera *) context;
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onReady");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void onActive(void* context, ACameraCaptureSession *session)
|
||||
{
|
||||
// SDL_Camera *_this = (SDL_Camera *) context;
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onActive");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void ANDROIDCAMERA_CloseDevice(SDL_Camera *device)
|
||||
{
|
||||
if (device && device->hidden) {
|
||||
struct SDL_PrivateCameraData *hidden = device->hidden;
|
||||
device->hidden = NULL;
|
||||
|
||||
if (hidden->reader) {
|
||||
pAImageReader_setImageListener(hidden->reader, NULL);
|
||||
}
|
||||
|
||||
if (hidden->session) {
|
||||
pACameraCaptureSession_close(hidden->session);
|
||||
}
|
||||
|
||||
if (hidden->request) {
|
||||
pACaptureRequest_free(hidden->request);
|
||||
}
|
||||
|
||||
if (hidden->outputTarget) {
|
||||
pACameraOutputTarget_free(hidden->outputTarget);
|
||||
}
|
||||
|
||||
if (hidden->sessionOutputContainer) {
|
||||
pACaptureSessionOutputContainer_free(hidden->sessionOutputContainer);
|
||||
}
|
||||
|
||||
if (hidden->sessionOutput) {
|
||||
pACaptureSessionOutput_free(hidden->sessionOutput);
|
||||
}
|
||||
|
||||
// we don't free hidden->window here, it'll be cleaned up by AImageReader_delete.
|
||||
|
||||
if (hidden->reader) {
|
||||
pAImageReader_delete(hidden->reader);
|
||||
}
|
||||
|
||||
if (hidden->device) {
|
||||
pACameraDevice_close(hidden->device);
|
||||
}
|
||||
|
||||
SDL_free(hidden);
|
||||
}
|
||||
}
|
||||
|
||||
// this is where the "opening" of the camera happens, after permission is granted.
|
||||
static bool PrepareCamera(SDL_Camera *device)
|
||||
{
|
||||
SDL_assert(device->hidden != NULL);
|
||||
|
||||
camera_status_t res;
|
||||
media_status_t res2;
|
||||
|
||||
ACameraDevice_StateCallbacks dev_callbacks;
|
||||
SDL_zero(dev_callbacks);
|
||||
dev_callbacks.context = device;
|
||||
dev_callbacks.onDisconnected = onDisconnected;
|
||||
dev_callbacks.onError = onError;
|
||||
|
||||
ACameraCaptureSession_stateCallbacks capture_callbacks;
|
||||
SDL_zero(capture_callbacks);
|
||||
capture_callbacks.context = device;
|
||||
capture_callbacks.onClosed = onClosed;
|
||||
capture_callbacks.onReady = onReady;
|
||||
capture_callbacks.onActive = onActive;
|
||||
|
||||
AImageReader_ImageListener imglistener;
|
||||
SDL_zero(imglistener);
|
||||
imglistener.context = device;
|
||||
imglistener.onImageAvailable = onImageAvailable;
|
||||
|
||||
// just in case SDL_OpenCamera is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy.
|
||||
const SDL_CameraSpec *spec = &device->hidden->requested_spec;
|
||||
|
||||
if ((res = pACameraManager_openCamera(cameraMgr, (const char *) device->handle, &dev_callbacks, &device->hidden->device)) != ACAMERA_OK) {
|
||||
return SetCameraError("Failed to open camera", res);
|
||||
} else if ((res2 = pAImageReader_new(spec->width, spec->height, format_sdl_to_android(spec->format), 10 /* nb buffers */, &device->hidden->reader)) != AMEDIA_OK) {
|
||||
return SetMediaError("Error AImageReader_new", res2);
|
||||
} else if ((res2 = pAImageReader_getWindow(device->hidden->reader, &device->hidden->window)) != AMEDIA_OK) {
|
||||
return SetMediaError("Error AImageReader_getWindow", res2);
|
||||
} else if ((res = pACaptureSessionOutput_create(device->hidden->window, &device->hidden->sessionOutput)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACaptureSessionOutput_create", res);
|
||||
} else if ((res = pACaptureSessionOutputContainer_create(&device->hidden->sessionOutputContainer)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACaptureSessionOutputContainer_create", res);
|
||||
} else if ((res = pACaptureSessionOutputContainer_add(device->hidden->sessionOutputContainer, device->hidden->sessionOutput)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACaptureSessionOutputContainer_add", res);
|
||||
} else if ((res = pACameraOutputTarget_create(device->hidden->window, &device->hidden->outputTarget)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACameraOutputTarget_create", res);
|
||||
} else if ((res = pACameraDevice_createCaptureRequest(device->hidden->device, TEMPLATE_RECORD, &device->hidden->request)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACameraDevice_createCaptureRequest", res);
|
||||
} else if ((res = pACaptureRequest_addTarget(device->hidden->request, device->hidden->outputTarget)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACaptureRequest_addTarget", res);
|
||||
} else if ((res = pACameraDevice_createCaptureSession(device->hidden->device, device->hidden->sessionOutputContainer, &capture_callbacks, &device->hidden->session)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACameraDevice_createCaptureSession", res);
|
||||
} else if ((res = pACameraCaptureSession_setRepeatingRequest(device->hidden->session, NULL, 1, &device->hidden->request, NULL)) != ACAMERA_OK) {
|
||||
return SetCameraError("Error ACameraCaptureSession_setRepeatingRequest", res);
|
||||
} else if ((res2 = pAImageReader_setImageListener(device->hidden->reader, &imglistener)) != AMEDIA_OK) {
|
||||
return SetMediaError("Error AImageReader_setImageListener", res2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void SDLCALL CameraPermissionCallback(void *userdata, const char *permission, bool granted)
|
||||
{
|
||||
SDL_Camera *device = (SDL_Camera *) userdata;
|
||||
if (device->hidden != NULL) { // if device was already closed, don't send an event.
|
||||
if (!granted) {
|
||||
SDL_CameraPermissionOutcome(device, false); // sorry, permission denied.
|
||||
} else if (!PrepareCamera(device)) { // permission given? Actually open the camera now.
|
||||
// uhoh, setup failed; since the app thinks we already "opened" the device, mark it as disconnected and don't report the permission.
|
||||
SDL_CameraDisconnected(device);
|
||||
} else {
|
||||
// okay! We have permission to use the camera _and_ opening the hardware worked out, report that the camera is usable!
|
||||
SDL_CameraPermissionOutcome(device, true); // go go go!
|
||||
}
|
||||
}
|
||||
|
||||
UnrefPhysicalCamera(device); // we ref'd this in OpenDevice, release the extra reference.
|
||||
}
|
||||
|
||||
|
||||
static bool ANDROIDCAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
|
||||
{
|
||||
#if 0 // !!! FIXME: for now, we'll just let this fail if it is going to fail, without checking for this
|
||||
/* Cannot open a second camera, while the first one is opened.
|
||||
* If you want to play several camera, they must all be opened first, then played.
|
||||
*
|
||||
* https://developer.android.com/reference/android/hardware/camera2/CameraManager
|
||||
* "All camera devices intended to be operated concurrently, must be opened using openCamera(String, CameraDevice.StateCallback, Handler),
|
||||
* before configuring sessions on any of the camera devices. * "
|
||||
*
|
||||
*/
|
||||
if (CheckDevicePlaying()) {
|
||||
return SDL_SetError("A camera is already playing");
|
||||
}
|
||||
#endif
|
||||
|
||||
device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
|
||||
if (device->hidden == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RefPhysicalCamera(device); // ref'd until permission callback fires.
|
||||
|
||||
// just in case SDL_OpenCamera is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy.
|
||||
SDL_copyp(&device->hidden->requested_spec, spec);
|
||||
if (!SDL_RequestAndroidPermission("android.permission.CAMERA", CameraPermissionCallback, device)) {
|
||||
UnrefPhysicalCamera(device);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // we don't open the camera until permission is granted, so always succeed for now.
|
||||
}
|
||||
|
||||
static void ANDROIDCAMERA_FreeDeviceHandle(SDL_Camera *device)
|
||||
{
|
||||
if (device) {
|
||||
SDL_free(device->handle);
|
||||
}
|
||||
}
|
||||
|
||||
static void GatherCameraSpecs(const char *devid, CameraFormatAddData *add_data, char **fullname, SDL_CameraPosition *position)
|
||||
{
|
||||
SDL_zerop(add_data);
|
||||
|
||||
ACameraMetadata *metadata = NULL;
|
||||
ACameraMetadata_const_entry cfgentry;
|
||||
ACameraMetadata_const_entry durentry;
|
||||
ACameraMetadata_const_entry infoentry;
|
||||
|
||||
// This can fail with an "unknown error" (with `adb logcat` reporting "no such file or directory")
|
||||
// for "LEGACY" level cameras. I saw this happen on a 30-dollar budget phone I have for testing
|
||||
// (but a different brand budget phone worked, so it's not strictly the low-end of Android devices).
|
||||
// LEGACY devices are seen by onCameraAvailable, but are not otherwise accessible through
|
||||
// libcamera2ndk. The Java camera2 API apparently _can_ access these cameras, but we're going on
|
||||
// without them here for now, in hopes that such hardware is a dying breed.
|
||||
if (pACameraManager_getCameraCharacteristics(cameraMgr, devid, &metadata) != ACAMERA_OK) {
|
||||
return; // oh well.
|
||||
} else if (pACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &cfgentry) != ACAMERA_OK) {
|
||||
pACameraMetadata_free(metadata);
|
||||
return; // oh well.
|
||||
} else if (pACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_MIN_FRAME_DURATIONS, &durentry) != ACAMERA_OK) {
|
||||
pACameraMetadata_free(metadata);
|
||||
return; // oh well.
|
||||
}
|
||||
|
||||
*fullname = NULL;
|
||||
if (pACameraMetadata_getConstEntry(metadata, ACAMERA_INFO_VERSION, &infoentry) == ACAMERA_OK) {
|
||||
*fullname = (char *) SDL_malloc(infoentry.count + 1);
|
||||
if (*fullname) {
|
||||
SDL_strlcpy(*fullname, (const char *) infoentry.data.u8, infoentry.count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
ACameraMetadata_const_entry posentry;
|
||||
if (pACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &posentry) == ACAMERA_OK) { // ignore this if it fails.
|
||||
if (*posentry.data.u8 == ACAMERA_LENS_FACING_FRONT) {
|
||||
*position = SDL_CAMERA_POSITION_FRONT_FACING;
|
||||
if (!*fullname) {
|
||||
*fullname = SDL_strdup("Front-facing camera");
|
||||
}
|
||||
} else if (*posentry.data.u8 == ACAMERA_LENS_FACING_BACK) {
|
||||
*position = SDL_CAMERA_POSITION_BACK_FACING;
|
||||
if (!*fullname) {
|
||||
*fullname = SDL_strdup("Back-facing camera");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!*fullname) {
|
||||
*fullname = SDL_strdup("Generic camera"); // we tried.
|
||||
}
|
||||
|
||||
const int32_t *i32ptr = cfgentry.data.i32;
|
||||
for (int i = 0; i < cfgentry.count; i++, i32ptr += 4) {
|
||||
const int32_t fmt = i32ptr[0];
|
||||
const int w = i32ptr[1];
|
||||
const int h = i32ptr[2];
|
||||
const int32_t type = i32ptr[3];
|
||||
SDL_PixelFormat sdlfmt = SDL_PIXELFORMAT_UNKNOWN;
|
||||
SDL_Colorspace colorspace = SDL_COLORSPACE_UNKNOWN;
|
||||
|
||||
if (type == ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT) {
|
||||
continue;
|
||||
} else if ((w <= 0) || (h <= 0)) {
|
||||
continue;
|
||||
} else {
|
||||
format_android_to_sdl(fmt, &sdlfmt, &colorspace);
|
||||
if (sdlfmt == SDL_PIXELFORMAT_UNKNOWN) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
#if 0 // !!! FIXME: these all come out with 0 durations on my test phone. :(
|
||||
const int64_t *i64ptr = durentry.data.i64;
|
||||
for (int j = 0; j < durentry.count; j++, i64ptr += 4) {
|
||||
const int32_t fpsfmt = (int32_t) i64ptr[0];
|
||||
const int fpsw = (int) i64ptr[1];
|
||||
const int fpsh = (int) i64ptr[2];
|
||||
const long long duration = (long long) i64ptr[3];
|
||||
SDL_Log("CAMERA: possible fps %s %dx%d duration=%lld", SDL_GetPixelFormatName(sdlfmt), fpsw, fpsh, duration);
|
||||
if ((duration > 0) && (fpsfmt == fmt) && (fpsw == w) && (fpsh == h)) {
|
||||
SDL_AddCameraFormat(add_data, sdlfmt, colorspace, w, h, 1000000000, duration);
|
||||
}
|
||||
}
|
||||
#else
|
||||
SDL_AddCameraFormat(add_data, sdlfmt, colorspace, w, h, 30, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
pACameraMetadata_free(metadata);
|
||||
}
|
||||
|
||||
static bool FindAndroidCameraByID(SDL_Camera *device, void *userdata)
|
||||
{
|
||||
const char *devid = (const char *) userdata;
|
||||
return (SDL_strcmp(devid, (const char *) device->handle) == 0);
|
||||
}
|
||||
|
||||
static void MaybeAddDevice(const char *devid)
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: MaybeAddDevice('%s')", devid);
|
||||
#endif
|
||||
|
||||
if (SDL_FindPhysicalCameraByCallback(FindAndroidCameraByID, (void *) devid)) {
|
||||
return; // already have this one.
|
||||
}
|
||||
|
||||
SDL_CameraPosition position = SDL_CAMERA_POSITION_UNKNOWN;
|
||||
char *fullname = NULL;
|
||||
CameraFormatAddData add_data;
|
||||
GatherCameraSpecs(devid, &add_data, &fullname, &position);
|
||||
if (add_data.num_specs > 0) {
|
||||
char *namecpy = SDL_strdup(devid);
|
||||
if (namecpy) {
|
||||
SDL_Camera *device = SDL_AddCamera(fullname, position, add_data.num_specs, add_data.specs, namecpy);
|
||||
if (!device) {
|
||||
SDL_free(namecpy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_free(fullname);
|
||||
SDL_free(add_data.specs);
|
||||
}
|
||||
|
||||
// note that camera "availability" covers both hotplugging and whether another
|
||||
// has the device opened, but for something like Android, it's probably fine
|
||||
// to treat both unplugging and loss of access as disconnection events. When
|
||||
// the other app closes the camera, we get an available event as if it was
|
||||
// just plugged back in.
|
||||
|
||||
static void onCameraAvailable(void *context, const char *cameraId)
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onCameraAvailable('%s')", cameraId);
|
||||
#endif
|
||||
SDL_assert(cameraId != NULL);
|
||||
MaybeAddDevice(cameraId);
|
||||
}
|
||||
|
||||
static void onCameraUnavailable(void *context, const char *cameraId)
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: CB onCameraUnvailable('%s')", cameraId);
|
||||
#endif
|
||||
|
||||
SDL_assert(cameraId != NULL);
|
||||
|
||||
// THIS CALLBACK FIRES WHEN YOU OPEN THE DEVICE YOURSELF. :(
|
||||
// Make sure we don't have the device opened, in which case onDisconnected will fire instead if actually lost.
|
||||
SDL_Camera *device = SDL_FindPhysicalCameraByCallback(FindAndroidCameraByID, (void *) cameraId);
|
||||
if (device && !device->hidden) {
|
||||
SDL_CameraDisconnected(device);
|
||||
}
|
||||
}
|
||||
|
||||
static const ACameraManager_AvailabilityCallbacks camera_availability_listener = {
|
||||
NULL,
|
||||
onCameraAvailable,
|
||||
onCameraUnavailable
|
||||
};
|
||||
|
||||
static void ANDROIDCAMERA_DetectDevices(void)
|
||||
{
|
||||
ACameraIdList *list = NULL;
|
||||
camera_status_t res = pACameraManager_getCameraIdList(cameraMgr, &list);
|
||||
|
||||
if ((res == ACAMERA_OK) && list) {
|
||||
const int total = list->numCameras;
|
||||
for (int i = 0; i < total; i++) {
|
||||
MaybeAddDevice(list->cameraIds[i]);
|
||||
}
|
||||
|
||||
pACameraManager_deleteCameraIdList(list);
|
||||
}
|
||||
|
||||
pACameraManager_registerAvailabilityCallback(cameraMgr, &camera_availability_listener);
|
||||
}
|
||||
|
||||
static void ANDROIDCAMERA_Deinitialize(void)
|
||||
{
|
||||
pACameraManager_unregisterAvailabilityCallback(cameraMgr, &camera_availability_listener);
|
||||
DestroyCameraManager();
|
||||
|
||||
dlclose(libcamera2ndk);
|
||||
libcamera2ndk = NULL;
|
||||
pACameraManager_create = NULL;
|
||||
pACameraManager_registerAvailabilityCallback = NULL;
|
||||
pACameraManager_unregisterAvailabilityCallback = NULL;
|
||||
pACameraManager_getCameraIdList = NULL;
|
||||
pACameraManager_deleteCameraIdList = NULL;
|
||||
pACameraCaptureSession_close = NULL;
|
||||
pACaptureRequest_free = NULL;
|
||||
pACameraOutputTarget_free = NULL;
|
||||
pACameraDevice_close = NULL;
|
||||
pACameraManager_delete = NULL;
|
||||
pACaptureSessionOutputContainer_free = NULL;
|
||||
pACaptureSessionOutput_free = NULL;
|
||||
pACameraManager_openCamera = NULL;
|
||||
pACameraDevice_createCaptureRequest = NULL;
|
||||
pACameraDevice_createCaptureSession = NULL;
|
||||
pACameraManager_getCameraCharacteristics = NULL;
|
||||
pACameraMetadata_free = NULL;
|
||||
pACameraMetadata_getConstEntry = NULL;
|
||||
pACameraCaptureSession_setRepeatingRequest = NULL;
|
||||
pACameraOutputTarget_create = NULL;
|
||||
pACaptureRequest_addTarget = NULL;
|
||||
pACaptureSessionOutputContainer_add = NULL;
|
||||
pACaptureSessionOutputContainer_create = NULL;
|
||||
pACaptureSessionOutput_create = NULL;
|
||||
|
||||
dlclose(libmediandk);
|
||||
libmediandk = NULL;
|
||||
pAImage_delete = NULL;
|
||||
pAImage_getTimestamp = NULL;
|
||||
pAImage_getNumberOfPlanes = NULL;
|
||||
pAImage_getPlaneRowStride = NULL;
|
||||
pAImage_getPlaneData = NULL;
|
||||
pAImageReader_acquireNextImage = NULL;
|
||||
pAImageReader_delete = NULL;
|
||||
pAImageReader_setImageListener = NULL;
|
||||
pAImageReader_getWindow = NULL;
|
||||
pAImageReader_new = NULL;
|
||||
}
|
||||
|
||||
static bool ANDROIDCAMERA_Init(SDL_CameraDriverImpl *impl)
|
||||
{
|
||||
// !!! FIXME: slide this off into a subroutine
|
||||
// system libraries are in android-24 and later; we currently target android-16 and later, so check if they exist at runtime.
|
||||
void *libcamera2 = dlopen("libcamera2ndk.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!libcamera2) {
|
||||
SDL_Log("CAMERA: libcamera2ndk.so can't be loaded: %s", dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
void *libmedia = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!libmedia) {
|
||||
SDL_Log("CAMERA: libmediandk.so can't be loaded: %s", dlerror());
|
||||
dlclose(libcamera2);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool okay = true;
|
||||
#define LOADSYM(lib, fn) if (okay) { p##fn = (pfn##fn) dlsym(lib, #fn); if (!p##fn) { SDL_Log("CAMERA: symbol '%s' can't be found in %s: %s", #fn, #lib "ndk.so", dlerror()); okay = false; } }
|
||||
//#define LOADSYM(lib, fn) p##fn = (pfn##fn) fn
|
||||
LOADSYM(libcamera2, ACameraManager_create);
|
||||
LOADSYM(libcamera2, ACameraManager_registerAvailabilityCallback);
|
||||
LOADSYM(libcamera2, ACameraManager_unregisterAvailabilityCallback);
|
||||
LOADSYM(libcamera2, ACameraManager_getCameraIdList);
|
||||
LOADSYM(libcamera2, ACameraManager_deleteCameraIdList);
|
||||
LOADSYM(libcamera2, ACameraCaptureSession_close);
|
||||
LOADSYM(libcamera2, ACaptureRequest_free);
|
||||
LOADSYM(libcamera2, ACameraOutputTarget_free);
|
||||
LOADSYM(libcamera2, ACameraDevice_close);
|
||||
LOADSYM(libcamera2, ACameraManager_delete);
|
||||
LOADSYM(libcamera2, ACaptureSessionOutputContainer_free);
|
||||
LOADSYM(libcamera2, ACaptureSessionOutput_free);
|
||||
LOADSYM(libcamera2, ACameraManager_openCamera);
|
||||
LOADSYM(libcamera2, ACameraDevice_createCaptureRequest);
|
||||
LOADSYM(libcamera2, ACameraDevice_createCaptureSession);
|
||||
LOADSYM(libcamera2, ACameraManager_getCameraCharacteristics);
|
||||
LOADSYM(libcamera2, ACameraMetadata_free);
|
||||
LOADSYM(libcamera2, ACameraMetadata_getConstEntry);
|
||||
LOADSYM(libcamera2, ACameraCaptureSession_setRepeatingRequest);
|
||||
LOADSYM(libcamera2, ACameraOutputTarget_create);
|
||||
LOADSYM(libcamera2, ACaptureRequest_addTarget);
|
||||
LOADSYM(libcamera2, ACaptureSessionOutputContainer_add);
|
||||
LOADSYM(libcamera2, ACaptureSessionOutputContainer_create);
|
||||
LOADSYM(libcamera2, ACaptureSessionOutput_create);
|
||||
LOADSYM(libmedia, AImage_delete);
|
||||
LOADSYM(libmedia, AImage_getTimestamp);
|
||||
LOADSYM(libmedia, AImage_getNumberOfPlanes);
|
||||
LOADSYM(libmedia, AImage_getPlaneRowStride);
|
||||
LOADSYM(libmedia, AImage_getPlaneData);
|
||||
LOADSYM(libmedia, AImageReader_acquireNextImage);
|
||||
LOADSYM(libmedia, AImageReader_delete);
|
||||
LOADSYM(libmedia, AImageReader_setImageListener);
|
||||
LOADSYM(libmedia, AImageReader_getWindow);
|
||||
LOADSYM(libmedia, AImageReader_new);
|
||||
LOADSYM(libmedia, AImage_getWidth);
|
||||
LOADSYM(libmedia, AImage_getHeight);
|
||||
|
||||
#undef LOADSYM
|
||||
|
||||
if (!okay) {
|
||||
dlclose(libmedia);
|
||||
dlclose(libcamera2);
|
||||
}
|
||||
|
||||
if (!CreateCameraManager()) {
|
||||
dlclose(libmedia);
|
||||
dlclose(libcamera2);
|
||||
return false;
|
||||
}
|
||||
|
||||
libcamera2ndk = libcamera2;
|
||||
libmediandk = libmedia;
|
||||
|
||||
impl->DetectDevices = ANDROIDCAMERA_DetectDevices;
|
||||
impl->OpenDevice = ANDROIDCAMERA_OpenDevice;
|
||||
impl->CloseDevice = ANDROIDCAMERA_CloseDevice;
|
||||
impl->WaitDevice = ANDROIDCAMERA_WaitDevice;
|
||||
impl->AcquireFrame = ANDROIDCAMERA_AcquireFrame;
|
||||
impl->ReleaseFrame = ANDROIDCAMERA_ReleaseFrame;
|
||||
impl->FreeDeviceHandle = ANDROIDCAMERA_FreeDeviceHandle;
|
||||
impl->Deinitialize = ANDROIDCAMERA_Deinitialize;
|
||||
|
||||
impl->ProvidesOwnCallbackThread = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CameraBootStrap ANDROIDCAMERA_bootstrap = {
|
||||
"android", "SDL Android camera driver", ANDROIDCAMERA_Init, false
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,508 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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_COREMEDIA
|
||||
|
||||
#include "../SDL_syscamera.h"
|
||||
#include "../SDL_camera_c.h"
|
||||
#include "../../thread/SDL_systhread.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
|
||||
/*
|
||||
* Need to link with:: CoreMedia CoreVideo
|
||||
*
|
||||
* Add in pInfo.list:
|
||||
* <key>NSCameraUsageDescription</key> <string>Access camera</string>
|
||||
*
|
||||
*
|
||||
* MACOSX:
|
||||
* Add to the Code Sign Entitlement file:
|
||||
* <key>com.apple.security.device.camera</key> <true/>
|
||||
*/
|
||||
|
||||
static void CoreMediaFormatToSDL(FourCharCode fmt, SDL_PixelFormat *pixel_format, SDL_Colorspace *colorspace)
|
||||
{
|
||||
switch (fmt) {
|
||||
#define CASE(x, y, z) case x: *pixel_format = y; *colorspace = z; return
|
||||
// the 16LE ones should use 16BE if we're on a Bigendian system like PowerPC,
|
||||
// but at current time there is no bigendian Apple platform that has CoreMedia.
|
||||
CASE(kCMPixelFormat_16LE555, SDL_PIXELFORMAT_XRGB1555, SDL_COLORSPACE_SRGB);
|
||||
CASE(kCMPixelFormat_16LE5551, SDL_PIXELFORMAT_RGBA5551, SDL_COLORSPACE_SRGB);
|
||||
CASE(kCMPixelFormat_16LE565, SDL_PIXELFORMAT_RGB565, SDL_COLORSPACE_SRGB);
|
||||
CASE(kCMPixelFormat_24RGB, SDL_PIXELFORMAT_RGB24, SDL_COLORSPACE_SRGB);
|
||||
CASE(kCMPixelFormat_32ARGB, SDL_PIXELFORMAT_ARGB32, SDL_COLORSPACE_SRGB);
|
||||
CASE(kCMPixelFormat_32BGRA, SDL_PIXELFORMAT_BGRA32, SDL_COLORSPACE_SRGB);
|
||||
CASE(kCMPixelFormat_422YpCbCr8, SDL_PIXELFORMAT_UYVY, SDL_COLORSPACE_BT709_LIMITED);
|
||||
CASE(kCMPixelFormat_422YpCbCr8_yuvs, SDL_PIXELFORMAT_YUY2, SDL_COLORSPACE_BT709_LIMITED);
|
||||
CASE(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, SDL_PIXELFORMAT_NV12, SDL_COLORSPACE_BT709_LIMITED);
|
||||
CASE(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, SDL_PIXELFORMAT_NV12, SDL_COLORSPACE_BT709_FULL);
|
||||
CASE(kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange, SDL_PIXELFORMAT_P010, SDL_COLORSPACE_BT2020_LIMITED);
|
||||
CASE(kCVPixelFormatType_420YpCbCr10BiPlanarFullRange, SDL_PIXELFORMAT_P010, SDL_COLORSPACE_BT2020_FULL);
|
||||
#undef CASE
|
||||
default:
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: Unknown format FourCharCode '%d'", (int) fmt);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
*pixel_format = SDL_PIXELFORMAT_UNKNOWN;
|
||||
*colorspace = SDL_COLORSPACE_UNKNOWN;
|
||||
}
|
||||
|
||||
@class SDLCaptureVideoDataOutputSampleBufferDelegate;
|
||||
|
||||
// just a simple wrapper to help ARC manage memory...
|
||||
@interface SDLPrivateCameraData : NSObject
|
||||
@property(nonatomic, retain) AVCaptureSession *session;
|
||||
@property(nonatomic, retain) SDLCaptureVideoDataOutputSampleBufferDelegate *delegate;
|
||||
@property(nonatomic, assign) CMSampleBufferRef current_sample;
|
||||
@end
|
||||
|
||||
@implementation SDLPrivateCameraData
|
||||
@end
|
||||
|
||||
|
||||
static bool CheckCameraPermissions(SDL_Camera *device)
|
||||
{
|
||||
if (device->permission == 0) { // still expecting a permission result.
|
||||
if (@available(macOS 14, *)) {
|
||||
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
||||
if (status != AVAuthorizationStatusNotDetermined) { // NotDetermined == still waiting for an answer from the user.
|
||||
SDL_CameraPermissionOutcome(device, (status == AVAuthorizationStatusAuthorized) ? true : false);
|
||||
}
|
||||
} else {
|
||||
SDL_CameraPermissionOutcome(device, true); // always allowed (or just unqueryable...?) on older macOS.
|
||||
}
|
||||
}
|
||||
|
||||
return (device->permission > 0);
|
||||
}
|
||||
|
||||
// this delegate just receives new video frames on a Grand Central Dispatch queue, and fires off the
|
||||
// main device thread iterate function directly to consume it.
|
||||
@interface SDLCaptureVideoDataOutputSampleBufferDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||
@property SDL_Camera *device;
|
||||
-(id) init:(SDL_Camera *) dev;
|
||||
-(void) captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
|
||||
@end
|
||||
|
||||
@implementation SDLCaptureVideoDataOutputSampleBufferDelegate
|
||||
|
||||
-(id) init:(SDL_Camera *) dev {
|
||||
if ( self = [super init] ) {
|
||||
_device = dev;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
|
||||
{
|
||||
SDL_Camera *device = self.device;
|
||||
if (!device || !device->hidden) {
|
||||
return; // oh well.
|
||||
}
|
||||
|
||||
if (!CheckCameraPermissions(device)) {
|
||||
return; // nothing to do right now, dump what is probably a completely black frame.
|
||||
}
|
||||
|
||||
SDLPrivateCameraData *hidden = (__bridge SDLPrivateCameraData *) device->hidden;
|
||||
hidden.current_sample = sampleBuffer;
|
||||
SDL_CameraThreadIterate(device);
|
||||
hidden.current_sample = NULL;
|
||||
}
|
||||
|
||||
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: Drop frame.");
|
||||
#endif
|
||||
}
|
||||
@end
|
||||
|
||||
static bool COREMEDIA_WaitDevice(SDL_Camera *device)
|
||||
{
|
||||
return true; // this isn't used atm, since we run our own thread out of Grand Central Dispatch.
|
||||
}
|
||||
|
||||
static SDL_CameraFrameResult COREMEDIA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
|
||||
{
|
||||
SDL_CameraFrameResult result = SDL_CAMERA_FRAME_READY;
|
||||
SDLPrivateCameraData *hidden = (__bridge SDLPrivateCameraData *) device->hidden;
|
||||
CMSampleBufferRef sample_buffer = hidden.current_sample;
|
||||
hidden.current_sample = NULL;
|
||||
SDL_assert(sample_buffer != NULL); // should only have been called from our delegate with a new frame.
|
||||
|
||||
CMSampleTimingInfo timinginfo;
|
||||
if (CMSampleBufferGetSampleTimingInfo(sample_buffer, 0, &timinginfo) == noErr) {
|
||||
*timestampNS = (Uint64) (CMTimeGetSeconds(timinginfo.presentationTimeStamp) * ((Float64) SDL_NS_PER_SECOND));
|
||||
} else {
|
||||
SDL_assert(!"this shouldn't happen, I think.");
|
||||
*timestampNS = 0;
|
||||
}
|
||||
|
||||
CVImageBufferRef image = CMSampleBufferGetImageBuffer(sample_buffer); // does not retain `image` (and we don't want it to).
|
||||
const int numPlanes = (int) CVPixelBufferGetPlaneCount(image);
|
||||
const int planar = (int) CVPixelBufferIsPlanar(image);
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
const int w = (int) CVPixelBufferGetWidth(image);
|
||||
const int h = (int) CVPixelBufferGetHeight(image);
|
||||
const int sz = (int) CVPixelBufferGetDataSize(image);
|
||||
const int pitch = (int) CVPixelBufferGetBytesPerRow(image);
|
||||
SDL_Log("CAMERA: buffer planar=%d numPlanes=%d %d x %d sz=%d pitch=%d", planar, numPlanes, w, h, sz, pitch);
|
||||
#endif
|
||||
|
||||
// !!! FIXME: this currently copies the data to the surface (see FIXME about non-contiguous planar surfaces, but in theory we could just keep this locked until ReleaseFrame...
|
||||
CVPixelBufferLockBaseAddress(image, 0);
|
||||
|
||||
frame->w = (int)CVPixelBufferGetWidth(image);
|
||||
frame->h = (int)CVPixelBufferGetHeight(image);
|
||||
|
||||
if ((planar == 0) && (numPlanes == 0)) {
|
||||
const int pitch = (int) CVPixelBufferGetBytesPerRow(image);
|
||||
const size_t buflen = pitch * frame->h;
|
||||
frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), buflen);
|
||||
if (frame->pixels == NULL) {
|
||||
result = SDL_CAMERA_FRAME_ERROR;
|
||||
} else {
|
||||
frame->pitch = pitch;
|
||||
SDL_memcpy(frame->pixels, CVPixelBufferGetBaseAddress(image), buflen);
|
||||
}
|
||||
} else {
|
||||
// !!! FIXME: we have an open issue in SDL3 to allow SDL_Surface to support non-contiguous planar data, but we don't have it yet.
|
||||
size_t buflen = 0;
|
||||
for (int i = 0; i < numPlanes; i++) {
|
||||
size_t plane_height = CVPixelBufferGetHeightOfPlane(image, i);
|
||||
size_t plane_pitch = CVPixelBufferGetBytesPerRowOfPlane(image, i);
|
||||
size_t plane_size = (plane_pitch * plane_height);
|
||||
buflen += plane_size;
|
||||
}
|
||||
|
||||
frame->pitch = (int)CVPixelBufferGetBytesPerRowOfPlane(image, 0); // this is what SDL3 currently expects
|
||||
frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), buflen);
|
||||
if (frame->pixels == NULL) {
|
||||
result = SDL_CAMERA_FRAME_ERROR;
|
||||
} else {
|
||||
Uint8 *dst = frame->pixels;
|
||||
for (int i = 0; i < numPlanes; i++) {
|
||||
const void *src = CVPixelBufferGetBaseAddressOfPlane(image, i);
|
||||
size_t plane_height = CVPixelBufferGetHeightOfPlane(image, i);
|
||||
size_t plane_pitch = CVPixelBufferGetBytesPerRowOfPlane(image, i);
|
||||
size_t plane_size = (plane_pitch * plane_height);
|
||||
SDL_memcpy(dst, src, plane_size);
|
||||
dst += plane_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(image, 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void COREMEDIA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
|
||||
{
|
||||
// !!! FIXME: this currently copies the data to the surface, but in theory we could just keep this locked until ReleaseFrame...
|
||||
SDL_aligned_free(frame->pixels);
|
||||
}
|
||||
|
||||
static void COREMEDIA_CloseDevice(SDL_Camera *device)
|
||||
{
|
||||
if (device && device->hidden) {
|
||||
SDLPrivateCameraData *hidden = (SDLPrivateCameraData *) CFBridgingRelease(device->hidden);
|
||||
device->hidden = NULL;
|
||||
|
||||
AVCaptureSession *session = hidden.session;
|
||||
if (session) {
|
||||
hidden.session = nil;
|
||||
[session stopRunning];
|
||||
[session removeInput:[session.inputs objectAtIndex:0]];
|
||||
[session removeOutput:(AVCaptureVideoDataOutput*)[session.outputs objectAtIndex:0]];
|
||||
session = nil;
|
||||
}
|
||||
|
||||
hidden.delegate = NULL;
|
||||
hidden.current_sample = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool COREMEDIA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
|
||||
{
|
||||
AVCaptureDevice *avdevice = (__bridge AVCaptureDevice *) device->handle;
|
||||
|
||||
// Pick format that matches the spec
|
||||
const int w = spec->width;
|
||||
const int h = spec->height;
|
||||
const float rate = (float)spec->framerate_numerator / spec->framerate_denominator;
|
||||
AVCaptureDeviceFormat *spec_format = nil;
|
||||
NSArray<AVCaptureDeviceFormat *> *formats = [avdevice formats];
|
||||
for (AVCaptureDeviceFormat *format in formats) {
|
||||
CMFormatDescriptionRef formatDescription = [format formatDescription];
|
||||
SDL_PixelFormat device_format = SDL_PIXELFORMAT_UNKNOWN;
|
||||
SDL_Colorspace device_colorspace = SDL_COLORSPACE_UNKNOWN;
|
||||
CoreMediaFormatToSDL(CMFormatDescriptionGetMediaSubType(formatDescription), &device_format, &device_colorspace);
|
||||
if (device_format != spec->format || device_colorspace != spec->colorspace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(formatDescription);
|
||||
if ((int)dim.width != w || (int)dim.height != h) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float FRAMERATE_EPSILON = 0.01f;
|
||||
for (AVFrameRateRange *framerate in format.videoSupportedFrameRateRanges) {
|
||||
if (rate > (framerate.minFrameRate - FRAMERATE_EPSILON) &&
|
||||
rate < (framerate.maxFrameRate + FRAMERATE_EPSILON)) {
|
||||
spec_format = format;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (spec_format != nil) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (spec_format == nil) {
|
||||
return SDL_SetError("camera spec format not available");
|
||||
} else if (![avdevice lockForConfiguration:NULL]) {
|
||||
return SDL_SetError("Cannot lockForConfiguration");
|
||||
}
|
||||
|
||||
avdevice.activeFormat = spec_format;
|
||||
[avdevice unlockForConfiguration];
|
||||
|
||||
AVCaptureSession *session = [[AVCaptureSession alloc] init];
|
||||
if (session == nil) {
|
||||
return SDL_SetError("Failed to allocate/init AVCaptureSession");
|
||||
}
|
||||
|
||||
session.sessionPreset = AVCaptureSessionPresetHigh;
|
||||
#if defined(SDL_PLATFORM_IOS)
|
||||
if (@available(iOS 10.0, tvOS 17.0, *)) {
|
||||
session.automaticallyConfiguresCaptureDeviceForWideColor = NO;
|
||||
}
|
||||
#endif
|
||||
|
||||
NSError *error = nil;
|
||||
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:avdevice error:&error];
|
||||
if (!input) {
|
||||
return SDL_SetError("Cannot create AVCaptureDeviceInput");
|
||||
}
|
||||
|
||||
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
|
||||
if (!output) {
|
||||
return SDL_SetError("Cannot create AVCaptureVideoDataOutput");
|
||||
}
|
||||
|
||||
output.videoSettings = @{
|
||||
(id)kCVPixelBufferWidthKey : @(spec->width),
|
||||
(id)kCVPixelBufferHeightKey : @(spec->height),
|
||||
(id)kCVPixelBufferPixelFormatTypeKey : @(CMFormatDescriptionGetMediaSubType([spec_format formatDescription]))
|
||||
};
|
||||
|
||||
char threadname[64];
|
||||
SDL_GetCameraThreadName(device, threadname, sizeof (threadname));
|
||||
dispatch_queue_t queue = dispatch_queue_create(threadname, NULL);
|
||||
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
if (!queue) {
|
||||
return SDL_SetError("dispatch_queue_create() failed");
|
||||
}
|
||||
|
||||
SDLCaptureVideoDataOutputSampleBufferDelegate *delegate = [[SDLCaptureVideoDataOutputSampleBufferDelegate alloc] init:device];
|
||||
if (delegate == nil) {
|
||||
return SDL_SetError("Cannot create SDLCaptureVideoDataOutputSampleBufferDelegate");
|
||||
}
|
||||
[output setSampleBufferDelegate:delegate queue:queue];
|
||||
|
||||
if (![session canAddInput:input]) {
|
||||
return SDL_SetError("Cannot add AVCaptureDeviceInput");
|
||||
}
|
||||
[session addInput:input];
|
||||
|
||||
if (![session canAddOutput:output]) {
|
||||
return SDL_SetError("Cannot add AVCaptureVideoDataOutput");
|
||||
}
|
||||
[session addOutput:output];
|
||||
|
||||
[session commitConfiguration];
|
||||
|
||||
SDLPrivateCameraData *hidden = [[SDLPrivateCameraData alloc] init];
|
||||
if (hidden == nil) {
|
||||
return SDL_SetError("Cannot create SDLPrivateCameraData");
|
||||
}
|
||||
|
||||
hidden.session = session;
|
||||
hidden.delegate = delegate;
|
||||
hidden.current_sample = NULL;
|
||||
device->hidden = (struct SDL_PrivateCameraData *)CFBridgingRetain(hidden);
|
||||
|
||||
[session startRunning]; // !!! FIXME: docs say this can block while camera warms up and shouldn't be done on main thread. Maybe push through `queue`?
|
||||
|
||||
CheckCameraPermissions(device); // check right away, in case the process is already granted permission.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void COREMEDIA_FreeDeviceHandle(SDL_Camera *device)
|
||||
{
|
||||
if (device && device->handle) {
|
||||
CFBridgingRelease(device->handle);
|
||||
}
|
||||
}
|
||||
|
||||
static void GatherCameraSpecs(AVCaptureDevice *device, CameraFormatAddData *add_data)
|
||||
{
|
||||
SDL_zerop(add_data);
|
||||
|
||||
for (AVCaptureDeviceFormat *fmt in device.formats) {
|
||||
if (CMFormatDescriptionGetMediaType(fmt.formatDescription) != kCMMediaType_Video) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//NSLog(@"Available camera format: %@\n", fmt);
|
||||
SDL_PixelFormat device_format = SDL_PIXELFORMAT_UNKNOWN;
|
||||
SDL_Colorspace device_colorspace = SDL_COLORSPACE_UNKNOWN;
|
||||
CoreMediaFormatToSDL(CMFormatDescriptionGetMediaSubType(fmt.formatDescription), &device_format, &device_colorspace);
|
||||
if (device_format == SDL_PIXELFORMAT_UNKNOWN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const CMVideoDimensions dims = CMVideoFormatDescriptionGetDimensions(fmt.formatDescription);
|
||||
const int w = (int) dims.width;
|
||||
const int h = (int) dims.height;
|
||||
for (AVFrameRateRange *framerate in fmt.videoSupportedFrameRateRanges) {
|
||||
int min_numerator = 0, min_denominator = 1;
|
||||
int max_numerator = 0, max_denominator = 1;
|
||||
|
||||
SDL_CalculateFraction(framerate.minFrameRate, &min_numerator, &min_denominator);
|
||||
SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, min_numerator, min_denominator);
|
||||
SDL_CalculateFraction(framerate.maxFrameRate, &max_numerator, &max_denominator);
|
||||
if (max_numerator != min_numerator || max_denominator != min_denominator) {
|
||||
SDL_AddCameraFormat(add_data, device_format, device_colorspace, w, h, max_numerator, max_denominator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool FindCoreMediaCameraByUniqueID(SDL_Camera *device, void *userdata)
|
||||
{
|
||||
NSString *uniqueid = (__bridge NSString *) userdata;
|
||||
AVCaptureDevice *avdev = (__bridge AVCaptureDevice *) device->handle;
|
||||
return ([uniqueid isEqualToString:avdev.uniqueID]) ? true : false;
|
||||
}
|
||||
|
||||
static void MaybeAddDevice(AVCaptureDevice *avdevice)
|
||||
{
|
||||
if (!avdevice.connected) {
|
||||
return; // not connected.
|
||||
} else if (![avdevice hasMediaType:AVMediaTypeVideo]) {
|
||||
return; // not a camera.
|
||||
} else if (SDL_FindPhysicalCameraByCallback(FindCoreMediaCameraByUniqueID, (__bridge void *) avdevice.uniqueID)) {
|
||||
return; // already have this one.
|
||||
}
|
||||
|
||||
CameraFormatAddData add_data;
|
||||
GatherCameraSpecs(avdevice, &add_data);
|
||||
if (add_data.num_specs > 0) {
|
||||
SDL_CameraPosition position = SDL_CAMERA_POSITION_UNKNOWN;
|
||||
if (avdevice.position == AVCaptureDevicePositionFront) {
|
||||
position = SDL_CAMERA_POSITION_FRONT_FACING;
|
||||
} else if (avdevice.position == AVCaptureDevicePositionBack) {
|
||||
position = SDL_CAMERA_POSITION_BACK_FACING;
|
||||
}
|
||||
SDL_AddCamera(avdevice.localizedName.UTF8String, position, add_data.num_specs, add_data.specs, (void *) CFBridgingRetain(avdevice));
|
||||
}
|
||||
|
||||
SDL_free(add_data.specs);
|
||||
}
|
||||
|
||||
static void COREMEDIA_DetectDevices(void)
|
||||
{
|
||||
NSArray<AVCaptureDevice *> *devices = nil;
|
||||
|
||||
if (@available(macOS 10.15, iOS 13, *)) {
|
||||
// kind of annoying that there isn't a "give me anything that looks like a camera" option,
|
||||
// so this list will need to be updated when Apple decides to add
|
||||
// AVCaptureDeviceTypeBuiltInQuadrupleCamera some day.
|
||||
NSArray *device_types = @[
|
||||
#ifdef SDL_PLATFORM_IOS
|
||||
AVCaptureDeviceTypeBuiltInTelephotoCamera,
|
||||
AVCaptureDeviceTypeBuiltInDualCamera,
|
||||
AVCaptureDeviceTypeBuiltInDualWideCamera,
|
||||
AVCaptureDeviceTypeBuiltInTripleCamera,
|
||||
AVCaptureDeviceTypeBuiltInUltraWideCamera,
|
||||
#else
|
||||
AVCaptureDeviceTypeExternalUnknown,
|
||||
#endif
|
||||
AVCaptureDeviceTypeBuiltInWideAngleCamera
|
||||
];
|
||||
|
||||
AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession
|
||||
discoverySessionWithDeviceTypes:device_types
|
||||
mediaType:AVMediaTypeVideo
|
||||
position:AVCaptureDevicePositionUnspecified];
|
||||
|
||||
devices = discoverySession.devices;
|
||||
// !!! FIXME: this can use Key Value Observation to get hotplug events.
|
||||
} else {
|
||||
// this is deprecated but works back to macOS 10.7; 10.15 added AVCaptureDeviceDiscoverySession as a replacement.
|
||||
devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
|
||||
// !!! FIXME: this can use AVCaptureDeviceWasConnectedNotification and AVCaptureDeviceWasDisconnectedNotification with NSNotificationCenter to get hotplug events.
|
||||
}
|
||||
|
||||
for (AVCaptureDevice *device in devices) {
|
||||
MaybeAddDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
static void COREMEDIA_Deinitialize(void)
|
||||
{
|
||||
// !!! FIXME: disable hotplug.
|
||||
}
|
||||
|
||||
static bool COREMEDIA_Init(SDL_CameraDriverImpl *impl)
|
||||
{
|
||||
impl->DetectDevices = COREMEDIA_DetectDevices;
|
||||
impl->OpenDevice = COREMEDIA_OpenDevice;
|
||||
impl->CloseDevice = COREMEDIA_CloseDevice;
|
||||
impl->WaitDevice = COREMEDIA_WaitDevice;
|
||||
impl->AcquireFrame = COREMEDIA_AcquireFrame;
|
||||
impl->ReleaseFrame = COREMEDIA_ReleaseFrame;
|
||||
impl->FreeDeviceHandle = COREMEDIA_FreeDeviceHandle;
|
||||
impl->Deinitialize = COREMEDIA_Deinitialize;
|
||||
|
||||
impl->ProvidesOwnCallbackThread = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CameraBootStrap COREMEDIA_bootstrap = {
|
||||
"coremedia", "SDL Apple CoreMedia camera driver", COREMEDIA_Init, false
|
||||
};
|
||||
|
||||
#endif // SDL_CAMERA_DRIVER_COREMEDIA
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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_DUMMY
|
||||
|
||||
#include "../SDL_syscamera.h"
|
||||
|
||||
static bool DUMMYCAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
|
||||
{
|
||||
return SDL_Unsupported();
|
||||
}
|
||||
|
||||
static void DUMMYCAMERA_CloseDevice(SDL_Camera *device)
|
||||
{
|
||||
}
|
||||
|
||||
static bool DUMMYCAMERA_WaitDevice(SDL_Camera *device)
|
||||
{
|
||||
return SDL_Unsupported();
|
||||
}
|
||||
|
||||
static SDL_CameraFrameResult DUMMYCAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
|
||||
{
|
||||
SDL_Unsupported();
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
|
||||
static void DUMMYCAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
|
||||
{
|
||||
}
|
||||
|
||||
static void DUMMYCAMERA_DetectDevices(void)
|
||||
{
|
||||
}
|
||||
|
||||
static void DUMMYCAMERA_FreeDeviceHandle(SDL_Camera *device)
|
||||
{
|
||||
}
|
||||
|
||||
static void DUMMYCAMERA_Deinitialize(void)
|
||||
{
|
||||
}
|
||||
|
||||
static bool DUMMYCAMERA_Init(SDL_CameraDriverImpl *impl)
|
||||
{
|
||||
impl->DetectDevices = DUMMYCAMERA_DetectDevices;
|
||||
impl->OpenDevice = DUMMYCAMERA_OpenDevice;
|
||||
impl->CloseDevice = DUMMYCAMERA_CloseDevice;
|
||||
impl->WaitDevice = DUMMYCAMERA_WaitDevice;
|
||||
impl->AcquireFrame = DUMMYCAMERA_AcquireFrame;
|
||||
impl->ReleaseFrame = DUMMYCAMERA_ReleaseFrame;
|
||||
impl->FreeDeviceHandle = DUMMYCAMERA_FreeDeviceHandle;
|
||||
impl->Deinitialize = DUMMYCAMERA_Deinitialize;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CameraBootStrap DUMMYCAMERA_bootstrap = {
|
||||
"dummy", "SDL dummy camera driver", DUMMYCAMERA_Init, true
|
||||
};
|
||||
|
||||
#endif // SDL_CAMERA_DRIVER_DUMMY
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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 "../../video/SDL_surface_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 bool EMSCRIPTENCAMERA_WaitDevice(SDL_Camera *device)
|
||||
{
|
||||
SDL_assert(!"This shouldn't be called"); // we aren't using SDL's internal thread.
|
||||
return false;
|
||||
}
|
||||
|
||||
static SDL_CameraFrameResult 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 SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
|
||||
*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 SDL_CAMERA_FRAME_ERROR; // something went wrong, maybe shutting down; just don't return a frame.
|
||||
}
|
||||
|
||||
frame->pixels = rgba;
|
||||
frame->pitch = device->actual_spec.width * 4;
|
||||
|
||||
return SDL_CAMERA_FRAME_READY;
|
||||
}
|
||||
|
||||
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 ? true : false);
|
||||
}
|
||||
|
||||
static bool 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 true; // 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 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 = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CameraBootStrap EMSCRIPTENCAMERA_bootstrap = {
|
||||
"emscripten", "SDL Emscripten MediaStream camera driver", EMSCRIPTENCAMERA_Init, false
|
||||
};
|
||||
|
||||
/* *INDENT-ON* */ // clang-format on
|
||||
|
||||
#endif // SDL_CAMERA_DRIVER_EMSCRIPTEN
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,911 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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_V4L2
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h> // low-level i/o
|
||||
#include <stddef.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#ifndef V4L2_CAP_DEVICE_CAPS
|
||||
// device_caps was added to struct v4l2_capability as of kernel 3.4.
|
||||
#define device_caps reserved[0]
|
||||
SDL_COMPILE_TIME_ASSERT(v4l2devicecaps, offsetof(struct v4l2_capability,device_caps) == offsetof(struct v4l2_capability,capabilities) + 4);
|
||||
#endif
|
||||
|
||||
#include "../SDL_syscamera.h"
|
||||
#include "../SDL_camera_c.h"
|
||||
#include "../../video/SDL_pixels_c.h"
|
||||
#include "../../video/SDL_surface_c.h"
|
||||
#include "../../thread/SDL_systhread.h"
|
||||
#include "../../core/linux/SDL_evdev_capabilities.h"
|
||||
#include "../../core/linux/SDL_udev.h"
|
||||
|
||||
#ifndef SDL_USE_LIBUDEV
|
||||
#include <dirent.h>
|
||||
#endif
|
||||
|
||||
typedef struct V4L2DeviceHandle
|
||||
{
|
||||
char *bus_info;
|
||||
char *path;
|
||||
} V4L2DeviceHandle;
|
||||
|
||||
|
||||
typedef enum io_method {
|
||||
IO_METHOD_INVALID,
|
||||
IO_METHOD_READ,
|
||||
IO_METHOD_MMAP,
|
||||
IO_METHOD_USERPTR
|
||||
} io_method;
|
||||
|
||||
struct buffer {
|
||||
void *start;
|
||||
size_t length;
|
||||
int available; // Is available in userspace
|
||||
};
|
||||
|
||||
struct SDL_PrivateCameraData
|
||||
{
|
||||
int fd;
|
||||
io_method io;
|
||||
int nb_buffers;
|
||||
struct buffer *buffers;
|
||||
int driver_pitch;
|
||||
};
|
||||
|
||||
static int xioctl(int fh, int request, void *arg)
|
||||
{
|
||||
int r;
|
||||
|
||||
do {
|
||||
r = ioctl(fh, request, arg);
|
||||
} while ((r == -1) && (errno == EINTR));
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
static bool V4L2_WaitDevice(SDL_Camera *device)
|
||||
{
|
||||
const int fd = device->hidden->fd;
|
||||
|
||||
int rc;
|
||||
|
||||
do {
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(fd, &fds);
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 100 * 1000;
|
||||
|
||||
rc = select(fd + 1, &fds, NULL, NULL, &tv);
|
||||
if ((rc == -1) && (errno == EINTR)) {
|
||||
rc = 0; // pretend it was a timeout, keep looping.
|
||||
} else if (rc > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Thread is requested to shut down
|
||||
if (SDL_GetAtomicInt(&device->shutdown)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} while (rc == 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static SDL_CameraFrameResult V4L2_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
|
||||
{
|
||||
const int fd = device->hidden->fd;
|
||||
const io_method io = device->hidden->io;
|
||||
size_t size = device->hidden->buffers[0].length;
|
||||
struct v4l2_buffer buf;
|
||||
|
||||
switch (io) {
|
||||
case IO_METHOD_READ:
|
||||
if (read(fd, device->hidden->buffers[0].start, size) == -1) {
|
||||
switch (errno) {
|
||||
case EAGAIN:
|
||||
return SDL_CAMERA_FRAME_SKIP;
|
||||
|
||||
case EIO:
|
||||
// Could ignore EIO, see spec.
|
||||
// fall through
|
||||
|
||||
default:
|
||||
SDL_SetError("read");
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
*timestampNS = SDL_GetTicksNS(); // oh well, close enough.
|
||||
frame->pixels = device->hidden->buffers[0].start;
|
||||
frame->pitch = device->hidden->driver_pitch;
|
||||
break;
|
||||
|
||||
case IO_METHOD_MMAP:
|
||||
SDL_zero(buf);
|
||||
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
|
||||
if (xioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
|
||||
switch (errno) {
|
||||
case EAGAIN:
|
||||
return SDL_CAMERA_FRAME_SKIP;
|
||||
|
||||
case EIO:
|
||||
// Could ignore EIO, see spec.
|
||||
// fall through
|
||||
|
||||
default:
|
||||
SDL_SetError("VIDIOC_DQBUF: %d", errno);
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
if ((int)buf.index < 0 || (int)buf.index >= device->hidden->nb_buffers) {
|
||||
SDL_SetError("invalid buffer index");
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
|
||||
frame->pixels = device->hidden->buffers[buf.index].start;
|
||||
frame->pitch = device->hidden->driver_pitch;
|
||||
device->hidden->buffers[buf.index].available = 1;
|
||||
|
||||
*timestampNS = (((Uint64) buf.timestamp.tv_sec) * SDL_NS_PER_SECOND) + SDL_US_TO_NS(buf.timestamp.tv_usec);
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: debug mmap: image %d/%d data[0]=%p", buf.index, device->hidden->nb_buffers, (void*)frame->pixels);
|
||||
#endif
|
||||
break;
|
||||
|
||||
case IO_METHOD_USERPTR:
|
||||
SDL_zero(buf);
|
||||
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_USERPTR;
|
||||
|
||||
if (xioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
|
||||
switch (errno) {
|
||||
case EAGAIN:
|
||||
return SDL_CAMERA_FRAME_SKIP;
|
||||
|
||||
case EIO:
|
||||
// Could ignore EIO, see spec.
|
||||
// fall through
|
||||
|
||||
default:
|
||||
SDL_SetError("VIDIOC_DQBUF");
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
int i;
|
||||
for (i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
if (buf.m.userptr == (unsigned long)device->hidden->buffers[i].start && buf.length == size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= device->hidden->nb_buffers) {
|
||||
SDL_SetError("invalid buffer index");
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
|
||||
frame->pixels = (void*)buf.m.userptr;
|
||||
frame->pitch = device->hidden->driver_pitch;
|
||||
device->hidden->buffers[i].available = 1;
|
||||
|
||||
*timestampNS = (((Uint64) buf.timestamp.tv_sec) * SDL_NS_PER_SECOND) + SDL_US_TO_NS(buf.timestamp.tv_usec);
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: debug userptr: image %d/%d data[0]=%p", buf.index, device->hidden->nb_buffers, (void*)frame->pixels);
|
||||
#endif
|
||||
break;
|
||||
|
||||
case IO_METHOD_INVALID:
|
||||
SDL_assert(!"Shouldn't have hit this");
|
||||
break;
|
||||
}
|
||||
|
||||
return SDL_CAMERA_FRAME_READY;
|
||||
}
|
||||
|
||||
static void V4L2_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
|
||||
{
|
||||
struct v4l2_buffer buf;
|
||||
const int fd = device->hidden->fd;
|
||||
const io_method io = device->hidden->io;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
if (frame->pixels == device->hidden->buffers[i].start) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= device->hidden->nb_buffers) {
|
||||
return; // oh well, we didn't own this.
|
||||
}
|
||||
|
||||
switch (io) {
|
||||
case IO_METHOD_READ:
|
||||
break;
|
||||
|
||||
case IO_METHOD_MMAP:
|
||||
SDL_zero(buf);
|
||||
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
|
||||
if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
// !!! FIXME: disconnect the device.
|
||||
return; //SDL_SetError("VIDIOC_QBUF");
|
||||
}
|
||||
device->hidden->buffers[i].available = 0;
|
||||
break;
|
||||
|
||||
case IO_METHOD_USERPTR:
|
||||
SDL_zero(buf);
|
||||
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_USERPTR;
|
||||
buf.index = i;
|
||||
buf.m.userptr = (unsigned long)frame->pixels;
|
||||
buf.length = (int) device->hidden->buffers[i].length;
|
||||
|
||||
if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
// !!! FIXME: disconnect the device.
|
||||
return; //SDL_SetError("VIDIOC_QBUF");
|
||||
}
|
||||
device->hidden->buffers[i].available = 0;
|
||||
break;
|
||||
|
||||
case IO_METHOD_INVALID:
|
||||
SDL_assert(!"Shouldn't have hit this");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static bool EnqueueBuffers(SDL_Camera *device)
|
||||
{
|
||||
const int fd = device->hidden->fd;
|
||||
const io_method io = device->hidden->io;
|
||||
switch (io) {
|
||||
case IO_METHOD_READ:
|
||||
break;
|
||||
|
||||
case IO_METHOD_MMAP:
|
||||
for (int i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
if (device->hidden->buffers[i].available == 0) {
|
||||
struct v4l2_buffer buf;
|
||||
|
||||
SDL_zero(buf);
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
|
||||
if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
return SDL_SetError("VIDIOC_QBUF");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case IO_METHOD_USERPTR:
|
||||
for (int i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
if (device->hidden->buffers[i].available == 0) {
|
||||
struct v4l2_buffer buf;
|
||||
|
||||
SDL_zero(buf);
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_USERPTR;
|
||||
buf.index = i;
|
||||
buf.m.userptr = (unsigned long)device->hidden->buffers[i].start;
|
||||
buf.length = (int) device->hidden->buffers[i].length;
|
||||
|
||||
if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
|
||||
return SDL_SetError("VIDIOC_QBUF");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case IO_METHOD_INVALID: SDL_assert(!"Shouldn't have hit this"); break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool AllocBufferRead(SDL_Camera *device, size_t buffer_size)
|
||||
{
|
||||
device->hidden->buffers[0].length = buffer_size;
|
||||
device->hidden->buffers[0].start = SDL_calloc(1, buffer_size);
|
||||
return (device->hidden->buffers[0].start != NULL);
|
||||
}
|
||||
|
||||
static bool AllocBufferMmap(SDL_Camera *device)
|
||||
{
|
||||
const int fd = device->hidden->fd;
|
||||
int i;
|
||||
for (i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
struct v4l2_buffer buf;
|
||||
|
||||
SDL_zero(buf);
|
||||
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
|
||||
if (xioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
|
||||
return SDL_SetError("VIDIOC_QUERYBUF");
|
||||
}
|
||||
|
||||
device->hidden->buffers[i].length = buf.length;
|
||||
device->hidden->buffers[i].start =
|
||||
mmap(NULL /* start anywhere */,
|
||||
buf.length,
|
||||
PROT_READ | PROT_WRITE /* required */,
|
||||
MAP_SHARED /* recommended */,
|
||||
fd, buf.m.offset);
|
||||
|
||||
if (MAP_FAILED == device->hidden->buffers[i].start) {
|
||||
return SDL_SetError("mmap");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool AllocBufferUserPtr(SDL_Camera *device, size_t buffer_size)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
device->hidden->buffers[i].length = buffer_size;
|
||||
device->hidden->buffers[i].start = SDL_calloc(1, buffer_size);
|
||||
|
||||
if (!device->hidden->buffers[i].start) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void format_v4l2_to_sdl(Uint32 fmt, SDL_PixelFormat *format, SDL_Colorspace *colorspace)
|
||||
{
|
||||
switch (fmt) {
|
||||
#define CASE(x, y, z) case x: *format = y; *colorspace = z; return
|
||||
CASE(V4L2_PIX_FMT_YUYV, SDL_PIXELFORMAT_YUY2, SDL_COLORSPACE_BT709_LIMITED);
|
||||
#undef CASE
|
||||
default:
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: Unknown format V4L2_PIX_FORMAT '%d'", fmt);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
*format = SDL_PIXELFORMAT_UNKNOWN;
|
||||
*colorspace = SDL_COLORSPACE_UNKNOWN;
|
||||
}
|
||||
|
||||
static Uint32 format_sdl_to_v4l2(SDL_PixelFormat fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
#define CASE(y, x) case x: return y
|
||||
CASE(V4L2_PIX_FMT_YUYV, SDL_PIXELFORMAT_YUY2);
|
||||
CASE(V4L2_PIX_FMT_MJPEG, SDL_PIXELFORMAT_UNKNOWN);
|
||||
#undef CASE
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void V4L2_CloseDevice(SDL_Camera *device)
|
||||
{
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device->hidden) {
|
||||
const io_method io = device->hidden->io;
|
||||
const int fd = device->hidden->fd;
|
||||
|
||||
if ((io == IO_METHOD_MMAP) || (io == IO_METHOD_USERPTR)) {
|
||||
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
xioctl(fd, VIDIOC_STREAMOFF, &type);
|
||||
}
|
||||
|
||||
if (device->hidden->buffers) {
|
||||
switch (io) {
|
||||
case IO_METHOD_INVALID:
|
||||
break;
|
||||
|
||||
case IO_METHOD_READ:
|
||||
SDL_free(device->hidden->buffers[0].start);
|
||||
break;
|
||||
|
||||
case IO_METHOD_MMAP:
|
||||
for (int i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
if (munmap(device->hidden->buffers[i].start, device->hidden->buffers[i].length) == -1) {
|
||||
SDL_SetError("munmap");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case IO_METHOD_USERPTR:
|
||||
for (int i = 0; i < device->hidden->nb_buffers; ++i) {
|
||||
SDL_free(device->hidden->buffers[i].start);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_free(device->hidden->buffers);
|
||||
}
|
||||
|
||||
if (fd != -1) {
|
||||
close(fd);
|
||||
}
|
||||
SDL_free(device->hidden);
|
||||
|
||||
device->hidden = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool V4L2_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
|
||||
{
|
||||
const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
|
||||
struct stat st;
|
||||
struct v4l2_capability cap;
|
||||
const int fd = open(handle->path, O_RDWR /* required */ | O_NONBLOCK, 0);
|
||||
|
||||
// most of this probably shouldn't fail unless the filesystem node changed out from under us since MaybeAddDevice().
|
||||
if (fd == -1) {
|
||||
return SDL_SetError("Cannot open '%s': %d, %s", handle->path, errno, strerror(errno));
|
||||
} else if (fstat(fd, &st) == -1) {
|
||||
close(fd);
|
||||
return SDL_SetError("Cannot identify '%s': %d, %s", handle->path, errno, strerror(errno));
|
||||
} else if (!S_ISCHR(st.st_mode)) {
|
||||
close(fd);
|
||||
return SDL_SetError("%s is not a character device", handle->path);
|
||||
} else if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
|
||||
const int err = errno;
|
||||
close(fd);
|
||||
if (err == EINVAL) {
|
||||
return SDL_SetError("%s is unexpectedly not a V4L2 device", handle->path);
|
||||
}
|
||||
return SDL_SetError("Error VIDIOC_QUERYCAP errno=%d device%s is no V4L2 device", err, handle->path);
|
||||
} else if ((cap.device_caps & V4L2_CAP_VIDEO_CAPTURE) == 0) {
|
||||
close(fd);
|
||||
return SDL_SetError("%s is unexpectedly not a video capture device", handle->path);
|
||||
}
|
||||
|
||||
device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
|
||||
if (device->hidden == NULL) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
device->hidden->fd = fd;
|
||||
device->hidden->io = IO_METHOD_INVALID;
|
||||
|
||||
// Select video input, video standard and tune here.
|
||||
// errors in the crop code are not fatal.
|
||||
struct v4l2_cropcap cropcap;
|
||||
SDL_zero(cropcap);
|
||||
cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(fd, VIDIOC_CROPCAP, &cropcap) == 0) {
|
||||
struct v4l2_crop crop;
|
||||
SDL_zero(crop);
|
||||
crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
crop.c = cropcap.defrect; // reset to default
|
||||
xioctl(fd, VIDIOC_S_CROP, &crop);
|
||||
}
|
||||
|
||||
struct v4l2_format fmt;
|
||||
SDL_zero(fmt);
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
fmt.fmt.pix.width = spec->width;
|
||||
fmt.fmt.pix.height = spec->height;
|
||||
fmt.fmt.pix.pixelformat = format_sdl_to_v4l2(spec->format);
|
||||
//fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
|
||||
fmt.fmt.pix.field = V4L2_FIELD_ANY;
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: set SDL format %s", SDL_GetPixelFormatName(spec->format));
|
||||
{ const Uint32 f = fmt.fmt.pix.pixelformat; SDL_Log("CAMERA: set format V4L2_format=%d %c%c%c%c", f, (f >> 0) & 0xff, (f >> 8) & 0xff, (f >> 16) & 0xff, (f >> 24) & 0xff); }
|
||||
#endif
|
||||
|
||||
if (xioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
|
||||
return SDL_SetError("Error VIDIOC_S_FMT");
|
||||
}
|
||||
|
||||
if (spec->framerate_numerator && spec->framerate_denominator) {
|
||||
struct v4l2_streamparm setfps;
|
||||
SDL_zero(setfps);
|
||||
setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(fd, VIDIOC_G_PARM, &setfps) == 0) {
|
||||
if ( (setfps.parm.capture.timeperframe.denominator != spec->framerate_numerator) ||
|
||||
(setfps.parm.capture.timeperframe.numerator = spec->framerate_denominator) ) {
|
||||
setfps.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
setfps.parm.capture.timeperframe.numerator = spec->framerate_denominator;
|
||||
setfps.parm.capture.timeperframe.denominator = spec->framerate_numerator;
|
||||
if (xioctl(fd, VIDIOC_S_PARM, &setfps) == -1) {
|
||||
return SDL_SetError("Error VIDIOC_S_PARM");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_zero(fmt);
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(fd, VIDIOC_G_FMT, &fmt) == -1) {
|
||||
return SDL_SetError("Error VIDIOC_G_FMT");
|
||||
}
|
||||
device->hidden->driver_pitch = fmt.fmt.pix.bytesperline;
|
||||
|
||||
io_method io = IO_METHOD_INVALID;
|
||||
if ((io == IO_METHOD_INVALID) && (cap.device_caps & V4L2_CAP_STREAMING)) {
|
||||
struct v4l2_requestbuffers req;
|
||||
SDL_zero(req);
|
||||
req.count = 8;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
if ((xioctl(fd, VIDIOC_REQBUFS, &req) == 0) && (req.count >= 2)) {
|
||||
io = IO_METHOD_MMAP;
|
||||
device->hidden->nb_buffers = req.count;
|
||||
} else { // mmap didn't work out? Try USERPTR.
|
||||
SDL_zero(req);
|
||||
req.count = 8;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_USERPTR;
|
||||
if (xioctl(fd, VIDIOC_REQBUFS, &req) == 0) {
|
||||
io = IO_METHOD_USERPTR;
|
||||
device->hidden->nb_buffers = 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((io == IO_METHOD_INVALID) && (cap.device_caps & V4L2_CAP_READWRITE)) {
|
||||
io = IO_METHOD_READ;
|
||||
device->hidden->nb_buffers = 1;
|
||||
}
|
||||
|
||||
if (io == IO_METHOD_INVALID) {
|
||||
return SDL_SetError("Don't have a way to talk to this device");
|
||||
}
|
||||
|
||||
device->hidden->io = io;
|
||||
|
||||
device->hidden->buffers = SDL_calloc(device->hidden->nb_buffers, sizeof(*device->hidden->buffers));
|
||||
if (!device->hidden->buffers) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t size, pitch;
|
||||
if (!SDL_CalculateSurfaceSize(device->spec.format, device->spec.width, device->spec.height, &size, &pitch, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool rc = true;
|
||||
switch (io) {
|
||||
case IO_METHOD_READ:
|
||||
rc = AllocBufferRead(device, size);
|
||||
break;
|
||||
|
||||
case IO_METHOD_MMAP:
|
||||
rc = AllocBufferMmap(device);
|
||||
break;
|
||||
|
||||
case IO_METHOD_USERPTR:
|
||||
rc = AllocBufferUserPtr(device, size);
|
||||
break;
|
||||
|
||||
case IO_METHOD_INVALID:
|
||||
SDL_assert(!"Shouldn't have hit this");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!rc) {
|
||||
return false;
|
||||
} else if (!EnqueueBuffers(device)) {
|
||||
return false;
|
||||
} else if (io != IO_METHOD_READ) {
|
||||
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(fd, VIDIOC_STREAMON, &type) == -1) {
|
||||
return SDL_SetError("VIDIOC_STREAMON");
|
||||
}
|
||||
}
|
||||
|
||||
// Currently there is no user permission prompt for camera access, but maybe there will be a D-Bus portal interface at some point.
|
||||
SDL_CameraPermissionOutcome(device, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool FindV4L2CameraByBusInfoCallback(SDL_Camera *device, void *userdata)
|
||||
{
|
||||
const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
|
||||
return (SDL_strcmp(handle->bus_info, (const char *) userdata) == 0);
|
||||
}
|
||||
|
||||
static bool AddCameraFormat(const int fd, CameraFormatAddData *data, SDL_PixelFormat sdlfmt, SDL_Colorspace colorspace, Uint32 v4l2fmt, int w, int h)
|
||||
{
|
||||
struct v4l2_frmivalenum frmivalenum;
|
||||
SDL_zero(frmivalenum);
|
||||
frmivalenum.pixel_format = v4l2fmt;
|
||||
frmivalenum.width = (Uint32) w;
|
||||
frmivalenum.height = (Uint32) h;
|
||||
|
||||
while (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmivalenum) == 0) {
|
||||
if (frmivalenum.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
|
||||
const int numerator = (int) frmivalenum.discrete.numerator;
|
||||
const int denominator = (int) frmivalenum.discrete.denominator;
|
||||
#if DEBUG_CAMERA
|
||||
const float fps = (float) denominator / (float) numerator;
|
||||
SDL_Log("CAMERA: * Has discrete frame interval (%d / %d), fps=%f", numerator, denominator, fps);
|
||||
#endif
|
||||
if (!SDL_AddCameraFormat(data, sdlfmt, colorspace, w, h, denominator, numerator)) {
|
||||
return false; // Probably out of memory; we'll go with what we have, if anything.
|
||||
}
|
||||
frmivalenum.index++; // set up for the next one.
|
||||
} else if ((frmivalenum.type == V4L2_FRMIVAL_TYPE_STEPWISE) || (frmivalenum.type == V4L2_FRMIVAL_TYPE_CONTINUOUS)) {
|
||||
int d = frmivalenum.stepwise.min.denominator;
|
||||
// !!! FIXME: should we step by the numerator...?
|
||||
for (int n = (int) frmivalenum.stepwise.min.numerator; n <= (int) frmivalenum.stepwise.max.numerator; n += (int) frmivalenum.stepwise.step.numerator) {
|
||||
#if DEBUG_CAMERA
|
||||
const float fps = (float) d / (float) n;
|
||||
SDL_Log("CAMERA: * Has %s frame interval (%d / %d), fps=%f", (frmivalenum.type == V4L2_FRMIVAL_TYPE_STEPWISE) ? "stepwise" : "continuous", n, d, fps);
|
||||
#endif
|
||||
// SDL expects framerate, V4L2 provides interval
|
||||
if (!SDL_AddCameraFormat(data, sdlfmt, colorspace, w, h, d, n)) {
|
||||
return false; // Probably out of memory; we'll go with what we have, if anything.
|
||||
}
|
||||
d += (int) frmivalenum.stepwise.step.denominator;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void MaybeAddDevice(const char *path)
|
||||
{
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
const int fd = open(path, O_RDWR /* required */ | O_NONBLOCK, 0);
|
||||
if (fd == -1) {
|
||||
return; // can't open it? skip it.
|
||||
} else if (fstat(fd, &st) == -1) {
|
||||
close(fd);
|
||||
return; // can't stat it? skip it.
|
||||
} else if (!S_ISCHR(st.st_mode)) {
|
||||
close(fd);
|
||||
return; // not a character device.
|
||||
}
|
||||
|
||||
struct v4l2_capability vcap;
|
||||
const int rc = ioctl(fd, VIDIOC_QUERYCAP, &vcap);
|
||||
if (rc != 0) {
|
||||
close(fd);
|
||||
return; // probably not a v4l2 device at all.
|
||||
} else if ((vcap.device_caps & V4L2_CAP_VIDEO_CAPTURE) == 0) {
|
||||
close(fd);
|
||||
return; // not a video capture device.
|
||||
} else if (SDL_FindPhysicalCameraByCallback(FindV4L2CameraByBusInfoCallback, vcap.bus_info)) {
|
||||
close(fd);
|
||||
return; // already have it.
|
||||
}
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: V4L2 camera path='%s' bus_info='%s' name='%s'", path, (const char *) vcap.bus_info, vcap.card);
|
||||
#endif
|
||||
|
||||
CameraFormatAddData add_data;
|
||||
SDL_zero(add_data);
|
||||
|
||||
struct v4l2_fmtdesc fmtdesc;
|
||||
SDL_zero(fmtdesc);
|
||||
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
|
||||
SDL_PixelFormat sdlfmt = SDL_PIXELFORMAT_UNKNOWN;
|
||||
SDL_Colorspace colorspace = SDL_COLORSPACE_UNKNOWN;
|
||||
format_v4l2_to_sdl(fmtdesc.pixelformat, &sdlfmt, &colorspace);
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: - Has format '%s'%s%s", SDL_GetPixelFormatName(sdlfmt),
|
||||
(fmtdesc.flags & V4L2_FMT_FLAG_EMULATED) ? " [EMULATED]" : "",
|
||||
(fmtdesc.flags & V4L2_FMT_FLAG_COMPRESSED) ? " [COMPRESSED]" : "");
|
||||
#endif
|
||||
|
||||
fmtdesc.index++; // prepare for next iteration.
|
||||
|
||||
if (sdlfmt == SDL_PIXELFORMAT_UNKNOWN) {
|
||||
continue; // unsupported by SDL atm.
|
||||
}
|
||||
|
||||
struct v4l2_frmsizeenum frmsizeenum;
|
||||
SDL_zero(frmsizeenum);
|
||||
frmsizeenum.pixel_format = fmtdesc.pixelformat;
|
||||
|
||||
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == 0) {
|
||||
if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
|
||||
const int w = (int) frmsizeenum.discrete.width;
|
||||
const int h = (int) frmsizeenum.discrete.height;
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: * Has discrete size %dx%d", w, h);
|
||||
#endif
|
||||
if (!AddCameraFormat(fd, &add_data, sdlfmt, colorspace, fmtdesc.pixelformat, w, h)) {
|
||||
break; // Probably out of memory; we'll go with what we have, if anything.
|
||||
}
|
||||
frmsizeenum.index++; // set up for the next one.
|
||||
} else if ((frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) || (frmsizeenum.type == V4L2_FRMSIZE_TYPE_CONTINUOUS)) {
|
||||
const int minw = (int) frmsizeenum.stepwise.min_width;
|
||||
const int minh = (int) frmsizeenum.stepwise.min_height;
|
||||
const int maxw = (int) frmsizeenum.stepwise.max_width;
|
||||
const int maxh = (int) frmsizeenum.stepwise.max_height;
|
||||
const int stepw = (int) frmsizeenum.stepwise.step_width;
|
||||
const int steph = (int) frmsizeenum.stepwise.step_height;
|
||||
for (int w = minw; w <= maxw; w += stepw) {
|
||||
for (int h = minh; w <= maxh; w += steph) {
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: * Has %s size %dx%d", (frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) ? "stepwise" : "continuous", w, h);
|
||||
#endif
|
||||
if (!AddCameraFormat(fd, &add_data, sdlfmt, colorspace, fmtdesc.pixelformat, w, h)) {
|
||||
break; // Probably out of memory; we'll go with what we have, if anything.
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(fd);
|
||||
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: (total specs: %d)", add_data.num_specs);
|
||||
#endif
|
||||
|
||||
if (add_data.num_specs > 0) {
|
||||
V4L2DeviceHandle *handle = (V4L2DeviceHandle *) SDL_calloc(1, sizeof (V4L2DeviceHandle));
|
||||
if (handle) {
|
||||
handle->path = SDL_strdup(path);
|
||||
if (handle->path) {
|
||||
handle->bus_info = SDL_strdup((char *)vcap.bus_info);
|
||||
if (handle->bus_info) {
|
||||
if (SDL_AddCamera((const char *) vcap.card, SDL_CAMERA_POSITION_UNKNOWN, add_data.num_specs, add_data.specs, handle)) {
|
||||
SDL_free(add_data.specs);
|
||||
return; // good to go.
|
||||
}
|
||||
SDL_free(handle->bus_info);
|
||||
}
|
||||
SDL_free(handle->path);
|
||||
}
|
||||
SDL_free(handle);
|
||||
}
|
||||
}
|
||||
SDL_free(add_data.specs);
|
||||
}
|
||||
|
||||
static void V4L2_FreeDeviceHandle(SDL_Camera *device)
|
||||
{
|
||||
if (device) {
|
||||
V4L2DeviceHandle *handle = (V4L2DeviceHandle *) device->handle;
|
||||
SDL_free(handle->path);
|
||||
SDL_free(handle->bus_info);
|
||||
SDL_free(handle);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SDL_USE_LIBUDEV
|
||||
static bool FindV4L2CameraByPathCallback(SDL_Camera *device, void *userdata)
|
||||
{
|
||||
const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
|
||||
return (SDL_strcmp(handle->path, (const char *) userdata) == 0);
|
||||
}
|
||||
|
||||
static void MaybeRemoveDevice(const char *path)
|
||||
{
|
||||
if (path) {
|
||||
SDL_CameraDisconnected(SDL_FindPhysicalCameraByCallback(FindV4L2CameraByPathCallback, (void *) path));
|
||||
}
|
||||
}
|
||||
|
||||
static void CameraUdevCallback(SDL_UDEV_deviceevent udev_type, int udev_class, const char *devpath)
|
||||
{
|
||||
if (devpath && (udev_class & SDL_UDEV_DEVICE_VIDEO_CAPTURE)) {
|
||||
if (udev_type == SDL_UDEV_DEVICEADDED) {
|
||||
MaybeAddDevice(devpath);
|
||||
} else if (udev_type == SDL_UDEV_DEVICEREMOVED) {
|
||||
MaybeRemoveDevice(devpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // SDL_USE_LIBUDEV
|
||||
|
||||
static void V4L2_Deinitialize(void)
|
||||
{
|
||||
#ifdef SDL_USE_LIBUDEV
|
||||
SDL_UDEV_DelCallback(CameraUdevCallback);
|
||||
SDL_UDEV_Quit();
|
||||
#endif // SDL_USE_LIBUDEV
|
||||
}
|
||||
|
||||
static void V4L2_DetectDevices(void)
|
||||
{
|
||||
#ifdef SDL_USE_LIBUDEV
|
||||
if (SDL_UDEV_Init()) {
|
||||
if (SDL_UDEV_AddCallback(CameraUdevCallback)) {
|
||||
SDL_UDEV_Scan(); // Force a scan to build the initial device list
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif // SDL_USE_LIBUDEV
|
||||
|
||||
DIR *dirp = opendir("/dev");
|
||||
if (dirp) {
|
||||
struct dirent *dent;
|
||||
while ((dent = readdir(dirp)) != NULL) {
|
||||
int num = 0;
|
||||
if (SDL_sscanf(dent->d_name, "video%d", &num) == 1) {
|
||||
char fullpath[64];
|
||||
SDL_snprintf(fullpath, sizeof (fullpath), "/dev/video%d", num);
|
||||
MaybeAddDevice(fullpath);
|
||||
}
|
||||
}
|
||||
closedir(dirp);
|
||||
}
|
||||
}
|
||||
|
||||
static bool V4L2_Init(SDL_CameraDriverImpl *impl)
|
||||
{
|
||||
impl->DetectDevices = V4L2_DetectDevices;
|
||||
impl->OpenDevice = V4L2_OpenDevice;
|
||||
impl->CloseDevice = V4L2_CloseDevice;
|
||||
impl->WaitDevice = V4L2_WaitDevice;
|
||||
impl->AcquireFrame = V4L2_AcquireFrame;
|
||||
impl->ReleaseFrame = V4L2_ReleaseFrame;
|
||||
impl->FreeDeviceHandle = V4L2_FreeDeviceHandle;
|
||||
impl->Deinitialize = V4L2_Deinitialize;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CameraBootStrap V4L2_bootstrap = {
|
||||
"v4l2", "SDL Video4Linux2 camera driver", V4L2_Init, false
|
||||
};
|
||||
|
||||
#endif // SDL_CAMERA_DRIVER_V4L2
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
Simple DirectMedia Layer
|
||||
Copyright (C) 1997-2025 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_VITA
|
||||
|
||||
#include "../SDL_syscamera.h"
|
||||
#include <psp2/camera.h>
|
||||
#include <psp2/kernel/sysmem.h>
|
||||
|
||||
static struct {
|
||||
Sint32 w;
|
||||
Sint32 h;
|
||||
Sint32 res;
|
||||
} resolutions[] = {
|
||||
{640, 480, SCE_CAMERA_RESOLUTION_640_480},
|
||||
{320, 240, SCE_CAMERA_RESOLUTION_320_240},
|
||||
{160, 120, SCE_CAMERA_RESOLUTION_160_120},
|
||||
{352, 288, SCE_CAMERA_RESOLUTION_352_288},
|
||||
{176, 144, SCE_CAMERA_RESOLUTION_176_144},
|
||||
{480, 272, SCE_CAMERA_RESOLUTION_480_272},
|
||||
{640, 360, SCE_CAMERA_RESOLUTION_640_360},
|
||||
{0, 0, 0}
|
||||
};
|
||||
|
||||
static Sint32 fps[] = {5, 10, 15, 20, 24, 25, 30, 60, 0};
|
||||
|
||||
static void GatherCameraSpecs(Sint32 devid, CameraFormatAddData *add_data, char **fullname, SDL_CameraPosition *position)
|
||||
{
|
||||
SDL_zerop(add_data);
|
||||
|
||||
if (devid == SCE_CAMERA_DEVICE_FRONT) {
|
||||
*position = SDL_CAMERA_POSITION_FRONT_FACING;
|
||||
*fullname = SDL_strdup("Front-facing camera");
|
||||
} else if (devid == SCE_CAMERA_DEVICE_BACK) {
|
||||
*position = SDL_CAMERA_POSITION_BACK_FACING;
|
||||
*fullname = SDL_strdup("Back-facing camera");
|
||||
}
|
||||
|
||||
if (!*fullname) {
|
||||
*fullname = SDL_strdup("Generic camera");
|
||||
}
|
||||
|
||||
// Note: there are actually more fps and pixelformats. Planar YUV is fastest. Support only YUV and integer fps for now
|
||||
Sint32 idx = 0;
|
||||
while (resolutions[idx].res > 0) {
|
||||
Sint32 fps_idx = 0;
|
||||
while (fps[fps_idx] > 0) {
|
||||
SDL_AddCameraFormat(add_data, SDL_PIXELFORMAT_IYUV, SDL_COLORSPACE_BT601_LIMITED, resolutions[idx].w, resolutions[idx].h, fps[fps_idx], 1); /* SCE_CAMERA_FORMAT_ARGB */
|
||||
fps_idx++;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
static bool FindVitaCameraByID(SDL_Camera *device, void *userdata)
|
||||
{
|
||||
Sint32 devid = (Sint32) userdata;
|
||||
return (devid == (Sint32)device->handle);
|
||||
}
|
||||
|
||||
static void MaybeAddDevice(Sint32 devid)
|
||||
{
|
||||
#if DEBUG_CAMERA
|
||||
SDL_Log("CAMERA: MaybeAddDevice('%d')", devid);
|
||||
#endif
|
||||
|
||||
if (SDL_FindPhysicalCameraByCallback(FindVitaCameraByID, (void *) devid)) {
|
||||
return; // already have this one.
|
||||
}
|
||||
|
||||
SDL_CameraPosition position = SDL_CAMERA_POSITION_UNKNOWN;
|
||||
char *fullname = NULL;
|
||||
CameraFormatAddData add_data;
|
||||
GatherCameraSpecs(devid, &add_data, &fullname, &position);
|
||||
|
||||
if (add_data.num_specs > 0) {
|
||||
SDL_AddCamera(fullname, position, add_data.num_specs, add_data.specs, (void*)devid);
|
||||
}
|
||||
|
||||
SDL_free(fullname);
|
||||
SDL_free(add_data.specs);
|
||||
}
|
||||
|
||||
static SceUID imbUid = -1;
|
||||
|
||||
static void freeBuffers(SceCameraInfo* info)
|
||||
{
|
||||
if (imbUid != -1) {
|
||||
sceKernelFreeMemBlock(imbUid);
|
||||
info->pIBase = NULL;
|
||||
imbUid = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool VITACAMERA_OpenDevice(SDL_Camera *device, const SDL_CameraSpec *spec)
|
||||
{
|
||||
// we can't open more than one camera, so error-out early
|
||||
if (imbUid != -1) {
|
||||
return SDL_SetError("Only one camera can be active");
|
||||
}
|
||||
|
||||
SceCameraInfo* info = (SceCameraInfo*)SDL_calloc(1, sizeof(SceCameraInfo));
|
||||
|
||||
info->size = sizeof(SceCameraInfo);
|
||||
info->priority = SCE_CAMERA_PRIORITY_SHARE;
|
||||
info->buffer = 0; // target buffer set by sceCameraOpen
|
||||
|
||||
info->framerate = spec->framerate_numerator / spec->framerate_denominator;
|
||||
|
||||
Sint32 idx = 0;
|
||||
while (resolutions[idx].res > 0) {
|
||||
if (spec->width == resolutions[idx].w && spec->height == resolutions[idx].h) {
|
||||
info->resolution = resolutions[idx].res;
|
||||
break;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
info->range = 1;
|
||||
info->format = SCE_CAMERA_FORMAT_YUV420_PLANE;
|
||||
info->pitch = 0; // same size surface
|
||||
|
||||
info->sizeIBase = spec->width*spec->height;;
|
||||
info->sizeUBase = ((spec->width+1)/2) * ((spec->height+1) / 2);
|
||||
info->sizeVBase = ((spec->width+1)/2) * ((spec->height+1) / 2);
|
||||
|
||||
// PHYCONT memory size *must* be a multiple of 1MB, we can just always spend 2MB, since we don't use PHYCONT anywhere else
|
||||
imbUid = sceKernelAllocMemBlock("CameraI", SCE_KERNEL_MEMBLOCK_TYPE_USER_MAIN_PHYCONT_NC_RW, 2*1024*1024 , NULL);
|
||||
if (imbUid < 0)
|
||||
{
|
||||
return SDL_SetError("sceKernelAllocMemBlock error: 0x%08X", imbUid);
|
||||
}
|
||||
sceKernelGetMemBlockBase(imbUid, &(info->pIBase));
|
||||
|
||||
info->pUBase = info->pIBase + info->sizeIBase;
|
||||
info->pVBase = info->pIBase + (info->sizeIBase + info->sizeUBase);
|
||||
|
||||
device->hidden = (struct SDL_PrivateCameraData *)info;
|
||||
|
||||
int ret = sceCameraOpen((int)device->handle, info);
|
||||
if (ret == 0) {
|
||||
ret = sceCameraStart((int)device->handle);
|
||||
if (ret == 0) {
|
||||
SDL_CameraPermissionOutcome(device, true);
|
||||
return true;
|
||||
} else {
|
||||
SDL_SetError("sceCameraStart error: 0x%08X", imbUid);
|
||||
}
|
||||
} else {
|
||||
SDL_SetError("sceCameraOpen error: 0x%08X", imbUid);
|
||||
}
|
||||
|
||||
freeBuffers(info);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void VITACAMERA_CloseDevice(SDL_Camera *device)
|
||||
{
|
||||
if (device->hidden) {
|
||||
sceCameraStop((int)device->handle);
|
||||
sceCameraClose((int)device->handle);
|
||||
freeBuffers((SceCameraInfo*)device->hidden);
|
||||
SDL_free(device->hidden);
|
||||
}
|
||||
}
|
||||
|
||||
static bool VITACAMERA_WaitDevice(SDL_Camera *device)
|
||||
{
|
||||
while(!sceCameraIsActive((int)device->handle)) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
static SDL_CameraFrameResult VITACAMERA_AcquireFrame(SDL_Camera *device, SDL_Surface *frame, Uint64 *timestampNS)
|
||||
{
|
||||
SceCameraRead read = {0};
|
||||
read.size = sizeof(SceCameraRead);
|
||||
read.mode = 1; // don't wait next frame
|
||||
|
||||
int ret = sceCameraRead((int)device->handle, &read);
|
||||
|
||||
if (ret < 0) {
|
||||
SDL_SetError("sceCameraRead error: 0x%08X", ret);
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
|
||||
*timestampNS = read.timestamp;
|
||||
|
||||
SceCameraInfo* info = (SceCameraInfo*)(device->hidden);
|
||||
|
||||
frame->pitch = info->width;
|
||||
frame->pixels = SDL_aligned_alloc(SDL_GetSIMDAlignment(), info->sizeIBase + info->sizeUBase + info->sizeVBase);
|
||||
|
||||
if (frame->pixels) {
|
||||
SDL_memcpy(frame->pixels, info->pIBase, info->sizeIBase + info->sizeUBase + info->sizeVBase);
|
||||
return SDL_CAMERA_FRAME_READY;
|
||||
}
|
||||
|
||||
return SDL_CAMERA_FRAME_ERROR;
|
||||
}
|
||||
|
||||
static void VITACAMERA_ReleaseFrame(SDL_Camera *device, SDL_Surface *frame)
|
||||
{
|
||||
SDL_aligned_free(frame->pixels);
|
||||
}
|
||||
|
||||
static void VITACAMERA_DetectDevices(void)
|
||||
{
|
||||
MaybeAddDevice(SCE_CAMERA_DEVICE_FRONT);
|
||||
MaybeAddDevice(SCE_CAMERA_DEVICE_BACK);
|
||||
}
|
||||
|
||||
static void VITACAMERA_FreeDeviceHandle(SDL_Camera *device)
|
||||
{
|
||||
}
|
||||
|
||||
static void VITACAMERA_Deinitialize(void)
|
||||
{
|
||||
}
|
||||
|
||||
static bool VITACAMERA_Init(SDL_CameraDriverImpl *impl)
|
||||
{
|
||||
impl->DetectDevices = VITACAMERA_DetectDevices;
|
||||
impl->OpenDevice = VITACAMERA_OpenDevice;
|
||||
impl->CloseDevice = VITACAMERA_CloseDevice;
|
||||
impl->WaitDevice = VITACAMERA_WaitDevice;
|
||||
impl->AcquireFrame = VITACAMERA_AcquireFrame;
|
||||
impl->ReleaseFrame = VITACAMERA_ReleaseFrame;
|
||||
impl->FreeDeviceHandle = VITACAMERA_FreeDeviceHandle;
|
||||
impl->Deinitialize = VITACAMERA_Deinitialize;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CameraBootStrap VITACAMERA_bootstrap = {
|
||||
"vita", "SDL PSVita camera driver", VITACAMERA_Init, false
|
||||
};
|
||||
|
||||
#endif // SDL_CAMERA_DRIVER_VITA
|
||||
Reference in New Issue
Block a user