using System.Net; using System.Text; using System.Collections.Concurrent; using DotNext; using DotNext.Threading; 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 AsyncLazy Camera { get; set; } public CancellationTokenSource CTS { get; set; } public readonly AsyncReaderWriterLock Lock = new(); public VideoStreamClient( string clientId, int width, int height, AsyncLazy camera) { ClientId = clientId; FrameWidth = width; FrameHeight = height; FrameRate = 30; 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 HttpListener? _httpListener; private readonly int _serverPort = 4321; private readonly ConcurrentDictionary _clientDict = new(); // USB Camera 相关 private AsyncLazy _usbCamera = new(async token => await InitializeUsbCamera(token)); private static async Task 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, 2592, 1994, 30); return camera; } catch (Exception ex) { logger.Error(ex, "Failed to start USB camera"); throw; } } private Optional TryGetClient(string boardId) { return _clientDict.TryGetValue(boardId, out var client) ? client : null; } private Optional GetOrCreateClient( string boardId, int initWidth, int initHeight) { if (_clientDict.TryGetValue(boardId, out var client)) { // 可在此处做分辨率/Camera等配置更新 return client; } var userManager = new Database.UserManager(); 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 AsyncLazy(async (_) => { 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; 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(); if (!client.Camera.IsValueCreated) continue; using (await client.Lock.AcquireWriteLockAsync(cancellationToken)) { var camera = await client.Camera.WithCancellation(cancellationToken); await 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["boardId"]; if (string.IsNullOrEmpty(boardId)) { await SendErrorAsync(context.Response, "Missing clientId"); return; } 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 token = CancellationTokenSource.CreateLinkedTokenSource( client.CTS.Token, cancellationToken).Token; try { token.ThrowIfCancellationRequested(); logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint); if (path == "/video") { // MJPEG 流请求(FPGA) await HandleMjpegStreamAsync(context.Response, client, token); } else if (path == "/usbCamera") { // USB Camera MJPEG流请求 await HandleUsbCameraStreamAsync(context.Response, client, token); } else if (path == "/snapshot") { // 单帧图像请求 await HandleSnapshotRequestAsync(context.Response, client, token); } 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流处理 private async Task HandleUsbCameraStreamAsync( HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken) { var camera = await _usbCamera.WithCancellation(cancellationToken); Action frameHandler = async (jpegData) => { try { 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); } catch { logger.Error("Error sending MJPEG frame"); } }; try { if (!camera.IsCapturing) { logger.Error("USB Camera is not capturing"); 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"); logger.Info("Start USB Camera MJPEG Stream"); camera.FrameReady += frameHandler; while (true) { cancellationToken.ThrowIfCancellationRequested(); await Task.Delay(-1, cancellationToken); } } catch (OperationCanceledException) { logger.Info("USB Camera MJPEG 串流取消"); } catch (Exception ex) { logger.Error(ex, "USB Camera MJPEG流处理异常"); } finally { camera.FrameReady -= frameHandler; logger.Info("Usb Camera Stream Stopped"); try { response.Close(); } catch { } } } private async Task HandleSnapshotRequestAsync( HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken) { // 读取 Camera 快照,返回 JPEG var camera = await client.Camera.WithCancellation(cancellationToken); var frameResult = await 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"); var camera = await client.Camera.WithCancellation(cancellationToken); while (!cancellationToken.IsCancellationRequested) { 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; 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 camera = await client.Camera.WithCancellation(cancellationToken); var result = await 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 = await client.Camera.WithCancellation(cancellationToken); 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 camera = await client.Camera.WithCancellation(cancellationToken); var result = await 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 camera = await client.Camera.WithCancellation(cancellationToken); var result = await 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, CancellationToken cancellationToken = default) { try { var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); var camera = await client.Camera.WithCancellation(cancellationToken); using (await client.Lock.AcquireWriteLockAsync(cancellationToken)) { var ret = await 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(cancellationToken)) { var ret = await 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.IsCancellationRequested) { client.CTS.Cancel(); client.CTS = new CancellationTokenSource(); } if (!client.Camera.IsValueCreated) return; 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"); else logger.Error($"Failed to disable camera {boardId} hardware transmission: {disableResult.Error}"); } } catch (Exception ex) { logger.Error(ex, $"Exception occurred while disabling video transmission for {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 = GetOrCreateClient(boardId, 640, 480).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() }; } }