From abfdd766e8ae8db69f3b67ee6ebacecf353acb5a Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 00:47:09 +0100 Subject: [PATCH 01/10] feat: LibV4L2 capture --- Baballonia.sln | 7 + .../Baballonia.Desktop.csproj | 3 + .../Baballonia.LibV4L2Capture.csproj | 14 + .../LibV4L2Capture.cs | 67 ++++ .../LibV4L2CaptureFactory.cs | 28 ++ src/Baballonia.LibV4L2Capture/V4L2/Data.cs | 208 +++++++++++ src/Baballonia.LibV4L2Capture/V4L2/Device.cs | 349 ++++++++++++++++++ src/Baballonia.LibV4L2Capture/V4L2/Enums.cs | 88 +++++ src/Baballonia.LibV4L2Capture/V4L2/Ioctl.cs | 93 +++++ .../V4L2/NativeMethods.cs | 30 ++ .../OpenCVCaptureFactory.cs | 6 +- 11 files changed, 891 insertions(+), 2 deletions(-) create mode 100644 src/Baballonia.LibV4L2Capture/Baballonia.LibV4L2Capture.csproj create mode 100644 src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs create mode 100644 src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs create mode 100644 src/Baballonia.LibV4L2Capture/V4L2/Data.cs create mode 100644 src/Baballonia.LibV4L2Capture/V4L2/Device.cs create mode 100644 src/Baballonia.LibV4L2Capture/V4L2/Enums.cs create mode 100644 src/Baballonia.LibV4L2Capture/V4L2/Ioctl.cs create mode 100644 src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs 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..6f4ff0af --- /dev/null +++ b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs @@ -0,0 +1,67 @@ +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); + + _device.StartCapture(); + IsReady = true; + } + catch (Exception e) { + Logger.LogError(e.ToString()); + return Task.FromResult(true); + } + + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + _captureTask = Task.Run(() => + { + while (!token.IsCancellationRequested) + { + try + { + byte[] frame = _device.CaptureFrame(); + Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale); + SetRawMat(mat); + } + catch(Exception e) + { + Logger.LogError(e.ToString()); + } + } + }, token); + + return Task.FromResult(true); + } + + 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..7f1f1659 --- /dev/null +++ b/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs @@ -0,0 +1,28 @@ +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..a63103d1 --- /dev/null +++ b/src/Baballonia.LibV4L2Capture/V4L2/Data.cs @@ -0,0 +1,208 @@ +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; + } +} diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs new file mode 100644 index 00000000..33d5e282 --- /dev/null +++ b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs @@ -0,0 +1,349 @@ +using System.Runtime.InteropServices; +using OpenCvSharp; + +namespace Baballonia.LibV4L2Capture.V4L2; + + +public class Device : IDisposable { + public void Dispose() + { + StopCapture(); + if (_fileDescriptor >= 0) + { + NativeMethods.v4l2_close(_fileDescriptor); + _fileDescriptor = -1; + } + } + + private const int O_RDWR = 2; + + public string Address { get; private set; } + public bool Connected { 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 => f.pixelformat == v4l2_pix_fmt.V4L2_PIX_FMT_MJPEG).ToList(); + + if (formats.Count <= 0) + throw new Exception("Device does not support MJPEG"); + + var format = formats[0]; + + Data.v4l2_frmivalenum bestInterval = default; + double maxFps = 0; + uint maxResolution = 0; + + var sizes = device.EnumerateFrameSizes(format.pixelformat); + foreach (var size in sizes) + { + var intervals = device.EnumerateFrameIntervals(format.pixelformat, size.discrete.width, size.discrete.height); + foreach (var interval in intervals) + { + double fps = 0d; + 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() + { + unsafe + { + Data.v4l2_capability cap = new Data.v4l2_capability(); + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_QUERYCAP, new IntPtr(&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) + { + unsafe + { + // declare fmt inside unsafe + Data.v4l2_fmtdesc fmt; + fmt.index = index; + fmt.type = type; + fmt.flags = 0; + fmt.pixelformat = 0; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_ENUM_FMT, (IntPtr)(&fmt)); + if (ret < 0) + break; // no more formats + + formats.Add(fmt); + index++; + } + } + + return formats; + } + + private Data.v4l2_format GetCurrentFormat() + { + unsafe + { + Data.v4l2_format fmt = new Data.v4l2_format(); + fmt.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_G_FMT, (IntPtr)(&fmt)); + if (ret < 0) + throw new Exception($"VIDIOC_G_FMT failed: errno={Marshal.GetLastWin32Error()}"); + + return fmt; + } + } + + public void SetFormat(Data.v4l2_frmivalenum format) + { + unsafe + { + Data.v4l2_format fmt; + 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(_fileDescriptor, Ioctl.VIDIOC_S_FMT, (IntPtr)(&fmt)); + if (ret < 0) + throw new Exception($"VIDIOC_S_FMT failed: errno={Marshal.GetLastWin32Error()}"); + } + } + + public List EnumerateFrameSizes(v4l2_pix_fmt pixelformat) + { + List sizes = new List(); + uint index = 0; + + while (true) + { + unsafe + { + Data.v4l2_frmsizeenum fsize = new Data.v4l2_frmsizeenum + { + index = index, + pixel_format = pixelformat + }; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_ENUM_FRAMESIZES, (IntPtr)(&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) + { + unsafe { + Data.v4l2_frmivalenum fival = new Data.v4l2_frmivalenum { + index = index, + pixel_format = pixelformat, + width = width, + height = height, + }; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_ENUM_FRAMEINTERVALS, (IntPtr)(&fival)); + + if (ret < 0) break; + intervals.Add(fival); + index++; + } + } + + return intervals; + } + + private Data.v4l2_requestbuffers GetBuffers() { + unsafe + { + Data.v4l2_requestbuffers req = new Data.v4l2_requestbuffers + { + count = 3, + type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory = v4l2_memory.V4L2_MEMORY_MMAP + }; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_REQBUFS, (IntPtr)(&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++) + { + unsafe + { + Data.v4l2_buffer buf = new Data.v4l2_buffer + { + type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory = v4l2_memory.V4L2_MEMORY_MMAP, + index = i + }; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_QUERYBUF, (IntPtr)(&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] == (IntPtr)(-1)) + throw new Exception($"mmap failed: errno={Marshal.GetLastWin32Error()}"); + } + } + } + + public void QueueAllBuffers() + { + for (uint i = 0; i < _bufferCount; i++) + { + unsafe + { + Data.v4l2_buffer buf = new Data.v4l2_buffer + { + type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory = v4l2_memory.V4L2_MEMORY_MMAP, + index = i + }; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_QBUF, (IntPtr)(&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; + unsafe + { + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_STREAMON, (IntPtr)(&type)); + if (ret < 0) throw new Exception($"VIDIOC_STREAMON failed: errno={Marshal.GetLastWin32Error()}"); + } + } + + public byte[] CaptureFrame() + { + unsafe + { + Data.v4l2_buffer buf = new Data.v4l2_buffer + { + type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, + memory = v4l2_memory.V4L2_MEMORY_MMAP + }; + + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_DQBUF, (IntPtr)(&buf)); + if (ret < 0) throw new Exception($"VIDIOC_DQBUF failed: errno={Marshal.GetLastWin32Error()}"); + + byte[] frame = new byte[buf.bytesused]; + Marshal.Copy(_bufferStarts[buf.index], frame, 0, (int)buf.bytesused); + + ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_QBUF, (IntPtr)(&buf)); + if (ret < 0) throw new Exception($"VIDIOC_QBUF failed: errno={Marshal.GetLastWin32Error()}"); + + return frame; + } + } + + public void StopStreaming() + { + v4l2_buf_type type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE; + unsafe + { + int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_STREAMOFF, (IntPtr)(&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..b5abe046 --- /dev/null +++ b/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs @@ -0,0 +1,88 @@ +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, +} + +[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..cbf62243 --- /dev/null +++ b/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices; + +namespace Baballonia.LibV4L2Capture.V4L2; + +internal static class NativeMethods { + [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int v4l2_open(string file, int flags); + + [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int v4l2_close(int fd); + + [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int v4l2_ioctl(int fd, uint request, IntPtr arg); + + [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int v4l2_read(int fd, byte[] buffer, int size); + + + [DllImport("libc.so.6", SetLastError = true)] + public static extern IntPtr mmap( + IntPtr addr, + uint length, + Prot prot, + MapFlags flags, + int fd, + IntPtr offset); + + [DllImport("libc.so.6", SetLastError = true)] + public static extern int munmap(IntPtr addr, uint length); +} diff --git a/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs b/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs index 4e5ba96c..57930d34 100644 --- a/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs +++ b/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs @@ -26,8 +26,10 @@ public bool CanConnect(string address) lowered.StartsWith("/dev/ttyacm");; if (serial) return false; - return lowered.StartsWith("/dev/video") || - lowered.EndsWith("appsink") || + if (lowered.StartsWith("/dev/video")) + return false; + + return lowered.EndsWith("appsink") || int.TryParse(address, out _) || Uri.TryCreate(address, UriKind.Absolute, out _); } From 0d7e3736ae433aace538ee5a711a9f85f6d38afc Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 21:51:40 +0100 Subject: [PATCH 02/10] fix: return false instead of true for failing to connect to camera --- src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs index 6f4ff0af..69a2578f 100644 --- a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs +++ b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs @@ -23,7 +23,7 @@ public override Task StartCapture() } catch (Exception e) { Logger.LogError(e.ToString()); - return Task.FromResult(true); + return Task.FromResult(false); } _cts = new CancellationTokenSource(); From 0a395778209601169fd104aa90f9bcdeac5f72d2 Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 21:51:51 +0100 Subject: [PATCH 03/10] refactor: move camera loop into it's own function --- .../LibV4L2Capture.cs | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs index 69a2578f..c1daf21b 100644 --- a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs +++ b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs @@ -29,24 +29,30 @@ public override Task StartCapture() _cts = new CancellationTokenSource(); var token = _cts.Token; - _captureTask = Task.Run(() => + _captureTask = Task.Run(() => VideoCapture_UpdateLoop(token), token); + + return Task.FromResult(true); + } + + private Task VideoCapture_UpdateLoop(CancellationToken ct) + { + while (!ct.IsCancellationRequested && _device != null) { - while (!token.IsCancellationRequested) + try { - try - { - byte[] frame = _device.CaptureFrame(); - Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale); - SetRawMat(mat); - } - catch(Exception e) - { - Logger.LogError(e.ToString()); - } + byte[] frame = _device.CaptureFrame(); + Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale); + SetRawMat(mat); } - }, token); + catch(Exception e) + { + Logger.LogError(e.ToString()); + _device.Dispose(); + break; + } + } - return Task.FromResult(true); + return Task.CompletedTask; } public override Task StopCapture() From c96439f0648afd57ebdc899d37633ea70f8c164f Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 21:52:25 +0100 Subject: [PATCH 04/10] fix: correctly dispose camera buffers --- src/Baballonia.LibV4L2Capture/V4L2/Device.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs index 33d5e282..4a3f1692 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs @@ -8,11 +8,27 @@ 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 const int O_RDWR = 2; From 6795ca233896d9aebad77839f033204030f2a985 Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 21:53:07 +0100 Subject: [PATCH 05/10] refactor: changed initialization of the struct to be consistant --- src/Baballonia.LibV4L2Capture/V4L2/Device.cs | 241 +++++++----------- .../V4L2/NativeMethods.cs | 24 +- 2 files changed, 116 insertions(+), 149 deletions(-) diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs index 4a3f1692..65cf6700 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using OpenCvSharp; namespace Baballonia.LibV4L2Capture.V4L2; @@ -8,6 +7,7 @@ public class Device : IDisposable { public void Dispose() { StopCapture(); + for (int i = 0; i < _bufferStarts.Length; i++) { if (_bufferStarts[i] != IntPtr.Zero && _bufferLengths[i] > 0) @@ -107,15 +107,12 @@ public void Dispose() public Data.v4l2_capability GetCapabilities() { - unsafe - { - Data.v4l2_capability cap = new Data.v4l2_capability(); - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_QUERYCAP, new IntPtr(&cap)); + 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; - } + if (ret < 0) + throw new Exception($"VIDIOC_QUERYCAP failed: errno={Marshal.GetLastWin32Error()}"); + return cap; } public List GetFormats() @@ -126,22 +123,19 @@ public Data.v4l2_capability GetCapabilities() while (true) { - unsafe - { - // declare fmt inside unsafe - Data.v4l2_fmtdesc fmt; - fmt.index = index; - fmt.type = type; - fmt.flags = 0; - fmt.pixelformat = 0; - - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_ENUM_FMT, (IntPtr)(&fmt)); - if (ret < 0) - break; // no more formats - - formats.Add(fmt); - index++; - } + // 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; @@ -149,33 +143,27 @@ public Data.v4l2_capability GetCapabilities() private Data.v4l2_format GetCurrentFormat() { - unsafe - { - Data.v4l2_format fmt = new Data.v4l2_format(); - fmt.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE; + Data.v4l2_format fmt = default; + fmt.type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE; - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_G_FMT, (IntPtr)(&fmt)); - if (ret < 0) - throw new Exception($"VIDIOC_G_FMT failed: errno={Marshal.GetLastWin32Error()}"); + 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; - } + return fmt; } public void SetFormat(Data.v4l2_frmivalenum format) { - unsafe - { - Data.v4l2_format fmt; - 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(_fileDescriptor, Ioctl.VIDIOC_S_FMT, (IntPtr)(&fmt)); - if (ret < 0) - throw new Exception($"VIDIOC_S_FMT failed: errno={Marshal.GetLastWin32Error()}"); - } + 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()}"); } public List EnumerateFrameSizes(v4l2_pix_fmt pixelformat) @@ -185,20 +173,15 @@ public void SetFormat(Data.v4l2_frmivalenum format) while (true) { - unsafe - { - Data.v4l2_frmsizeenum fsize = new Data.v4l2_frmsizeenum - { - index = index, - pixel_format = pixelformat - }; + Data.v4l2_frmsizeenum fsize = default; + fsize.index = index; + fsize.pixel_format = pixelformat; - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_ENUM_FRAMESIZES, (IntPtr)(&fsize)); + int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_ENUM_FRAMESIZES, ref fsize); - if (ret < 0) break; - sizes.Add(fsize); - index++; - } + if (ret < 0) break; + sizes.Add(fsize); + index++; } return sizes; @@ -211,40 +194,33 @@ public void SetFormat(Data.v4l2_frmivalenum format) while (true) { - unsafe { - Data.v4l2_frmivalenum fival = new Data.v4l2_frmivalenum { - index = index, - pixel_format = pixelformat, - width = width, - height = height, - }; - - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_ENUM_FRAMEINTERVALS, (IntPtr)(&fival)); - - if (ret < 0) break; - intervals.Add(fival); - index++; - } + 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() { - unsafe - { - Data.v4l2_requestbuffers req = new Data.v4l2_requestbuffers - { - count = 3, - type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, - memory = v4l2_memory.V4L2_MEMORY_MMAP - }; + 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(_fileDescriptor, Ioctl.VIDIOC_REQBUFS, (IntPtr)(&req)); - if (ret < 0) throw new Exception($"VIDIOC_REQBUFS failed: errno={Marshal.GetLastWin32Error()}"); + 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; - } + return req; } public void InitMMapBuffers() @@ -257,28 +233,23 @@ public void InitMMapBuffers() for (uint i = 0; i < _bufferCount; i++) { - unsafe - { - Data.v4l2_buffer buf = new Data.v4l2_buffer - { - type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, - memory = v4l2_memory.V4L2_MEMORY_MMAP, - index = i - }; - - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_QUERYBUF, (IntPtr)(&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] == (IntPtr)(-1)) - throw new Exception($"mmap failed: errno={Marshal.GetLastWin32Error()}"); - } + 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] == (IntPtr)(-1)) + throw new Exception($"mmap failed: errno={Marshal.GetLastWin32Error()}"); } } @@ -286,62 +257,46 @@ public void QueueAllBuffers() { for (uint i = 0; i < _bufferCount; i++) { - unsafe - { - Data.v4l2_buffer buf = new Data.v4l2_buffer - { - type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, - memory = v4l2_memory.V4L2_MEMORY_MMAP, - index = 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(_fileDescriptor, Ioctl.VIDIOC_QBUF, (IntPtr)(&buf)); - if (ret < 0) throw new Exception($"VIDIOC_QBUF failed: errno={Marshal.GetLastWin32Error()}"); - } + 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; - unsafe - { - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_STREAMON, (IntPtr)(&type)); - if (ret < 0) throw new Exception($"VIDIOC_STREAMON failed: errno={Marshal.GetLastWin32Error()}"); - } + 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 byte[] CaptureFrame() { - unsafe - { - Data.v4l2_buffer buf = new Data.v4l2_buffer - { - type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE, - memory = v4l2_memory.V4L2_MEMORY_MMAP - }; + 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(_fileDescriptor, Ioctl.VIDIOC_DQBUF, (IntPtr)(&buf)); - if (ret < 0) throw new Exception($"VIDIOC_DQBUF failed: errno={Marshal.GetLastWin32Error()}"); + int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_DQBUF, ref buf); + if (ret < 0) throw new Exception($"VIDIOC_DQBUF failed: errno={Marshal.GetLastWin32Error()}"); - byte[] frame = new byte[buf.bytesused]; - Marshal.Copy(_bufferStarts[buf.index], frame, 0, (int)buf.bytesused); + byte[] frame = new byte[buf.bytesused]; + Marshal.Copy(_bufferStarts[buf.index], frame, 0, (int)buf.bytesused); - ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_QBUF, (IntPtr)(&buf)); - if (ret < 0) throw new Exception($"VIDIOC_QBUF failed: errno={Marshal.GetLastWin32Error()}"); + ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_QBUF, ref buf); + if (ret < 0) throw new Exception($"VIDIOC_QBUF failed: errno={Marshal.GetLastWin32Error()}"); - return frame; - } + return frame; } public void StopStreaming() { v4l2_buf_type type = v4l2_buf_type.V4L2_BUF_TYPE_VIDEO_CAPTURE; - unsafe - { - int ret = NativeMethods.v4l2_ioctl(_fileDescriptor, Ioctl.VIDIOC_STREAMOFF, (IntPtr)(&type)); - if (ret < 0) throw new Exception($"VIDIOC_STREAMOFF failed: errno={Marshal.GetLastWin32Error()}"); - } + 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() diff --git a/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs b/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs index cbf62243..228c3a96 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs @@ -3,20 +3,32 @@ namespace Baballonia.LibV4L2Capture.V4L2; internal static class NativeMethods { - [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [DllImport("libv4l2", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int v4l2_open(string file, int flags); - [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [DllImport("libv4l2", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int v4l2_close(int fd); - [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [DllImport("libv4l2", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int v4l2_ioctl(int fd, uint request, IntPtr arg); - [DllImport("libv4l2.so", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + [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.so.6", SetLastError = true)] + + [DllImport("libc", SetLastError = true)] public static extern IntPtr mmap( IntPtr addr, uint length, @@ -25,6 +37,6 @@ public static extern IntPtr mmap( int fd, IntPtr offset); - [DllImport("libc.so.6", SetLastError = true)] + [DllImport("libc", SetLastError = true)] public static extern int munmap(IntPtr addr, uint length); } From 41e65f4bf78cb06a8a485bb05e4673c1b1b12d6f Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 22:34:49 +0100 Subject: [PATCH 06/10] feat: wait for frame to be available instead of blocking call --- .../LibV4L2Capture.cs | 21 ++++--- src/Baballonia.LibV4L2Capture/V4L2/Data.cs | 12 ++++ src/Baballonia.LibV4L2Capture/V4L2/Device.cs | 60 ++++++++++++------- .../V4L2/NativeMethods.cs | 3 + 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs index c1daf21b..a9cefc46 100644 --- a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs +++ b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs @@ -34,25 +34,28 @@ public override Task StartCapture() return Task.FromResult(true); } - private Task VideoCapture_UpdateLoop(CancellationToken ct) - { + private async Task VideoCapture_UpdateLoop(CancellationToken ct) { while (!ct.IsCancellationRequested && _device != null) { - try - { - byte[] frame = _device.CaptureFrame(); - Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale); - SetRawMat(mat); + try { + if (_device.CaptureFrame(out byte[]? frame)) + { + Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale); + SetRawMat(mat); + } + else + { + await Task.Delay(1, ct); + } } catch(Exception e) { + IsReady = false; Logger.LogError(e.ToString()); _device.Dispose(); break; } } - - return Task.CompletedTask; } public override Task StopCapture() diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Data.cs b/src/Baballonia.LibV4L2Capture/V4L2/Data.cs index a63103d1..bb4b92d0 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Data.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Data.cs @@ -205,4 +205,16 @@ public struct v4l2_buffer [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 index 65cf6700..b0b48bfa 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs @@ -32,6 +32,9 @@ public void Dispose() } private const int O_RDWR = 2; + private const int O_NONBLOCK = 0x800; + + private const int EAGAIN = 11; // Resource temporarily unavailable public string Address { get; private set; } public bool Connected { get; private set; } @@ -53,16 +56,13 @@ public void Dispose() 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.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)"); + 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 => f.pixelformat == v4l2_pix_fmt.V4L2_PIX_FMT_MJPEG).ToList(); - if (formats.Count <= 0) - throw new Exception("Device does not support MJPEG"); + if (formats.Count <= 0) throw new Exception("Device does not support MJPEG"); var format = formats[0]; @@ -110,8 +110,7 @@ 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()}"); + if (ret < 0) throw new Exception($"VIDIOC_QUERYCAP failed: errno={Marshal.GetLastWin32Error()}"); return cap; } @@ -131,8 +130,7 @@ public Data.v4l2_capability GetCapabilities() fmt.pixelformat = 0; int ret = NativeMethods.v4l2_ioctl_safe(_fileDescriptor, Ioctl.VIDIOC_ENUM_FMT, ref fmt); - if (ret < 0) - break; // no more formats + if (ret < 0) break; // no more formats formats.Add(fmt); index++; @@ -147,8 +145,7 @@ private Data.v4l2_format GetCurrentFormat() 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()}"); + if (ret < 0) throw new Exception($"VIDIOC_G_FMT failed: errno={Marshal.GetLastWin32Error()}"); return fmt; } @@ -162,8 +159,7 @@ public void SetFormat(Data.v4l2_frmivalenum format) 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()}"); + if (ret < 0) throw new Exception($"VIDIOC_S_FMT failed: errno={Marshal.GetLastWin32Error()}"); } public List EnumerateFrameSizes(v4l2_pix_fmt pixelformat) @@ -248,8 +244,7 @@ public void InitMMapBuffers() MapFlags.MAP_SHARED, _fileDescriptor, new IntPtr(buf.offset)); - if (_bufferStarts[i] == (IntPtr)(-1)) - throw new Exception($"mmap failed: errno={Marshal.GetLastWin32Error()}"); + if (_bufferStarts[i] == -1) throw new Exception($"mmap failed: errno={Marshal.GetLastWin32Error()}"); } } @@ -274,29 +269,50 @@ public void StartStreaming() if (ret < 0) throw new Exception($"VIDIOC_STREAMON failed: errno={Marshal.GetLastWin32Error()}"); } - public byte[] CaptureFrame() + 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()}"); + + return ret > 0 && (fds[0].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()}"); + if (ret != 0) + throw new Exception($"VIDIOC_DQBUF failed: errno={Marshal.GetLastWin32Error()}"); - byte[] frame = new byte[buf.bytesused]; + 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 frame; + 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()}"); + //if (ret < 0) throw new Exception($"VIDIOC_STREAMOFF failed: errno={Marshal.GetLastWin32Error()}"); } public void StartCapture() diff --git a/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs b/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs index 228c3a96..35dc30a6 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/NativeMethods.cs @@ -39,4 +39,7 @@ public static extern IntPtr mmap( [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); } From 40529fc16f6e77c58ead2106c15c59e9acf3c804 Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 22:42:30 +0100 Subject: [PATCH 07/10] chore: fixup code styling --- .../LibV4L2Capture.cs | 18 ++++++++++++------ .../LibV4L2CaptureFactory.cs | 3 ++- src/Baballonia.LibV4L2Capture/V4L2/Enums.cs | 15 ++++++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs index a9cefc46..b224ae94 100644 --- a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs +++ b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs @@ -5,14 +5,16 @@ namespace Baballonia.LibV4L2Capture; -public sealed class LibV4L2Capture(string source, ILogger logger) : Capture(source, logger) { +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 { + try + { _device = Device.Connect(Source); if (_device == null) @@ -21,7 +23,8 @@ public override Task StartCapture() _device.StartCapture(); IsReady = true; } - catch (Exception e) { + catch (Exception e) + { Logger.LogError(e.ToString()); return Task.FromResult(false); } @@ -34,10 +37,12 @@ public override Task StartCapture() return Task.FromResult(true); } - private async Task VideoCapture_UpdateLoop(CancellationToken ct) { + private async Task VideoCapture_UpdateLoop(CancellationToken ct) + { while (!ct.IsCancellationRequested && _device != null) { - try { + try + { if (_device.CaptureFrame(out byte[]? frame)) { Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale); @@ -63,7 +68,8 @@ public override Task StopCapture() if (_device is null) return Task.FromResult(false); - if (_captureTask != null) { + if (_captureTask != null) + { _cts?.Cancel(); _captureTask.Wait(); } diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs b/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs index 7f1f1659..c0dd22e5 100644 --- a/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs +++ b/src/Baballonia.LibV4L2Capture/LibV4L2CaptureFactory.cs @@ -17,7 +17,8 @@ public Capture Create(string address) return new LibV4L2Capture(address, _loggerFactory.CreateLogger()); } - public bool CanConnect(string address) { + public bool CanConnect(string address) + { return address.StartsWith("/dev/video"); } diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs b/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs index b5abe046..f13d68b1 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs @@ -1,6 +1,7 @@ namespace Baballonia.LibV4L2Capture.V4L2; -public enum v4l2_buf_type : uint { +public enum v4l2_buf_type : uint +{ V4L2_BUF_TYPE_VIDEO_CAPTURE = 1, V4L2_BUF_TYPE_VIDEO_OUTPUT = 2, V4L2_BUF_TYPE_VIDEO_OVERLAY = 3, @@ -19,12 +20,14 @@ public enum v4l2_buf_type : uint { V4L2_BUF_TYPE_PRIVATE = 0x80, } -public enum v4l2_pix_fmt : uint { +public enum v4l2_pix_fmt : uint +{ V4L2_PIX_FMT_MJPEG = 1196444237, } [Flags] -public enum V4L2Capabilities : uint { +public enum V4L2Capabilities : uint +{ VIDEO_CAPTURE = 0x00000001, VIDEO_OUTPUT = 0x00000002, VIDEO_OVERLAY = 0x00000004, @@ -55,13 +58,15 @@ public enum V4L2Capabilities : uint { DEVICE_CAPS = 0x80000000 } -public enum v4l2_frmivaltypes : uint { +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 { +public enum v4l2_memory : uint +{ V4L2_MEMORY_MMAP = 1, V4L2_MEMORY_USERPTR = 2, V4L2_MEMORY_OVERLAY = 3, From 5bf4d2acad2143e67132a0e9769d1283e26e07f2 Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 22:42:55 +0100 Subject: [PATCH 08/10] feat: stop trying to capture on device error --- src/Baballonia.LibV4L2Capture/V4L2/Device.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs index b0b48bfa..13c88316 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs @@ -284,7 +284,12 @@ public bool FrameReady(int timeoutMs = 0) if (ret < 0) throw new Exception($"poll failed: errno={Marshal.GetLastWin32Error()}"); - return ret > 0 && (fds[0].revents & Data.POLLIN) != 0; + 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) { From b99959f39b19dc054c62533df91ae95d7218d7c0 Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 22:45:49 +0100 Subject: [PATCH 09/10] revert: add /dev/video* back to opencvcapture --- src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs b/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs index 57930d34..4e5ba96c 100644 --- a/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs +++ b/src/Baballonia.OpenCVCapture/OpenCVCaptureFactory.cs @@ -26,10 +26,8 @@ public bool CanConnect(string address) lowered.StartsWith("/dev/ttyacm");; if (serial) return false; - if (lowered.StartsWith("/dev/video")) - return false; - - return lowered.EndsWith("appsink") || + return lowered.StartsWith("/dev/video") || + lowered.EndsWith("appsink") || int.TryParse(address, out _) || Uri.TryCreate(address, UriKind.Absolute, out _); } From bfef1dae86dc5fe911ab3d9085b0fa6173b823c5 Mon Sep 17 00:00:00 2001 From: DeltaNeverUsed Date: Sat, 13 Dec 2025 23:45:52 +0100 Subject: [PATCH 10/10] feat: support for adding more pixel formats + added YUYV as a supported format for future usage for the vive face tracker --- .../LibV4L2Capture.cs | 39 +++++++++++++++++-- src/Baballonia.LibV4L2Capture/V4L2/Device.cs | 28 ++++++++----- src/Baballonia.LibV4L2Capture/V4L2/Enums.cs | 1 + 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs index b224ae94..569d262a 100644 --- a/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs +++ b/src/Baballonia.LibV4L2Capture/LibV4L2Capture.cs @@ -1,4 +1,5 @@ -using Baballonia.LibV4L2Capture.V4L2; +using System.Runtime.InteropServices; +using Baballonia.LibV4L2Capture.V4L2; using Microsoft.Extensions.Logging; using OpenCvSharp; using Capture = Baballonia.SDK.Capture; @@ -20,6 +21,8 @@ public override Task StartCapture() if (_device == null) return Task.FromResult(false); + Logger.LogInformation($"Using pixel format: {_device.PixelFormat}"); + _device.StartCapture(); IsReady = true; } @@ -37,6 +40,22 @@ public override Task StartCapture() 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) @@ -45,8 +64,21 @@ private async Task VideoCapture_UpdateLoop(CancellationToken ct) { if (_device.CaptureFrame(out byte[]? frame)) { - Mat mat = Cv2.ImDecode(frame, ImreadModes.Grayscale); - SetRawMat(mat); + 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 { @@ -55,6 +87,7 @@ private async Task VideoCapture_UpdateLoop(CancellationToken ct) } catch(Exception e) { + SetRawMat(new Mat()); IsReady = false; Logger.LogError(e.ToString()); _device.Dispose(); diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs index 13c88316..8eefe917 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Device.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Device.cs @@ -31,13 +31,13 @@ public void Dispose() } - private const int O_RDWR = 2; - private const int O_NONBLOCK = 0x800; + private static readonly v4l2_pix_fmt[] SupportedFormats = [v4l2_pix_fmt.V4L2_PIX_FMT_MJPEG, v4l2_pix_fmt.V4L2_PIX_FMT_YUYV]; - private const int EAGAIN = 11; // Resource temporarily unavailable + private const int O_RDWR = 2; public string Address { get; private set; } - public bool Connected { get; private set; } + public v4l2_pix_fmt PixelFormat { get; private set; } + public Data.v4l2_format CurrentFormat { get; private set; } private int _fileDescriptor; @@ -60,23 +60,31 @@ public void Dispose() 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 => f.pixelformat == v4l2_pix_fmt.V4L2_PIX_FMT_MJPEG).ToList(); + var formats = device.GetFormats().Where(f => SupportedFormats.Contains(f.pixelformat)).ToList(); - if (formats.Count <= 0) throw new Exception("Device does not support MJPEG"); + 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(format.pixelformat); + var sizes = device.EnumerateFrameSizes(device.PixelFormat); foreach (var size in sizes) { - var intervals = device.EnumerateFrameIntervals(format.pixelformat, size.discrete.width, size.discrete.height); + var intervals = device.EnumerateFrameIntervals(device.PixelFormat, size.discrete.width, size.discrete.height); foreach (var interval in intervals) { - double fps = 0d; + double fps; switch (interval.type) { case v4l2_frmivaltypes.V4L2_FRMIVAL_TYPE_DISCRETE: @@ -160,6 +168,8 @@ public void SetFormat(Data.v4l2_frmivalenum 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) diff --git a/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs b/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs index f13d68b1..04a9c903 100644 --- a/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs +++ b/src/Baballonia.LibV4L2Capture/V4L2/Enums.cs @@ -23,6 +23,7 @@ public enum v4l2_buf_type : uint public enum v4l2_pix_fmt : uint { V4L2_PIX_FMT_MJPEG = 1196444237, + V4L2_PIX_FMT_YUYV = 1448695129, } [Flags]