Bladeren bron

Add SDL Video Capture, with back-end for linux/macos/ios/android

Sylvain 1 jaar geleden
bovenliggende
commit
59f93e20a7

+ 9 - 0
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)

+ 3 - 0
VisualC-WinRT/SDL-UWP.vcxproj

@@ -89,6 +89,7 @@
     <ClInclude Include="..\include\SDL3\SDL_types.h" />
     <ClInclude Include="..\include\SDL3\SDL_version.h" />
     <ClInclude Include="..\include\SDL3\SDL_video.h" />
+    <ClInclude Include="..\include\SDL3\SDL_video_capture.h" />
     <ClInclude Include="..\src\audio\disk\SDL_diskaudio.h" />
     <ClInclude Include="..\src\audio\dummy\SDL_dummyaudio.h" />
     <ClInclude Include="..\src\audio\SDL_audiodev_c.h" />
@@ -180,6 +181,7 @@
     <ClInclude Include="..\src\video\SDL_RLEaccel_c.h" />
     <ClInclude Include="..\src\video\SDL_shape_internals.h" />
     <ClInclude Include="..\src\video\SDL_sysvideo.h" />
+    <ClInclude Include="..\src\video\SDL_sysvidocapture.h" />
     <ClInclude Include="..\src\video\SDL_yuv_c.h" />
     <ClInclude Include="..\src\video\winrt\SDL_winrtevents_c.h" />
     <ClInclude Include="..\src\video\winrt\SDL_winrtgamebar_cpp.h" />
@@ -520,6 +522,7 @@
     <ClCompile Include="..\src\video\SDL_stretch.c" />
     <ClCompile Include="..\src\video\SDL_surface.c" />
     <ClCompile Include="..\src\video\SDL_video.c" />
+    <ClCompile Include="..\src\video\SDL_video_capture.c" />
     <ClCompile Include="..\src\video\SDL_video_unsupported.c" />
     <ClCompile Include="..\src\video\SDL_yuv.c" />
     <ClCompile Include="..\src\video\winrt\SDL_winrtevents.cpp">

+ 9 - 0
VisualC-WinRT/SDL-UWP.vcxproj.filters

@@ -165,6 +165,9 @@
     <ClInclude Include="..\include\SDL3\SDL_video.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\include\SDL3\SDL_video_capture.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="..\src\joystick\SDL_gamepad_c.h">
       <Filter>Header Files</Filter>
     </ClInclude>
@@ -405,6 +408,9 @@
     <ClInclude Include="..\src\video\SDL_sysvideo.h">
       <Filter>Source Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\src\video\SDL_sysvideocapture.h">
+      <Filter>Source Files</Filter>
+    </ClInclude>
     <ClInclude Include="..\src\video\winrt\SDL_winrtevents_c.h">
       <Filter>Source Files</Filter>
     </ClInclude>
@@ -807,6 +813,9 @@
     <ClCompile Include="..\src\video\SDL_video.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\video\SDL_video_capture.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\video\SDL_video_unsupported.c">
       <Filter>Source Files</Filter>
     </ClCompile>

+ 1 - 0
VisualC/SDL/SDL.vcxproj

@@ -653,6 +653,7 @@
     <ClCompile Include="..\..\src\video\SDL_surface.c" />
     <ClCompile Include="..\..\src\video\SDL_video.c" />
     <ClCompile Include="..\..\src\video\SDL_video_unsupported.c" />
+    <ClCompile Include="..\..\src\video\SDL_video_capture.c" />
     <ClCompile Include="..\..\src\video\SDL_vulkan_utils.c" />
     <ClCompile Include="..\..\src\video\SDL_yuv.c" />
     <ClCompile Include="..\..\src\video\windows\SDL_windowsclipboard.c" />

+ 3 - 0
VisualC/SDL/SDL.vcxproj.filters

@@ -1185,6 +1185,9 @@
     <ClCompile Include="..\..\src\video\SDL_video_unsupported.c">
       <Filter>video</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\video\SDL_video_capture.c">
+      <Filter>video</Filter>
+    </ClCompile>
     <ClCompile Include="..\..\src\video\SDL_yuv.c">
       <Filter>video</Filter>
     </ClCompile>

+ 1 - 0
include/SDL3/SDL.h

@@ -76,6 +76,7 @@
 #include <SDL3/SDL_touch.h>
 #include <SDL3/SDL_version.h>
 #include <SDL3/SDL_video.h>
+#include "SDL3/SDL_video_capture.h"
 #include <SDL3/SDL_oldnames.h>
 
 #endif /* SDL_h_ */

+ 369 - 0
include/SDL3/SDL_video_capture.h

