From 59f93e20a714cf4f6ff3dd3ed7daf3a73e7e7656 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Thu, 9 Nov 2023 11:11:07 +0100 Subject: [PATCH] Add SDL Video Capture, with back-end for linux/macos/ios/android --- CMakeLists.txt | 9 + VisualC-WinRT/SDL-UWP.vcxproj | 3 + VisualC-WinRT/SDL-UWP.vcxproj.filters | 9 + VisualC/SDL/SDL.vcxproj | 1 + VisualC/SDL/SDL.vcxproj.filters | 3 + include/SDL3/SDL.h | 1 + include/SDL3/SDL_video_capture.h | 369 +++++++ include/build_config/SDL_build_config.h.cmake | 4 + include/build_config/SDL_build_config_ios.h | 2 + include/build_config/SDL_build_config_macos.h | 2 + src/dynapi/SDL_dynapi.sym | 16 + src/dynapi/SDL_dynapi_overrides.h | 16 + src/dynapi/SDL_dynapi_procs.h | 16 + src/video/SDL_sysvideocapture.h | 90 ++ src/video/SDL_video.c | 10 + src/video/SDL_video_capture.c | 948 +++++++++++++++++ src/video/SDL_video_capture_apple.m | 615 +++++++++++ src/video/SDL_video_capture_c.h | 33 + src/video/SDL_video_capture_v4l2.c | 965 ++++++++++++++++++ src/video/android/SDL_android_video_capture.c | 678 ++++++++++++ test/CMakeLists.txt | 2 + test/testvideocapture.c | 770 ++++++++++++++ test/testvideocaptureminimal.c | 206 ++++ 23 files changed, 4768 insertions(+) create mode 100644 include/SDL3/SDL_video_capture.h create mode 100644 src/video/SDL_sysvideocapture.h create mode 100644 src/video/SDL_video_capture.c create mode 100644 src/video/SDL_video_capture_apple.m create mode 100644 src/video/SDL_video_capture_c.h create mode 100644 src/video/SDL_video_capture_v4l2.c create mode 100644 src/video/android/SDL_android_video_capture.c create mode 100644 test/testvideocapture.c create mode 100644 test/testvideocaptureminimal.c diff --git a/CMakeLists.txt b/CMakeLists.txt index ca03351ed..5564c7560 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -343,6 +343,7 @@ set_option(SDL_METAL "Enable Metal support" ${APPLE}) set_option(SDL_KMSDRM "Use KMS DRM video driver" ${UNIX_SYS}) dep_option(SDL_KMSDRM_SHARED "Dynamically load KMS DRM support" ON "SDL_KMSDRM" OFF) set_option(SDL_OFFSCREEN "Use offscreen video driver" ON) +dep_option(SDL_VIDEO_CAPTURE "Enable video capturing" ON SDL_VIDEO OFF) option_string(SDL_BACKGROUNDING_SIGNAL "number to use for magic backgrounding signal or 'OFF'" OFF) option_string(SDL_FOREGROUNDING_SIGNAL "number to use for magic foregrounding signal or 'OFF'" OFF) dep_option(SDL_HIDAPI "Enable the HIDAPI subsystem" ON "NOT VISIONOS" OFF) @@ -2047,6 +2048,10 @@ elseif(APPLE) set(HAVE_SDL_FILE TRUE) endif() + if(IOS OR TVOS OR MACOSX OR DARWIN) + sdl_sources("${SDL3_SOURCE_DIR}/src/video/SDL_video_capture_apple.m") + endif() + if(SDL_MISC) if(IOS OR TVOS OR VISIONOS) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/misc/ios/*.m") @@ -2230,6 +2235,10 @@ elseif(APPLE) # Actually load the frameworks at the end so we don't duplicate include. if(SDL_FRAMEWORK_COREVIDEO) + find_library(COREMEDIA CoreMedia) + if(COREMEDIA) + sdl_link_dependency(corevideo LINK_OPTIONS "-Wl,-framework,CoreMedia") + endif() sdl_link_dependency(corevideo LINK_OPTIONS "-Wl,-framework,CoreVideo") endif() if(SDL_FRAMEWORK_COCOA) diff --git a/VisualC-WinRT/SDL-UWP.vcxproj b/VisualC-WinRT/SDL-UWP.vcxproj index 1085c0f45..f7eca1ef7 100644 --- a/VisualC-WinRT/SDL-UWP.vcxproj +++ b/VisualC-WinRT/SDL-UWP.vcxproj @@ -89,6 +89,7 @@ + @@ -180,6 +181,7 @@ + @@ -520,6 +522,7 @@ + diff --git a/VisualC-WinRT/SDL-UWP.vcxproj.filters b/VisualC-WinRT/SDL-UWP.vcxproj.filters index ab0ca0cfb..fc905ac9e 100644 --- a/VisualC-WinRT/SDL-UWP.vcxproj.filters +++ b/VisualC-WinRT/SDL-UWP.vcxproj.filters @@ -165,6 +165,9 @@ Header Files + + Header Files + Header Files @@ -405,6 +408,9 @@ Source Files + + Source Files + Source Files @@ -807,6 +813,9 @@ Source Files + + Source Files + Source Files diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index 35edb9b8a..fc53003e7 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -653,6 +653,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index b367e7695..c9d6f36e5 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -1185,6 +1185,9 @@ video + + video + video diff --git a/include/SDL3/SDL.h b/include/SDL3/SDL.h index 498f0520c..6db0e69d4 100644 --- a/include/SDL3/SDL.h +++ b/include/SDL3/SDL.h @@ -76,6 +76,7 @@ #include #include #include +#include "SDL3/SDL_video_capture.h" #include #endif /* SDL_h_ */ diff --git a/include/SDL3/SDL_video_capture.h b/include/SDL3/SDL_video_capture.h new file mode 100644 index 000000000..3d3ce7e64 --- /dev/null +++ b/include/SDL3/SDL_video_capture.h @@ -0,0 +1,369 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/** + * \file SDL_video_capture.h + * + * Video Capture for the SDL library. + */ + +#ifndef SDL_video_capture_h_ +#define SDL_video_capture_h_ + +#include "SDL3/SDL_video.h" + +#include +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +/** + * This is a unique ID for a video capture device for the time it is connected to the system, + * and is never reused for the lifetime of the application. If the device is + * disconnected and reconnected, it will get a new ID. + * + * The ID value starts at 1 and increments from there. The value 0 is an invalid ID. + * + * \sa SDL_GetVideoCaptureDevices + */ +typedef Uint32 SDL_VideoCaptureDeviceID; + + +/** + * The structure used to identify an SDL video capture device + */ +struct SDL_VideoCaptureDevice; +typedef struct SDL_VideoCaptureDevice SDL_VideoCaptureDevice; + +#define SDL_VIDEO_CAPTURE_ALLOW_ANY_CHANGE 1 + +/** + * SDL_VideoCaptureSpec structure + * + * Only those field can be 'desired' when configuring the device: + * - format + * - width + * - height + * + * \sa SDL_GetVideoCaptureFormat + * \sa SDL_GetVideoCaptureFrameSize + * + */ +typedef struct SDL_VideoCaptureSpec +{ + Uint32 format; /**< Frame SDL_PixelFormatEnum format */ + int width; /**< Frame width */ + int height; /**< Frame height */ +} SDL_VideoCaptureSpec; + +/** + * SDL Video Capture Status + * + * Change states but calling the function in this order: + * + * SDL_OpenVideoCapture() + * SDL_SetVideoCaptureSpec() -> Init + * SDL_StartVideoCapture() -> Playing + * SDL_StopVideoCapture() -> Stopped + * SDL_CloseVideoCapture() + * + */ +typedef enum +{ + SDL_VIDEO_CAPTURE_FAIL = -1, /**< Failed */ + SDL_VIDEO_CAPTURE_INIT = 0, /**< Init, spec hasn't been set */ + SDL_VIDEO_CAPTURE_STOPPED, /**< Stopped */ + SDL_VIDEO_CAPTURE_PLAYING /**< Playing */ +} SDL_VideoCaptureStatus; + +/** + * SDL Video Capture Status + */ +typedef struct SDL_VideoCaptureFrame +{ + Uint64 timestampNS; /**< Frame timestamp in nanoseconds when read from the driver */ + int num_planes; /**< Number of planes */ + Uint8 *data[3]; /**< Pointer to data of i-th plane */ + int pitch[3]; /**< Pitch of i-th plane */ + void *internal; /**< Private field */ +} SDL_VideoCaptureFrame; + + +/** + * Get a list of currently connected video capture devices. + * + * \param count a pointer filled in with the number of video capture devices + * \returns a 0 terminated array of video capture instance IDs which should be + * freed with SDL_free(), or NULL on error; call SDL_GetError() for + * more details. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_OpenVideoCapture + */ +extern DECLSPEC SDL_VideoCaptureDeviceID *SDLCALL SDL_GetVideoCaptureDevices(int *count); + +/** + * Open a Video Capture device + * + * \param instance_id the video capture device instance ID + * \returns device, or NULL on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetVideoCaptureDeviceName + * \sa SDL_GetVideoCaptureDevices + * \sa SDL_OpenVideoCaptureWithSpec + */ +extern DECLSPEC SDL_VideoCaptureDevice *SDLCALL SDL_OpenVideoCapture(SDL_VideoCaptureDeviceID instance_id); + +/** + * Set specification + * + * \param device opened video capture device + * \param desired desired video capture spec + * \param obtained obtained video capture spec + * \param allowed_changes allow changes or not + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_OpenVideoCapture + * \sa SDL_OpenVideoCaptureWithSpec + * \sa SDL_GetVideoCaptureSpec + */ +extern DECLSPEC int SDLCALL SDL_SetVideoCaptureSpec(SDL_VideoCaptureDevice *device, + const SDL_VideoCaptureSpec *desired, + SDL_VideoCaptureSpec *obtained, + int allowed_changes); + +/** + * Open a Video Capture device and set specification + * + * \param instance_id the video capture device instance ID + * \param desired desired video capture spec + * \param obtained obtained video capture spec + * \param allowed_changes allow changes or not + * \returns device, or NULL on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_OpenVideoCapture + * \sa SDL_SetVideoCaptureSpec + * \sa SDL_GetVideoCaptureSpec + */ +extern DECLSPEC SDL_VideoCaptureDevice *SDLCALL SDL_OpenVideoCaptureWithSpec(SDL_VideoCaptureDeviceID instance_id, + const SDL_VideoCaptureSpec *desired, + SDL_VideoCaptureSpec *obtained, + int allowed_changes); +/** + * Get device name + * + * \param instance_id the video capture device instance ID + * \returns device name, shouldn't be freed + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetVideoCaptureDevices + */ +extern DECLSPEC const char * SDLCALL SDL_GetVideoCaptureDeviceName(SDL_VideoCaptureDeviceID instance_id); + +/** + * Get the obtained video capture spec + * + * \param device opened video capture device + * \param spec The SDL_VideoCaptureSpec to be initialized by this function. + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_SetVideoCaptureSpec + * \sa SDL_OpenVideoCaptureWithSpec + */ +extern DECLSPEC int SDLCALL SDL_GetVideoCaptureSpec(SDL_VideoCaptureDevice *device, SDL_VideoCaptureSpec *spec); + + +/** + * Get frame format of video capture device. + * The value can be used to fill SDL_VideoCaptureSpec structure. + * + * \param device opened video capture device + * \param index format between 0 and num -1 + * \param format pointer output format (SDL_PixelFormatEnum) + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetNumVideoCaptureFormats + */ +extern DECLSPEC int SDLCALL SDL_GetVideoCaptureFormat(SDL_VideoCaptureDevice *device, + int index, + Uint32 *format); + +/** + * Number of available formats for the device + * + * \param device opened video capture device + * \returns number of formats or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetVideoCaptureFormat + * \sa SDL_SetVideoCaptureSpec + */ +extern DECLSPEC int SDLCALL SDL_GetNumVideoCaptureFormats(SDL_VideoCaptureDevice *device); + +/** + * Get frame sizes of the device and the specified input format. + * The value can be used to fill SDL_VideoCaptureSpec structure. + * + * \param device opened video capture device + * \param format a format that can be used by the device (SDL_PixelFormatEnum) + * \param index framesize between 0 and num -1 + * \param width output width + * \param height output height + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetNumVideoCaptureFrameSizes + */ +extern DECLSPEC int SDLCALL SDL_GetVideoCaptureFrameSize(SDL_VideoCaptureDevice *device, Uint32 format, int index, int *width, int *height); + +/** + * Number of different framesizes available for the device and pixel format. + * + * \param device opened video capture device + * \param format frame pixel format (SDL_PixelFormatEnum) + * \returns number of framesizes or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_GetVideoCaptureFrameSize + * \sa SDL_SetVideoCaptureSpec + */ +extern DECLSPEC int SDLCALL SDL_GetNumVideoCaptureFrameSizes(SDL_VideoCaptureDevice *device, Uint32 format); + + +/** + * Get video capture status + * + * \param device opened video capture device + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_VideoCaptureStatus + */ +extern DECLSPEC SDL_VideoCaptureStatus SDLCALL SDL_GetVideoCaptureStatus(SDL_VideoCaptureDevice *device); + +/** + * Start video capture + * + * \param device opened video capture device + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_StopVideoCapture + */ +extern DECLSPEC int SDLCALL SDL_StartVideoCapture(SDL_VideoCaptureDevice *device); + +/** + * Acquire a frame. + * The frame is a memory pointer to the image data, whose size and format + * are given by the the obtained spec. + * + * Non blocking API. If there is a frame available, frame->num_planes is non 0. + * If frame->num_planes is 0 and returned code is 0, there is no frame at that time. + * + * After used, the frame should be released with SDL_ReleaseVideoCaptureFrame + * + * \param device opened video capture device + * \param frame pointer to get the frame + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_ReleaseVideoCaptureFrame + */ +extern DECLSPEC int SDLCALL SDL_AcquireVideoCaptureFrame(SDL_VideoCaptureDevice *device, SDL_VideoCaptureFrame *frame); + +/** + * Release a frame. Let the back-end re-use the internal buffer for video capture. + * + * All acquired frames should be released before closing the device. + * + * \param device opened video capture device + * \param frame frame pointer. + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_AcquireVideoCaptureFrame + */ +extern DECLSPEC int SDLCALL SDL_ReleaseVideoCaptureFrame(SDL_VideoCaptureDevice *device, SDL_VideoCaptureFrame *frame); + +/** + * Stop Video Capture + * + * \param device opened video capture device + * \returns 0 on success or a negative error code on failure; call + * SDL_GetError() for more information. + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_StartVideoCapture + */ +extern DECLSPEC int SDLCALL SDL_StopVideoCapture(SDL_VideoCaptureDevice *device); + +/** + * Use this function to shut down video_capture processing and close the video_capture device. + * + * \param device opened video capture device + * + * \since This function is available since SDL 3.0.0. + * + * \sa SDL_OpenVideoCaptureWithSpec + * \sa SDL_OpenVideoCapture + */ +extern DECLSPEC void SDLCALL SDL_CloseVideoCapture(SDL_VideoCaptureDevice *device); + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif +#include + +#endif /* SDL_video_capture_h_ */ diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake index b776251f8..8b26e0dea 100644 --- a/include/build_config/SDL_build_config.h.cmake +++ b/include/build_config/SDL_build_config.h.cmake @@ -244,11 +244,15 @@ #cmakedefine USE_POSIX_SPAWN @USE_POSIX_SPAWN@ +#cmakedefine HAVE_COREMEDIA + /* SDL internal assertion support */ #if @SDL_DEFAULT_ASSERT_LEVEL_CONFIGURED@ #cmakedefine SDL_DEFAULT_ASSERT_LEVEL @SDL_DEFAULT_ASSERT_LEVEL@ #endif +#cmakedefine SDL_VIDEO_CAPTURE + /* Allow disabling of core subsystems */ #cmakedefine SDL_ATOMIC_DISABLED @SDL_ATOMIC_DISABLED@ #cmakedefine SDL_AUDIO_DISABLED @SDL_AUDIO_DISABLED@ diff --git a/include/build_config/SDL_build_config_ios.h b/include/build_config/SDL_build_config_ios.h index 96391f848..340bbc0c2 100644 --- a/include/build_config/SDL_build_config_ios.h +++ b/include/build_config/SDL_build_config_ios.h @@ -197,6 +197,8 @@ #define SDL_VIDEO_METAL 1 #endif +#define HAVE_COREMEDIA 1 + /* Enable system power support */ #define SDL_POWER_UIKIT 1 diff --git a/include/build_config/SDL_build_config_macos.h b/include/build_config/SDL_build_config_macos.h index 43400d3f6..bf1b682ed 100644 --- a/include/build_config/SDL_build_config_macos.h +++ b/include/build_config/SDL_build_config_macos.h @@ -260,6 +260,8 @@ #endif #endif +#define HAVE_COREMEDIA 1 + /* Enable system power support */ #define SDL_POWER_MACOSX 1 diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index e6fd996c6..96e662f2c 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -923,6 +923,22 @@ SDL3_0.0.0 { SDL_SetPropertyWithCleanup; SDL_SetX11EventHook; SDL_GetGlobalProperties; + SDL_OpenVideoCapture; + SDL_SetVideoCaptureSpec; + SDL_OpenVideoCaptureWithSpec; + SDL_GetVideoCaptureDeviceName; + SDL_GetVideoCaptureSpec; + SDL_GetVideoCaptureFormat; + SDL_GetNumVideoCaptureFormats; + SDL_GetVideoCaptureFrameSize; + SDL_GetNumVideoCaptureFrameSizes; + SDL_GetVideoCaptureStatus; + SDL_StartVideoCapture; + SDL_AcquireVideoCaptureFrame; + SDL_ReleaseVideoCaptureFrame; + SDL_StopVideoCapture; + SDL_CloseVideoCapture; + SDL_GetVideoCaptureDevices; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 97d05e88b..adb202523 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -948,3 +948,19 @@ #define SDL_SetPropertyWithCleanup SDL_SetPropertyWithCleanup_REAL #define SDL_SetX11EventHook SDL_SetX11EventHook_REAL #define SDL_GetGlobalProperties SDL_GetGlobalProperties_REAL +#define SDL_OpenVideoCapture SDL_OpenVideoCapture_REAL +#define SDL_SetVideoCaptureSpec SDL_SetVideoCaptureSpec_REAL +#define SDL_OpenVideoCaptureWithSpec SDL_OpenVideoCaptureWithSpec_REAL +#define SDL_GetVideoCaptureDeviceName SDL_GetVideoCaptureDeviceName_REAL +#define SDL_GetVideoCaptureSpec SDL_GetVideoCaptureSpec_REAL +#define SDL_GetVideoCaptureFormat SDL_GetVideoCaptureFormat_REAL +#define SDL_GetNumVideoCaptureFormats SDL_GetNumVideoCaptureFormats_REAL +#define SDL_GetVideoCaptureFrameSize SDL_GetVideoCaptureFrameSize_REAL +#define SDL_GetNumVideoCaptureFrameSizes SDL_GetNumVideoCaptureFrameSizes_REAL +#define SDL_GetVideoCaptureStatus SDL_GetVideoCaptureStatus_REAL +#define SDL_StartVideoCapture SDL_StartVideoCapture_REAL +#define SDL_AcquireVideoCaptureFrame SDL_AcquireVideoCaptureFrame_REAL +#define SDL_ReleaseVideoCaptureFrame SDL_ReleaseVideoCaptureFrame_REAL +#define SDL_StopVideoCapture SDL_StopVideoCapture_REAL +#define SDL_CloseVideoCapture SDL_CloseVideoCapture_REAL +#define SDL_GetVideoCaptureDevices SDL_GetVideoCaptureDevices_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 640225222..7f3b0e521 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -981,3 +981,19 @@ SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetDisplayProperties,(SDL_DisplayID a),(a), SDL_DYNAPI_PROC(int,SDL_SetPropertyWithCleanup,(SDL_PropertiesID a, const char *b, void *c, void (SDLCALL *d)(void *userdata, void *value), void *e),(a,b,c,d,e),return) SDL_DYNAPI_PROC(void,SDL_SetX11EventHook,(SDL_X11EventHook a, void *b),(a,b),) SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetGlobalProperties,(void),(),return) +SDL_DYNAPI_PROC(SDL_VideoCaptureDevice*,SDL_OpenVideoCapture,(SDL_VideoCaptureDeviceID a),(a),return) +SDL_DYNAPI_PROC(int,SDL_SetVideoCaptureSpec,(SDL_VideoCaptureDevice *a, const SDL_VideoCaptureSpec *b, SDL_VideoCaptureSpec *c, int d),(a,b,c,d),return) +SDL_DYNAPI_PROC(SDL_VideoCaptureDevice*,SDL_OpenVideoCaptureWithSpec,(SDL_VideoCaptureDeviceID a, const SDL_VideoCaptureSpec *b, SDL_VideoCaptureSpec *c, int d),(a,b,c,d),return) +SDL_DYNAPI_PROC(const char*,SDL_GetVideoCaptureDeviceName,(SDL_VideoCaptureDeviceID a),(a),return) +SDL_DYNAPI_PROC(int,SDL_GetVideoCaptureSpec,(SDL_VideoCaptureDevice *a, SDL_VideoCaptureSpec *b),(a,b),return) +SDL_DYNAPI_PROC(int,SDL_GetVideoCaptureFormat,(SDL_VideoCaptureDevice *a, int b, Uint32 *c),(a,b,c),return) +SDL_DYNAPI_PROC(int,SDL_GetNumVideoCaptureFormats,(SDL_VideoCaptureDevice *a),(a),return) +SDL_DYNAPI_PROC(int,SDL_GetVideoCaptureFrameSize,(SDL_VideoCaptureDevice *a, Uint32 b, int c, int *d, int *e),(a,b,c,d,e),return) +SDL_DYNAPI_PROC(int,SDL_GetNumVideoCaptureFrameSizes,(SDL_VideoCaptureDevice *a, Uint32 b),(a,b),return) +SDL_DYNAPI_PROC(SDL_VideoCaptureStatus,SDL_GetVideoCaptureStatus,(SDL_VideoCaptureDevice *a),(a),return) +SDL_DYNAPI_PROC(int,SDL_StartVideoCapture,(SDL_VideoCaptureDevice *a),(a),return) +SDL_DYNAPI_PROC(int,SDL_AcquireVideoCaptureFrame,(SDL_VideoCaptureDevice *a, SDL_VideoCaptureFrame *b),(a,b),return) +SDL_DYNAPI_PROC(int,SDL_ReleaseVideoCaptureFrame,(SDL_VideoCaptureDevice *a, SDL_VideoCaptureFrame *b),(a,b),return) +SDL_DYNAPI_PROC(int,SDL_StopVideoCapture,(SDL_VideoCaptureDevice *a),(a),return) +SDL_DYNAPI_PROC(void,SDL_CloseVideoCapture,(SDL_VideoCaptureDevice *a),(a),) +SDL_DYNAPI_PROC(SDL_VideoCaptureDeviceID*,SDL_GetVideoCaptureDevices,(int *a),(a),return) diff --git a/src/video/SDL_sysvideocapture.h b/src/video/SDL_sysvideocapture.h new file mode 100644 index 000000000..fe71664f5 --- /dev/null +++ b/src/video/SDL_sysvideocapture.h @@ -0,0 +1,90 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "../SDL_internal.h" + +#ifndef SDL_sysvideocapture_h_ +#define SDL_sysvideocapture_h_ + +#include "../SDL_list.h" + +/* The SDL video_capture driver */ +typedef struct SDL_VideoCaptureDevice SDL_VideoCaptureDevice; + +/* Define the SDL video_capture driver structure */ +struct SDL_VideoCaptureDevice +{ + /* * * */ + /* Data common to all devices */ + + /* The device's current video_capture specification */ + SDL_VideoCaptureSpec spec; + + /* Device name */ + char *dev_name; + + /* Current state flags */ + SDL_AtomicInt shutdown; + SDL_AtomicInt enabled; + SDL_bool is_spec_set; + + /* A mutex for locking the queue buffers */ + SDL_Mutex *device_lock; + SDL_Mutex *acquiring_lock; + + /* A thread to feed the video_capture device */ + SDL_Thread *thread; + SDL_threadID threadid; + + /* Queued buffers (if app not using callback). */ + SDL_ListNode *buffer_queue; + + /* * * */ + /* Data private to this driver */ + struct SDL_PrivateVideoCaptureData *hidden; +}; + +extern int OpenDevice(SDL_VideoCaptureDevice *_this); +extern void CloseDevice(SDL_VideoCaptureDevice *_this); + +extern int InitDevice(SDL_VideoCaptureDevice *_this); + +extern int GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec); + +extern int StartCapture(SDL_VideoCaptureDevice *_this); +extern int StopCapture(SDL_VideoCaptureDevice *_this); + +extern int AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame); +extern int ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame); + +extern int GetNumFormats(SDL_VideoCaptureDevice *_this); +extern int GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format); + +extern int GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format); +extern int GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height); + +extern int GetDeviceName(int index, char *buf, int size); +extern int GetNumDevices(void); + + +extern SDL_bool check_all_device_closed(void); +extern SDL_bool check_device_playing(void); + +#endif /* SDL_sysvideocapture_h_ */ diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index df0ad2351..d4ab9a475 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -31,6 +31,7 @@ #include "SDL_video_c.h" #include "../events/SDL_events_c.h" #include "../timer/SDL_timer_c.h" +#include "SDL_video_capture_c.h" #ifdef SDL_VIDEO_OPENGL #include @@ -443,6 +444,7 @@ int SDL_VideoInit(const char *driver_name) SDL_bool init_keyboard = SDL_FALSE; SDL_bool init_mouse = SDL_FALSE; SDL_bool init_touch = SDL_FALSE; + SDL_bool init_video_capture = SDL_FALSE; int i = 0; /* Check to make sure we don't overwrite '_this' */ @@ -471,6 +473,10 @@ int SDL_VideoInit(const char *driver_name) goto pre_driver_error; } init_touch = SDL_TRUE; + if (SDL_VideoCaptureInit() < 0) { + goto pre_driver_error; + } + init_video_capture = SDL_TRUE; /* Select the proper video driver */ video = NULL; @@ -565,6 +571,9 @@ int SDL_VideoInit(const char *driver_name) pre_driver_error: SDL_assert(_this == NULL); + if (init_video_capture) { + SDL_QuitVideoCapture(); + } if (init_touch) { SDL_QuitTouch(); } @@ -3684,6 +3693,7 @@ void SDL_VideoQuit(void) SDL_ClearClipboardData(); /* Halt event processing before doing anything else */ + SDL_QuitVideoCapture(); SDL_QuitTouch(); SDL_QuitMouse(); SDL_QuitKeyboard(); diff --git a/src/video/SDL_video_capture.c b/src/video/SDL_video_capture.c new file mode 100644 index 000000000..bd4fc8257 --- /dev/null +++ b/src/video/SDL_video_capture.c @@ -0,0 +1,948 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL3/SDL.h" +#include "SDL3/SDL_video_capture.h" +#include "SDL_sysvideocapture.h" +#include "SDL_video_capture_c.h" +#include "SDL_pixels_c.h" +#include "../thread/SDL_systhread.h" + +#define DEBUG_VIDEO_CAPTURE_CAPTURE 1 + + +#ifdef SDL_VIDEO_CAPTURE +/* list node entries to share frames between SDL and user app */ +typedef struct entry_t +{ + SDL_VideoCaptureFrame frame; +} entry_t; + +static SDL_VideoCaptureDevice *open_devices[16]; + +static void +close_device(SDL_VideoCaptureDevice *device) +{ + if (!device) { + return; + } + + SDL_AtomicSet(&device->shutdown, 1); + SDL_AtomicSet(&device->enabled, 1); + + if (device->thread != NULL) { + SDL_WaitThread(device->thread, NULL); + } + if (device->device_lock != NULL) { + SDL_DestroyMutex(device->device_lock); + } + if (device->acquiring_lock != NULL) { + SDL_DestroyMutex(device->acquiring_lock); + } + + { + int i, n = SDL_arraysize(open_devices); + for (i = 0; i < n; i++) { + if (open_devices[i] == device) { + open_devices[i] = NULL; + } + } + } + + { + entry_t *entry = NULL; + while (device->buffer_queue != NULL) { + SDL_ListPop(&device->buffer_queue, (void**)&entry); + if (entry) { + SDL_VideoCaptureFrame f = entry->frame; + /* Release frames not acquired, if any */ + if (f.timestampNS) { + ReleaseFrame(device, &f); + } + SDL_free(entry); + } + } + } + + CloseDevice(device); + + SDL_free(device->dev_name); + SDL_free(device); +} + +/* Tell if all device are closed */ +SDL_bool check_all_device_closed(void) +{ + int i, n = SDL_arraysize(open_devices); + int all_closed = SDL_TRUE; + for (i = 0; i < n; i++) { + if (open_devices[i]) { + all_closed = SDL_FALSE; + break; + } + } + return all_closed; +} + +/* Tell if at least one device is in playing state */ +SDL_bool check_device_playing(void) +{ + int i, n = SDL_arraysize(open_devices); + for (i = 0; i < n; i++) { + if (open_devices[i]) { + if (SDL_GetVideoCaptureStatus(open_devices[i]) == SDL_VIDEO_CAPTURE_PLAYING) { + return SDL_TRUE; + } + } + } + return SDL_FALSE; +} + + +#endif /* SDL_VIDEO_CAPTURE */ + +void +SDL_CloseVideoCapture(SDL_VideoCaptureDevice *device) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + SDL_InvalidParamError("device"); + return; + } + close_device(device); +#endif +} + +int +SDL_StartVideoCapture(SDL_VideoCaptureDevice *device) +{ +#ifdef SDL_VIDEO_CAPTURE + SDL_VideoCaptureStatus status; + int result; + if (!device) { + return SDL_InvalidParamError("device"); + } + + if (device->is_spec_set == SDL_FALSE) { + return SDL_SetError("no spec set"); + } + + status = SDL_GetVideoCaptureStatus(device); + if (status != SDL_VIDEO_CAPTURE_INIT) { + return SDL_SetError("invalid state"); + } + + result = StartCapture(device); + if (result < 0) { + return result; + } + + SDL_AtomicSet(&device->enabled, 1); + + return 0; +#else + return SDL_Unsupported(); +#endif +} + +int +SDL_GetVideoCaptureSpec(SDL_VideoCaptureDevice *device, SDL_VideoCaptureSpec *spec) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + return SDL_InvalidParamError("device"); + } + + if (!spec) { + return SDL_InvalidParamError("spec"); + } + + SDL_zerop(spec); + + return GetDeviceSpec(device, spec); +#else + return SDL_Unsupported(); +#endif +} + +int +SDL_StopVideoCapture(SDL_VideoCaptureDevice *device) +{ +#ifdef SDL_VIDEO_CAPTURE + SDL_VideoCaptureStatus status; + int ret; + if (!device) { + return SDL_InvalidParamError("device"); + } + + status = SDL_GetVideoCaptureStatus(device); + + if (status != SDL_VIDEO_CAPTURE_PLAYING) { + return SDL_SetError("invalid state"); + } + + SDL_AtomicSet(&device->enabled, 0); + SDL_AtomicSet(&device->shutdown, 1); + + SDL_LockMutex(device->acquiring_lock); + ret = StopCapture(device); + SDL_UnlockMutex(device->acquiring_lock); + + if (ret < 0) { + return -1; + } + + return 0; +#else + return SDL_Unsupported(); +#endif +} + +#ifdef SDL_VIDEO_CAPTURE + +/* Check spec has valid format and frame size */ +static int +prepare_video_capturespec(SDL_VideoCaptureDevice *device, const SDL_VideoCaptureSpec *desired, SDL_VideoCaptureSpec *obtained, int allowed_changes) +{ + /* Check format */ + { + int i, num = SDL_GetNumVideoCaptureFormats(device); + int is_format_valid = 0; + + for (i = 0; i < num; i++) { + Uint32 format; + if (SDL_GetVideoCaptureFormat(device, i, &format) == 0) { + if (format == desired->format && format != SDL_PIXELFORMAT_UNKNOWN) { + is_format_valid = 1; + obtained->format = format; + break; + } + } + } + + if (!is_format_valid) { + if (allowed_changes) { + for (i = 0; i < num; i++) { + Uint32 format; + if (SDL_GetVideoCaptureFormat(device, i, &format) == 0) { + if (format != SDL_PIXELFORMAT_UNKNOWN) { + obtained->format = format; + is_format_valid = 1; + break; + } + } + } + + } else { + SDL_SetError("Not allowed to change the format"); + return -1; + } + } + + if (!is_format_valid) { + SDL_SetError("Invalid format"); + return -1; + } + } + + /* Check frame size */ + { + int i, num = SDL_GetNumVideoCaptureFrameSizes(device, obtained->format); + int is_framesize_valid = 0; + + for (i = 0; i < num; i++) { + int w, h; + if (SDL_GetVideoCaptureFrameSize(device, obtained->format, i, &w, &h) == 0) { + if (desired->width == w && desired->height == h) { + is_framesize_valid = 1; + obtained->width = w; + obtained->height = h; + break; + } + } + } + + if (!is_framesize_valid) { + if (allowed_changes) { + int w, h; + if (SDL_GetVideoCaptureFrameSize(device, obtained->format, 0, &w, &h) == 0) { + is_framesize_valid = 1; + obtained->width = w; + obtained->height = h; + } + } else { + SDL_SetError("Not allowed to change the frame size"); + return -1; + } + } + + if (!is_framesize_valid) { + SDL_SetError("Invalid frame size"); + return -1; + } + + } + + return 0; +} + +#endif /* SDL_VIDEO_CAPTURE */ + +const char * +SDL_GetVideoCaptureDeviceName(SDL_VideoCaptureDeviceID instance_id) +{ +#ifdef SDL_VIDEO_CAPTURE + int index = instance_id - 1; + static char buf[256]; + buf[0] = 0; + buf[255] = 0; + + if (instance_id == 0) { + SDL_InvalidParamError("instance_id"); + return NULL; + } + + if (GetDeviceName(index, buf, sizeof (buf)) < 0) { + buf[0] = 0; + } + return buf; +#else + SDL_Unsupported(); + return NULL; +#endif +} + + +SDL_VideoCaptureDeviceID * +SDL_GetVideoCaptureDevices(int *count) +{ + + int i; +#ifdef SDL_VIDEO_CAPTURE + int num = GetNumDevices(); +#else + int num = 0; +#endif + SDL_VideoCaptureDeviceID *ret; + + ret = (SDL_VideoCaptureDeviceID *)SDL_malloc((num + 1) * sizeof(*ret)); + + if (ret == NULL) { + SDL_OutOfMemory(); + if (count) { + *count = 0; + } + return NULL; + } + + for (i = 0; i < num; i++) { + ret[i] = i + 1; + } + ret[num] = 0; + + if (count) { + *count = num; + } + + return ret; +} + +#ifdef SDL_VIDEO_CAPTURE + +/* Video capture thread function */ +static int SDLCALL +SDL_CaptureVideoThread(void *devicep) +{ + const int delay = 20; + SDL_VideoCaptureDevice *device = (SDL_VideoCaptureDevice *) devicep; + +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("Start thread 'SDL_CaptureVideo'"); +#endif + + +#ifdef SDL_VIDEO_DRIVER_ANDROID + // TODO + /* + { + // Set thread priority to THREAD_PRIORITY_VIDEO + extern void Android_JNI_VideoCaptureSetThreadPriority(int, int); + Android_JNI_VideoCaptureSetThreadPriority(device->iscapture, device); + }*/ +#else + /* The video_capture mixing is always a high priority thread */ + SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); +#endif + + /* Perform any thread setup */ + device->threadid = SDL_ThreadID(); + + /* Init state */ + while (!SDL_AtomicGet(&device->enabled)) { + SDL_Delay(delay); + } + + /* Loop, filling the video_capture buffers */ + while (!SDL_AtomicGet(&device->shutdown)) { + SDL_VideoCaptureFrame f; + int ret; + entry_t *entry; + + SDL_zero(f); + + SDL_LockMutex(device->acquiring_lock); + ret = AcquireFrame(device, &f); + SDL_UnlockMutex(device->acquiring_lock); + + if (ret == 0) { + if (f.num_planes == 0) { + continue; + } + } + + if (ret < 0) { + /* Flag it as an error */ +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("dev[%p] error AcquireFrame: %d %s", (void *)device, ret, SDL_GetError()); +#endif + f.num_planes = 0; + } + + + entry = SDL_malloc(sizeof (entry_t)); + if (entry == NULL) { + goto error_mem; + } + + entry->frame = f; + + SDL_LockMutex(device->device_lock); + ret = SDL_ListAdd(&device->buffer_queue, entry); + SDL_UnlockMutex(device->device_lock); + + if (ret < 0) { + SDL_free(entry); + goto error_mem; + } + } + +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("dev[%p] End thread 'SDL_CaptureVideo'", (void *)device); +#endif + return 0; + +error_mem: +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("dev[%p] End thread 'SDL_CaptureVideo' with error: %s", (void *)device, SDL_GetError()); +#endif + SDL_AtomicSet(&device->shutdown, 1); + SDL_OutOfMemory(); + return 0; +} +#endif + +SDL_VideoCaptureDevice * +SDL_OpenVideoCapture(SDL_VideoCaptureDeviceID instance_id) +{ +#ifdef SDL_VIDEO_CAPTURE + int i, n = SDL_arraysize(open_devices); + int id = -1; + SDL_VideoCaptureDevice *device = NULL; + const char *device_name = NULL; + + if (!SDL_WasInit(SDL_INIT_VIDEO)) { + SDL_SetError("Video subsystem is not initialized"); + goto error; + } + + /* !!! FIXME: there is a race condition here if two devices open from two threads at once. */ + /* Find an available device ID... */ + for (i = 0; i < n; i++) { + if (open_devices[i] == NULL) { + id = i; + break; + } + } + + if (id == -1) { + SDL_SetError("Too many open video capture devices"); + goto error; + } + + if (instance_id != 0) { + device_name = SDL_GetVideoCaptureDeviceName(instance_id); + if (device_name == NULL) { + goto error; + } + } else { + SDL_VideoCaptureDeviceID *devices = SDL_GetVideoCaptureDevices(NULL); + if (devices && devices[0]) { + device_name = SDL_GetVideoCaptureDeviceName(devices[0]); + SDL_free(devices); + } + } + + /* Let the user override. */ + { + const char *dev = SDL_getenv("SDL_VIDEO_CAPTURE_DEVICE_NAME"); + if (dev && dev[0]) { + device_name = dev; + } + } + + if (device_name == NULL) { + goto error; + } + + device = (SDL_VideoCaptureDevice *) SDL_calloc(1, sizeof (SDL_VideoCaptureDevice)); + if (device == NULL) { + SDL_OutOfMemory(); + goto error; + } + device->dev_name = SDL_strdup(device_name); + + + SDL_AtomicSet(&device->shutdown, 0); + SDL_AtomicSet(&device->enabled, 0); + + device->device_lock = SDL_CreateMutex(); + if (device->device_lock == NULL) { + SDL_SetError("Couldn't create acquiring_lock"); + goto error; + } + + device->acquiring_lock = SDL_CreateMutex(); + if (device->acquiring_lock == NULL) { + SDL_SetError("Couldn't create acquiring_lock"); + goto error; + } + + if (OpenDevice(device) < 0) { + goto error; + } + + /* empty */ + device->buffer_queue = NULL; + open_devices[id] = device; /* add it to our list of open devices. */ + + + /* Start the video_capture thread */ + { + const size_t stacksize = 64 * 1024; + char threadname[64]; + + SDL_snprintf(threadname, sizeof (threadname), "SDLVideoC%d", id); + device->thread = SDL_CreateThreadInternal(SDL_CaptureVideoThread, threadname, stacksize, device); + + if (device->thread == NULL) { + SDL_SetError("Couldn't create video_capture thread"); + goto error; + } + } + + return device; + +error: + close_device(device); + return NULL; +#else + SDL_Unsupported(); + return NULL; +#endif /* SDL_VIDEO_CAPTURE */ +} + +int +SDL_SetVideoCaptureSpec(SDL_VideoCaptureDevice *device, + const SDL_VideoCaptureSpec *desired, + SDL_VideoCaptureSpec *obtained, + int allowed_changes) +{ +#ifdef SDL_VIDEO_CAPTURE + SDL_VideoCaptureSpec _obtained; + SDL_VideoCaptureSpec _desired; + int result; + + if (!device) { + return SDL_InvalidParamError("device"); + } + + if (device->is_spec_set == SDL_TRUE) { + return SDL_SetError("already configured"); + } + + if (!desired) { + SDL_zero(_desired); + desired = &_desired; + allowed_changes = SDL_VIDEO_CAPTURE_ALLOW_ANY_CHANGE; + } else { + /* in case desired == obtained */ + _desired = *desired; + desired = &_desired; + } + + if (!obtained) { + obtained = &_obtained; + } + + SDL_zerop(obtained); + + if (prepare_video_capturespec(device, desired, obtained, allowed_changes) < 0) { + return -1; + } + + device->spec = *obtained; + + result = InitDevice(device); + if (result < 0) { + return result; + } + + *obtained = device->spec; + + device->is_spec_set = SDL_TRUE; + + return 0; +#else + SDL_zero(*obtained); + return SDL_Unsupported(); +#endif /* SDL_VIDEO_CAPTURE */ +} + +int +SDL_AcquireVideoCaptureFrame(SDL_VideoCaptureDevice *device, SDL_VideoCaptureFrame *frame) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + return SDL_InvalidParamError("device"); + } + + if (!frame) { + return SDL_InvalidParamError("frame"); + } + + SDL_zerop(frame); + + if (device->thread == NULL) { + int ret; + + /* Wait for a frame */ + while ((ret = AcquireFrame(device, frame)) == 0) { + if (frame->num_planes) { + return 0; + } + } + return -1; + } else { + entry_t *entry = NULL; + + SDL_LockMutex(device->device_lock); + SDL_ListPop(&device->buffer_queue, (void**)&entry); + SDL_UnlockMutex(device->device_lock); + + if (entry) { + *frame = entry->frame; + SDL_free(entry); + + /* Error from thread */ + if (frame->num_planes == 0 && frame->timestampNS == 0) { + return SDL_SetError("error from acquisition thread"); + } + + + } else { + /* Queue is empty. Not an error. */ + } + } + + return 0; +#else + return SDL_Unsupported(); +#endif /* SDL_VIDEO_CAPTURE */ +} + +int +SDL_ReleaseVideoCaptureFrame(SDL_VideoCaptureDevice *device, SDL_VideoCaptureFrame *frame) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + return SDL_InvalidParamError("device"); + } + + if (frame == NULL) { + return SDL_InvalidParamError("frame"); + } + + if (ReleaseFrame(device, frame) < 0) { + return -1; + } + + SDL_zerop(frame); + + return 0; +#else + return SDL_Unsupported(); +#endif /* SDL_VIDEO_CAPTURE */ +} + +int +SDL_GetNumVideoCaptureFormats(SDL_VideoCaptureDevice *device) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + return SDL_InvalidParamError("device"); + } + return GetNumFormats(device); +#else + return 0; +#endif /* SDL_VIDEO_CAPTURE */ +} + +int +SDL_GetVideoCaptureFormat(SDL_VideoCaptureDevice *device, int index, Uint32 *format) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + return SDL_InvalidParamError("device"); + } + if (!format) { + return SDL_InvalidParamError("format"); + } + *format = 0; + return GetFormat(device, index, format); +#else + return SDL_Unsupported(); +#endif /* SDL_VIDEO_CAPTURE */ +} + +int +SDL_GetNumVideoCaptureFrameSizes(SDL_VideoCaptureDevice *device, Uint32 format) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + return SDL_InvalidParamError("device"); + } + return GetNumFrameSizes(device, format); +#else + return 0; +#endif /* SDL_VIDEO_CAPTURE */ +} + +int +SDL_GetVideoCaptureFrameSize(SDL_VideoCaptureDevice *device, Uint32 format, int index, int *width, int *height) +{ +#ifdef SDL_VIDEO_CAPTURE + if (!device) { + return SDL_InvalidParamError("device"); + } + if (!width) { + return SDL_InvalidParamError("width"); + } + if (!height) { + return SDL_InvalidParamError("height"); + } + *width = 0; + *height = 0; + return GetFrameSize(device, format, index, width, height); +#else + return SDL_Unsupported(); +#endif +} + +SDL_VideoCaptureDevice * +SDL_OpenVideoCaptureWithSpec( + SDL_VideoCaptureDeviceID instance_id, + const SDL_VideoCaptureSpec *desired, + SDL_VideoCaptureSpec *obtained, + int allowed_changes) +{ +#ifdef SDL_VIDEO_CAPTURE + SDL_VideoCaptureDevice *device; + + if ((device = SDL_OpenVideoCapture(instance_id)) == NULL) { + return NULL; + } + + if (SDL_SetVideoCaptureSpec(device, desired, obtained, allowed_changes) < 0) { + SDL_CloseVideoCapture(device); + return NULL; + } + return device; +#else + SDL_Unsupported(); + return NULL; +#endif +} + +SDL_VideoCaptureStatus +SDL_GetVideoCaptureStatus(SDL_VideoCaptureDevice *device) +{ +#ifdef SDL_VIDEO_CAPTURE + if (device == NULL) { + return SDL_VIDEO_CAPTURE_INIT; + } + + if (device->is_spec_set == SDL_FALSE) { + return SDL_VIDEO_CAPTURE_INIT; + } + + if (SDL_AtomicGet(&device->shutdown)) { + return SDL_VIDEO_CAPTURE_STOPPED; + } + + if (SDL_AtomicGet(&device->enabled)) { + return SDL_VIDEO_CAPTURE_PLAYING; + } + return SDL_VIDEO_CAPTURE_INIT; +#else + SDL_Unsupported(); + return SDL_VIDEO_CAPTURE_FAIL; +#endif +} + +int +SDL_VideoCaptureInit(void) +{ +#ifdef SDL_VIDEO_CAPTURE + SDL_zeroa(open_devices); + return 0; +#else + return 0; +#endif +} + +void +SDL_QuitVideoCapture(void) +{ +#ifdef SDL_VIDEO_CAPTURE + int i, n = SDL_arraysize(open_devices); + for (i = 0; i < n; i++) { + close_device(open_devices[i]); + } + + SDL_zeroa(open_devices); +#endif +} + +#ifdef SDL_VIDEO_CAPTURE + +#if defined(__linux__) && !defined(__ANDROID__) + +/* See SDL_video_capture_v4l2.c */ + +#elif defined(__ANDROID__) && __ANDROID_API__ >= 24 + +/* See SDL_android_video_capture.c */ + +#elif defined(__IPHONEOS__) || defined(__MACOS__) + +/* See SDL_video_capture_apple.m */ +#else + +int +OpenDevice(SDL_VideoCaptureDevice *_this) +{ + return SDL_SetError("not implemented"); +} + +void +CloseDevice(SDL_VideoCaptureDevice *_this) +{ + return; +} + +int +InitDevice(SDL_VideoCaptureDevice *_this) +{ + size_t size, pitch; + SDL_CalculateSize(_this->spec.format, _this->spec.width, _this->spec.height, &size, &pitch, SDL_FALSE); + SDL_Log("Buffer size: %d x %d", _this->spec.width, _this->spec.height); + return -1; +} + +int +GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec) +{ + return SDL_Unsupported(); +} + +int +StartCapture(SDL_VideoCaptureDevice *_this) +{ + return SDL_Unsupported(); +} + +int +StopCapture(SDL_VideoCaptureDevice *_this) +{ + return -1; +} + +int +AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + return -1; +} + +int +ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + return -1; +} + +int +GetNumFormats(SDL_VideoCaptureDevice *_this) +{ + return -1; +} + +int +GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format) +{ + return -1; +} + +int +GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format) +{ + return -1; +} + +int +GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height) +{ + return -1; +} + +int +GetDeviceName(int index, char *buf, int size) +{ + return -1; +} + +int +GetNumDevices(void) +{ + return -1; +} +#endif + +#endif /* SDL_VIDEO_CAPTURE */ diff --git a/src/video/SDL_video_capture_apple.m b/src/video/SDL_video_capture_apple.m new file mode 100644 index 000000000..3e58cf4e7 --- /dev/null +++ b/src/video/SDL_video_capture_apple.m @@ -0,0 +1,615 @@ +/* + Simple DirectMedia Layer + Copyright (C) 2021 Valve Corporation + + 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_VIDEO_CAPTURE + +#include "SDL3/SDL.h" +#include "SDL3/SDL_video_capture.h" +#include "SDL_sysvideocapture.h" +#include "SDL_video_capture_c.h" +#include "../thread/SDL_systhread.h" + +#if defined(HAVE_COREMEDIA) && defined(__MACOS__) && (__MAC_OS_X_VERSION_MAX_ALLOWED < 101500) +/* AVCaptureDeviceTypeBuiltInWideAngleCamera requires macOS SDK 10.15 */ +#undef HAVE_COREMEDIA +#endif + +#if TARGET_OS_TV +#undef HAVE_COREMEDIA +#endif + +#ifndef HAVE_COREMEDIA +int InitDevice(SDL_VideoCaptureDevice *_this) { + return -1; +} +int OpenDevice(SDL_VideoCaptureDevice *_this) { + return -1; +} +int AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) { + return -1; +} +void CloseDevice(SDL_VideoCaptureDevice *_this) { +} +int GetDeviceName(int index, char *buf, int size) { + return -1; +} +int GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec) { + return -1; +} +int GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format) { + return -1; +} +int GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height) { + return -1; +} +int GetNumDevices(void) { + return 0; +} +int GetNumFormats(SDL_VideoCaptureDevice *_this) { + return 0; +} +int GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format) { + return 0; +} +int ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) { + return 0; +} +int StartCapture(SDL_VideoCaptureDevice *_this) { + return 0; +} +int StopCapture(SDL_VideoCaptureDevice *_this) { + return 0; +} + +#else + +#import +#import + +/* + * Need to link with:: CoreMedia CoreVideo + * + * Add in pInfo.list: + * NSCameraUsageDescription Access camera + * + * + * MACOSX: + * Add to the Code Sign Entitlement file: + * com.apple.security.device.camera + * + * + * IOS: + * + * - Need to link with:: CoreMedia CoreVideo + * - Add #define SDL_VIDEO_CAPTURE 1 + * to SDL_build_config_ios.h + */ + +@class MySampleBufferDelegate; + +struct SDL_PrivateVideoCaptureData +{ + dispatch_queue_t queue; + MySampleBufferDelegate *delegate; + AVCaptureSession *session; + CMSimpleQueueRef frame_queue; +}; + +static NSString * +fourcc_to_nstring(Uint32 code) +{ + Uint8 buf[4]; + *(Uint32 *)buf = code; + return [NSString stringWithFormat:@"%c%c%c%c", buf[3], buf[2], buf[1], buf[0]]; +} + +static NSArray * +discover_devices() +{ + NSArray *deviceType = @[AVCaptureDeviceTypeBuiltInWideAngleCamera]; + + AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession + discoverySessionWithDeviceTypes:deviceType + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; + + NSArray *devices = discoverySession.devices; + + if ([devices count] > 0) { + return devices; + } else { + AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + if (captureDevice == nil) { + return devices; + } else { + NSArray *default_device = @[ captureDevice ]; + return default_device; + } + } + + return devices; +} + +static AVCaptureDevice * +get_device_by_name(const char *dev_name) +{ + NSArray *devices = discover_devices(); + + for (AVCaptureDevice *device in devices) { + char buf[1024]; + NSString *cameraID = [device localizedName]; + const char *str = [cameraID UTF8String]; + SDL_snprintf(buf, sizeof (buf) - 1, "%s", str); + if (SDL_strcmp(buf, dev_name) == 0) { + return device; + } + } + return nil; +} + +static Uint32 +nsfourcc_to_sdlformat(NSString *nsfourcc) +{ + const char *str = [nsfourcc UTF8String]; + + /* FIXME + * on IOS this mode gives 2 planes, and it's NV12 + * on macos, 1 plane/ YVYU + * + */ +#ifdef __MACOS__ + if (SDL_strcmp("420v", str) == 0) return SDL_PIXELFORMAT_YVYU; +#else + if (SDL_strcmp("420v", str) == 0) return SDL_PIXELFORMAT_NV12; +#endif + if (SDL_strcmp("yuvs", str) == 0) return SDL_PIXELFORMAT_UYVY; + if (SDL_strcmp("420f", str) == 0) return SDL_PIXELFORMAT_UNKNOWN; + + SDL_Log("Unknown format '%s'", str); + + return SDL_PIXELFORMAT_UNKNOWN; +} + +static NSString * +sdlformat_to_nsfourcc(Uint32 fmt) +{ + const char *str = ""; + NSString *result; + +#ifdef __MACOS__ + if (fmt == SDL_PIXELFORMAT_YVYU) str = "420v"; +#else + if (fmt == SDL_PIXELFORMAT_NV12) str = "420v"; +#endif + if (fmt == SDL_PIXELFORMAT_UYVY) str = "yuvs"; + + result = [[NSString alloc] initWithUTF8String: str]; + + return result; +} + + +@interface MySampleBufferDelegate : NSObject + @property struct SDL_PrivateVideoCaptureData *hidden; + - (void) set: (struct SDL_PrivateVideoCaptureData *) val; +@end + +@implementation MySampleBufferDelegate + + - (void) set: (struct SDL_PrivateVideoCaptureData *) val { + _hidden = val; + } + + - (void) captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *) connection { + CFRetain(sampleBuffer); + CMSimpleQueueEnqueue(_hidden->frame_queue, sampleBuffer); + } + + - (void)captureOutput:(AVCaptureOutput *)output + didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + SDL_Log("Drop frame.."); + } +@end + +int +OpenDevice(SDL_VideoCaptureDevice *_this) +{ + _this->hidden = (struct SDL_PrivateVideoCaptureData *) SDL_calloc(1, sizeof (struct SDL_PrivateVideoCaptureData)); + if (_this->hidden == NULL) { + SDL_OutOfMemory(); + goto error; + } + + return 0; + +error: + return -1; +} + +void +CloseDevice(SDL_VideoCaptureDevice *_this) +{ + if (!_this) { + return; + } + + if (_this->hidden) { + AVCaptureSession *session = _this->hidden->session; + + if (session) { + AVCaptureInput *input; + AVCaptureVideoDataOutput *output; + input = [session.inputs objectAtIndex:0]; + [session removeInput:input]; + output = (AVCaptureVideoDataOutput*)[session.outputs objectAtIndex:0]; + [session removeOutput:output]; + // TODO more cleanup ? + } + + if (_this->hidden->frame_queue) { + CFRelease(_this->hidden->frame_queue); + } + + SDL_free(_this->hidden); + _this->hidden = NULL; + } +} + +int +InitDevice(SDL_VideoCaptureDevice *_this) +{ + NSString *fmt = sdlformat_to_nsfourcc(_this->spec.format); + int w = _this->spec.width; + int h = _this->spec.height; + + NSError *error = nil; + AVCaptureDevice *device = nil; + AVCaptureDeviceInput *input = nil; + AVCaptureVideoDataOutput *output = nil; + + AVCaptureDeviceFormat *spec_format = nil; + +#ifdef __MACOS__ + if (@available(macOS 10.15, *)) { + /* good. */ + } else { + return -1; + } +#endif + + device = get_device_by_name(_this->dev_name); + if (!device) { + goto error; + } + + _this->hidden->session = [[AVCaptureSession alloc] init]; + if (_this->hidden->session == nil) { + goto error; + } + + [_this->hidden->session setSessionPreset:AVCaptureSessionPresetHigh]; + + // Pick format that matches the spec + { + NSArray *formats = [device formats]; + for (AVCaptureDeviceFormat *format in formats) { + CMFormatDescriptionRef formatDescription = [format formatDescription]; + FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription); + NSString *str = fourcc_to_nstring(mediaSubType); + if (str == fmt) { + CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(formatDescription); + if (dim.width == w && dim.height == h) { + spec_format = format; + break; + } + } + } + } + + if (spec_format == nil) { + SDL_SetError("format not found"); + goto error; + } + + // Set format + if ([device lockForConfiguration:NULL] == YES) { + device.activeFormat = spec_format; + [device unlockForConfiguration]; + } else { + SDL_SetError("Cannot lockForConfiguration"); + goto error; + } + + // Input + input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; + if (!input) { + SDL_SetError("Cannot create AVCaptureDeviceInput"); + goto error; + } + + // Output + output = [[AVCaptureVideoDataOutput alloc] init]; + +#ifdef __MACOS__ + // FIXME this now fail on ios ... but not using anything works... + + // Specify the pixel format + output.videoSettings = + [NSDictionary dictionaryWithObject: + [NSNumber numberWithInt:kCVPixelFormatType_422YpCbCr8] + forKey:(id)kCVPixelBufferPixelFormatTypeKey]; +#endif + + _this->hidden->delegate = [[MySampleBufferDelegate alloc] init]; + [_this->hidden->delegate set:_this->hidden]; + + + CMSimpleQueueCreate(kCFAllocatorDefault, 30 /* buffers */, &_this->hidden->frame_queue); + if (_this->hidden->frame_queue == nil) { + goto error; + } + + _this->hidden->queue = dispatch_queue_create("my_queue", NULL); + [output setSampleBufferDelegate:_this->hidden->delegate queue:_this->hidden->queue]; + + + if ([_this->hidden->session canAddInput:input] ){ + [_this->hidden->session addInput:input]; + } else { + SDL_SetError("Cannot add AVCaptureDeviceInput"); + goto error; + } + + if ([_this->hidden->session canAddOutput:output] ){ + [_this->hidden->session addOutput:output]; + } else { + SDL_SetError("Cannot add AVCaptureVideoDataOutput"); + goto error; + } + + [_this->hidden->session commitConfiguration]; + + return 0; + +error: + return -1; +} + +int +GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec) +{ + if (spec) { + *spec = _this->spec; + return 0; + } + return -1; +} + +int +StartCapture(SDL_VideoCaptureDevice *_this) +{ + [_this->hidden->session startRunning]; + return 0; +} + +int +StopCapture(SDL_VideoCaptureDevice *_this) +{ + [_this->hidden->session stopRunning]; + return 0; +} + +int +AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + if (CMSimpleQueueGetCount(_this->hidden->frame_queue) > 0) { + int i, numPlanes, planar; + CMSampleBufferRef sampleBuffer; + CVImageBufferRef image; + + sampleBuffer = (CMSampleBufferRef)CMSimpleQueueDequeue(_this->hidden->frame_queue); + frame->internal = (void *) sampleBuffer; + frame->timestampNS = SDL_GetTicksNS(); + + i = 0; + image = CMSampleBufferGetImageBuffer(sampleBuffer); + numPlanes = CVPixelBufferGetPlaneCount(image); + planar = CVPixelBufferIsPlanar(image); + +#if 0 + int w = CVPixelBufferGetWidth(image); + int h = CVPixelBufferGetHeight(image); + int sz = CVPixelBufferGetDataSize(image); + int pitch = CVPixelBufferGetBytesPerRow(image); + SDL_Log("buffer planar=%d count:%d %d x %d sz=%d pitch=%d", planar, numPlanes, w, h, sz, pitch); +#endif + + CVPixelBufferLockBaseAddress(image, 0); + + if (planar == 0 && numPlanes == 0) { + frame->pitch[0] = CVPixelBufferGetBytesPerRow(image); + frame->data[0] = CVPixelBufferGetBaseAddress(image); + frame->num_planes = 1; + } else { + for (i = 0; i < numPlanes && i < 3; i++) { + int rowStride = 0; + uint8_t *data = NULL; + frame->num_planes += 1; + + rowStride = CVPixelBufferGetBytesPerRowOfPlane(image, i); + data = CVPixelBufferGetBaseAddressOfPlane(image, i); + frame->data[i] = data; + frame->pitch[i] = rowStride; + } + } + + /* Unlocked when frame is released */ + + } else { + // no frame + SDL_Delay(20); // TODO fix some delay + } + return 0; +} + +int +ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + if (frame->internal){ + CMSampleBufferRef sampleBuffer = (CMSampleBufferRef) frame->internal; + + CVImageBufferRef image = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferUnlockBaseAddress(image, 0); + + CFRelease(sampleBuffer); + } + return 0; +} + +int +GetNumFormats(SDL_VideoCaptureDevice *_this) +{ + AVCaptureDevice *device = get_device_by_name(_this->dev_name); + if (device) { + // LIST FORMATS + NSMutableOrderedSet *array_formats = [NSMutableOrderedSet new]; + NSArray *formats = [device formats]; + for (AVCaptureDeviceFormat *format in formats) { + // NSLog(@"%@", formats); + CMFormatDescriptionRef formatDescription = [format formatDescription]; + //NSLog(@"%@", formatDescription); + FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription); + NSString *str = fourcc_to_nstring(mediaSubType); + [array_formats addObject:str]; + } + return [array_formats count]; + } + return 0; +} + +int +GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format) +{ + AVCaptureDevice *device = get_device_by_name(_this->dev_name); + if (device) { + // LIST FORMATS + NSMutableOrderedSet *array_formats = [NSMutableOrderedSet new]; + NSArray *formats = [device formats]; + NSString *str; + + for (AVCaptureDeviceFormat *f in formats) { + FourCharCode mediaSubType; + CMFormatDescriptionRef formatDescription; + + formatDescription = [f formatDescription]; + mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription); + str = fourcc_to_nstring(mediaSubType); + [array_formats addObject:str]; + } + + str = array_formats[index]; + *format = nsfourcc_to_sdlformat(str); + + return 0; + } + return -1; +} + +int +GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format) +{ + AVCaptureDevice *device = get_device_by_name(_this->dev_name); + if (device) { + NSString *fmt = sdlformat_to_nsfourcc(format); + int count = 0; + + NSArray *formats = [device formats]; + for (AVCaptureDeviceFormat *f in formats) { + CMFormatDescriptionRef formatDescription = [f formatDescription]; + FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription); + NSString *str = fourcc_to_nstring(mediaSubType); + + if (str == fmt) { + count += 1; + } + } + return count; + } + return 0; +} + +int +GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height) +{ + AVCaptureDevice *device = get_device_by_name(_this->dev_name); + if (device) { + NSString *fmt = sdlformat_to_nsfourcc(format); + int count = 0; + + NSArray *formats = [device formats]; + for (AVCaptureDeviceFormat *f in formats) { + CMFormatDescriptionRef formatDescription = [f formatDescription]; + FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription); + NSString *str = fourcc_to_nstring(mediaSubType); + + if (str == fmt) { + if (index == count) { + CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(formatDescription); + *width = dim.width; + *height = dim.height; + return 0; + } + count += 1; + } + } + } + return -1; +} + +int +GetDeviceName(int index, char *buf, int size) +{ + NSArray *devices = discover_devices(); + if (index < [devices count]) { + AVCaptureDevice *device = devices[index]; + NSString *cameraID = [device localizedName]; + const char *str = [cameraID UTF8String]; + SDL_snprintf(buf, size, "%s", str); + return 0; + } + return -1; +} + +int +GetNumDevices(void) +{ + NSArray *devices = discover_devices(); + return [devices count]; +} + +#endif /* HAVE_COREMEDIA */ + +#endif /* SDL_VIDEO_CAPTURE */ + diff --git a/src/video/SDL_video_capture_c.h b/src/video/SDL_video_capture_c.h new file mode 100644 index 000000000..36f2494d6 --- /dev/null +++ b/src/video/SDL_video_capture_c.h @@ -0,0 +1,33 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "../SDL_internal.h" +#include "../../include/SDL3/SDL_video_capture.h" + +#ifndef SDL_video_capture_c_h_ +#define SDL_video_capture_c_h_ + +/* Initialize the video_capture subsystem */ +int SDL_VideoCaptureInit(void); + +/* Shutdown the video_capture subsystem */ +void SDL_QuitVideoCapture(void); + +#endif /* SDL_video_capture_c_h_ */ diff --git a/src/video/SDL_video_capture_v4l2.c b/src/video/SDL_video_capture_v4l2.c new file mode 100644 index 000000000..539cc1711 --- /dev/null +++ b/src/video/SDL_video_capture_v4l2.c @@ -0,0 +1,965 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_CAPTURE + +#include "SDL3/SDL.h" +#include "SDL3/SDL_video_capture.h" +#include "SDL_sysvideocapture.h" +#include "SDL_video_capture_c.h" +#include "SDL_pixels_c.h" +#include "../thread/SDL_systhread.h" + +#define DEBUG_VIDEO_CAPTURE_CAPTURE 1 + +#if defined(__linux__) && !defined(__ANDROID__) + +enum io_method { + IO_METHOD_READ, + IO_METHOD_MMAP, + IO_METHOD_USERPTR +}; + +struct buffer { + void *start; + size_t length; + int available; /* Is available in userspace */ +}; + +struct SDL_PrivateVideoCaptureData +{ + int fd; + enum io_method io; + int nb_buffers; + struct buffer *buffers; + int first_start; + int driver_pitch; +}; + +#include +#include +#include /* low-level i/o */ +#include +#include +#include +#include + +static int +xioctl(int fh, int request, void *arg) +{ + int r; + + do { + r = ioctl(fh, request, arg); + } while (r == -1 && errno == EINTR); + + return r; +} + +/* -1:error 1:frame 0:no frame*/ +static int +acquire_frame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + struct v4l2_buffer buf; + int i; + + int fd = _this->hidden->fd; + enum io_method io = _this->hidden->io; + size_t size = _this->hidden->buffers[0].length; + + switch (io) { + case IO_METHOD_READ: + if (read(fd, _this->hidden->buffers[0].start, size) == -1) { + switch (errno) { + case EAGAIN: + return 0; + + case EIO: + /* Could ignore EIO, see spec. */ + + /* fall through */ + + default: + return SDL_SetError("read"); + } + } + + frame->num_planes = 1; + frame->data[0] = _this->hidden->buffers[0].start; + frame->pitch[0] = _this->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 0; + + case EIO: + /* Could ignore EIO, see spec. */ + + /* fall through */ + + default: + return SDL_SetError("VIDIOC_DQBUF: %d", errno); + } + } + + if ((int)buf.index < 0 || (int)buf.index >= _this->hidden->nb_buffers) { + return SDL_SetError("invalid buffer index"); + } + + frame->num_planes = 1; + frame->data[0] = _this->hidden->buffers[buf.index].start; + frame->pitch[0] = _this->hidden->driver_pitch; + _this->hidden->buffers[buf.index].available = 1; + +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("debug mmap: image %d/%d num_planes:%d data[0]=%p", buf.index, _this->hidden->nb_buffers, frame->num_planes, (void*)frame->data[0]); +#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 0; + + case EIO: + /* Could ignore EIO, see spec. */ + + /* fall through */ + + default: + return SDL_SetError("VIDIOC_DQBUF"); + } + } + + for (i = 0; i < _this->hidden->nb_buffers; ++i) { + if (buf.m.userptr == (unsigned long)_this->hidden->buffers[i].start && buf.length == size) { + break; + } + } + + if (i >= _this->hidden->nb_buffers) { + return SDL_SetError("invalid buffer index"); + } + + frame->num_planes = 1; + frame->data[0] = (void*)buf.m.userptr; + frame->pitch[0] = _this->hidden->driver_pitch; + _this->hidden->buffers[i].available = 1; +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("debug userptr: image %d/%d num_planes:%d data[0]=%p", buf.index, _this->hidden->nb_buffers, frame->num_planes, (void*)frame->data[0]); +#endif + break; + } + + return 1; +} + + +int +ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + struct v4l2_buffer buf; + int i; + int fd = _this->hidden->fd; + enum io_method io = _this->hidden->io; + + for (i = 0; i < _this->hidden->nb_buffers; ++i) { + if (frame->num_planes && frame->data[0] == _this->hidden->buffers[i].start) { + break; + } + } + + if (i >= _this->hidden->nb_buffers) { + return SDL_SetError("invalid buffer index"); + } + + 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) { + return SDL_SetError("VIDIOC_QBUF"); + } + _this->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->data[0]; + buf.length = (int) _this->hidden->buffers[i].length; + + if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) { + return SDL_SetError("VIDIOC_QBUF"); + } + _this->hidden->buffers[i].available = 0; + break; + } + + return 0; +} + + +int +AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + fd_set fds; + struct timeval tv; + int ret; + + int fd = _this->hidden->fd; + + FD_ZERO(&fds); + FD_SET(fd, &fds); + + /* Timeout. */ + tv.tv_sec = 0; + tv.tv_usec = 300 * 1000; + + ret = select(fd + 1, &fds, NULL, NULL, &tv); + + if (ret == -1) { + if (errno == EINTR) { +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("continue .."); +#endif + return 0; + } + return SDL_SetError("select"); + } + + if (ret == 0) { + /* Timeout. Not an error */ + SDL_SetError("timeout select"); + return 0; + } + + ret = acquire_frame(_this, frame); + if (ret < 0) { + return -1; + } + + if (ret == 1){ + frame->timestampNS = SDL_GetTicksNS(); + } else if (ret == 0) { +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("No frame continue: %s", SDL_GetError()); +#endif + } + + /* EAGAIN - continue select loop. */ + return 0; +} + + +int +StopCapture(SDL_VideoCaptureDevice *_this) +{ + enum v4l2_buf_type type; + int fd = _this->hidden->fd; + enum io_method io = _this->hidden->io; + + switch (io) { + case IO_METHOD_READ: + break; + + case IO_METHOD_MMAP: + case IO_METHOD_USERPTR: + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(fd, VIDIOC_STREAMOFF, &type) == -1) { + return SDL_SetError("VIDIOC_STREAMOFF"); + } + break; + } + + return 0; +} + +static int +enqueue_buffers(SDL_VideoCaptureDevice *_this) +{ + int i; + int fd = _this->hidden->fd; + enum io_method io = _this->hidden->io; + switch (io) { + case IO_METHOD_READ: + break; + + case IO_METHOD_MMAP: + for (i = 0; i < _this->hidden->nb_buffers; ++i) { + if (_this->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 (i = 0; i < _this->hidden->nb_buffers; ++i) { + if (_this->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)_this->hidden->buffers[i].start; + buf.length = (int) _this->hidden->buffers[i].length; + + if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) { + return SDL_SetError("VIDIOC_QBUF"); + } + } + } + break; + } + return 0; +} + +static int +pre_enqueue_buffers(SDL_VideoCaptureDevice *_this) +{ + struct v4l2_requestbuffers req; + int fd = _this->hidden->fd; + enum io_method io = _this->hidden->io; + + switch (io) { + case IO_METHOD_READ: + break; + + case IO_METHOD_MMAP: + { + SDL_zero(req); + req.count = _this->hidden->nb_buffers; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + + if (xioctl(fd, VIDIOC_REQBUFS, &req) == -1) { + if (errno == EINVAL) { + return SDL_SetError("Does not support memory mapping"); + } else { + return SDL_SetError("VIDIOC_REQBUFS"); + } + } + + if (req.count < 2) { + return SDL_SetError("Insufficient buffer memory"); + } + + _this->hidden->nb_buffers = req.count; + } + break; + + case IO_METHOD_USERPTR: + { + SDL_zero(req); + req.count = _this->hidden->nb_buffers; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_USERPTR; + + if (xioctl(fd, VIDIOC_REQBUFS, &req) == -1) { + if (errno == EINVAL) { + return SDL_SetError("Does not support user pointer i/o"); + } else { + return SDL_SetError("VIDIOC_REQBUFS"); + } + } + } + break; + } + return 0; +} + +int +StartCapture(SDL_VideoCaptureDevice *_this) +{ + enum v4l2_buf_type type; + + int fd = _this->hidden->fd; + enum io_method io = _this->hidden->io; + + + if (_this->hidden->first_start == 0) { + _this->hidden->first_start = 1; + } else { + int old = _this->hidden->nb_buffers; + // TODO mmap; doesn't work with stop->start +#if 1 + /* Can change nb_buffers for mmap */ + if (pre_enqueue_buffers(_this) < 0) { + return -1; + } + if (old != _this->hidden->nb_buffers) { + SDL_SetError("different nb of buffers requested"); + return -1; + } +#endif + _this->hidden->first_start = 1; + } + + if (enqueue_buffers(_this) < 0) { + return -1; + } + + switch (io) { + case IO_METHOD_READ: + break; + + case IO_METHOD_MMAP: + case IO_METHOD_USERPTR: + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(fd, VIDIOC_STREAMON, &type) == -1) { + return SDL_SetError("VIDIOC_STREAMON"); + } + break; + } + + return 0; +} + +static int alloc_buffer_read(SDL_VideoCaptureDevice *_this, size_t buffer_size) +{ + _this->hidden->buffers[0].length = buffer_size; + _this->hidden->buffers[0].start = SDL_calloc(1, buffer_size); + + if (!_this->hidden->buffers[0].start) { + return SDL_OutOfMemory(); + } + return 0; +} + +static int +alloc_buffer_mmap(SDL_VideoCaptureDevice *_this) +{ + int fd = _this->hidden->fd; + int i; + for (i = 0; i < _this->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"); + } + + _this->hidden->buffers[i].length = buf.length; + _this->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 == _this->hidden->buffers[i].start) { + return SDL_SetError("mmap"); + } + } + return 0; +} + +static int +alloc_buffer_userp(SDL_VideoCaptureDevice *_this, size_t buffer_size) +{ + int i; + for (i = 0; i < _this->hidden->nb_buffers; ++i) { + _this->hidden->buffers[i].length = buffer_size; + _this->hidden->buffers[i].start = SDL_calloc(1, buffer_size); + + if (!_this->hidden->buffers[i].start) { + return SDL_OutOfMemory(); + } + } + return 0; +} + +static Uint32 +format_v4l2_2_sdl(Uint32 fmt) +{ + switch (fmt) { +#define CASE(x, y) case x: return y + CASE(V4L2_PIX_FMT_YUYV, SDL_PIXELFORMAT_YUY2); + CASE(V4L2_PIX_FMT_MJPEG, SDL_PIXELFORMAT_UNKNOWN); +#undef CASE + default: + SDL_Log("Unknown format V4L2_PIX_FORMAT '%d'", fmt); + return SDL_PIXELFORMAT_UNKNOWN; + } +} + +static Uint32 +format_sdl_2_v4l2(Uint32 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 0; + } +} + +int +GetNumFormats(SDL_VideoCaptureDevice *_this) +{ + int fd = _this->hidden->fd; + int i = 0; + struct v4l2_fmtdesc fmtdesc; + + SDL_zero(fmtdesc); + fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + while (ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) == 0) { + fmtdesc.index++; + i++; + } + return i; +} + +int +GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format) +{ + int fd = _this->hidden->fd; + struct v4l2_fmtdesc fmtdesc; + + SDL_zero(fmtdesc); + fmtdesc.index = index; + fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) == 0) { + *format = format_v4l2_2_sdl(fmtdesc.pixelformat); + +#if DEBUG_VIDEO_CAPTURE_CAPTURE + if (fmtdesc.flags & V4L2_FMT_FLAG_EMULATED) { + SDL_Log("%s format emulated", SDL_GetPixelFormatName(*format)); + } + if (fmtdesc.flags & V4L2_FMT_FLAG_COMPRESSED) { + SDL_Log("%s format compressed", SDL_GetPixelFormatName(*format)); + } +#endif + return 0; + } + + return -1; +} + +int +GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format) +{ + int fd = _this->hidden->fd; + int i = 0; + struct v4l2_frmsizeenum frmsizeenum; + + SDL_zero(frmsizeenum); + frmsizeenum.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + frmsizeenum.pixel_format = format_sdl_2_v4l2(format); + while (ioctl(fd,VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == 0) { + frmsizeenum.index++; + if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + i++; + } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) { + i += (1 + (frmsizeenum.stepwise.max_width - frmsizeenum.stepwise.min_width) / frmsizeenum.stepwise.step_width) + * (1 + (frmsizeenum.stepwise.max_height - frmsizeenum.stepwise.min_height) / frmsizeenum.stepwise.step_height); + } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) { + SDL_SetError("V4L2_FRMSIZE_TYPE_CONTINUOUS not handled"); + } + } + return i; +} + +int +GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height) +{ + int fd = _this->hidden->fd; + struct v4l2_frmsizeenum frmsizeenum; + int i = 0; + + SDL_zero(frmsizeenum); + frmsizeenum.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + frmsizeenum.pixel_format = format_sdl_2_v4l2(format); + while (ioctl(fd,VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == 0) { + frmsizeenum.index++; + + if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) { + if (i == index) { + *width = frmsizeenum.discrete.width; + *height = frmsizeenum.discrete.height; + return 0; + } + i++; + } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) { + unsigned int w; + for (w = frmsizeenum.stepwise.min_width; w <= frmsizeenum.stepwise.max_width; w += frmsizeenum.stepwise.step_width) { + unsigned int h; + for (h = frmsizeenum.stepwise.min_height; h <= frmsizeenum.stepwise.max_height; h += frmsizeenum.stepwise.step_height) { + if (i == index) { + *width = h; + *height = w; + return 0; + } + i++; + } + } + } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) { + } + } + + return -1; +} + + + +static void +dbg_v4l2_pixelformat(const char *str, int f) { + SDL_Log("%s V4L2_format=%d %c%c%c%c", str, f, + (f >> 0) & 0xff, + (f >> 8) & 0xff, + (f >> 16) & 0xff, + (f >> 24) & 0xff); +} + +int +GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec) +{ + struct v4l2_format fmt; + int fd = _this->hidden->fd; + unsigned int min; + + SDL_zero(fmt); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + /* Preserve original settings as set by v4l2-ctl for example */ + if (xioctl(fd, VIDIOC_G_FMT, &fmt) == -1) { + return SDL_SetError("Error VIDIOC_G_FMT"); + } + + /* Buggy driver paranoia. */ + min = fmt.fmt.pix.width * 2; + if (fmt.fmt.pix.bytesperline < min) { + fmt.fmt.pix.bytesperline = min; + } + min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height; + if (fmt.fmt.pix.sizeimage < min) { + fmt.fmt.pix.sizeimage = min; + } + + //spec->width = fmt.fmt.pix.width; + //spec->height = fmt.fmt.pix.height; + _this->hidden->driver_pitch = fmt.fmt.pix.bytesperline; + //spec->format = format_v4l2_2_sdl(fmt.fmt.pix.pixelformat); + + return 0; +} + +int +InitDevice(SDL_VideoCaptureDevice *_this) +{ + struct v4l2_cropcap cropcap; + struct v4l2_crop crop; + + int fd = _this->hidden->fd; + enum io_method io = _this->hidden->io; + int ret = -1; + + /* Select video input, video standard and tune here. */ + SDL_zero(cropcap); + + cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + if (xioctl(fd, VIDIOC_CROPCAP, &cropcap) == 0) { + crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + crop.c = cropcap.defrect; /* reset to default */ + + if (xioctl(fd, VIDIOC_S_CROP, &crop) == -1) { + switch (errno) { + case EINVAL: + /* Cropping not supported. */ + break; + default: + /* Errors ignored. */ + break; + } + } + } else { + /* Errors ignored. */ + } + + + { + struct v4l2_format fmt; + SDL_zero(fmt); + + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + fmt.fmt.pix.width = _this->spec.width; + fmt.fmt.pix.height = _this->spec.height; + + + fmt.fmt.pix.pixelformat = format_sdl_2_v4l2(_this->spec.format); + // fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + +#if DEBUG_VIDEO_CAPTURE_CAPTURE + SDL_Log("set SDL format %s", SDL_GetPixelFormatName(_this->spec.format)); + dbg_v4l2_pixelformat("set format", fmt.fmt.pix.pixelformat); +#endif + + if (xioctl(fd, VIDIOC_S_FMT, &fmt) == -1) { + return SDL_SetError("Error VIDIOC_S_FMT"); + } + } + + GetDeviceSpec(_this, &_this->spec); + + if (pre_enqueue_buffers(_this) < 0) { + return -1; + } + + { + _this->hidden->buffers = SDL_calloc(_this->hidden->nb_buffers, sizeof(*_this->hidden->buffers)); + if (!_this->hidden->buffers) { + return SDL_OutOfMemory(); + } + } + + { + size_t size, pitch; + SDL_CalculateSize(_this->spec.format, _this->spec.width, _this->spec.height, &size, &pitch, SDL_FALSE); + + switch (io) { + case IO_METHOD_READ: + ret = alloc_buffer_read(_this, size); + break; + + case IO_METHOD_MMAP: + ret = alloc_buffer_mmap(_this); + break; + + case IO_METHOD_USERPTR: + ret = alloc_buffer_userp(_this, size); + break; + } + } + + if (ret < 0) { + return -1; + } + + return 0; +} + +void +CloseDevice(SDL_VideoCaptureDevice *_this) +{ + if (!_this) { + return; + } + + if (_this->hidden) { + if (_this->hidden->buffers) { + int i; + enum io_method io = _this->hidden->io; + + switch (io) { + case IO_METHOD_READ: + SDL_free(_this->hidden->buffers[0].start); + break; + + case IO_METHOD_MMAP: + for (i = 0; i < _this->hidden->nb_buffers; ++i) { + if (munmap(_this->hidden->buffers[i].start, _this->hidden->buffers[i].length) == -1) { + SDL_SetError("munmap"); + } + } + break; + + case IO_METHOD_USERPTR: + for (i = 0; i < _this->hidden->nb_buffers; ++i) { + SDL_free(_this->hidden->buffers[i].start); + } + break; + } + + SDL_free(_this->hidden->buffers); + } + + if (_this->hidden->fd != -1) { + if (close(_this->hidden->fd)) { + SDL_SetError("close video capture device"); + } + } + SDL_free(_this->hidden); + + _this->hidden = NULL; + } +} + + +int +OpenDevice(SDL_VideoCaptureDevice *_this) +{ + struct stat st; + struct v4l2_capability cap; + int fd; + enum io_method io; + + _this->hidden = (struct SDL_PrivateVideoCaptureData *) SDL_calloc(1, sizeof (struct SDL_PrivateVideoCaptureData)); + if (_this->hidden == NULL) { + SDL_OutOfMemory(); + return -1; + } + + _this->hidden->fd = -1; + + if (stat(_this->dev_name, &st) == -1) { + SDL_SetError("Cannot identify '%s': %d, %s", _this->dev_name, errno, strerror(errno)); + return -1; + } + + if (!S_ISCHR(st.st_mode)) { + SDL_SetError("%s is no device", _this->dev_name); + return -1; + } + + fd = open(_this->dev_name, O_RDWR /* required */ | O_NONBLOCK, 0); + if (fd == -1) { + SDL_SetError("Cannot open '%s': %d, %s", _this->dev_name, errno, strerror(errno)); + return -1; + } + + _this->hidden->fd = fd; + _this->hidden->io = IO_METHOD_MMAP; +// _this->hidden->io = IO_METHOD_USERPTR; +// _this->hidden->io = IO_METHOD_READ; +// + if (_this->hidden->io == IO_METHOD_READ) { + _this->hidden->nb_buffers = 1; + } else { + _this->hidden->nb_buffers = 8; /* Number of image as internal buffer, */ + } + io = _this->hidden->io; + + if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) { + if (errno == EINVAL) { + return SDL_SetError("%s is no V4L2 device", _this->dev_name); + } else { + return SDL_SetError("Error VIDIOC_QUERYCAP errno=%d device%s is no V4L2 device", errno, _this->dev_name); + } + } + + if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { + return SDL_SetError("%s is no video capture device", _this->dev_name); + } + +#if 0 + if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { + SDL_Log("%s is video capture device - single plane", _this->dev_name); + } + if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE)) { + SDL_Log("%s is video capture device - multiple planes", _this->dev_name); + } +#endif + + switch (io) { + case IO_METHOD_READ: + if (!(cap.capabilities & V4L2_CAP_READWRITE)) { + return SDL_SetError("%s does not support read i/o", _this->dev_name); + } + break; + + case IO_METHOD_MMAP: + case IO_METHOD_USERPTR: + if (!(cap.capabilities & V4L2_CAP_STREAMING)) { + return SDL_SetError("%s does not support streaming i/o", _this->dev_name); + } + break; + } + + + + + return 0; +} + + + +int +GetDeviceName(int index, char *buf, int size) { + SDL_snprintf(buf, size, "/dev/video%d", index); + return 0; +} + +int +GetNumDevices(void) { + int num; + for (num = 0; num < 128; num++) { + static char buf[256]; + buf[0] = 0; + buf[255] = 0; + GetDeviceName(num, buf, sizeof (buf)); + SDL_RWops *src = SDL_RWFromFile(buf, "rb"); + if (src == NULL) { + // When file does not exist, an error is set. Clear it. + SDL_ClearError(); + return num; + } + SDL_RWclose(src); + } + return num; +} + +#endif + +#endif /* SDL_VIDEO_CAPTURE */ diff --git a/src/video/android/SDL_android_video_capture.c b/src/video/android/SDL_android_video_capture.c new file mode 100644 index 000000000..244d779c2 --- /dev/null +++ b/src/video/android/SDL_android_video_capture.c @@ -0,0 +1,678 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL3/SDL.h" +#include "SDL3/SDL_video_capture.h" +#include "../SDL_sysvideocapture.h" +#include "../SDL_video_capture_c.h" +#include "../SDL_pixels_c.h" +#include "../../thread/SDL_systhread.h" + +#define DEBUG_VIDEO_CAPTURE_CAPTURE 1 + +#if defined(__ANDROID__) && __ANDROID_API__ >= 24 + +/* + * APP_PLATFORM=android-24 + * minSdkVersion=24 + * + * link with: -lcamera2ndk -lmediandk + * + * AndroidManifest.xml: + * + * + * + * + * Add: #define SDL_VIDEO_CAPTURE 1 + * in: include/build_config/SDL_build_config_android.h + * + * + * 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. * " + */ + +#include +#include +#include +#include + +#include "../../core/android/SDL_android.h" + + +static ACameraManager *cameraMgr = NULL; +static ACameraIdList *cameraIdList = NULL; + +static void +create_cameraMgr(void) +{ + if (cameraMgr == NULL) { + + if (!Android_JNI_RequestPermission("android.permission.CAMERA")) { + SDL_SetError("This app doesn't have CAMERA permission"); + return; + } + + cameraMgr = ACameraManager_create(); + if (cameraMgr == NULL) { + SDL_Log("Error creating ACameraManager"); + } else { + SDL_Log("Create ACameraManager"); + } + } +} + +static void +delete_cameraMgr(void) +{ + if (cameraIdList) { + ACameraManager_deleteCameraIdList(cameraIdList); + cameraIdList = NULL; + } + + if (cameraMgr) { + ACameraManager_delete(cameraMgr); + cameraMgr = NULL; + } +} + +struct SDL_PrivateVideoCaptureData +{ + ACameraDevice *device; + ACameraCaptureSession *session; + ACameraDevice_StateCallbacks dev_callbacks; + ACameraCaptureSession_stateCallbacks capture_callbacks; + ACaptureSessionOutputContainer *sessionOutputContainer; + AImageReader *reader; + int num_formats; + int count_formats[6]; // see format_2_id +}; + + +/**/ +#define FORMAT_SDL SDL_PIXELFORMAT_NV12 + +static int +format_2_id(int fmt) { + switch (fmt) { +#define CASE(x, y) case x: return y + CASE(FORMAT_SDL, 0); + CASE(SDL_PIXELFORMAT_RGB565, 1); + CASE(SDL_PIXELFORMAT_RGB888, 2); + CASE(SDL_PIXELFORMAT_RGBA8888, 3); + CASE(SDL_PIXELFORMAT_RGBX8888, 4); + CASE(SDL_PIXELFORMAT_UNKNOWN, 5); +#undef CASE + default: + return 5; + } +} + +static int +id_2_format(int fmt) { + switch (fmt) { +#define CASE(x, y) case y: return x + CASE(FORMAT_SDL, 0); + CASE(SDL_PIXELFORMAT_RGB565, 1); + CASE(SDL_PIXELFORMAT_RGB888, 2); + CASE(SDL_PIXELFORMAT_RGBA8888, 3); + CASE(SDL_PIXELFORMAT_RGBX8888, 4); + CASE(SDL_PIXELFORMAT_UNKNOWN, 5); +#undef CASE + default: + return SDL_PIXELFORMAT_UNKNOWN; + } +} + +static Uint32 +format_android_2_sdl(Uint32 fmt) +{ + switch (fmt) { +#define CASE(x, y) case x: return y + CASE(AIMAGE_FORMAT_YUV_420_888, FORMAT_SDL); + CASE(AIMAGE_FORMAT_RGB_565, SDL_PIXELFORMAT_RGB565); + CASE(AIMAGE_FORMAT_RGB_888, SDL_PIXELFORMAT_RGB888); + CASE(AIMAGE_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888); + CASE(AIMAGE_FORMAT_RGBX_8888, SDL_PIXELFORMAT_RGBX8888); + + CASE(AIMAGE_FORMAT_RGBA_FP16, SDL_PIXELFORMAT_UNKNOWN); // 64bits + CASE(AIMAGE_FORMAT_RAW_PRIVATE, SDL_PIXELFORMAT_UNKNOWN); + CASE(AIMAGE_FORMAT_JPEG, SDL_PIXELFORMAT_UNKNOWN); +#undef CASE + default: + SDL_Log("Unknown format AIMAGE_FORMAT '%d'", fmt); + return SDL_PIXELFORMAT_UNKNOWN; + } +} + +static Uint32 +format_sdl_2_android(Uint32 fmt) +{ + switch (fmt) { +#define CASE(x, y) case y: return x + CASE(AIMAGE_FORMAT_YUV_420_888, FORMAT_SDL); + CASE(AIMAGE_FORMAT_RGB_565, SDL_PIXELFORMAT_RGB565); + CASE(AIMAGE_FORMAT_RGB_888, SDL_PIXELFORMAT_RGB888); + CASE(AIMAGE_FORMAT_RGBA_8888, SDL_PIXELFORMAT_RGBA8888); + CASE(AIMAGE_FORMAT_RGBX_8888, SDL_PIXELFORMAT_RGBX8888); +#undef CASE + default: + return 0; + } +} + + +static void +onDisconnected(void *context, ACameraDevice *device) +{ + // SDL_VideoCaptureDevice *_this = (SDL_VideoCaptureDevice *) context; + SDL_Log("CB onDisconnected"); +} + +static void +onError(void *context, ACameraDevice *device, int error) +{ + // SDL_VideoCaptureDevice *_this = (SDL_VideoCaptureDevice *) context; + SDL_Log("CB onError"); +} + + +static void +onClosed(void* context, ACameraCaptureSession *session) +{ + // SDL_VideoCaptureDevice *_this = (SDL_VideoCaptureDevice *) context; + SDL_Log("CB onClosed"); +} + +static void +onReady(void* context, ACameraCaptureSession *session) +{ + // SDL_VideoCaptureDevice *_this = (SDL_VideoCaptureDevice *) context; + SDL_Log("CB onReady"); +} + +static void +onActive(void* context, ACameraCaptureSession *session) +{ + // SDL_VideoCaptureDevice *_this = (SDL_VideoCaptureDevice *) context; + SDL_Log("CB onActive"); +} + +int +OpenDevice(SDL_VideoCaptureDevice *_this) +{ + camera_status_t res; + + /* 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 (check_device_playing()) { + return SDL_SetError("A camera is already playing"); + } + + _this->hidden = (struct SDL_PrivateVideoCaptureData *) SDL_calloc(1, sizeof (struct SDL_PrivateVideoCaptureData)); + if (_this->hidden == NULL) { + return SDL_OutOfMemory(); + } + + create_cameraMgr(); + + _this->hidden->dev_callbacks.context = (void *) _this; + _this->hidden->dev_callbacks.onDisconnected = onDisconnected; + _this->hidden->dev_callbacks.onError = onError; + + res = ACameraManager_openCamera(cameraMgr, _this->dev_name, &_this->hidden->dev_callbacks, &_this->hidden->device); + if (res != ACAMERA_OK) { + return SDL_SetError("Failed to open camera"); + } + + return 0; +} + +void +CloseDevice(SDL_VideoCaptureDevice *_this) +{ + if (_this && _this->hidden) { + if (_this->hidden->session) { + ACameraCaptureSession_close(_this->hidden->session); + } + + if (_this->hidden->sessionOutputContainer) { + ACaptureSessionOutputContainer_free(_this->hidden->sessionOutputContainer); + } + + if (_this->hidden->reader) { + AImageReader_delete(_this->hidden->reader); + } + + if (_this->hidden->device) { + ACameraDevice_close(_this->hidden->device); + } + + SDL_free(_this->hidden); + + _this->hidden = NULL; + } + + if (check_all_device_closed()) { + delete_cameraMgr(); + } +} + +int +InitDevice(SDL_VideoCaptureDevice *_this) +{ + size_t size, pitch; + SDL_CalculateSize(_this->spec.format, _this->spec.width, _this->spec.height, &size, &pitch, SDL_FALSE); + SDL_Log("Buffer size: %d x %d", _this->spec.width, _this->spec.height); + return 0; +} + +int +GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec) +{ + if (spec) { + *spec = _this->spec; + return 0; + } + return -1; +} + +int +StartCapture(SDL_VideoCaptureDevice *_this) +{ + camera_status_t res; + media_status_t res2; + ANativeWindow *window = NULL; + ACaptureSessionOutput *sessionOutput; + ACameraOutputTarget *outputTarget; + ACaptureRequest *request; + + res2 = AImageReader_new(_this->spec.width, _this->spec.height, format_sdl_2_android(_this->spec.format), 10 /* nb buffers */, &_this->hidden->reader); + if (res2 != AMEDIA_OK) { + SDL_SetError("Error AImageReader_new"); + goto error; + } + res2 = AImageReader_getWindow(_this->hidden->reader, &window); + if (res2 != AMEDIA_OK) { + SDL_SetError("Error AImageReader_new"); + goto error; + + } + + + res = ACaptureSessionOutput_create(window, &sessionOutput); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACaptureSessionOutput_create"); + goto error; + } + res = ACaptureSessionOutputContainer_create(&_this->hidden->sessionOutputContainer); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACaptureSessionOutputContainer_create"); + goto error; + } + res = ACaptureSessionOutputContainer_add(_this->hidden->sessionOutputContainer, sessionOutput); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACaptureSessionOutputContainer_add"); + goto error; + } + + + res = ACameraOutputTarget_create(window, &outputTarget); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACameraOutputTarget_create"); + goto error; + } + + + res = ACameraDevice_createCaptureRequest(_this->hidden->device, TEMPLATE_RECORD, &request); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACameraDevice_createCaptureRequest"); + goto error; + } + + res = ACaptureRequest_addTarget(request, outputTarget); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACaptureRequest_addTarget"); + goto error; + } + + + _this->hidden->capture_callbacks.context = (void *) _this; + _this->hidden->capture_callbacks.onClosed = onClosed; + _this->hidden->capture_callbacks.onReady = onReady; + _this->hidden->capture_callbacks.onActive = onActive; + + res = ACameraDevice_createCaptureSession(_this->hidden->device, + _this->hidden->sessionOutputContainer, + &_this->hidden->capture_callbacks, + &_this->hidden->session); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACameraDevice_createCaptureSession"); + goto error; + } + + res = ACameraCaptureSession_setRepeatingRequest(_this->hidden->session, NULL, 1, &request, NULL); + if (res != ACAMERA_OK) { + SDL_SetError("Error ACameraDevice_createCaptureSession"); + goto error; + } + + return 0; + +error: + return -1; +} + +int +StopCapture(SDL_VideoCaptureDevice *_this) +{ + ACameraCaptureSession_close(_this->hidden->session); + _this->hidden->session = NULL; + return 0; +} + +int +AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + media_status_t res; + AImage *image; + res = AImageReader_acquireNextImage(_this->hidden->reader, &image); + /* We could also use this one: + res = AImageReader_acquireLatestImage(_this->hidden->reader, &image); + */ + if (res == AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE ) { + + SDL_Delay(20); // TODO fix some delay +#if DEBUG_VIDEO_CAPTURE_CAPTURE +// SDL_Log("AImageReader_acquireNextImage: AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE"); +#endif + return 0; + } else if (res == AMEDIA_OK ) { + int i = 0; + int32_t numPlanes = 0; + AImage_getNumberOfPlanes(image, &numPlanes); + + frame->timestampNS = SDL_GetTicksNS(); + + for (i = 0; i < numPlanes && i < 3; i++) { + int dataLength = 0; + int rowStride = 0; + uint8_t *data = NULL; + frame->num_planes += 1; + AImage_getPlaneRowStride(image, i, &rowStride); + res = AImage_getPlaneData(image, i, &data, &dataLength); + if (res == AMEDIA_OK) { + frame->data[i] = data; + frame->pitch[i] = rowStride; + } + } + + if (frame->num_planes == 3) { + /* plane 2 and 3 are interleaved NV12. SDL only takes two planes for this format */ + int pixelStride = 0; + AImage_getPlanePixelStride(image, 1, &pixelStride); + if (pixelStride == 2) { + frame->num_planes -= 1; + } + } + + frame->internal = (void*)image; + return 0; + } else if (res == AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED) { + SDL_SetError("AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED"); + } else { + SDL_SetError("AImageReader_acquireNextImage: %d", res); + } + + return -1; +} + +int +ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) +{ + if (frame->internal){ + AImage_delete((AImage *)frame->internal); + } + return 0; +} + +int +GetNumFormats(SDL_VideoCaptureDevice *_this) +{ + camera_status_t res; + int i; + int unknown = 0; + ACameraMetadata *metadata; + ACameraMetadata_const_entry entry; + + if (_this->hidden->num_formats != 0) { + return _this->hidden->num_formats; + } + + res = ACameraManager_getCameraCharacteristics(cameraMgr, _this->dev_name, &metadata); + if (res != ACAMERA_OK) { + return -1; + } + + res = ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry); + if (res != ACAMERA_OK) { + return -1; + } + + SDL_Log("got entry ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS"); + + for (i = 0; i < entry.count; i += 4) { + int32_t format = entry.data.i32[i + 0]; + int32_t type = entry.data.i32[i + 3]; + Uint32 fmt; + + if (type == ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT) { + continue; + } + + fmt = format_android_2_sdl(format); + _this->hidden->count_formats[format_2_id(fmt)] += 1; + +#if DEBUG_VIDEO_CAPTURE_CAPTURE + if (fmt != SDL_PIXELFORMAT_UNKNOWN) { + int w = entry.data.i32[i + 1]; + int h = entry.data.i32[i + 2]; + SDL_Log("Got format android 0x%08x -> %s %d x %d", format, SDL_GetPixelFormatName(fmt), w, h); + } else { + unknown += 1; + } +#endif + } + +#if DEBUG_VIDEO_CAPTURE_CAPTURE + if (unknown) { + SDL_Log("Got unknown android"); + } +#endif + + + if ( _this->hidden->count_formats[0]) _this->hidden->num_formats += 1; + if ( _this->hidden->count_formats[1]) _this->hidden->num_formats += 1; + if ( _this->hidden->count_formats[2]) _this->hidden->num_formats += 1; + if ( _this->hidden->count_formats[3]) _this->hidden->num_formats += 1; + if ( _this->hidden->count_formats[4]) _this->hidden->num_formats += 1; + if ( _this->hidden->count_formats[5]) _this->hidden->num_formats += 1; + + return _this->hidden->num_formats; +} + +int +GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format) +{ + int i; + int i2 = 0; + + if (_this->hidden->num_formats == 0) { + GetNumFormats(_this); + } + + if (index < 0 || index >= _this->hidden->num_formats) { + return -1; + } + + for (i = 0; i < SDL_arraysize(_this->hidden->count_formats); i++) { + if (_this->hidden->count_formats[i] == 0) { + continue; + } + + if (i2 == index) { + *format = id_2_format(i); + } + + i2++; + + } + return 0; +} + +int +GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format) +{ + int i, i2 = 0, index; + if (_this->hidden->num_formats == 0) { + GetNumFormats(_this); + } + + index = format_2_id(format); + + for (i = 0; i < SDL_arraysize(_this->hidden->count_formats); i++) { + if (_this->hidden->count_formats[i] == 0) { + continue; + } + + if (i2 == index) { + /* number of resolution for this format */ + return _this->hidden->count_formats[i]; + } + + i2++; + } + + return -1; +} + +int +GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height) +{ + camera_status_t res; + int i, i2 = 0; + ACameraMetadata *metadata; + ACameraMetadata_const_entry entry; + + if (_this->hidden->num_formats == 0) { + GetNumFormats(_this); + } + + res = ACameraManager_getCameraCharacteristics(cameraMgr, _this->dev_name, &metadata); + if (res != ACAMERA_OK) { + return -1; + } + + res = ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry); + if (res != ACAMERA_OK) { + return -1; + } + + for (i = 0; i < entry.count; i += 4) { + int32_t f = entry.data.i32[i + 0]; + int w = entry.data.i32[i + 1]; + int h = entry.data.i32[i + 2]; + int32_t type = entry.data.i32[i + 3]; + Uint32 fmt; + + if (type == ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT) { + continue; + } + + + fmt = format_android_2_sdl(f); + if (fmt != format) { + continue; + } + + if (i2 == index) { + *width = w; + *height = h; + return 0; + } + + i2++; + } + return -1; +} + +int +GetDeviceName(int index, char *buf, int size) +{ + create_cameraMgr(); + + if (cameraIdList == NULL) { + GetNumDevices(); + } + + if (cameraIdList) { + if (index >= 0 && index < cameraIdList->numCameras) { + SDL_snprintf(buf, size, "%s", cameraIdList->cameraIds[index]); + return 0; + } + } + + return -1; +} + +int +GetNumDevices(void) +{ + camera_status_t res; + create_cameraMgr(); + + if (cameraIdList) { + ACameraManager_deleteCameraIdList(cameraIdList); + cameraIdList = NULL; + } + + res = ACameraManager_getCameraIdList(cameraMgr, &cameraIdList); + + if (res == ACAMERA_OK) { + if (cameraIdList) { + return cameraIdList->numCameras; + } + } + return -1; +} + +#endif + + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8d7ff5904..bac37027a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -358,6 +358,8 @@ add_sdl_test_executable(teststreaming NEEDS_RESOURCES TESTUTILS SOURCES teststre add_sdl_test_executable(testtimer NONINTERACTIVE NONINTERACTIVE_ARGS --no-interactive NONINTERACTIVE_TIMEOUT 60 SOURCES testtimer.c) add_sdl_test_executable(testurl SOURCES testurl.c) add_sdl_test_executable(testver NONINTERACTIVE SOURCES testver.c) +add_sdl_test_executable(testvideocapture SOURCES testvideocapture.c) +add_sdl_test_executable(testvideocaptureminimal SOURCES testvideocaptureminimal.c) add_sdl_test_executable(testviewport NEEDS_RESOURCES TESTUTILS SOURCES testviewport.c) add_sdl_test_executable(testwm SOURCES testwm.c) add_sdl_test_executable(testyuv NONINTERACTIVE NONINTERACTIVE_ARGS "--automated" NEEDS_RESOURCES TESTUTILS SOURCES testyuv.c testyuv_cvt.c) diff --git a/test/testvideocapture.c b/test/testvideocapture.c new file mode 100644 index 000000000..aff7642b1 --- /dev/null +++ b/test/testvideocapture.c @@ -0,0 +1,770 @@ +/* + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ +#include "SDL3/SDL_main.h" +#include "SDL3/SDL.h" +#include "SDL3/SDL_test.h" +#include "SDL3/SDL_video_capture.h" +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +static const char *usage = "\ + \n\ + =========================================================================\n\ + \n\ +Use keyboards:\n\ + o: open first video capture device. (close previously opened)\n\ + l: switch to, and list video capture devices\n\ + i: information about status (Init, Playing, Stopped)\n\ + f: formats and resolutions available\n\ + s: start / stop capture\n\ + h: display help\n\ + esc: exit \n\ + \n\ + =========================================================================\n\ + \n\ +"; + +typedef struct { + Uint64 next_check; + int frame_counter; + int check_delay; + double last_fps; +} measure_fps_t; + +static void +update_fps(measure_fps_t *m) +{ + Uint64 now = SDL_GetTicks(); + Uint64 deadline; + m->frame_counter++; + if (m->check_delay == 0) { + m->check_delay = 1500; + } + deadline = m->next_check; + if (now >= deadline) { + /* Print out some timing information */ + const Uint64 then = m->next_check - m->check_delay; + m->last_fps = ((double) m->frame_counter * 1000) / (now - then); + m->next_check = now + m->check_delay; + m->frame_counter = 0; + } +} + +#if defined(__linux__) && !defined(__ANDROID__) +static void load_average(float *val) +{ + FILE *fp = 0; + char line[1024]; + fp = fopen("/proc/loadavg", "rt"); + if (fp) { + char *s = fgets(line, sizeof(line), fp); + if (s) { + SDL_sscanf(s, "%f", val); + } + fclose(fp); + } +} +#endif + + +struct data_capture_t { + SDL_VideoCaptureDevice *device; + SDL_VideoCaptureSpec obtained; + int stopped; + SDL_VideoCaptureFrame frame_current; + measure_fps_t fps_capture; + SDL_Texture *texture; + int texture_updated; +}; + +#define SAVE_CAPTURE_STATE(x) \ + data_capture_tab[(x)].device = device; \ + data_capture_tab[(x)].obtained = obtained; \ + data_capture_tab[(x)].stopped = stopped; \ + data_capture_tab[(x)].frame_current = frame_current; \ + data_capture_tab[(x)].fps_capture = fps_capture; \ + data_capture_tab[(x)].texture = texture; \ + data_capture_tab[(x)].texture_updated = texture_updated; \ + + +#define RESTORE_CAPTURE_STATE(x) \ + device = data_capture_tab[(x)].device; \ + obtained = data_capture_tab[(x)].obtained; \ + stopped = data_capture_tab[(x)].stopped; \ + frame_current = data_capture_tab[(x)].frame_current; \ + fps_capture = data_capture_tab[(x)].fps_capture; \ + texture = data_capture_tab[(x)].texture; \ + texture_updated = data_capture_tab[(x)].texture_updated; \ + + + + + +static SDL_VideoCaptureDeviceID get_instance_id(int index) { + int ret = 0; + int num = 0; + SDL_VideoCaptureDeviceID *devices; + devices = SDL_GetVideoCaptureDevices(&num); + if (devices) { + if (index >= 0 && index < num) { + ret = devices[index]; + } + SDL_free(devices); + } + + if (ret == 0) { + SDL_Log("invalid index"); + } + + return ret; +} + + + +int main(int argc, char **argv) +{ + SDL_Window *window = NULL; + SDL_Renderer *renderer = NULL; + SDL_Event evt; + int quit = 0; + + SDLTest_CommonState *state; + + int current_dev = 0; + measure_fps_t fps_main; + + + SDL_FRect r_playstop = { 50, 50, 120, 50 }; + SDL_FRect r_close = { 50 + (120 + 50) * 1, 50, 120, 50 }; + + SDL_FRect r_open = { 50 + (120 + 50) * 2, 50, 120, 50 }; + + SDL_FRect r_format = { 50 + (120 + 50) * 3, 50, 120, 50 }; + SDL_FRect r_listdev = { 50 + (120 + 50) * 4, 50, 120, 50 }; + + SDL_VideoCaptureDevice *device; + SDL_VideoCaptureSpec obtained; + int stopped = 0; + SDL_VideoCaptureFrame frame_current; + measure_fps_t fps_capture; + SDL_Texture *texture = NULL; + int texture_updated = 0; + + struct data_capture_t data_capture_tab[16]; + const int data_capture_tab_size = SDL_arraysize(data_capture_tab); + + SDL_zero(fps_main); + SDL_zero(fps_capture); + SDL_zero(frame_current); + SDL_zeroa(data_capture_tab); + + /* Set 0 to disable TouchEvent to be duplicated as MouseEvent with SDL_TOUCH_MOUSEID */ + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); + /* Set 0 to disable MouseEvent to be duplicated as TouchEvent with SDL_MOUSE_TOUCHID */ + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); + + { + int i; + for (i = 0; i < data_capture_tab_size; i++) { + data_capture_tab[i].device = NULL; + } + } + + /* Initialize test framework */ + state = SDLTest_CommonCreateState(argv, 0); + if (state == NULL) { + return 1; + } + + /* Enable standard application logging */ + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO); + + /* Parse commandline */ + { + int i; + for (i = 1; i < argc;) { + int consumed; + + consumed = SDLTest_CommonArg(state, i); + if (consumed <= 0) { + static const char *options[] = {NULL}; + SDLTest_CommonLogUsage(state, argv[0], options); + SDLTest_CommonDestroyState(state); + return 1; + } + + i += consumed; + } + } + + SDL_Log("%s", usage); + + /* Load the SDL library */ + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s", SDL_GetError()); + return 1; + } + + window = SDL_CreateWindow("Local Video", 1000, 800, 0); + if (window == NULL) { + SDL_Log("Couldn't create window: %s", SDL_GetError()); + return 1; + } + + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); + + renderer = SDL_CreateRenderer(window, NULL, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (renderer == NULL) { + /* SDL_Log("Couldn't create renderer: %s", SDL_GetError()); */ + return 1; + } + + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_INFO); + + device = SDL_OpenVideoCapture(0); + + if (!device) { + SDL_Log("Error SDL_OpenVideoCapture: %s", SDL_GetError()); + } + + { + /* List formats */ + int i, num = SDL_GetNumVideoCaptureFormats(device); + for (i = 0; i < num; i++) { + Uint32 format; + SDL_GetVideoCaptureFormat(device, i, &format); + SDL_Log("format %d/%d: %s", i, num, SDL_GetPixelFormatName(format)); + { + int w, h; + int j, num2 = SDL_GetNumVideoCaptureFrameSizes(device, format); + for (j = 0; j < num2; j++) { + SDL_GetVideoCaptureFrameSize(device, format, j, &w, &h); + SDL_Log(" framesizes %d/%d : %d x %d", j, num2, w, h); + } + } + } + } + + /* Set Spec */ + { + int ret; + /* forced_format */ + SDL_VideoCaptureSpec desired; + SDL_zero(desired); + desired.width = 640 * 2; + desired.height = 360 * 2; + desired.format = SDL_PIXELFORMAT_NV12; + ret = SDL_SetVideoCaptureSpec(device, &desired, &obtained, SDL_VIDEO_CAPTURE_ALLOW_ANY_CHANGE); + + if (ret < 0) { + SDL_SetVideoCaptureSpec(device, NULL, &obtained, 0); + } + } + + SDL_Log("Open capture video device. Obtained spec: size=%d x %d format=%s", + obtained.width, obtained.height, SDL_GetPixelFormatName(obtained.format)); + + { + SDL_VideoCaptureSpec spec; + if (SDL_GetVideoCaptureSpec(device, &spec) == 0) { + SDL_Log("Read spec: size=%d x %d format=%s", + spec.width, spec.height, SDL_GetPixelFormatName(spec.format)); + } else { + SDL_Log("Error read spec: %s", SDL_GetError()); + } + } + + if (SDL_StartVideoCapture(device) < 0) { + SDL_Log("error SDL_StartVideoCapture(): %s", SDL_GetError()); + } + + while (!quit) { + + SDL_SetRenderDrawColor(renderer, 0x99, 0x99, 0x99, 255); + SDL_RenderClear(renderer); + + SDL_SetRenderDrawColor(renderer, 0x33, 0x33, 0x33, 255); + + SDL_RenderFillRect(renderer, &r_playstop); + SDL_RenderFillRect(renderer, &r_close); + SDL_RenderFillRect(renderer, &r_open); + SDL_RenderFillRect(renderer, &r_format); + SDL_RenderFillRect(renderer, &r_listdev); + + SDL_SetRenderDrawColor(renderer, 0xcc, 0xcc, 0xcc, 255); + + SDLTest_DrawString(renderer, r_playstop.x + 5, r_playstop.y + 5, "play stop"); + SDLTest_DrawString(renderer, r_close.x + 5, r_close.y + 5, "close"); + SDLTest_DrawString(renderer, r_open.x + 5, r_open.y + 5, "open dev"); + SDLTest_DrawString(renderer, r_format.x + 5, r_format.y + 5, "formats"); + + { + char buf[256]; + SDL_snprintf(buf, 256, "device %d", current_dev); + SDLTest_DrawString(renderer, r_listdev.x + 5, r_listdev.y + 5, buf); + } + + while (SDL_PollEvent(&evt)) { + SDL_FRect *r = NULL; + SDL_FPoint pt; + int sym = 0; + + pt.x = 0; + pt.y = 0; + + SDL_ConvertEventToRenderCoordinates(renderer, &evt); + + switch (evt.type) + { + case SDL_EVENT_KEY_DOWN: + { + sym = evt.key.keysym.sym; + break; + } + case SDL_EVENT_QUIT: + { + quit = 1; + SDL_Log("Ctlr+C : Quit!"); + } + break; + + case SDL_EVENT_FINGER_DOWN: + { + pt.x = evt.tfinger.x; + pt.y = evt.tfinger.y; + } + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + { + pt.x = evt.button.x; + pt.y = evt.button.y; + } + break; + } + + if (pt.x != 0 && pt.y != 0) { + if (SDL_PointInRectFloat(&pt, &r_playstop)) { + r = &r_playstop; + sym = SDLK_s; + } + if (SDL_PointInRectFloat(&pt, &r_close)) { + r = &r_close; + sym = SDLK_c; + } + if (SDL_PointInRectFloat(&pt, &r_open)) { + r = &r_open; + sym = SDLK_o; + } + + if (SDL_PointInRectFloat(&pt, &r_format)) { + r = &r_format; + sym = SDLK_f; + } + if (SDL_PointInRectFloat(&pt, &r_listdev)) { + r = &r_listdev; + sym = SDLK_l; + } + } + + + if (r) { + SDL_SetRenderDrawColor(renderer, 0x33, 0, 0, 255); + SDL_RenderFillRect(renderer, r); + } + + + if (sym == SDLK_c) { + if (frame_current.num_planes) { + SDL_ReleaseVideoCaptureFrame(device, &frame_current); + } + SDL_CloseVideoCapture(device); + device = NULL; + SDL_Log("Close"); + } + + if (sym == SDLK_o) { + if (device) { + SDL_Log("Close previous .."); + if (frame_current.num_planes) { + SDL_ReleaseVideoCaptureFrame(device, &frame_current); + } + SDL_CloseVideoCapture(device); + } + + texture_updated = 0; + + SDL_ClearError(); + + SDL_Log("Try to open:%s", SDL_GetVideoCaptureDeviceName(get_instance_id(current_dev))); + + obtained.width = 640 * 2; + obtained.height = 360 * 2; + device = SDL_OpenVideoCaptureWithSpec(get_instance_id(current_dev), &obtained, &obtained, SDL_VIDEO_CAPTURE_ALLOW_ANY_CHANGE); + + /* spec may have changed because of re-open */ + if (texture) { + SDL_DestroyTexture(texture); + texture = NULL; + } + + SDL_Log("Open device:%p %s", (void*)device, SDL_GetError()); + stopped = 0; + } + + if (sym == SDLK_l) { + int num = 0; + SDL_VideoCaptureDeviceID *devices; + int i; + devices = SDL_GetVideoCaptureDevices(&num); + + SDL_Log("Num devices : %d", num); + for (i = 0; i < num; i++) { + SDL_Log("Device %d/%d : %s", i, num, SDL_GetVideoCaptureDeviceName(devices[i])); + } + SDL_free(devices); + + SAVE_CAPTURE_STATE(current_dev); + + current_dev += 1; + if (current_dev == num || current_dev >= (int) SDL_arraysize(data_capture_tab)) { + current_dev = 0; + } + + RESTORE_CAPTURE_STATE(current_dev); + SDL_Log("--> select dev %d / %d", current_dev, num); + } + + if (sym == SDLK_i) { + SDL_VideoCaptureStatus status = SDL_GetVideoCaptureStatus(device); + if (status == SDL_VIDEO_CAPTURE_STOPPED) { SDL_Log("STOPPED"); } + if (status == SDL_VIDEO_CAPTURE_PLAYING) { SDL_Log("PLAYING"); } + if (status == SDL_VIDEO_CAPTURE_INIT) { SDL_Log("INIT"); } + } + + if (sym == SDLK_s) { + if (stopped) { + SDL_Log("Stop"); + SDL_StopVideoCapture(device); + } else { + SDL_Log("Start"); + SDL_StartVideoCapture(device); + } + stopped = !stopped; + } + + if (sym == SDLK_f) { + SDL_Log("List formats"); + + if (!device) { + device = SDL_OpenVideoCapture(get_instance_id(current_dev)); + } + + /* List formats */ + { + int i, num = SDL_GetNumVideoCaptureFormats(device); + for (i = 0; i < num; i++) { + Uint32 format; + SDL_GetVideoCaptureFormat(device, i, &format); + SDL_Log("format %d/%d : %s", i, num, SDL_GetPixelFormatName(format)); + { + int w, h; + int j, num2 = SDL_GetNumVideoCaptureFrameSizes(device, format); + for (j = 0; j < num2; j++) { + SDL_GetVideoCaptureFrameSize(device, format, j, &w, &h); + SDL_Log(" framesizes %d/%d : %d x %d", j, num2, w, h); + } + } + } + } + } + if (sym == SDLK_ESCAPE || sym == SDLK_AC_BACK) { + quit = 1; + SDL_Log("Key : Escape!"); + } + + if (sym == SDLK_h || sym == SDLK_F1) { + SDL_Log("%s", usage); + } + } + + + SAVE_CAPTURE_STATE(current_dev); + + { + int i, n = SDL_arraysize(data_capture_tab); + for (i = 0; i < n; i++) { + RESTORE_CAPTURE_STATE(i); + + if (!device) { + /* device has been closed */ + frame_current.num_planes = 0; + texture_updated = 0; + } else { + int ret; + SDL_VideoCaptureFrame frame_next; + SDL_zero(frame_next); + + ret = SDL_AcquireVideoCaptureFrame(device, &frame_next); + if (ret < 0) { + SDL_Log("dev[%d] err SDL_AcquireVideoCaptureFrame: %s", i, SDL_GetError()); + } +#if 1 + if (frame_next.num_planes) { + SDL_Log("dev[%d] frame: %p at %" SDL_PRIu64, i, (void*)frame_next.data[0], frame_next.timestampNS); + } +#endif + + if (frame_next.num_planes) { + + update_fps(&fps_capture); + + if (frame_current.num_planes) { + ret = SDL_ReleaseVideoCaptureFrame(device, &frame_current); + if (ret < 0) { + SDL_Log("dev[%d] err SDL_ReleaseVideoCaptureFrame: %s", i, SDL_GetError()); + } + } + frame_current = frame_next; + texture_updated = 0; + } + } + + SAVE_CAPTURE_STATE(i); + } + } + + + RESTORE_CAPTURE_STATE(current_dev); + + + + /* Moving square */ + SDL_SetRenderDrawColor(renderer, 0, 0xff, 0, 255); + { + SDL_FRect r; + static float x = 0; + x += 10; + if (x > 1000) { + x = 0; + } + r.x = x; + r.y = 100; + r.w = r.h = 10; + SDL_RenderFillRect(renderer, &r); + } + + SDL_SetRenderDrawColor(renderer, 0x33, 0x33, 0x33, 255); + + + SAVE_CAPTURE_STATE(current_dev); + + { + int i, n = SDL_arraysize(data_capture_tab); + for (i = 0; i < n; i++) { + RESTORE_CAPTURE_STATE(i); + + /* Update SDL_Texture with last video frame (only once per new frame) */ + if (frame_current.num_planes && texture_updated == 0) { + + /* Create texture with appropriate format (for DMABUF or not) */ + if (texture == NULL) { + Uint32 format = obtained.format; + texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_STATIC, obtained.width, obtained.height); + if (texture == NULL) { + SDL_Log("Couldn't create texture: %s", SDL_GetError()); + return 1; + } + } + + { + /* Use software data */ + if (frame_current.num_planes == 1) { + SDL_UpdateTexture(texture, NULL, + frame_current.data[0], frame_current.pitch[0]); + } else if (frame_current.num_planes == 2) { + SDL_UpdateNVTexture(texture, NULL, + frame_current.data[0], frame_current.pitch[0], + frame_current.data[1], frame_current.pitch[1]); + } else if (frame_current.num_planes == 3) { + SDL_UpdateYUVTexture(texture, NULL, frame_current.data[0], frame_current.pitch[0], + frame_current.data[1], frame_current.pitch[1], + frame_current.data[2], frame_current.pitch[2]); + } + texture_updated = 1; + } + } + + SAVE_CAPTURE_STATE(i); + } + } + + + RESTORE_CAPTURE_STATE(current_dev); + + { + int i, n = SDL_arraysize(data_capture_tab); + int win_w, win_h; + int total_texture_updated = 0; + int curr_texture_updated = 0; + for (i = 0; i < n; i++) { + if (data_capture_tab[i].texture_updated) { + total_texture_updated += 1; + } + } + + SDL_GetRenderOutputSize(renderer, &win_w, &win_h); + + + for (i = 0; i < n; i++) { + RESTORE_CAPTURE_STATE(i); + /* RenderCopy the SDL_Texture */ + if (texture_updated == 1) { + /* Scale texture to fit the screen */ + + int tw, th; + int w; + SDL_FRect d; + SDL_QueryTexture(texture, NULL, NULL, &tw, &th); + + w = win_w / total_texture_updated; + + if (tw > w - 20) { + float scale = (float) (w - 20) / (float) tw; + tw = w - 20; + th = (int)((float) th * scale); + } + d.x = (float)(10 + curr_texture_updated * w); + d.y = (float)(win_h - th); + d.w = (float)tw; + d.h = (float)(th - 10); + SDL_RenderTexture(renderer, texture, NULL, &d); + + curr_texture_updated += 1; + } + } + + } + + RESTORE_CAPTURE_STATE(current_dev); + + + /* display status and FPS */ + if (!device) { +#ifdef __IOS__ + const float x_offset = 500; +#else + const float x_offset = 0; +#endif + char buf[256]; + SDL_snprintf(buf, 256, "Device %d (%s) is not opened", current_dev, SDL_GetVideoCaptureDeviceName(get_instance_id(current_dev))); + SDLTest_DrawString(renderer, x_offset + 10, 10, buf); + } else { +#ifdef __IOS__ + const float x_offset = 500; +#else + const float x_offset = 0; +#endif + const char *status = "no status"; + char buf[256]; + + if (device) { + SDL_VideoCaptureStatus s = SDL_GetVideoCaptureStatus(device); + if (s == SDL_VIDEO_CAPTURE_INIT) { + status = "init"; + } else if (s == SDL_VIDEO_CAPTURE_PLAYING) { + status = "playing"; + } else if (s == SDL_VIDEO_CAPTURE_STOPPED) { + status = "stopped"; + } else if (s == SDL_VIDEO_CAPTURE_FAIL) { + status = "failed"; + } + + } + + /* capture device, capture fps, capture status */ + SDL_snprintf(buf, 256, "Device %d - %2.2f fps - %s", current_dev, fps_capture.last_fps, status); + SDLTest_DrawString(renderer, x_offset + 10, 10, buf); + + /* capture spec */ + SDL_snprintf(buf, sizeof(buf), "%d x %d %s", obtained.width, obtained.height, SDL_GetPixelFormatName(obtained.format)); + SDLTest_DrawString(renderer, x_offset + 10, 20, buf); + + /* video fps */ + SDL_snprintf(buf, sizeof(buf), "%2.2f fps", fps_main.last_fps); + SDLTest_DrawString(renderer, x_offset + 10, 30, buf); + + } + + /* display last error */ + { + SDLTest_DrawString(renderer, 400, 10, SDL_GetError()); + } + + /* display load average */ +#if defined(__linux__) && !defined(__ANDROID__) + { + float val = 0.0f; + char buf[128]; + load_average(&val); + if (val != 0.0f) { + SDL_snprintf(buf, sizeof(buf), "load avg %2.2f percent", val); + SDLTest_DrawString(renderer, 800, 10, buf); + } + } +#endif + + + SDL_Delay(20); + SDL_RenderPresent(renderer); + + update_fps(&fps_main); + + } + + + + SAVE_CAPTURE_STATE(current_dev); + + { + int i, n = SDL_arraysize(data_capture_tab); + for (i = 0; i < n; i++) { + RESTORE_CAPTURE_STATE(i); + + if (device) { + if (SDL_StopVideoCapture(device) < 0) { + SDL_Log("error SDL_StopVideoCapture(): %s", SDL_GetError()); + } + if (frame_current.num_planes) { + SDL_ReleaseVideoCaptureFrame(device, &frame_current); + } + SDL_CloseVideoCapture(device); + } + + if (texture) { + SDL_DestroyTexture(texture); + } + } + } + + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + + SDL_Quit(); + + SDLTest_CommonDestroyState(state); + + return 0; +} diff --git a/test/testvideocaptureminimal.c b/test/testvideocaptureminimal.c new file mode 100644 index 000000000..870dcab66 --- /dev/null +++ b/test/testvideocaptureminimal.c @@ -0,0 +1,206 @@ +/* + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely. +*/ +#include "SDL3/SDL_main.h" +#include "SDL3/SDL.h" +#include "SDL3/SDL_test.h" +#include "SDL3/SDL_video_capture.h" +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int main(int argc, char **argv) +{ + SDL_Window *window = NULL; + SDL_Renderer *renderer = NULL; + SDL_Event evt; + int quit = 0; + SDLTest_CommonState *state = NULL; + + SDL_VideoCaptureDevice *device = NULL; + SDL_VideoCaptureSpec obtained; + + SDL_VideoCaptureFrame frame_current; + SDL_Texture *texture = NULL; + int texture_updated = 0; + + SDL_zero(evt); + SDL_zero(obtained); + SDL_zero(frame_current); + + /* Set 0 to disable TouchEvent to be duplicated as MouseEvent with SDL_TOUCH_MOUSEID */ + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); + /* Set 0 to disable MouseEvent to be duplicated as TouchEvent with SDL_MOUSE_TOUCHID */ + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); + + /* Initialize test framework */ + state = SDLTest_CommonCreateState(argv, 0); + if (state == NULL) { + return 1; + } + + /* Enable standard application logging */ + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO); + + /* Load the SDL library */ + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s", SDL_GetError()); + return 1; + } + + window = SDL_CreateWindow("Local Video", 1000, 800, 0); + if (window == NULL) { + SDL_Log("Couldn't create window: %s", SDL_GetError()); + return 1; + } + + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); + + renderer = SDL_CreateRenderer(window, NULL, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (renderer == NULL) { + /* SDL_Log("Couldn't create renderer: %s", SDL_GetError()); */ + return 1; + } + + device = SDL_OpenVideoCaptureWithSpec(0, NULL, &obtained, SDL_VIDEO_CAPTURE_ALLOW_ANY_CHANGE); + if (!device) { + SDL_Log("No video capture? %s", SDL_GetError()); + return 1; + } + + if (SDL_StartVideoCapture(device) < 0) { + SDL_Log("error SDL_StartVideoCapture(): %s", SDL_GetError()); + return 1; + } + + /* Create texture with appropriate format */ + if (texture == NULL) { + texture = SDL_CreateTexture(renderer, obtained.format, SDL_TEXTUREACCESS_STATIC, obtained.width, obtained.height); + if (texture == NULL) { + SDL_Log("Couldn't create texture: %s", SDL_GetError()); + return 1; + } + } + + while (!quit) { + while (SDL_PollEvent(&evt)) { + int sym = 0; + switch (evt.type) + { + case SDL_EVENT_KEY_DOWN: + { + sym = evt.key.keysym.sym; + break; + } + + case SDL_EVENT_QUIT: + { + quit = 1; + SDL_Log("Ctlr+C : Quit!"); + } + } + + if (sym == SDLK_ESCAPE || sym == SDLK_AC_BACK) { + quit = 1; + SDL_Log("Key : Escape!"); + } + } + + { + SDL_VideoCaptureFrame frame_next; + SDL_zero(frame_next); + + if (SDL_AcquireVideoCaptureFrame(device, &frame_next) < 0) { + SDL_Log("err SDL_AcquireVideoCaptureFrame: %s", SDL_GetError()); + } +#if 0 + if (frame_next.num_planes) { + SDL_Log("frame: %p at %" SDL_PRIu64, (void*)frame_next.data[0], frame_next.timestampNS); + } +#endif + + if (frame_next.num_planes) { + if (frame_current.num_planes) { + if (SDL_ReleaseVideoCaptureFrame(device, &frame_current) < 0) { + SDL_Log("err SDL_ReleaseVideoCaptureFrame: %s", SDL_GetError()); + } + } + + /* It's not needed to keep the frame once updated the texture is updated. + * But in case of 0-copy, it's needed to have the frame while using the texture. + */ + frame_current = frame_next; + texture_updated = 0; + } + } + + /* Update SDL_Texture with last video frame (only once per new frame) */ + if (frame_current.num_planes && texture_updated == 0) { + /* Use software data */ + if (frame_current.num_planes == 1) { + SDL_UpdateTexture(texture, NULL, + frame_current.data[0], frame_current.pitch[0]); + } else if (frame_current.num_planes == 2) { + SDL_UpdateNVTexture(texture, NULL, + frame_current.data[0], frame_current.pitch[0], + frame_current.data[1], frame_current.pitch[1]); + } else if (frame_current.num_planes == 3) { + SDL_UpdateYUVTexture(texture, NULL, frame_current.data[0], frame_current.pitch[0], + frame_current.data[1], frame_current.pitch[1], + frame_current.data[2], frame_current.pitch[2]); + } + texture_updated = 1; + } + + SDL_SetRenderDrawColor(renderer, 0x99, 0x99, 0x99, 255); + SDL_RenderClear(renderer); + { + int win_w, win_h, tw, th, w; + SDL_FRect d; + SDL_QueryTexture(texture, NULL, NULL, &tw, &th); + SDL_GetRenderOutputSize(renderer, &win_w, &win_h); + w = win_w; + if (tw > w - 20) { + float scale = (float) (w - 20) / (float) tw; + tw = w - 20; + th = (int)((float) th * scale); + } + d.x = (float)(10 ); + d.y = (float)(win_h - th); + d.w = (float)tw; + d.h = (float)(th - 10); + SDL_RenderTexture(renderer, texture, NULL, &d); + } + SDL_Delay(10); + SDL_RenderPresent(renderer); + } + + if (SDL_StopVideoCapture(device) < 0) { + SDL_Log("error SDL_StopVideoCapture(): %s", SDL_GetError()); + } + if (frame_current.num_planes) { + SDL_ReleaseVideoCaptureFrame(device, &frame_current); + } + SDL_CloseVideoCapture(device); + + if (texture) { + SDL_DestroyTexture(texture); + } + + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + SDLTest_CommonDestroyState(state); + + return 0; +}