Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab
This commit is contained in:
		@@ -34,6 +34,7 @@
 | 
			
		||||
              dotnetCorePackages.sdk_8_0
 | 
			
		||||
            ])
 | 
			
		||||
            nuget
 | 
			
		||||
            mono
 | 
			
		||||
            # msbuild
 | 
			
		||||
            omnisharp-roslyn
 | 
			
		||||
            csharpier
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
    <PackageReference Include="ArpLookup" Version="2.0.3" />
 | 
			
		||||
    <PackageReference Include="DotNext" Version="5.23.0" />
 | 
			
		||||
    <PackageReference Include="DotNext.Threading" Version="5.23.0" />
 | 
			
		||||
    <PackageReference Include="FlashCap" Version="1.11.0" />
 | 
			
		||||
    <PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" />
 | 
			
		||||
    <PackageReference Include="linq2db.AspNet" Version="5.4.1" />
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
 | 
			
		||||
@@ -29,8 +30,6 @@
 | 
			
		||||
    <PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
 | 
			
		||||
    <PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
 | 
			
		||||
    <PackageReference Include="NSwag.CodeGeneration.TypeScript" Version="14.4.0" />
 | 
			
		||||
    <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
 | 
			
		||||
    <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
 | 
			
		||||
    <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
 | 
			
		||||
    <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
 | 
			
		||||
    <PackageReference Include="Tapper.Analyzer" Version="1.13.1">
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,7 @@ using System.Text;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using DotNext.Threading;
 | 
			
		||||
 | 
			
		||||
#if USB_CAMERA
 | 
			
		||||
using OpenCvSharp;
 | 
			
		||||
#endif
 | 
			
		||||
using FlashCap;
 | 
			
		||||
 | 
			
		||||
namespace server.Services;
 | 
			
		||||
 | 
			
		||||
