From 1fa8bb4b4c132a3a06bdf4aa3435967157e45c51 Mon Sep 17 00:00:00 2001 From: Fro Zen Date: Tue, 9 Dec 2025 17:08:57 -0600 Subject: [PATCH 1/5] Initial LibUVC implementation --- Baballonia.sln | 7 ++ .../Baballonia.Desktop.csproj | 5 + .../Baballonia.LibUVCCapture.csproj | 13 +++ src/Baballonia.LibUVCCapture/LibUVCCapture.cs | 104 ++++++++++++++++++ .../LibUVCCaptureFactory.cs | 18 +++ 5 files changed, 147 insertions(+) create mode 100644 src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj create mode 100644 src/Baballonia.LibUVCCapture/LibUVCCapture.cs create mode 100644 src/Baballonia.LibUVCCapture/LibUVCCaptureFactory.cs diff --git a/Baballonia.sln b/Baballonia.sln index eb8a3ad8..c897b3da 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.LibUVCCapture", "src\Baballonia.LibUVCCapture\Baballonia.LibUVCCapture.csproj", "{03715536-9789-4282-82A1-02693A5FE696}" +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 + {03715536-9789-4282-82A1-02693A5FE696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03715536-9789-4282-82A1-02693A5FE696}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03715536-9789-4282-82A1-02693A5FE696}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03715536-9789-4282-82A1-02693A5FE696}.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} + {03715536-9789-4282-82A1-02693A5FE696} = {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..f16d1722 100644 --- a/src/Baballonia.Desktop/Baballonia.Desktop.csproj +++ b/src/Baballonia.Desktop/Baballonia.Desktop.csproj @@ -45,6 +45,7 @@ + @@ -53,6 +54,7 @@ + @@ -135,10 +137,12 @@ + + @@ -149,6 +153,7 @@ + diff --git a/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj b/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj new file mode 100644 index 00000000..227d3534 --- /dev/null +++ b/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + disable + + + + + + + + diff --git a/src/Baballonia.LibUVCCapture/LibUVCCapture.cs b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs new file mode 100644 index 00000000..f6badf7a --- /dev/null +++ b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Baballonia.SDK; +using Microsoft.Extensions.Logging; +using OpenCvSharp; +using Uvc.Net; + +namespace Baballonia.LibUVCCapture; + +public sealed class LibUVCCapture(string source, ILogger logger) : Capture(source, logger) +{ + private Context _context; + private Device _device; + private DeviceHandle _deviceHandle; + private bool _connected; + public override bool CanConnect(string connectionString) => connectionString.StartsWith("/dev/video"); + + public override Task StartCapture() + { + _context = new Context(); + _device = FindDeviceByPath(Source, _context); + if (_device is null) return Task.FromResult(false); + return Task.Run(() => + { + var open = _device.Open(); + try + { + var formats = open.GetStreamControlFormats().ToArray().OrderBy(i => i.Width).ToList(); + var formatIndex = formats.FindIndex(i => i is { Width: > 256, Height: > 256, Format: FrameFormat.Mjpeg }); + if (formatIndex == -1) return false; + var format = formats[formatIndex]; + open.GetStreamControlFormatSize(format.Format, format.Width, format.Height, format.Fps, out var control); + try + { + open.StartStreaming(ref control, Callback); + } + catch + { + open.StopStreaming(); + + open.Dispose(); + _device.Dispose(); + _device = null; + _context.Dispose(); + _context = null; + return false; + } + } + catch + { + open.Dispose(); + _device?.Dispose(); + _device = null; + _context?.Dispose(); + _context = null; + return false; + } + _deviceHandle = open; + _connected = true; + return true; + }); + } + + public override Task StopCapture() + { + _connected = false; + if (_deviceHandle is null) return Task.FromResult(true); + + _deviceHandle.StopStreaming(); + _deviceHandle.Dispose(); + _deviceHandle = null; + + _context.Dispose(); + _context = null; + + return Task.FromResult(true); + } + + private void Callback(ref Frame frame, IntPtr userPtr) + { + if (!_connected) return; + var data = frame.GetData(); + if (data.Length == 0) return; + SetRawMat(Mat.FromImageData(data)); + } + + public static Device FindDeviceByPath(string path, Context context) + { + if (!path.Contains("/dev/video")) return null; + var videoIndex = path.Replace("/dev/", ""); + var ueventFilePath = $"/sys/class/video4linux/{videoIndex}/device/uevent"; + if (!File.Exists(ueventFilePath)) return null; + var ueventText = File.ReadAllLines(ueventFilePath); + var line = ueventText.FirstOrDefault(i => i.StartsWith("PRODUCT=")); + if (line is null) return null; + var numbers = line.Replace("PRODUCT=", "").Split('/').Select(i => Convert.ToUInt16(i, 16)).ToArray(); + if (numbers.Length < 2) return null; + var vendor = numbers[0]; + var product = numbers[1]; + return context.FindDevice(vendor, product); + } +} diff --git a/src/Baballonia.LibUVCCapture/LibUVCCaptureFactory.cs b/src/Baballonia.LibUVCCapture/LibUVCCaptureFactory.cs new file mode 100644 index 00000000..44b109b2 --- /dev/null +++ b/src/Baballonia.LibUVCCapture/LibUVCCaptureFactory.cs @@ -0,0 +1,18 @@ +using System; +using Baballonia.SDK; +using Microsoft.Extensions.Logging; + +namespace Baballonia.LibUVCCapture; + +public class LibUVCCaptureFactory : ICaptureFactory +{ + private readonly ILoggerFactory _loggerFactory; + + public LibUVCCaptureFactory(ILoggerFactory loggerFactory) => _loggerFactory = loggerFactory; + + public Capture Create(string address) => new LibUVCCapture(address, _loggerFactory.CreateLogger()); + + public bool CanConnect(string address) => address.StartsWith("/dev/video"); + + public string GetProviderName() => nameof(LibUVCCapture); +} From 9a609a3985efe5760ccb606dce02efb17b1bfd6c Mon Sep 17 00:00:00 2001 From: Fro Zen Date: Thu, 11 Dec 2025 02:00:07 -0600 Subject: [PATCH 2/5] oops --- src/Baballonia.LibUVCCapture/LibUVCCapture.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Baballonia.LibUVCCapture/LibUVCCapture.cs b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs index f6badf7a..d64b8660 100644 --- a/src/Baballonia.LibUVCCapture/LibUVCCapture.cs +++ b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs @@ -21,15 +21,28 @@ public override Task StartCapture() { _context = new Context(); _device = FindDeviceByPath(Source, _context); - if (_device is null) return Task.FromResult(false); + if (_device is null) + { + _context.Dispose(); + _context = null; + return Task.FromResult(false); + } return Task.Run(() => { var open = _device.Open(); try { var formats = open.GetStreamControlFormats().ToArray().OrderBy(i => i.Width).ToList(); - var formatIndex = formats.FindIndex(i => i is { Width: > 256, Height: > 256, Format: FrameFormat.Mjpeg }); - if (formatIndex == -1) return false; + var formatIndex = formats.FindIndex(i => i is { Width: > 256, Height: > 256 }); + if (formatIndex == -1) + { + open.Dispose(); + _device.Dispose(); + _device = null; + _context.Dispose(); + _context = null; + return false; + } var format = formats[formatIndex]; open.GetStreamControlFormatSize(format.Format, format.Width, format.Height, format.Fps, out var control); try @@ -81,6 +94,7 @@ public override Task StopCapture() private void Callback(ref Frame frame, IntPtr userPtr) { if (!_connected) return; + if (frame.FrameFormat is not FrameFormat.Mjpeg) return; var data = frame.GetData(); if (data.Length == 0) return; SetRawMat(Mat.FromImageData(data)); From 0ed9f1e2177b6efbcfdf046a669ebc61538cc7f0 Mon Sep 17 00:00:00 2001 From: Fro Zen Date: Thu, 11 Dec 2025 15:41:55 -0600 Subject: [PATCH 3/5] remove CanConnect --- src/Baballonia.LibUVCCapture/LibUVCCapture.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Baballonia.LibUVCCapture/LibUVCCapture.cs b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs index d64b8660..0583a2d5 100644 --- a/src/Baballonia.LibUVCCapture/LibUVCCapture.cs +++ b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs @@ -15,7 +15,6 @@ public sealed class LibUVCCapture(string source, ILogger logger) private Device _device; private DeviceHandle _deviceHandle; private bool _connected; - public override bool CanConnect(string connectionString) => connectionString.StartsWith("/dev/video"); public override Task StartCapture() { From aabad3cd38236d8e3c7c6de0ddf4df23a5a85319 Mon Sep 17 00:00:00 2001 From: Fro Zen Date: Thu, 11 Dec 2025 17:49:57 -0600 Subject: [PATCH 4/5] Update Uvc.NET --- src/Baballonia.Desktop/Baballonia.Desktop.csproj | 2 +- src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Baballonia.Desktop/Baballonia.Desktop.csproj b/src/Baballonia.Desktop/Baballonia.Desktop.csproj index f16d1722..82c23012 100644 --- a/src/Baballonia.Desktop/Baballonia.Desktop.csproj +++ b/src/Baballonia.Desktop/Baballonia.Desktop.csproj @@ -45,7 +45,7 @@ - + diff --git a/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj b/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj index 227d3534..6775f2ba 100644 --- a/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj +++ b/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj @@ -7,7 +7,7 @@ - + From d823511141ad37a033bdf09caf4aecff38a0b461 Mon Sep 17 00:00:00 2001 From: Fro Zen Date: Fri, 12 Dec 2025 16:51:53 -0600 Subject: [PATCH 5/5] More testing --- .../Baballonia.Desktop.csproj | 2 +- .../Baballonia.LibUVCCapture.csproj | 2 +- src/Baballonia.LibUVCCapture/LibUVCCapture.cs | 19 ++++++++++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Baballonia.Desktop/Baballonia.Desktop.csproj b/src/Baballonia.Desktop/Baballonia.Desktop.csproj index 82c23012..67c0a190 100644 --- a/src/Baballonia.Desktop/Baballonia.Desktop.csproj +++ b/src/Baballonia.Desktop/Baballonia.Desktop.csproj @@ -45,7 +45,7 @@ - + diff --git a/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj b/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj index 6775f2ba..1317b3c3 100644 --- a/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj +++ b/src/Baballonia.LibUVCCapture/Baballonia.LibUVCCapture.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Baballonia.LibUVCCapture/LibUVCCapture.cs b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs index 0583a2d5..993c91d5 100644 --- a/src/Baballonia.LibUVCCapture/LibUVCCapture.cs +++ b/src/Baballonia.LibUVCCapture/LibUVCCapture.cs @@ -32,9 +32,10 @@ public override Task StartCapture() try { var formats = open.GetStreamControlFormats().ToArray().OrderBy(i => i.Width).ToList(); - var formatIndex = formats.FindIndex(i => i is { Width: > 256, Height: > 256 }); + var formatIndex = formats.FindIndex(i => i is { Width: > 256, Height: > 256, Format: FrameFormat.Mjpeg }); if (formatIndex == -1) { + Logger.LogInformation("Couldn't find format index"); open.Dispose(); _device.Dispose(); _device = null; @@ -42,11 +43,18 @@ public override Task StartCapture() _context = null; return false; } + Logger.LogInformation("Found format index"); var format = formats[formatIndex]; - open.GetStreamControlFormatSize(format.Format, format.Width, format.Height, format.Fps, out var control); + var control = open.GetStreamControlFormatSize(format.Format, format.Width, format.Height, 0); try { + Logger.LogInformation("Starting stream"); open.StartStreaming(ref control, Callback); + Logger.LogInformation("Started stream"); + _deviceHandle = open; + _connected = true; + Logger.LogInformation("Blah"); + return true; } catch { @@ -69,9 +77,6 @@ public override Task StartCapture() _context = null; return false; } - _deviceHandle = open; - _connected = true; - return true; }); } @@ -92,10 +97,14 @@ public override Task StopCapture() private void Callback(ref Frame frame, IntPtr userPtr) { + Logger.LogInformation("Callback called"); if (!_connected) return; + Logger.LogInformation("IsConnected"); if (frame.FrameFormat is not FrameFormat.Mjpeg) return; + Logger.LogInformation("CorrectFormat"); var data = frame.GetData(); if (data.Length == 0) return; + Logger.LogInformation("HasData"); SetRawMat(Mat.FromImageData(data)); }