using System.Net; using System.Text; using System.Collections.Concurrent; using DotNext; using DotNext.Threading; #if USB_CAMERA using OpenCvSharp; #endif namespace server.Services; public class VideoStreamClient { public string? ClientId { get; set; } = string.Empty; public bool IsEnabled { get; set; } = true; public int FrameWidth { get; set; } public int FrameHeight { get; set; } public int FrameRate { get; set; } public 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) { ClientId = clientId; FrameWidth = width; FrameHeight = height; FrameRate = 0; Camera = camera; CTS = new CancellationTokenSource(); } } /// /// 表示摄像头连接状态信息 /// public class VideoStreamEndpoint { public required string BoardId { get; set; } = ""; public required string MjpegUrl { get; set; } = ""; public required string VideoUrl { get; set; } = ""; public required string SnapshotUrl { get; set; } = ""; public required string HtmlUrl { get; set; } = ""; public required string UsbCameraUrl { get; set; } = ""; public required bool IsEnabled { get; set; } /// /// 视频流的帧率(FPS) /// public required int FrameRate { get; set; } public int FrameWidth { get; set; } public int FrameHeight { get; set; } /// /// 视频分辨率(如 640x480) /// public string Resolution => $"{FrameWidth}x{FrameHeight}"; } /// /// 表示视频流服务的运行状态 /// public class VideoStreamServiceStatus { /// /// 服务是否正在运行 /// public bool IsRunning { get; set; } /// /// 服务监听的端口号 /// public int ServerPort { get; set; } /// /// 当前连接的客户端端点列表 /// public List ClientEndpoints { get; set; } = new(); /// /// 当前连接的客户端数量 /// public int ConnectedClientsNum => ClientEndpoints.Count; } /// /// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页 /// 支持动态配置摄像头地址和端口 /// public class HttpVideoStreamService : BackgroundService { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private readonly Database.UserManager _userManager; private HttpListener? _httpListener; private readonly int _serverPort = 4321; private readonly ConcurrentDictionary _clientDict = new(); // USB Camera 相关 #if USB_CAMERA private VideoCapture? _usbCamera; private bool _usbCameraEnable = false; private readonly object _usbCameraLock = new object(); #endif public HttpVideoStreamService(Database.UserManager userManager) { _userManager = userManager; } private Optional TryGetClient(string boardId) { if (_clientDict.TryGetValue(boardId, out var client)) { return client; } return null; } private async Task GetOrCreateClientAsync(string boardId, int initWidth, int initHeight) { if (_clientDict.TryGetValue(boardId, out var client)) { // 可在此处做分辨率/Camera等配置更新 return client; } var boardRet = _userManager.GetBoardByID(Guid.Parse(boardId)); if (!boardRet.IsSuccessful || !boardRet.Value.HasValue) { logger.Error($"Failed to get board with ID {boardId}"); return null; } 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) { logger.Error("Camera Init Failed!"); return null; } client = new VideoStreamClient(boardId, initWidth, initHeight, camera); _clientDict[boardId] = client; return client; } /// /// 初始化 HttpVideoStreamService /// public override async Task StartAsync(CancellationToken cancellationToken) { _httpListener = new HttpListener(); _httpListener.Prefixes.Add($"http://{Global.LocalHost}:{_serverPort}/"); _httpListener.Start(); logger.Info($"Video Stream Service started on port {_serverPort}"); await base.StartAsync(cancellationToken); } /// /// 停止 HTTP 视频流服务 /// public override async Task StopAsync(CancellationToken cancellationToken) { foreach (var clientKey in _clientDict.Keys) { var client = _clientDict[clientKey]; client.CTS.Cancel(); using (await client.Lock.AcquireWriteLockAsync(cancellationToken)) { await client.Camera.EnableHardwareTrans(false); } } _clientDict.Clear(); await base.StopAsync(cancellationToken); } /// /// 执行 HTTP 视频流服务 /// /// 取消令牌 /// 任务 protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { if (_httpListener == null) continue; try { logger.Debug("Waiting for HTTP request..."); var contextTask = _httpListener.GetContextAsync(); var completedTask = await Task.WhenAny(contextTask, Task.Delay(-1, stoppingToken)); if (completedTask == contextTask) { var context = contextTask.Result; logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}"); if (context != null) _ = HandleRequestAsync(context, stoppingToken); } else { break; } } catch (Exception ex) { logger.Error(ex, "Error in GetContextAsync"); break; } } } 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; if (string.IsNullOrEmpty(boardId)) { await SendErrorAsync(context.Response, "Missing clientId"); return; } var client = await GetOrCreateClientAsync(boardId, width, height); if (client == null) { await SendErrorAsync(context.Response, "Invalid clientId or camera not available"); return; } var clientToken = client.CTS.Token; try { logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint); if (path == "/video-stream") { // MJPEG 流请求(FPGA) await HandleMjpegStreamAsync(context.Response, client, cancellationToken); } #if USB_CAMERA else if (requestPath == "/usb-camera") { // USB Camera MJPEG流请求 await HandleUsbCameraStreamAsync(response, cancellationToken); } #endif else if (path == "/snapshot") { // 单帧图像请求 await HandleSnapshotRequestAsync(context.Response, client, cancellationToken); } else if (path == "/html") { // HTML页面请求 await SendIndexHtmlPageAsync(context.Response); } else { // 默认返回简单的HTML页面,提供链接到视频页面 await SendIndexHtmlPageAsync(context.Response); } } catch (Exception ex) { logger.Error(ex, "接受HTTP客户端连接时发生错误"); } } private async Task SendErrorAsync(HttpListenerResponse response, string message) { response.StatusCode = 400; await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message)); response.Close(); } // USB Camera MJPEG流处理 #if USB_CAMERA private async Task HandleUsbCameraStreamAsync(HttpListenerResponse response, 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()) { response.StatusCode = 500; await response.OutputStream.FlushAsync(cancellationToken); response.Close(); return; } 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"); using (var mat = new Mat()) { while (!cancellationToken.IsCancellationRequested) { 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); } } } 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) { // 读取 Camera 快照,返回 JPEG var frameResult = await client.Camera.ReadFrame(); if (!frameResult.IsSuccessful || frameResult.Value == null) { response.StatusCode = 500; await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("Failed to get snapshot")); response.Close(); return; } var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80); if (!jpegResult.IsSuccessful) { response.StatusCode = 500; await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("JPEG conversion failed")); response.Close(); return; } response.ContentType = "image/jpeg"; response.ContentLength64 = jpegResult.Value.Length; await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken); response.Close(); } 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"); while (!cancellationToken.IsCancellationRequested) { var frameResult = await client.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; var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegResult.Value.Length + "\r\n\r\n"); await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken); await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken); await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); await response.OutputStream.FlushAsync(cancellationToken); await Task.Delay(1000 / client.FrameWidth, cancellationToken); } response.Close(); } private async Task SendVideoHtmlPageAsync(HttpListenerResponse response) { string html = $@" FPGA 视频流

