diff --git a/Baballonia.sln b/Baballonia.sln
index eb8a3ad8..e151a5ef 100644
--- a/Baballonia.sln
+++ b/Baballonia.sln
@@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baballonia.FastCorruptionDe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baballonia.CaptureBin.IO", "src\Baballonia.CaptureBin.IO\Baballonia.CaptureBin.IO.csproj", "{8D298C1C-774E-4BF0-A107-D5D3985C7A9B}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baballonia.LibV4L2Capture", "src\Baballonia.LibV4L2Capture\Baballonia.LibV4L2Capture.csproj", "{02E9F6A2-A443-491D-93FF-6F002F3C494F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -120,6 +122,10 @@ Global
{8D298C1C-774E-4BF0-A107-D5D3985C7A9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D298C1C-774E-4BF0-A107-D5D3985C7A9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D298C1C-774E-4BF0-A107-D5D3985C7A9B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {02E9F6A2-A443-491D-93FF-6F002F3C494F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {02E9F6A2-A443-491D-93FF-6F002F3C494F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {02E9F6A2-A443-491D-93FF-6F002F3C494F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {02E9F6A2-A443-491D-93FF-6F002F3C494F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -135,6 +141,7 @@ Global
{575BB586-5394-4F94-A178-DF4062EC44A1} = {82C6A0B4-5014-4BE4-9F52-7DC1989134E3}
{4B08BDE8-099A-405F-AB71-094E8FC24AB1} = {82C6A0B4-5014-4BE4-9F52-7DC1989134E3}
{8D298C1C-774E-4BF0-A107-D5D3985C7A9B} = {82C6A0B4-5014-4BE4-9F52-7DC1989134E3}
+ {02E9F6A2-A443-491D-93FF-6F002F3C494F} = {82C6A0B4-5014-4BE4-9F52-7DC1989134E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8D6643ED-DDC1-4236-9471-024DCCFC81F6}
diff --git a/src/Baballonia.Desktop/Baballonia.Desktop.csproj b/src/Baballonia.Desktop/Baballonia.Desktop.csproj
index 70d9febf..f0568851 100644
--- a/src/Baballonia.Desktop/Baballonia.Desktop.csproj
+++ b/src/Baballonia.Desktop/Baballonia.Desktop.csproj
@@ -45,6 +45,7 @@
+
@@ -131,10 +132,12 @@
+
+
diff --git a/src/Baballonia.LibV4L2Capture/Baballonia.LibV4L2Capture.csproj b/src/Baballonia.LibV4L2Capture/Baballonia.LibV4L2Capture.csproj
new file mode 100644
index 00000000..e6d41887
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/Baballonia.LibV4L2Capture.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs
new file mode 100644
index 00000000..569d262a
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs
@@ -0,0 +1,115 @@
+using System.Runtime.InteropServices;
+using Baballonia.LibV4L2Capture.V4L2;
+using Microsoft.Extensions.Logging;
+using OpenCvSharp;
+using Capture = Baballonia.SDK.Capture;
+
+namespace Baballonia.LibV4L2Capture;
+
+public sealed class LibV4L2Capture(string source, ILogger logger) : Capture(source, logger)
+{
+ private Device? _device;
+ private CancellationTokenSource? _cts;
+ private Task? _captureTask;
+
+ public override Task StartCapture()
+ {
+ try
+ {
+ _device = Device.Connect(Source);
+
+ if (_device == null)
+ return Task.FromResult(false);
+
+ Logger.LogInformation($"Using pixel format: {_device.PixelFormat}");
+
+ _device.StartCapture();
+ IsReady = true;
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e.ToString());
+ return Task.FromResult(false);
+ }
+
+ _cts = new CancellationTokenSource();
+ var token = _cts.Token;
+
+ _captureTask = Task.Run(() => VideoCapture_UpdateLoop(token), token);
+
+ return Task.FromResult(true);
+ }
+
+ private void DecodeMJPEG(byte[] frame)
+ {
+ Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale);
+ SetRawMat(mat);
+ }
+
+ private void DecodeYUYV(byte[] frame, uint width, uint height)
+ {
+ Mat yuyvMat = new Mat((int)height, (int)width, MatType.CV_8UC2);
+ Marshal.Copy(frame, 0, yuyvMat.Data, frame.Length);
+
+ Mat grayMat = new Mat();
+ Cv2.CvtColor(yuyvMat, grayMat, ColorConversionCodes.YUV2GRAY_YUY2);
+ SetRawMat(grayMat);
+ }
+
+ private async Task VideoCapture_UpdateLoop(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested && _device != null)
+ {
+ try
+ {
+ if (_device.CaptureFrame(out byte[]? frame))
+ {
+ if (frame is { Length: > 0 })
+ {
+ switch (_device.PixelFormat)
+ {
+ case v4l2_pix_fmt.V4L2_PIX_FMT_MJPEG:
+ DecodeMJPEG(frame);
+ break;
+ case v4l2_pix_fmt.V4L2_PIX_FMT_YUYV:
+ var pix = _device.CurrentFormat.pix;
+ DecodeYUYV(frame, pix.width, pix.height);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+ else
+ {
+ await Task.Delay(1, ct);
+ }
+ }
+ catch(Exception e)
+ {
+ SetRawMat(new Mat());
+ IsReady = false;
+ Logger.LogError(e.ToString());
+ _device.Dispose();
+ break;
+ }
+ }
+ }
+
+ public override Task StopCapture()
+ {
+ if (_device is null)
+ return Task.FromResult(false);
+
+ if (_captureTask != null)
+ {
+ _cts?.Cancel();
+ _captureTask.Wait();
+ }
+
+ IsReady = false;
+ _device?.Dispose();
+ _device = null;
+ return Task.FromResult(true);
+ }
+}
diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs b/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs
new file mode 100644
index 00000000..c0dd22e5
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs
@@ -0,0 +1,29 @@
+using Baballonia.SDK;
+using Microsoft.Extensions.Logging;
+
+namespace Baballonia.LibV4L2Capture;
+
+public class LibV4L2CaptureFactory : ICaptureFactory
+{
+ private readonly ILoggerFactory _loggerFactory;
+
+ public LibV4L2CaptureFactory(ILoggerFactory loggerFactory)
+ {
+ _loggerFactory = loggerFactory;
+ }
+
+ public Capture Create(string address)
+ {
+ return new LibV4L2Capture(address, _loggerFactory.CreateLogger());
+ }
+
+ public bool CanConnect(string address)
+ {
+ return address.StartsWith("/dev/video");
+ }
+
+ public string GetProviderName()
+ {
+ return "Video4Linux2";
+ }
+}
diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Data.cs b/src/Baballonia.LibV4L2Capture/V4L2/Data.cs
new file mode 100644
index 00000000..bb4b92d0
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/V4L2/Data.cs
@@ -0,0 +1,220 @@
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Baballonia.LibV4L2Capture.V4L2;
+
+public class Data {
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public unsafe struct v4l2_capability
+ {
+ public fixed byte driver[16];
+ public fixed byte card[32];
+ public fixed byte bus_info[32];
+
+ public uint version;
+ public V4L2Capabilities capabilities;
+ public uint device_caps;
+ public fixed uint reserved[3];
+
+ public bool HasFlag(V4L2Capabilities flag)
+ {
+ return (capabilities & flag) != 0;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public unsafe struct v4l2_fmtdesc
+ {
+ public uint index;
+ public v4l2_buf_type type;
+ public uint flags;
+
+ public fixed byte description[32]; // fixed buffer instead of byte[]
+ public v4l2_pix_fmt pixelformat;
+ public fixed uint reserved[4];
+
+ public string StringDescription
+ {
+ get
+ {
+ fixed (byte* p = description)
+ {
+ int len = 0;
+ while (len < 32 && p[len] != 0) len++;
+ return Encoding.ASCII.GetString(p, len);
+ }
+ }
+ }
+ }
+
+ public const int VIDEO_MAX_PLANES = 8;
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct v4l2_pix_format
+ {
+ public uint width;
+ public uint height;
+ public v4l2_pix_fmt pixelformat;
+ public uint field;
+ public uint bytesperline;
+ public uint sizeimage;
+ public uint colorspace;
+ public uint priv;
+ public uint flags;
+ public uint ycbcr_enc;
+ public uint quantization;
+ public uint xfer_func;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct v4l2_rect
+ {
+ public uint left;
+ public uint top;
+ public uint width;
+ public uint height;
+ };
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 208)] // I have no idea why this isn't 204, but check in C the size of the struct is 208
+ public unsafe struct v4l2_format
+ {
+ [FieldOffset(0)] public v4l2_buf_type type;
+
+ [FieldOffset(8)] public v4l2_pix_format pix;
+
+ // Replace byte[] with fixed buffer
+ [FieldOffset(8)]
+ public fixed byte raw_data[200];
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct v4l2_fract
+ {
+ public uint numerator;
+ public uint denominator;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct v4l2_frmival_stepwise
+ {
+ public v4l2_fract min;
+ public v4l2_fract max;
+ public v4l2_fract step;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct v4l2_frmsize_discrete
+ {
+ public uint width;
+ public uint height;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct v4l2_frmsize_stepwise
+ {
+ public uint min_width;
+ public uint max_width;
+ public uint step_width;
+ public uint min_height;
+ public uint max_height;
+ public uint step_height;
+ }
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1)]
+ public unsafe struct v4l2_frmsizeenum
+ {
+ [FieldOffset(0)] public uint index;
+ [FieldOffset(4)] public v4l2_pix_fmt pixel_format;
+ [FieldOffset(8)] public uint type;
+
+ // Union: discrete or stepwise
+ [FieldOffset(12)] public v4l2_frmsize_discrete discrete;
+ [FieldOffset(12)] public v4l2_frmsize_stepwise stepwise;
+
+ [FieldOffset(36)]
+ public fixed uint reserved[2];
+ }
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1)]
+ public unsafe struct v4l2_frmivalenum
+ {
+ [FieldOffset(0)] public uint index;
+ [FieldOffset(4)] public v4l2_pix_fmt pixel_format;
+ [FieldOffset(8)] public uint width;
+ [FieldOffset(12)] public uint height;
+ [FieldOffset(16)] public v4l2_frmivaltypes type;
+
+ // Union: discrete or stepwise
+ [FieldOffset(20)] public v4l2_fract discrete;
+ [FieldOffset(20)] public v4l2_frmival_stepwise stepwise;
+
+ [FieldOffset(44)]
+ public fixed uint reserved[2];
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public unsafe struct v4l2_requestbuffers
+ {
+ public uint count;
+ public v4l2_buf_type type;
+ public v4l2_memory memory;
+
+ public fixed uint reserved[2];
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct timeval
+ {
+ public long tv_sec; // seconds
+ public long tv_usec; // microseconds
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public unsafe struct v4l2_timecode
+ {
+ public uint type;
+ public uint flags;
+ public byte frames;
+ public byte seconds;
+ public byte minutes;
+ public byte hours;
+ public fixed byte userbits[4];
+ }
+
+ [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 88)] // same as for v4l2_format??
+ public struct v4l2_buffer
+ {
+ [FieldOffset(0)] public uint index;
+ [FieldOffset(4)] public v4l2_buf_type type;
+ [FieldOffset(8)] public uint bytesused;
+ [FieldOffset(12)] public uint flags;
+ [FieldOffset(16)] public uint field;
+ [FieldOffset(24)] public timeval timestamp;
+ [FieldOffset(40)] public v4l2_timecode timecode;
+ [FieldOffset(56)] public uint sequence;
+
+ [FieldOffset(60)] public v4l2_memory memory;
+
+ // Union of memory offsets/pointers
+ [FieldOffset(64)] public uint offset; // for MMAP
+ [FieldOffset(64)] public UIntPtr userptr; // for USERPTR
+ //[FieldOffset(64)] public v4l2_plane* planes; // pointer to v4l2_plane array
+ [FieldOffset(64)] public int fd; // for DMABUF
+
+ [FieldOffset(72)] public uint length;
+ [FieldOffset(76)] public uint reserved2;
+ [FieldOffset(80)] public uint reserved;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct pollfd
+ {
+ public int fd;
+ public short events;
+ public short revents;
+ }
+
+ public const short POLLIN = 0x0001;
+ public const short POLLERR = 0x0008;
+ public const short POLLHUP = 0x0010;
+}
diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs
new file mode 100644
index 00000000..8eefe917
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs
@@ -0,0 +1,351 @@
+using System.Runtime.InteropServices;
+
+namespace Baballonia.LibV4L2Capture.V4L2;
+
+
+public class Device : IDisposable {
+ public void Dispose()
+ {
+ StopCapture();
+
+ for (int i = 0; i < _bufferStarts.Length; i++)
+ {
+ if (_bufferStarts[i] != IntPtr.Zero && _bufferLengths[i] > 0)
+ NativeMethods.munmap(_bufferStarts[i], _bufferLengths[i]);
+ }
+
+ Data.v4l2_requestbuffers req = default;
+ req.count = 0;
+ req.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ req.memory = v4l2_memory.V4L2_MEMORY_MMAP;
+ NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_REQBUFS, ref req);
+
+ _bufferCount = 0;
+
+ if (_fileDescriptor >= 0)
+ {
+ NativeMethods.v4l2_close(_fileDescriptor);
+ _fileDescriptor = -1;
+ }
+
+
+ }
+
+ private static readonly v4l2_pix_fmt[] SupportedFormats = [v4l2_pix_fmt.V4L2_PIX_FMT_MJPEG, v4l2_pix_fmt.V4L2_PIX_FMT_YUYV];
+
+ private const int O_RDWR = 2;
+
+ public string Address { get; private set; }
+ public v4l2_pix_fmt PixelFormat { get; private set; }
+ public Data.v4l2_format CurrentFormat { get; private set; }
+
+ private int _fileDescriptor;
+
+ private IntPtr[] _bufferStarts;
+ private uint[] _bufferLengths;
+ private uint _bufferCount;
+
+ public static Device? Connect(string address)
+ {
+ Device device = new Device
+ {
+ Address = address
+ };
+
+ if (!device.AttemptOpen())
+ return null;
+ Data.v4l2_capability caps = device.GetCapabilities();
+
+ if (!caps.HasFlag(V4L2Capabilities.VIDEO_CAPTURE)) throw new Exception("Device cannot capture video");
+
+ if (!caps.HasFlag(V4L2Capabilities.STREAMING)) throw new Exception("Device does not support streaming (required for mmap or userptr buffers)");
+
+ var formats = device.GetFormats().Where(f => SupportedFormats.Contains(f.pixelformat)).ToList();
+
+ if (formats.Count <= 0) throw new Exception($"Device does not support {string.Join(", ", SupportedFormats.Select(f => Enum.GetName(typeof(v4l2_pix_fmt), f)))}");
+
+ formats.Sort((a, b) =>
+ {
+ int indexA = Array.IndexOf(SupportedFormats, a.pixelformat);
+ int indexB = Array.IndexOf(SupportedFormats, b.pixelformat);
+ return indexA.CompareTo(indexB);
+ });
+
+ var format = formats[0];
+ device.PixelFormat = format.pixelformat;
+
+ Data.v4l2_frmivalenum bestInterval = default;
+ double maxFps = 0;
+ uint maxResolution = 0;
+
+ var sizes = device.EnumerateFrameSizes(device.PixelFormat);
+ foreach (var size in sizes)
+ {
+ var intervals = device.EnumerateFrameIntervals(device.PixelFormat, size.discrete.width, size.discrete.height);
+ foreach (var interval in intervals)
+ {
+ double fps;
+ switch (interval.type)
+ {
+ case v4l2_frmivaltypes.V4L2_FRMIVAL_TYPE_DISCRETE:
+ fps = (double)interval.discrete.denominator / interval.discrete.numerator;
+ break;
+ case v4l2_frmivaltypes.V4L2_FRMIVAL_TYPE_CONTINUOUS:
+ case v4l2_frmivaltypes.V4L2_FRMIVAL_TYPE_STEPWISE:
+ fps = (double)interval.stepwise.min.denominator / interval.stepwise.min.numerator;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ uint resolution = size.discrete.width * size.discrete.height;
+
+ if (fps > maxFps || (Math.Abs(fps - maxFps) < 0.001 && resolution > maxResolution))
+ {
+ maxFps = fps;
+ maxResolution = resolution;
+ bestInterval = interval;
+ }
+ }
+ }
+
+ device.SetFormat(bestInterval);
+
+ return device;
+ }
+
+ public Data.v4l2_capability GetCapabilities()
+ {
+ Data.v4l2_capability cap = default;
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_QUERYCAP, ref cap);
+
+ if (ret < 0) throw new Exception($"VIDIOC_QUERYCAP failed: errno={Marshal.GetLastWin32Error()}");
+ return cap;
+ }
+
+ public List GetFormats()
+ {
+ List formats = new List();
+ uint index = 0;
+ v4l2_buf_type type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+ while (true)
+ {
+ // declare fmt inside unsafe
+ Data.v4l2_fmtdesc fmt = default;
+ fmt.index = index;
+ fmt.type = type;
+ fmt.flags = 0;
+ fmt.pixelformat = 0;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_ENUM_FMT, ref fmt);
+ if (ret < 0) break; // no more formats
+
+ formats.Add(fmt);
+ index++;
+ }
+
+ return formats;
+ }
+
+ private Data.v4l2_format GetCurrentFormat()
+ {
+ Data.v4l2_format fmt = default;
+ fmt.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_G_FMT, ref fmt);
+ if (ret < 0) throw new Exception($"VIDIOC_G_FMT failed: errno={Marshal.GetLastWin32Error()}");
+
+ return fmt;
+ }
+
+ public void SetFormat(Data.v4l2_frmivalenum format)
+ {
+ Data.v4l2_format fmt = default;
+ fmt.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ fmt.pix.width = format.width;
+ fmt.pix.height = format.height;
+ fmt.pix.pixelformat = format.pixel_format;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_S_FMT, ref fmt);
+ if (ret < 0) throw new Exception($"VIDIOC_S_FMT failed: errno={Marshal.GetLastWin32Error()}");
+
+ CurrentFormat = fmt;
+ }
+
+ public List EnumerateFrameSizes(v4l2_pix_fmt pixelformat)
+ {
+ List sizes = new List();
+ uint index = 0;
+
+ while (true)
+ {
+ Data.v4l2_frmsizeenum fsize = default;
+ fsize.index = index;
+ fsize.pixel_format = pixelformat;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_ENUM_FRAMESIZES, ref fsize);
+
+ if (ret < 0) break;
+ sizes.Add(fsize);
+ index++;
+ }
+
+ return sizes;
+ }
+
+ public List EnumerateFrameIntervals(v4l2_pix_fmt pixelformat, uint width, uint height)
+ {
+ List intervals = new List();
+ uint index = 0;
+
+ while (true)
+ {
+ Data.v4l2_frmivalenum fival = default;
+ fival.index = index;
+ fival.pixel_format = pixelformat;
+ fival.width = width;
+ fival.height = height;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_ENUM_FRAMEINTERVALS, ref fival);
+
+ if (ret < 0) break;
+ intervals.Add(fival);
+ index++;
+ }
+
+ return intervals;
+ }
+
+ private Data.v4l2_requestbuffers GetBuffers()
+ {
+ Data.v4l2_requestbuffers req = default;
+ req.count = 3;
+ req.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ req.memory = v4l2_memory.V4L2_MEMORY_MMAP;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_REQBUFS, ref req);
+ if (ret < 0) throw new Exception($"VIDIOC_REQBUFS failed: errno={Marshal.GetLastWin32Error()}");
+
+ return req;
+ }
+
+ public void InitMMapBuffers()
+ {
+ var req = GetBuffers();
+ _bufferCount = req.count;
+
+ _bufferStarts = new IntPtr[_bufferCount];
+ _bufferLengths = new uint[_bufferCount];
+
+ for (uint i = 0; i < _bufferCount; i++)
+ {
+ Data.v4l2_buffer buf = default;
+ buf.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = v4l2_memory.V4L2_MEMORY_MMAP;
+ buf.index = i;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_QUERYBUF, ref buf);
+ if (ret < 0) throw new Exception($"VIDIOC_QUERYBUF failed: errno={Marshal.GetLastWin32Error()}");
+
+ _bufferLengths[i] = buf.length;
+ _bufferStarts[i] = NativeMethods.mmap(
+ IntPtr.Zero, buf.length,
+ Prot.PROT_READ | Prot.PROT_WRITE,
+ MapFlags.MAP_SHARED,
+ _fileDescriptor, new IntPtr(buf.offset));
+
+ if (_bufferStarts[i] == -1) throw new Exception($"mmap failed: errno={Marshal.GetLastWin32Error()}");
+ }
+ }
+
+ public void QueueAllBuffers()
+ {
+ for (uint i = 0; i < _bufferCount; i++)
+ {
+ Data.v4l2_buffer buf = default;
+ buf.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = v4l2_memory.V4L2_MEMORY_MMAP;
+ buf.index = i;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_QBUF, ref buf);
+ if (ret < 0) throw new Exception($"VIDIOC_QBUF failed: errno={Marshal.GetLastWin32Error()}");
+ }
+ }
+
+ public void StartStreaming()
+ {
+ v4l2_buf_type type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_STREAMON, ref type);
+ if (ret < 0) throw new Exception($"VIDIOC_STREAMON failed: errno={Marshal.GetLastWin32Error()}");
+ }
+
+ public bool FrameReady(int timeoutMs = 0)
+ {
+ Data.pollfd[] fds =
+ [
+ new()
+ {
+ fd = _fileDescriptor,
+ events = Data.POLLIN
+ }
+ ];
+
+ int ret = NativeMethods.poll(fds, 1, timeoutMs);
+ if (ret < 0)
+ throw new Exception($"poll failed: errno={Marshal.GetLastWin32Error()}");
+
+ short revents = fds[0].revents;
+
+ if ((revents & Data.POLLERR) != 0 || (revents & Data.POLLHUP) != 0)
+ throw new Exception("Device disconnected or error on file descriptor");
+
+ return (revents & Data.POLLIN) != 0;
+ }
+
+ public bool CaptureFrame(out byte[]? frame) {
+ frame = null;
+ if (!FrameReady())
+ return false;
+
+ Data.v4l2_buffer buf = default;
+ buf.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = v4l2_memory.V4L2_MEMORY_MMAP;
+
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_DQBUF, ref buf);
+ if (ret != 0)
+ throw new Exception($"VIDIOC_DQBUF failed: errno={Marshal.GetLastWin32Error()}");
+
+ frame = new byte[buf.bytesused];
+ Marshal.Copy(_bufferStarts[buf.index], frame, 0, (int)buf.bytesused);
+
+ ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_QBUF, ref buf);
+ if (ret < 0) throw new Exception($"VIDIOC_QBUF failed: errno={Marshal.GetLastWin32Error()}");
+ return true;
+ }
+
+ public void StopStreaming()
+ {
+ v4l2_buf_type type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_STREAMOFF, ref type);
+ //if (ret < 0) throw new Exception($"VIDIOC_STREAMOFF failed: errno={Marshal.GetLastWin32Error()}");
+ }
+
+ public void StartCapture()
+ {
+ var f = GetCurrentFormat();
+ InitMMapBuffers();
+ QueueAllBuffers();
+ StartStreaming();
+ }
+
+ public void StopCapture()
+ {
+ StopStreaming();
+ }
+
+ private bool AttemptOpen()
+ {
+ _fileDescriptor = NativeMethods.v4l2_open(Address, O_RDWR);
+ return _fileDescriptor >= 0;
+ }
+}
diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs b/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs
new file mode 100644
index 00000000..04a9c903
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs
@@ -0,0 +1,94 @@
+namespace Baballonia.LibV4L2Capture.V4L2;
+
+public enum v4l2_buf_type : uint
+{
+ V4L2_BUF_TYPE_VIDEO_CAPTURE = 1,
+ V4L2_BUF_TYPE_VIDEO_OUTPUT = 2,
+ V4L2_BUF_TYPE_VIDEO_OVERLAY = 3,
+ V4L2_BUF_TYPE_VBI_CAPTURE = 4,
+ V4L2_BUF_TYPE_VBI_OUTPUT = 5,
+ V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6,
+ V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7,
+ V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,
+ V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9,
+ V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE = 10,
+ V4L2_BUF_TYPE_SDR_CAPTURE = 11,
+ V4L2_BUF_TYPE_SDR_OUTPUT = 12,
+ V4L2_BUF_TYPE_META_CAPTURE = 13,
+
+ // Deprecated, do not use
+ V4L2_BUF_TYPE_PRIVATE = 0x80,
+}
+
+public enum v4l2_pix_fmt : uint
+{
+ V4L2_PIX_FMT_MJPEG = 1196444237,
+ V4L2_PIX_FMT_YUYV = 1448695129,
+}
+
+[Flags]
+public enum V4L2Capabilities : uint
+{
+ VIDEO_CAPTURE = 0x00000001,
+ VIDEO_OUTPUT = 0x00000002,
+ VIDEO_OVERLAY = 0x00000004,
+ VBI_CAPTURE = 0x00000010,
+ VBI_OUTPUT = 0x00000020,
+ SLICED_VBI_CAPTURE = 0x00000040,
+ SLICED_VBI_OUTPUT = 0x00000080,
+ RDS_CAPTURE = 0x00000100,
+ VIDEO_OUTPUT_OVERLAY = 0x00000200,
+ HW_FREQ_SEEK = 0x00000400,
+ RDS_OUTPUT = 0x00000800,
+ VIDEO_CAPTURE_MPLANE = 0x00001000,
+ VIDEO_OUTPUT_MPLANE = 0x00002000,
+ VIDEO_M2M_MPLANE = 0x00004000,
+ VIDEO_M2M = 0x00008000,
+ TUNER = 0x00010000,
+ AUDIO = 0x00020000,
+ RADIO = 0x00040000,
+ MODULATOR = 0x00080000,
+ SDR_CAPTURE = 0x00100000,
+ EXT_PIX_FORMAT = 0x00200000,
+ SDR_OUTPUT = 0x00400000,
+ META_CAPTURE = 0x00800000,
+ READWRITE = 0x01000000,
+ ASYNCIO = 0x02000000,
+ STREAMING = 0x04000000,
+ TOUCH = 0x10000000,
+ DEVICE_CAPS = 0x80000000
+}
+
+public enum v4l2_frmivaltypes : uint
+{
+ V4L2_FRMIVAL_TYPE_DISCRETE = 1,
+ V4L2_FRMIVAL_TYPE_CONTINUOUS = 2,
+ V4L2_FRMIVAL_TYPE_STEPWISE = 3
+}
+
+public enum v4l2_memory : uint
+{
+ V4L2_MEMORY_MMAP = 1,
+ V4L2_MEMORY_USERPTR = 2,
+ V4L2_MEMORY_OVERLAY = 3,
+ V4L2_MEMORY_DMABUF = 4
+}
+
+
+[Flags]
+internal enum Prot : int
+{
+ PROT_NONE = 0,
+ PROT_READ = 1,
+ PROT_WRITE = 2,
+ PROT_EXEC = 4
+}
+
+[Flags]
+internal enum MapFlags : int
+{
+ MAP_SHARED = 1,
+ MAP_PRIVATE = 2,
+ MAP_FIXED = 0x10,
+ MAP_ANONYMOUS = 0x20
+}
diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Ioctl.cs b/src/Baballonia.LibV4L2Capture/V4L2/Ioctl.cs
new file mode 100644
index 00000000..0793dd7f
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/V4L2/Ioctl.cs
@@ -0,0 +1,93 @@
+using System.Runtime.InteropServices;
+
+namespace Baballonia.LibV4L2Capture.V4L2;
+
+public static class Ioctl
+{
+ private const int IOC_NRBITS = 8;
+ private const int IOC_TYPEBITS = 8;
+ private const int IOC_SIZEBITS = 14;
+ private const int IOC_DIRBITS = 2;
+
+ private const int IOC_NRSHIFT = 0;
+ private const int IOC_TYPESHIFT = IOC_NRSHIFT + IOC_NRBITS;
+ private const int IOC_SIZESHIFT = IOC_TYPESHIFT + IOC_TYPEBITS;
+ private const int IOC_DIRSHIFT = IOC_SIZESHIFT + IOC_SIZEBITS;
+
+ private const int IOC_NONE = 0;
+ private const int IOC_WRITE = 1;
+ private const int IOC_READ = 2;
+
+ private static uint IOC(int dir, int type, int nr, int size)
+ => (uint)((dir << IOC_DIRSHIFT) | (type << IOC_TYPESHIFT) | (nr << IOC_NRSHIFT) | (size << IOC_SIZESHIFT));
+
+ private static uint IO(char type, int nr) => IOC(IOC_NONE, type, nr, 0);
+ private static uint IOR(char type, int nr) => IOC(IOC_READ, type, nr, Marshal.SizeOf());
+ private static uint IOW(char type, int nr) => IOC(IOC_WRITE, type, nr, Marshal.SizeOf());
+ private static uint IOWR(char type, int nr) => IOC(IOC_READ | IOC_WRITE, type, nr, Marshal.SizeOf());
+
+ // V4L2 ioctl codes
+ public static readonly uint VIDIOC_QUERYCAP = IOR('V', 0);
+ public static readonly uint VIDIOC_RESERVED = IO('V', 1);
+ public static readonly uint VIDIOC_ENUM_FMT = IOWR('V', 2);
+ public static readonly uint VIDIOC_G_FMT = IOWR('V', 4);
+ public static readonly uint VIDIOC_S_FMT = IOWR('V', 5);
+ public static readonly uint VIDIOC_REQBUFS = IOWR('V', 8);
+ public static readonly uint VIDIOC_QUERYBUF = IOWR('V', 9);
+ //public static readonly uint VIDIOC_G_FBUF = IOR('V', 10);
+ //public static readonly uint VIDIOC_S_FBUF = IOW('V', 11);
+ public static readonly uint VIDIOC_OVERLAY = IOW('V', 14);
+ public static readonly uint VIDIOC_QBUF = IOWR('V', 15);
+ //public static readonly uint VIDIOC_EXPBUF = IOWR('V', 16);
+ public static readonly uint VIDIOC_DQBUF = IOWR('V', 17);
+ public static readonly uint VIDIOC_STREAMON = IOW('V', 18);
+ public static readonly uint VIDIOC_STREAMOFF = IOW('V', 19);
+ //public static readonly uint VIDIOC_G_PARM = IOWR('V', 21);
+ //public static readonly uint VIDIOC_S_PARM = IOWR('V', 22);
+ public static readonly uint VIDIOC_G_STD = IOR('V', 23);
+ public static readonly uint VIDIOC_S_STD = IOW('V', 24);
+ //public static readonly uint VIDIOC_ENUMSTD = IOWR('V', 25);
+ //public static readonly uint VIDIOC_ENUMINPUT = IOWR('V', 26);
+ //public static readonly uint VIDIOC_G_CTRL = IOWR('V', 27);
+ //public static readonly uint VIDIOC_S_CTRL = IOWR('V', 28);
+ //public static readonly uint VIDIOC_G_TUNER = IOWR('V', 29);
+ //public static readonly uint VIDIOC_S_TUNER = IOW('V', 30);
+ //public static readonly uint VIDIOC_G_AUDIO = IOR('V', 33);
+ //public static readonly uint VIDIOC_S_AUDIO = IOW('V', 34);
+ //public static readonly uint VIDIOC_QUERYCTRL = IOWR('V', 36);
+ //public static readonly uint VIDIOC_QUERYMENU = IOWR('V', 37);
+ public static readonly uint VIDIOC_G_INPUT = IOR('V', 38);
+ public static readonly uint VIDIOC_S_INPUT = IOWR('V', 39);
+ //public static readonly uint VIDIOC_G_EDID = IOWR('V', 40);
+ //public static readonly uint VIDIOC_S_EDID = IOWR('V', 41);
+ public static readonly uint VIDIOC_G_OUTPUT = IOR('V', 46);
+ public static readonly uint VIDIOC_S_OUTPUT = IOWR('V', 47);
+ //public static readonly uint VIDIOC_ENUMOUTPUT = IOWR('V', 48);
+ //public static readonly uint VIDIOC_G_AUDOUT = IOR('V', 49);
+ //public static readonly uint VIDIOC_S_AUDOUT = IOW('V', 50);
+ //public static readonly uint VIDIOC_G_MODULATOR = IOWR('V', 54);
+ //public static readonly uint VIDIOC_S_MODULATOR = IOW('V', 55);
+ //public static readonly uint VIDIOC_G_FREQUENCY = IOWR('V', 56);
+ //public static readonly uint VIDIOC_S_FREQUENCY = IOW('V', 57);
+ //public static readonly uint VIDIOC_CROPCAP = IOWR('V', 58);
+ //public static readonly uint VIDIOC_G_CROP = IOWR('V', 59);
+ //public static readonly uint VIDIOC_S_CROP = IOW('V', 60);
+ //public static readonly uint VIDIOC_G_JPEGCOMP = IOR('V', 61);
+ //public static readonly uint VIDIOC_S_JPEGCOMP = IOW('V', 62);
+ public static readonly uint VIDIOC_QUERYSTD = IOR('V', 63);
+ public static readonly uint VIDIOC_TRY_FMT = IOWR('V', 64);
+ //public static readonly uint VIDIOC_ENUMAUDIO = IOWR('V', 65);
+ //public static readonly uint VIDIOC_ENUMAUDOUT = IOWR('V', 66);
+ public static readonly uint VIDIOC_G_PRIORITY = IOR('V', 67);
+ public static readonly uint VIDIOC_S_PRIORITY = IOW('V', 68);
+ //public static readonly uint VIDIOC_G_SLICED_VBI_CAP = IOWR('V', 69);
+ public static readonly uint VIDIOC_LOG_STATUS = IO('V', 70);
+ //public static readonly uint VIDIOC_G_EXT_CTRLS = IOWR('V', 71);
+ //public static readonly uint VIDIOC_S_EXT_CTRLS = IOWR('V', 72);
+ //public static readonly uint VIDIOC_TRY_EXT_CTRLS = IOWR('V', 73);
+ public static readonly uint VIDIOC_ENUM_FRAMESIZES = IOWR('V', 74);
+ public static readonly uint VIDIOC_ENUM_FRAMEINTERVALS = IOWR('V', 75);
+ //public static readonly uint VIDIOC_G_ENC_INDEX = IOR('V', 76);
+ //public static readonly uint VIDIOC_ENCODER_CMD = IOWR('V', 77);
+ //public static readonly uint VIDIOC_TRY_ENCODER_CMD = IOWR('V', 78);
+}
diff --git a/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs b/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs
new file mode 100644
index 00000000..35dc30a6
--- /dev/null
+++ b/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs
@@ -0,0 +1,45 @@
+using System.Runtime.InteropServices;
+
+namespace Baballonia.LibV4L2Capture.V4L2;
+
+internal static class NativeMethods {
+ [DllImport("libv4l2", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
+ public static extern int v4l2_open(string file, int flags);
+
+ [DllImport("libv4l2", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
+ public static extern int v4l2_close(int fd);
+
+ [DllImport("libv4l2", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
+ public static extern int v4l2_ioctl(int fd, uint request, IntPtr arg);
+
+ [DllImport("libv4l2", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
+ public static extern int v4l2_read(int fd, byte[] buffer, int size);
+
+ public static int v4l2_ioctl_safe(int fd, uint request, ref T arg)
+ where T : unmanaged
+ {
+ unsafe
+ {
+ fixed (T* p = &arg)
+ {
+ return v4l2_ioctl(fd, request, (IntPtr)p);
+ }
+ }
+ }
+
+
+ [DllImport("libc", SetLastError = true)]
+ public static extern IntPtr mmap(
+ IntPtr addr,
+ uint length,
+ Prot prot,
+ MapFlags flags,
+ int fd,
+ IntPtr offset);
+
+ [DllImport("libc", SetLastError = true)]
+ public static extern int munmap(IntPtr addr, uint length);
+
+ [DllImport("libc", SetLastError = true)]
+ public static extern int poll([In, Out] Data.pollfd[] fds, uint nfds, int timeout);
+}