@@ -0,0 +1,369 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+/**
+ *  \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 <SDL3/SDL_begin_code.h>
+/* 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 <SDL3/SDL_close_code.h>
+
+#endif /* SDL_video_capture_h_ */

+ 4 - 0
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@

+ 2 - 0
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
 

+ 2 - 0
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
 

+ 16 - 0
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: *;
 };

+ 16 - 0
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

+ 16 - 0
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)

+ 90 - 0
src/video/SDL_sysvideocapture.h

@@ -0,0 +1,90 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../SDL_internal.h"
+
+#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_ */

+ 10 - 0
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 <SDL3/SDL_opengl.h>
@@ -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();

+ 948 - 0
src/video/SDL_video_capture.c

@@ -0,0 +1,948 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#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 */

+ 615 - 0
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 <AVFoundation/AVFoundation.h>
+#import <CoreMedia/CoreMedia.h>
+
+/*
+ * Need to link with:: CoreMedia CoreVideo
+ *
+ * Add in pInfo.list:
+ *  <key>NSCameraUsageDescription</key> <string>Access camera</string>
+ *
+ *
+ * MACOSX:
+ * Add to the Code Sign Entitlement file:
+ * <key>com.apple.security.device.camera</key> <true/>
+ *
+ *
+ * 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<AVCaptureDevice *> *
+discover_devices()
+{
+    NSArray *deviceType = @[AVCaptureDeviceTypeBuiltInWideAngleCamera];
+
+    AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession
+                    discoverySessionWithDeviceTypes:deviceType
+                    mediaType:AVMediaTypeVideo
+                    position:AVCaptureDevicePositionUnspecified];
+
+    NSArray<AVCaptureDevice *> *devices = discoverySession.devices;
+
+    if ([devices count] > 0) {
+        return devices;
+    } else {
+        AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
+        if (captureDevice == nil) {
+            return devices;
+        } else {
+            NSArray<AVCaptureDevice *> *default_device = @[ captureDevice ];
+            return default_device;
+        }
+    }
+
+    return devices;
+}
+
+static AVCaptureDevice *
+get_device_by_name(const char *dev_name)
+{
+    NSArray<AVCaptureDevice *> *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<AVCaptureVideoDataOutputSampleBufferDelegate>
+    @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<AVCaptureDeviceFormat *> *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<NSString *> *array_formats = [NSMutableOrderedSet new];
+        NSArray<AVCaptureDeviceFormat *> *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<NSString *> *array_formats = [NSMutableOrderedSet new];
+        NSArray<AVCaptureDeviceFormat *> *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<AVCaptureDeviceFormat *> *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<AVCaptureDeviceFormat *> *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<AVCaptureDevice *> *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<AVCaptureDevice *> *devices = discover_devices();
+    return [devices count];
+}
+
+#endif /* HAVE_COREMEDIA */
+
+#endif /* SDL_VIDEO_CAPTURE */
+

+ 33 - 0
src/video/SDL_video_capture_c.h

@@ -0,0 +1,33 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../SDL_internal.h"
+#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_ */

+ 965 - 0
src/video/SDL_video_capture_v4l2.c

@@ -0,0 +1,965 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_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 <unistd.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>              /* low-level i/o */
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <linux/videodev2.h>
+
+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 */

+ 678 - 0
src/video/android/SDL_android_video_capture.c

@@ -0,0 +1,678 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#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:
+ *   <uses-permission android:name="android.permission.CAMERA"></uses-permission>
+ *   <uses-feature android:name="android.hardware.camera" />
+ *
+ *
+ * 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 <camera/NdkCameraDevice.h>
+#include <camera/NdkCameraManager.h>
+#include <media/NdkImage.h>
+#include <media/NdkImageReader.h>
+
+#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
+
+

+ 2 - 0
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)

+ 770 - 0
test/testvideocapture.c

@@ -0,0 +1,770 @@
+/*
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+#include "SDL3/SDL_main.h"
+#include "SDL3/SDL.h"
+#include "SDL3/SDL_test.h"
+#include "SDL3/SDL_video_capture.h"
+#include <stdio.h>
+
+#ifdef __EMSCRIPTEN__
+#include <emscripten/emscripten.h>
+#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;
+}

+ 206 - 0
test/testvideocaptureminimal.c

@@ -0,0 +1,206 @@
+/*
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+#include "SDL3/SDL_main.h"
+#include "SDL3/SDL.h"
+#include "SDL3/SDL_test.h"
+#include "SDL3/SDL_video_capture.h"
+#include <stdio.h>
+
+#ifdef __EMSCRIPTEN__
+#include <emscripten/emscripten.h>
+#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;
+}