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); +}