FPGA 实时视频流

状态: 连接中...
"; response.ContentType = "text/html"; response.ContentEncoding = Encoding.UTF8; byte[] buffer = Encoding.UTF8.GetBytes(html); response.ContentLength64 = buffer.Length; await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.Close(); } private async Task SendIndexHtmlPageAsync(HttpListenerResponse response) { string html = $@" FPGA WebLab 视频服务

FPGA WebLab 视频服务

观看实时视频 获取当前快照

HTTP流媒体服务端口: {_serverPort}

"; response.ContentType = "text/html"; response.ContentEncoding = Encoding.UTF8; byte[] buffer = Encoding.UTF8.GetBytes(html); response.ContentLength64 = buffer.Length; await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.Close(); } /// /// 从 FPGA 获取图像数据 /// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24 /// private async Task GetFPGAImageData( VideoStreamClient client, CancellationToken cancellationToken = default) { try { using (await client.Lock.AcquireWriteLockAsync(cancellationToken)) { // 从摄像头读取帧数据 var readStartTime = DateTime.UtcNow; var result = await client.Camera.ReadFrame(); var readEndTime = DateTime.UtcNow; var readTime = (readEndTime - readStartTime).TotalMilliseconds; if (!result.IsSuccessful) { logger.Error("读取摄像头帧数据失败: {Error}", result.Error); return new byte[0]; } var rgb565Data = result.Value; // 验证数据长度是否正确 if (!Common.Image.ValidateImageDataLength(rgb565Data, client.FrameWidth, client.FrameHeight, 2)) { logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}", client.FrameWidth * client.FrameHeight * 2, rgb565Data.Length); } // 将 RGB565 转换为 RGB24 var convertStartTime = DateTime.UtcNow; var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, client.FrameWidth, client.FrameHeight, isLittleEndian: false); var convertEndTime = DateTime.UtcNow; var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds; if (!rgb24Result.IsSuccessful) { logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error); return new byte[0]; } return rgb24Result.Value; } } catch (Exception ex) { logger.Error(ex, "获取FPGA图像数据时发生错误"); return new byte[0]; } } /// /// 设置视频流分辨率 /// /// 板卡ID /// 宽度 /// 高度 /// 超时时间(毫秒) /// 取消令牌 /// 设置结果 public async Task> SetResolutionAsync( string boardId, int width, int height, int timeout = 100, CancellationToken cancellationToken = default) { try { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); using (await client.Lock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken)) { var currentCamera = client.Camera; if (currentCamera == null) { var message = $"获取摄像头失败"; logger.Error(message); return new(new Exception(message)); } // 设置摄像头分辨率 var ret = await currentCamera.ChangeResolution(width, height); if (!ret.IsSuccessful) { var message = $"设置摄像头分辨率失败: {ret.Error}"; logger.Error(message); return new(new Exception(message)); } if (!ret.Value) { logger.Warn($"设置摄像头分辨率失败"); return false; } // 更新HTTP服务的分辨率配置 client.FrameWidth = width; client.FrameHeight = height; logger.Info($"视频流分辨率已成功设置为 {width}x{height}"); return true; } } catch (Exception ex) { var message = $"设置分辨率时发生错误: {ex.Message}"; logger.Error(ex, message); return new(new Exception(message)); } } /// /// 初始化摄像头自动对焦功能 /// /// 初始化结果 public async Task InitAutoFocusAsync( string boardId, int timeout = 1000, CancellationToken cancellationToken = default) { try { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); using (await client.Lock.AcquireWriteLockAsync( TimeSpan.FromMilliseconds(timeout), cancellationToken)) { var result = await client.Camera.InitAutoFocus(); if (result.IsSuccessful && result.Value) { logger.Info($"Board{boardId}摄像头自动对焦功能初始化成功"); return true; } else { logger.Error($"Board{boardId}摄像头自动对焦功能初始化失败: {result.Error?.Message ?? "未知错误"}"); return false; } } } catch (Exception ex) { logger.Error(ex, $"Board{boardId}初始化摄像头自动对焦功能时发生异常"); return false; } } /// /// 执行摄像头自动对焦 /// /// 对焦结果 public async Task PerformAutoFocusAsync( string boardId, int timeout = 1000, CancellationToken cancellationToken = default) { try { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); logger.Info($"Board{boardId}开始执行摄像头自动对焦"); var result = await client.Camera.PerformAutoFocus(); if (result.IsSuccessful && result.Value) { logger.Info($"Board{boardId}摄像头自动对焦成功"); return true; } else { logger.Error($"Board{boardId}摄像头自动对焦执行失败: {result.Error?.Message ?? "未知错误"}"); return false; } } catch (Exception ex) { logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常"); return false; } } /// /// 配置摄像头连接参数 /// /// 板卡ID /// 配置是否成功 public async Task ConfigureCameraAsync(string boardId) { try { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); using (await client.Lock.AcquireWriteLockAsync()) { var ret = await client.Camera.Init(); if (!ret.IsSuccessful) { logger.Error(ret.Error); throw ret.Error; } if (!ret.Value) { logger.Error($"Camera Init Failed!"); throw new Exception($"Camera Init Failed!"); } } using (await client.Lock.AcquireWriteLockAsync()) { var ret = await client.Camera.ChangeResolution(client.FrameWidth, client.FrameHeight); if (!ret.IsSuccessful) { logger.Error(ret.Error); throw ret.Error; } if (!ret.Value) { logger.Error($"Camera Resolution Change Failed!"); throw new Exception($"Camera Resolution Change Failed!"); } } return true; } catch (Exception ex) { logger.Error(ex, "配置摄像头连接时发生错误"); return false; } } public async Task SetVideoStreamEnableAsync(string boardId, bool enable) { try { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); if (client.IsEnabled == enable) return; using (await client.Lock.AcquireWriteLockAsync()) { if (enable) { client.CTS = new CancellationTokenSource(); } else { client.CTS.Cancel(); } var camera = client.Camera; var disableResult = await camera.EnableHardwareTrans(enable); if (disableResult.IsSuccessful && disableResult.Value) logger.Info($"Successfully disabled camera {boardId} hardware transmission"); else logger.Error($"Failed to disable camera {boardId} hardware transmission: {disableResult.Error}"); } } catch (Exception ex) { logger.Error(ex, $"Exception occurred while disabling HDMI transmission for camera {boardId}"); } } public async ValueTask TestCameraConnection(string boardId) { try { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); var imageData = await GetFPGAImageData(client); if (imageData == null || imageData.Length == 0) return false; return true; } catch (Exception ex) { logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常"); return false; } } public VideoStreamEndpoint GetVideoEndpoint(string boardId) { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); return new VideoStreamEndpoint { BoardId = boardId, MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={boardId}", VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={boardId}", SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={boardId}", UsbCameraUrl = $"http://{Global.LocalHost}:{_serverPort}/usbCamera?boardId={boardId}", HtmlUrl = $"http://{Global.LocalHost}:{_serverPort}/html?boardId={boardId}", IsEnabled = client.IsEnabled, FrameRate = client.FrameRate }; } public List GetAllVideoEndpoints() { var endpoints = new List(); foreach (var boardId in _clientDict.Keys) endpoints.Add(GetVideoEndpoint(boardId)); return endpoints; } public VideoStreamServiceStatus GetServiceStatus() { return new VideoStreamServiceStatus { IsRunning = true, ServerPort = _serverPort, ClientEndpoints = GetAllVideoEndpoints() }; } }