@@ -17,17 +14,17 @@ public class VideoStreamClient
 | 
			
		||||
    public int FrameWidth { get; set; }
 | 
			
		||||
    public int FrameHeight { get; set; }
 | 
			
		||||
    public int FrameRate { get; set; }
 | 
			
		||||
    public Peripherals.CameraClient.Camera Camera { get; set; }
 | 
			
		||||
    public AsyncLazy<Peripherals.CameraClient.Camera> Camera { get; set; }
 | 
			
		||||
    public CancellationTokenSource CTS { get; set; }
 | 
			
		||||
    public readonly AsyncReaderWriterLock Lock = new();
 | 
			
		||||
 | 
			
		||||
    public VideoStreamClient(
 | 
			
		||||
        string clientId, int width, int height, Peripherals.CameraClient.Camera camera)
 | 
			
		||||
        string clientId, int width, int height, AsyncLazy<Peripherals.CameraClient.Camera> camera)
 | 
			
		||||
    {
 | 
			
		||||
        ClientId = clientId;
 | 
			
		||||
        FrameWidth = width;
 | 
			
		||||
        FrameHeight = height;
 | 
			
		||||
        FrameRate = 0;
 | 
			
		||||
        FrameRate = 30;
 | 
			
		||||
        Camera = camera;
 | 
			
		||||
        CTS = new CancellationTokenSource();
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,11 +98,25 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    private readonly ConcurrentDictionary<string, VideoStreamClient> _clientDict = new();
 | 
			
		||||
 | 
			
		||||
    // USB Camera 相关
 | 
			
		||||
#if USB_CAMERA
 | 
			
		||||
    private VideoCapture? _usbCamera;
 | 
			
		||||
    private bool _usbCameraEnable = false;
 | 
			
		||||
    private readonly object _usbCameraLock = new object();
 | 
			
		||||
#endif
 | 
			
		||||
    private AsyncLazy<UsbCameraCapture> _usbCamera = new(async token => await InitializeUsbCamera(token));
 | 
			
		||||
 | 
			
		||||
    private static async Task<UsbCameraCapture> InitializeUsbCamera(CancellationToken token)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var camera = new UsbCameraCapture();
 | 
			
		||||
            var devices = camera.GetDevices();
 | 
			
		||||
            for (int i = 0; i < devices.Count; i++)
 | 
			
		||||
                logger.Info($"Device[{i}]: {devices[i].Name}");
 | 
			
		||||
            await camera.StartAsync(1, 3840, 2160, 30);
 | 
			
		||||
            return camera;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "Failed to start USB camera");
 | 
			
		||||
            throw;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<VideoStreamClient> TryGetClient(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
@@ -116,7 +127,7 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<VideoStreamClient?> GetOrCreateClientAsync(string boardId, int initWidth, int initHeight)
 | 
			
		||||
    private Optional<VideoStreamClient> GetOrCreateClient(string boardId, int initWidth, int initHeight)
 | 
			
		||||
    {
 | 
			
		||||
        if (_clientDict.TryGetValue(boardId, out var client))
 | 
			
		||||
        {
 | 
			
		||||
@@ -135,13 +146,17 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
        var board = boardRet.Value.Value;
 | 
			
		||||
 | 
			
		||||
        var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
 | 
			
		||||
        var ret = await camera.Init();
 | 
			
		||||
        if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
        var camera = new AsyncLazy<Peripherals.CameraClient.Camera>(async (_) =>
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("Camera Init Failed!");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
            var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
 | 
			
		||||
            var ret = await camera.Init();
 | 
			
		||||
            if (!ret.IsSuccessful || !ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("Camera Init Failed!");
 | 
			
		||||
                throw new Exception("Camera Init Failed!");
 | 
			
		||||
            }
 | 
			
		||||
            return camera;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        client = new VideoStreamClient(boardId, initWidth, initHeight, camera);
 | 
			
		||||
        _clientDict[boardId] = client;
 | 
			
		||||
@@ -172,7 +187,8 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
            client.CTS.Cancel();
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
 | 
			
		||||
            {
 | 
			
		||||
                await client.Camera.EnableHardwareTrans(false);
 | 
			
		||||
                var camera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
                await camera.EnableHardwareTrans(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        _clientDict.Clear();
 | 
			
		||||
@@ -217,40 +233,40 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var path = context.Request.Url?.AbsolutePath ?? "/";
 | 
			
		||||
        var boardId = context.Request.QueryString["board"];
 | 
			
		||||
        var width = int.TryParse(context.Request.QueryString["width"], out var w) ? w : 640;
 | 
			
		||||
        var height = int.TryParse(context.Request.QueryString["height"], out var h) ? h : 480;
 | 
			
		||||
 | 
			
		||||
        var boardId = context.Request.QueryString["boardId"];
 | 
			
		||||
        if (string.IsNullOrEmpty(boardId))
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(context.Response, "Missing clientId");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var client = await GetOrCreateClientAsync(boardId, width, height);
 | 
			
		||||
        if (client == null)
 | 
			
		||||
        var width = int.TryParse(context.Request.QueryString["width"], out var w) ? w : 640;
 | 
			
		||||
        var height = int.TryParse(context.Request.QueryString["height"], out var h) ? h : 480;
 | 
			
		||||
 | 
			
		||||
        var clientOpt = GetOrCreateClient(boardId, width, height);
 | 
			
		||||
        if (!clientOpt.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            await SendErrorAsync(context.Response, "Invalid clientId or camera not available");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var client = clientOpt.Value;
 | 
			
		||||
 | 
			
		||||
        var clientToken = client.CTS.Token;
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint);
 | 
			
		||||
 | 
			
		||||
            if (path == "/video-stream")
 | 
			
		||||
            if (path == "/video")
 | 
			
		||||
            {
 | 
			
		||||
                // MJPEG 流请求(FPGA)
 | 
			
		||||
                await HandleMjpegStreamAsync(context.Response, client, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
#if USB_CAMERA
 | 
			
		||||
            else if (requestPath == "/usb-camera")
 | 
			
		||||
            else if (path == "/usbCamera")
 | 
			
		||||
            {
 | 
			
		||||
                // USB Camera MJPEG流请求
 | 
			
		||||
                await HandleUsbCameraStreamAsync(response, cancellationToken);
 | 
			
		||||
                await HandleUsbCameraStreamAsync(context.Response, client, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
#endif
 | 
			
		||||
            else if (path == "/snapshot")
 | 
			
		||||
            {
 | 
			
		||||
                // 单帧图像请求
 | 
			
		||||
@@ -281,24 +297,16 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // USB Camera MJPEG流处理
 | 
			
		||||
#if USB_CAMERA
 | 
			
		||||
    private async Task HandleUsbCameraStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken)
 | 
			
		||||
    private async Task HandleUsbCameraStreamAsync(
 | 
			
		||||
        HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            lock (_usbCameraLock)
 | 
			
		||||
            {
 | 
			
		||||
                if (_usbCamera == null)
 | 
			
		||||
                {
 | 
			
		||||
                    _usbCamera = new VideoCapture(1);
 | 
			
		||||
                    _usbCamera.Fps = _frameRate;
 | 
			
		||||
                    _usbCamera.FrameWidth = _frameWidth;
 | 
			
		||||
                    _usbCamera.FrameHeight = _frameHeight;
 | 
			
		||||
                    _usbCameraEnable = _usbCamera.IsOpened();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (!_usbCameraEnable || _usbCamera == null || !_usbCamera.IsOpened())
 | 
			
		||||
            var camera = await _usbCamera.WithCancellation(cancellationToken);
 | 
			
		||||
 | 
			
		||||
            if (!camera.IsCapturing)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("USB Camera is not capturing");
 | 
			
		||||
                response.StatusCode = 500;
 | 
			
		||||
                await response.OutputStream.FlushAsync(cancellationToken);
 | 
			
		||||
                response.Close();
 | 
			
		||||
@@ -310,61 +318,52 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
            response.Headers.Add("Pragma", "no-cache");
 | 
			
		||||
            response.Headers.Add("Expires", "0");
 | 
			
		||||
 | 
			
		||||
            using (var mat = new Mat())
 | 
			
		||||
            logger.Info("Start USB Camera MJPEG Stream");
 | 
			
		||||
 | 
			
		||||
            while (true)
 | 
			
		||||
            {
 | 
			
		||||
                while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
                cancellationToken.ThrowIfCancellationRequested();
 | 
			
		||||
 | 
			
		||||
                var jpegData = camera.GetLatestFrame();
 | 
			
		||||
                if (jpegData == null)
 | 
			
		||||
                {
 | 
			
		||||
                    bool grabbed;
 | 
			
		||||
                    lock (_usbCameraLock)
 | 
			
		||||
                    {
 | 
			
		||||
                        grabbed = _usbCamera.Read(mat);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (!grabbed || mat.Empty())
 | 
			
		||||
                    {
 | 
			
		||||
                        await Task.Delay(50, cancellationToken);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // 编码为JPEG
 | 
			
		||||
                    byte[]? jpegData = null;
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        jpegData = mat.ToBytes(".jpg", new int[] { (int)ImwriteFlags.JpegQuality, 80 });
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        logger.Error(ex, "USB Camera帧编码JPEG失败");
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
                    if (jpegData == null)
 | 
			
		||||
                        continue;
 | 
			
		||||
 | 
			
		||||
                    // MJPEG帧头
 | 
			
		||||
                    var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegData.Length + "\r\n\r\n");
 | 
			
		||||
                    await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
 | 
			
		||||
                    await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
 | 
			
		||||
                    await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); // \r\n
 | 
			
		||||
                    await response.OutputStream.FlushAsync(cancellationToken);
 | 
			
		||||
 | 
			
		||||
                    await Task.Delay(1000 / _frameRate, cancellationToken);
 | 
			
		||||
                    logger.Warn("USB Camera MJPEG帧获取失败");
 | 
			
		||||
                    await Task.Delay(1000 / client.FrameRate, cancellationToken);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // MJPEG帧头
 | 
			
		||||
                var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegData.Length + "\r\n\r\n");
 | 
			
		||||
                await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
 | 
			
		||||
                await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
 | 
			
		||||
                await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); // \r\n
 | 
			
		||||
                await response.OutputStream.FlushAsync(cancellationToken);
 | 
			
		||||
 | 
			
		||||
                await Task.Delay(1000 / client.FrameRate, cancellationToken);
 | 
			
		||||
                logger.Info("USB Camera MJPEG帧发送成功");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (OperationCanceledException ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info(ex, "USB Camera MJPEG 串流取消");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "USB Camera MJPEG流处理异常");
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
            try { response.Close(); } catch { }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
 | 
			
		||||
    private async Task HandleSnapshotRequestAsync(
 | 
			
		||||
        HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        // 读取 Camera 快照,返回 JPEG
 | 
			
		||||
        var frameResult = await client.Camera.ReadFrame();
 | 
			
		||||
        var camera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
        var frameResult = await camera.ReadFrame();
 | 
			
		||||
        if (!frameResult.IsSuccessful || frameResult.Value == null)
 | 
			
		||||
        {
 | 
			
		||||
            response.StatusCode = 500;
 | 
			
		||||
@@ -386,16 +385,18 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
        response.Close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task HandleMjpegStreamAsync(HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
 | 
			
		||||
    private async Task HandleMjpegStreamAsync(
 | 
			
		||||
        HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
 | 
			
		||||
        response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
			
		||||
        response.Headers.Add("Pragma", "no-cache");
 | 
			
		||||
        response.Headers.Add("Expires", "0");
 | 
			
		||||
 | 
			
		||||
        var camera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
        while (!cancellationToken.IsCancellationRequested)
 | 
			
		||||
        {
 | 
			
		||||
            var frameResult = await client.Camera.ReadFrame();
 | 
			
		||||
            var frameResult = await camera.ReadFrame();
 | 
			
		||||
            if (!frameResult.IsSuccessful || frameResult.Value == null) continue;
 | 
			
		||||
            var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80);
 | 
			
		||||
            if (!jpegResult.IsSuccessful) continue;
 | 
			
		||||
@@ -508,7 +509,8 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
            {
 | 
			
		||||
                // 从摄像头读取帧数据
 | 
			
		||||
                var readStartTime = DateTime.UtcNow;
 | 
			
		||||
                var result = await client.Camera.ReadFrame();
 | 
			
		||||
                var camera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
                var result = await camera.ReadFrame();
 | 
			
		||||
                var readEndTime = DateTime.UtcNow;
 | 
			
		||||
                var readTime = (readEndTime - readStartTime).TotalMilliseconds;
 | 
			
		||||
 | 
			
		||||
@@ -568,7 +570,7 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken))
 | 
			
		||||
            {
 | 
			
		||||
                var currentCamera = client.Camera;
 | 
			
		||||
                var currentCamera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
                if (currentCamera == null)
 | 
			
		||||
                {
 | 
			
		||||
                    var message = $"获取摄像头失败";
 | 
			
		||||
@@ -621,7 +623,8 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync(
 | 
			
		||||
                TimeSpan.FromMilliseconds(timeout), cancellationToken))
 | 
			
		||||
            {
 | 
			
		||||
                var result = await client.Camera.InitAutoFocus();
 | 
			
		||||
                var camera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
                var result = await camera.InitAutoFocus();
 | 
			
		||||
 | 
			
		||||
                if (result.IsSuccessful && result.Value)
 | 
			
		||||
                {
 | 
			
		||||
@@ -655,7 +658,8 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
            logger.Info($"Board{boardId}开始执行摄像头自动对焦");
 | 
			
		||||
 | 
			
		||||
            var result = await client.Camera.PerformAutoFocus();
 | 
			
		||||
            var camera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
            var result = await camera.PerformAutoFocus();
 | 
			
		||||
 | 
			
		||||
            if (result.IsSuccessful && result.Value)
 | 
			
		||||
            {
 | 
			
		||||
@@ -679,16 +683,18 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    /// 配置摄像头连接参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="boardId">板卡ID</param>
 | 
			
		||||
    /// <param name="cancellationToken">取消令牌</param>
 | 
			
		||||
    /// <returns>配置是否成功</returns>
 | 
			
		||||
    public async Task<bool> ConfigureCameraAsync(string boardId)
 | 
			
		||||
    public async Task<bool> ConfigureCameraAsync(string boardId, CancellationToken cancellationToken = default)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
            var camera = await client.Camera.WithCancellation(cancellationToken);
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
 | 
			
		||||
            {
 | 
			
		||||
                var ret = await client.Camera.Init();
 | 
			
		||||
                var ret = await camera.Init();
 | 
			
		||||
                if (!ret.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error(ret.Error);
 | 
			
		||||
@@ -702,9 +708,9 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
 | 
			
		||||
            {
 | 
			
		||||
                var ret = await client.Camera.ChangeResolution(client.FrameWidth, client.FrameHeight);
 | 
			
		||||
                var ret = await camera.ChangeResolution(client.FrameWidth, client.FrameHeight);
 | 
			
		||||
                if (!ret.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error(ret.Error);
 | 
			
		||||
@@ -747,7 +753,7 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
                    client.CTS.Cancel();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var camera = client.Camera;
 | 
			
		||||
                var camera = await client.Camera.WithCancellation(client.CTS.Token);
 | 
			
		||||
                var disableResult = await camera.EnableHardwareTrans(enable);
 | 
			
		||||
                if (disableResult.IsSuccessful && disableResult.Value)
 | 
			
		||||
                    logger.Info($"Successfully disabled camera {boardId} hardware transmission");
 | 
			
		||||
@@ -782,7 +788,7 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
    public VideoStreamEndpoint GetVideoEndpoint(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
        var client = GetOrCreateClient(boardId, 640, 480).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
 | 
			
		||||
        return new VideoStreamEndpoint
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										222
									
								
								server/src/Services/UsbCameraCapture.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								server/src/Services/UsbCameraCapture.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,222 @@
 | 
			
		||||
// using System.Drawing;
 | 
			
		||||
using FlashCap;
 | 
			
		||||
 | 
			
		||||
namespace server.Services;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Simple USB camera capture service following Linus principles:
 | 
			
		||||
/// - Single responsibility: just capture frames
 | 
			
		||||
/// - No special cases: uniform error handling
 | 
			
		||||
/// - Good taste: clean data structures
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class UsbCameraCapture : IDisposable
 | 
			
		||||
{
 | 
			
		||||
    private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    private readonly CaptureDevices _captureDevices;
 | 
			
		||||
    private CaptureDevice? _device;
 | 
			
		||||
    private CaptureDeviceDescriptor? _descriptor;
 | 
			
		||||
    private VideoCharacteristics? _characteristics;
 | 
			
		||||
 | 
			
		||||
    // Single source of truth for latest frame - no redundant buffering
 | 
			
		||||
    private volatile byte[]? _latestFrame;
 | 
			
		||||
    private volatile bool _isCapturing;
 | 
			
		||||
    private bool _disposed;
 | 
			
		||||
 | 
			
		||||
    public event Action<byte[]>? FrameReady;
 | 
			
		||||
    public event Action<Exception>? Error;
 | 
			
		||||
 | 
			
		||||
    public bool IsCapturing => _isCapturing;
 | 
			
		||||
    public VideoCharacteristics? CurrentCharacteristics => _characteristics;
 | 
			
		||||
    public CaptureDeviceDescriptor? CurrentDevice => _descriptor;
 | 
			
		||||
 | 
			
		||||
    public UsbCameraCapture()
 | 
			
		||||
    {
 | 
			
		||||
        _captureDevices = new CaptureDevices();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get all available camera devices
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyList<CaptureDeviceDescriptor> GetDevices()
 | 
			
		||||
    {
 | 
			
		||||
        return _captureDevices.EnumerateDescriptors().ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Start capturing from specified device with best matching characteristics
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async Task StartAsync(int deviceIndex, int width = 640, int height = 480, int frameRate = 30)
 | 
			
		||||
    {
 | 
			
		||||
        var devices = GetDevices();
 | 
			
		||||
        if (deviceIndex >= devices.Count)
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(deviceIndex));
 | 
			
		||||
 | 
			
		||||
        var descriptor = devices[deviceIndex];
 | 
			
		||||
        var characteristics = FindBestMatch(descriptor, width, height, frameRate);
 | 
			
		||||
 | 
			
		||||
        await StartAsync(descriptor, characteristics);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Start capturing with exact device and characteristics
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async Task StartAsync(CaptureDeviceDescriptor descriptor, VideoCharacteristics characteristics)
 | 
			
		||||
    {
 | 
			
		||||
        if (_isCapturing)
 | 
			
		||||
            await StopAsync();
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            _descriptor = descriptor;
 | 
			
		||||
            _characteristics = characteristics;
 | 
			
		||||
            _device = await descriptor.OpenAsync(characteristics, OnFrameCaptured);
 | 
			
		||||
 | 
			
		||||
            await _device.StartAsync();
 | 
			
		||||
            _isCapturing = true;
 | 
			
		||||
            logger.Debug("Started capturing");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            await CleanupAsync();
 | 
			
		||||
            Error?.Invoke(ex);
 | 
			
		||||
            throw;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Stop capturing and cleanup
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public async Task StopAsync()
 | 
			
		||||
    {
 | 
			
		||||
        if (!_isCapturing)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        _isCapturing = false;
 | 
			
		||||
        await CleanupAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get the latest captured frame (returns copy for thread safety)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public byte[]? GetLatestFrame()
 | 
			
		||||
    {
 | 
			
		||||
        return _latestFrame;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // /// <summary>
 | 
			
		||||
    // /// Get latest frame as bitmap
 | 
			
		||||
    // /// </summary>
 | 
			
		||||
    // public Bitmap? GetLatestFrameAsBitmap()
 | 
			
		||||
    // {
 | 
			
		||||
    //     var frameData = _latestFrame;
 | 
			
		||||
    //     if (frameData == null)
 | 
			
		||||
    //         return null;
 | 
			
		||||
 | 
			
		||||
    //     try
 | 
			
		||||
    //     {
 | 
			
		||||
    //         using var ms = new MemoryStream(frameData);
 | 
			
		||||
    //         return new Bitmap(ms);
 | 
			
		||||
    //     }
 | 
			
		||||
    //     catch
 | 
			
		||||
    //     {
 | 
			
		||||
    //         return null;
 | 
			
		||||
    //     }
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Get supported video characteristics for current device
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyList<VideoCharacteristics> GetSupportedCharacteristics()
 | 
			
		||||
    {
 | 
			
		||||
        return _descriptor?.Characteristics.ToArray() ?? Array.Empty<VideoCharacteristics>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private VideoCharacteristics FindBestMatch(CaptureDeviceDescriptor descriptor, int width, int height, int frameRate)
 | 
			
		||||
    {
 | 
			
		||||
        var characteristics = descriptor.Characteristics;
 | 
			
		||||
 | 
			
		||||
        // Exact match first
 | 
			
		||||
        var exact = characteristics.FirstOrDefault(c =>
 | 
			
		||||
            c.Width == width && c.Height == height && Math.Abs(c.FramesPerSecond - frameRate) < 1);
 | 
			
		||||
        if (exact != null)
 | 
			
		||||
            return exact;
 | 
			
		||||
 | 
			
		||||
        // Resolution match with best framerate
 | 
			
		||||
        var resolution = characteristics
 | 
			
		||||
            .Where(c => c.Width == width && c.Height == height)
 | 
			
		||||
            .OrderByDescending(c => c.FramesPerSecond)
 | 
			
		||||
            .FirstOrDefault();
 | 
			
		||||
        if (resolution != null)
 | 
			
		||||
            return resolution;
 | 
			
		||||
 | 
			
		||||
        // Closest resolution
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var closest = characteristics
 | 
			
		||||
                .OrderBy(c => Math.Abs(c.Width - width) + Math.Abs(c.Height - height))
 | 
			
		||||
                .ThenByDescending(c => c.FramesPerSecond)
 | 
			
		||||
                .First();
 | 
			
		||||
 | 
			
		||||
            return closest;
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            for (int i = 0; i < characteristics.Length; i++)
 | 
			
		||||
                logger.Error($"Characteristics[{i}]: {characteristics[i].Width}x{characteristics[i].Height} @ {characteristics[i].FramesPerSecond}fps");
 | 
			
		||||
            throw;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void OnFrameCaptured(PixelBufferScope bufferScope)
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info("Frame captured");
 | 
			
		||||
        if (!_isCapturing)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // Simple: extract and store. No queues, no locks, no complexity.
 | 
			
		||||
            var imageData = bufferScope.Buffer.CopyImage();
 | 
			
		||||
            _latestFrame = imageData;
 | 
			
		||||
            FrameReady?.Invoke(imageData);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Error?.Invoke(ex);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task CleanupAsync()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (_device != null)
 | 
			
		||||
            {
 | 
			
		||||
                await _device.StopAsync();
 | 
			
		||||
                _device.Dispose();
 | 
			
		||||
                _device = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Error?.Invoke(ex);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _latestFrame = null;
 | 
			
		||||
            _descriptor = null;
 | 
			
		||||
            _characteristics = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Dispose()
 | 
			
		||||
    {
 | 
			
		||||
        if (_disposed) return;
 | 
			
		||||
 | 
			
		||||
        if (_isCapturing) StopAsync().Wait();
 | 
			
		||||
 | 
			
		||||
        _device?.Dispose();
 | 
			
		||||
        _disposed = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user