From bed0158a5f9836d0d54ec939c0b7e877f3755fe1 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Thu, 3 Jul 2025 14:52:00 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E7=BB=A7=E7=BB=AD=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Program.cs | 3 +- server/src/Controllers/JtagController.cs | 14 +- .../src/Controllers/RemoteUpdateController.cs | 8 +- .../src/Controllers/VideoStreamController.cs | 59 +- server/src/HttpVideoStreamService.cs | 575 ------------------ server/src/Peripherals/CameraClient.cs | 0 server/src/{ => Peripherals}/JtagClient.cs | 3 +- .../{ => Peripherals}/RemoteUpdateClient.cs | 3 +- server/src/Services/HttpVideoStreamService.cs | 575 ++++++++++++++++++ 9 files changed, 624 insertions(+), 616 deletions(-) delete mode 100644 server/src/HttpVideoStreamService.cs create mode 100644 server/src/Peripherals/CameraClient.cs rename server/src/{ => Peripherals}/JtagClient.cs (99%) rename server/src/{ => Peripherals}/RemoteUpdateClient.cs (99%) create mode 100644 server/src/Services/HttpVideoStreamService.cs diff --git a/server/Program.cs b/server/Program.cs index 1b4e2ad..cbcbf05 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -3,7 +3,8 @@ using Microsoft.Extensions.FileProviders; using Newtonsoft.Json; using NLog; using NLog.Web; -using server.src; + +using server.Services; // Early init of NLog to allow startup and exception logging, before host is built var logger = NLog.LogManager.Setup() diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index a4f0da5..8411454 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -34,7 +34,7 @@ public class JtagController : ControllerBase [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public async ValueTask GetDeviceIDCode(string address, int port) { - var jtagCtrl = new JtagClient.Jtag(address, port); + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var ret = await jtagCtrl.ReadIDCode(); if (ret.IsSuccessful) @@ -59,13 +59,13 @@ public class JtagController : ControllerBase [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async ValueTask ReadStatusReg(string address, int port) { - var jtagCtrl = new JtagClient.Jtag(address, port); + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var ret = await jtagCtrl.ReadStatusReg(); if (ret.IsSuccessful) { var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0')); - var decodeValue = new JtagClient.JtagStatusReg(ret.Value); + var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value); logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}"); return TypedResults.Ok(new { @@ -174,7 +174,7 @@ public class JtagController : ControllerBase var fileBytes = memoryStream.ToArray(); // 下载比特流 - var jtagCtrl = new JtagClient.Jtag(address, port); + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var ret = await jtagCtrl.DownloadBitstream(fileBytes); if (ret.IsSuccessful) @@ -215,7 +215,7 @@ public class JtagController : ControllerBase [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public async ValueTask BoundaryScanAllPorts(string address, int port) { - var jtagCtrl = new JtagClient.Jtag(address, port); + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var ret = await jtagCtrl.BoundaryScan(); if (!ret.IsSuccessful) { @@ -239,7 +239,7 @@ public class JtagController : ControllerBase [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public async ValueTask BoundaryScanLogicalPorts(string address, int port) { - var jtagCtrl = new JtagClient.Jtag(address, port); + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var ret = await jtagCtrl.BoundaryScanLogicalPorts(); if (!ret.IsSuccessful) { @@ -264,7 +264,7 @@ public class JtagController : ControllerBase [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public async ValueTask SetSpeed(string address, int port, UInt32 speed) { - var jtagCtrl = new JtagClient.Jtag(address, port); + var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var ret = await jtagCtrl.SetSpeed(speed); if (!ret.IsSuccessful) { diff --git a/server/src/Controllers/RemoteUpdateController.cs b/server/src/Controllers/RemoteUpdateController.cs index 36eb84e..a296a1a 100644 --- a/server/src/Controllers/RemoteUpdateController.cs +++ b/server/src/Controllers/RemoteUpdateController.cs @@ -150,7 +150,7 @@ public class RemoteUpdateController : ControllerBase if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error); // 下载比特流 - var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port); + var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port); var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value); if (ret.IsSuccessful) @@ -210,7 +210,7 @@ public class RemoteUpdateController : ControllerBase } // 下载比特流 - var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port); + var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port); { var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]); if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error); @@ -246,7 +246,7 @@ public class RemoteUpdateController : ControllerBase [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public async ValueTask HotResetBitstream(string address, int port, int bitstreamNum) { - var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port); + var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port); var ret = await remoteUpdater.HotResetBitstream(bitstreamNum); if (ret.IsSuccessful) @@ -274,7 +274,7 @@ public class RemoteUpdateController : ControllerBase [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public async ValueTask GetFirmwareVersion(string address, int port) { - var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port); + var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port); var ret = await remoteUpdater.GetVersion(); if (ret.IsSuccessful) diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs index 35b8e4e..fa936c3 100644 --- a/server/src/Controllers/VideoStreamController.cs +++ b/server/src/Controllers/VideoStreamController.cs @@ -1,40 +1,46 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; -namespace server.src.Controllers; /// - /// HTTP 视频流控制器 - /// - [ApiController] - [Route("api/[controller]")] - public class VideoStreamController : ControllerBase - { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private readonly server.src.HttpVideoStreamService _videoStreamService; /// +/// +/// [TODO:description] +/// +[ApiController] +[Route("api/[controller]")] +public class VideoStreamController : ControllerBase +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly server.Services.HttpVideoStreamService _videoStreamService; + + /// /// 初始化HTTP视频流控制器 /// /// HTTP视频流服务 - public VideoStreamController(server.src.HttpVideoStreamService videoStreamService) + public VideoStreamController(server.Services.HttpVideoStreamService videoStreamService) { logger.Info("创建VideoStreamController,命名空间:{Namespace}", this.GetType().Namespace); _videoStreamService = videoStreamService; - } /// + } + + /// /// 获取 HTTP 视频流服务状态 /// /// 服务状态信息 [HttpGet("Status")] [EnableCors("Users")] [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public IResult GetStatus() + [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + public IResult GetStatus() { try { logger.Info("GetStatus方法被调用,控制器:{Controller},路径:api/VideoStream/Status", this.GetType().Name); - + // 使用HttpVideoStreamService提供的状态信息 var status = _videoStreamService.GetServiceStatus(); - + // 转换为小写首字母的JSON属性(符合前端惯例) - return TypedResults.Ok(new - { + return TypedResults.Ok(new + { isRunning = true, // HTTP视频流服务作为后台服务始终运行 serverPort = _videoStreamService.ServerPort, streamUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html", @@ -43,12 +49,13 @@ namespace server.src.Controllers; /// connectedClients = _videoStreamService.ConnectedClientsCount, clientEndpoints = _videoStreamService.GetConnectedClientEndpoints() }); - } catch (Exception ex) + } + catch (Exception ex) { - logger.Error(ex, "获取 HTTP 视频流服务状态失败"); return TypedResults.InternalServerError(ex.Message); + logger.Error(ex, "获取 HTTP 视频流服务状态失败"); return TypedResults.InternalServerError(ex.Message); } } - + /// /// 获取 HTTP 视频流信息 /// @@ -62,8 +69,8 @@ namespace server.src.Controllers; /// try { logger.Info("获取 HTTP 视频流信息"); - return TypedResults.Ok(new - { + return TypedResults.Ok(new + { frameRate = _videoStreamService.FrameRate, frameWidth = _videoStreamService.FrameWidth, frameHeight = _videoStreamService.FrameHeight, @@ -75,10 +82,10 @@ namespace server.src.Controllers; /// } catch (Exception ex) { - logger.Error(ex, "获取 HTTP 视频流信息失败"); return TypedResults.InternalServerError(ex.Message); + logger.Error(ex, "获取 HTTP 视频流信息失败"); return TypedResults.InternalServerError(ex.Message); } } - + /// /// 测试 HTTP 视频流连接 /// @@ -92,16 +99,16 @@ namespace server.src.Controllers; /// try { logger.Info("测试 HTTP 视频流连接"); - + // 尝试通过HTTP请求检查视频流服务是否可访问 using (var httpClient = new HttpClient()) { httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间 var response = await httpClient.GetAsync($"http://localhost:{_videoStreamService.ServerPort}/"); - + // 只要能连接上就认为成功,不管返回状态 bool isConnected = response.IsSuccessStatusCode; - + return TypedResults.Ok(isConnected); } } @@ -112,4 +119,4 @@ namespace server.src.Controllers; /// return TypedResults.Ok(false); } } -} \ No newline at end of file +} diff --git a/server/src/HttpVideoStreamService.cs b/server/src/HttpVideoStreamService.cs deleted file mode 100644 index c74c723..0000000 --- a/server/src/HttpVideoStreamService.cs +++ /dev/null @@ -1,575 +0,0 @@ -using System.Net; -using System.Text; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.PixelFormats; - -namespace server.src -{ - /// - /// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页 - /// 简化版本实现,先建立基础框架 - /// - public class HttpVideoStreamService : BackgroundService - { - private readonly ILogger _logger; - private HttpListener? _httpListener; - private readonly int _serverPort = 8080; - private readonly int _frameRate = 30; // 30 FPS - private readonly int _frameWidth = 640; - private readonly int _frameHeight = 480; - - // 模拟 FPGA 图像数据 - private int _frameCounter = 0; - private readonly List _activeClients = new List(); - private readonly object _clientsLock = new object(); - - /// - /// 获取当前连接的客户端数量 - /// - public int ConnectedClientsCount - { - get - { - lock (_clientsLock) - { - return _activeClients.Count; - } - } - } - - /// - /// 获取服务端口 - /// - public int ServerPort => _serverPort; - - /// - /// 获取帧宽度 - /// - public int FrameWidth => _frameWidth; - - /// - /// 获取帧高度 - /// - public int FrameHeight => _frameHeight; - - /// - /// 获取帧率 - /// - public int FrameRate => _frameRate; - - /// - /// 初始化 HttpVideoStreamService - /// - /// 日志记录器 - public HttpVideoStreamService(ILogger logger) - { - _logger = logger; - } - - /// - /// 执行 HTTP 视频流服务 - /// - /// 取消令牌 - /// 任务 - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - _logger.LogInformation("启动 HTTP 视频流服务,端口: {Port}", _serverPort); - // 创建 HTTP 监听器 - _httpListener = new HttpListener(); - _httpListener.Prefixes.Add($"http://localhost:{_serverPort}/"); - _httpListener.Start(); - - _logger.LogInformation("HTTP 视频流服务已启动,监听端口: {Port}", _serverPort); - - // 开始接受客户端连接 - _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken); - - // 开始生成视频帧 - await GenerateVideoFrames(stoppingToken); - } - catch (HttpListenerException ex) - { - _logger.LogError(ex, "HTTP 视频流服务启动失败,请确保您有管理员权限或使用netsh配置URL前缀权限"); - } - catch (Exception ex) - { - _logger.LogError(ex, "HTTP 视频流服务启动失败"); - } - } - - private async Task AcceptClientsAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested && _httpListener != null && _httpListener.IsListening) - { - try - { - // 等待客户端连接 - var context = await _httpListener.GetContextAsync(); - var request = context.Request; - var response = context.Response; - - _logger.LogInformation("新HTTP客户端连接: {RemoteEndPoint}", request.RemoteEndPoint); - // 处理不同的请求路径 - var requestPath = request.Url?.AbsolutePath ?? "/"; - - if (requestPath == "/video-stream") - { - // MJPEG 流请求 - _ = Task.Run(() => HandleMjpegStreamAsync(response, cancellationToken), cancellationToken); - } - else if (requestPath == "/snapshot") - { - // 单帧图像请求 - await HandleSnapshotRequestAsync(response, cancellationToken); - } - else if (requestPath == "/video-feed.html") - { - // HTML页面请求 - await SendVideoHtmlPageAsync(response); - } - else - { - // 默认返回简单的HTML页面,提供链接到视频页面 - await SendIndexHtmlPageAsync(response); - } - } - catch (HttpListenerException) - { - // HTTP监听器可能已停止 - break; - } - catch (ObjectDisposedException) - { - // 对象可能已被释放 - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "接受HTTP客户端连接时发生错误"); - } - } - } - - private async Task HandleMjpegStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken) - { - try - { - // 设置MJPEG流的响应头 - 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"); - - // 跟踪活跃的客户端 - lock (_clientsLock) - { - _activeClients.Add(response); - } - - _logger.LogDebug("已启动MJPEG流,客户端: {RemoteEndPoint}", response.OutputStream?.GetHashCode() ?? 0); - - // 保持连接直到取消或出错 - try - { - while (!cancellationToken.IsCancellationRequested) - { - await Task.Delay(100, cancellationToken); // 简单的保活循环 - } - } - catch (TaskCanceledException) - { - // 预期的取消 - } - - _logger.LogDebug("MJPEG流已结束,客户端: {ClientId}", response.OutputStream?.GetHashCode() ?? 0); - } - catch (Exception ex) - { - _logger.LogError(ex, "处理MJPEG流时出错"); - } - finally - { - lock (_clientsLock) - { - _activeClients.Remove(response); - } - - try - { - response.Close(); - } - catch - { - // 忽略关闭时的错误 - } - } - } - - private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, CancellationToken cancellationToken) - { - try - { - // 获取当前帧 - var imageData = await GetFPGAImageData(); - var jpegData = ConvertToJpeg(imageData); - - // 设置响应头 - response.ContentType = "image/jpeg"; - response.ContentLength64 = jpegData.Length; - response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); - - // 发送JPEG数据 - await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); - await response.OutputStream.FlushAsync(cancellationToken); - - _logger.LogDebug("已发送快照图像,大小:{Size} 字节", jpegData.Length); - } - catch (Exception ex) - { - _logger.LogError(ex, "处理快照请求时出错"); - } - finally - { - 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(); - } - - private async Task GenerateVideoFrames(CancellationToken cancellationToken) - { - var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - // 从 FPGA 获取图像数据(模拟) - var imageData = await GetFPGAImageData(); - - // 将图像数据转换为 JPEG - var jpegData = ConvertToJpeg(imageData); - - // 向所有连接的客户端发送帧 - await BroadcastFrameAsync(jpegData, cancellationToken); - - _frameCounter++; - - // 等待下一帧 - await Task.Delay(frameInterval, cancellationToken); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "生成视频帧时发生错误"); - await Task.Delay(1000, cancellationToken); // 错误恢复延迟 - } - } - } - - /// - /// 模拟从 FPGA 获取图像数据的函数 - /// 实际实现时,这里应该通过 UDP 连接读取 FPGA 特定地址范围的数据 - /// - private async Task GetFPGAImageData() - { - // 模拟异步 FPGA 数据读取 - await Task.Delay(1); - - // 简化的模拟图像数据生成 - var random = new Random(_frameCounter); - var imageData = new byte[_frameWidth * _frameHeight * 3]; // RGB24 格式 - - // 生成简单的彩色噪声图案 - for (int i = 0; i < imageData.Length; i += 3) - { - // 基于帧计数器和位置生成颜色 - var baseColor = (_frameCounter + i / 3) % 256; - imageData[i] = (byte)((baseColor + random.Next(0, 50)) % 256); // R - imageData[i + 1] = (byte)((baseColor * 2 + random.Next(0, 50)) % 256); // G - imageData[i + 2] = (byte)((baseColor * 3 + random.Next(0, 50)) % 256); // B - } - - if (_frameCounter % 30 == 0) // 每秒更新一次日志 - { - _logger.LogDebug("生成第 {FrameNumber} 帧", _frameCounter); - } - - return imageData; - } - - /// - /// 将 RGB 图像数据转换为 JPEG 格式 - /// - private byte[] ConvertToJpeg(byte[] rgbData) - { - using var image = new Image(_frameWidth, _frameHeight); - - // 将 RGB 数据复制到 ImageSharp 图像 - for (int y = 0; y < _frameHeight; y++) - { - for (int x = 0; x < _frameWidth; x++) - { - int index = (y * _frameWidth + x) * 3; - var pixel = new Rgb24(rgbData[index], rgbData[index + 1], rgbData[index + 2]); - image[x, y] = pixel; - } - } - - using var stream = new MemoryStream(); - image.SaveAsJpeg(stream, new JpegEncoder { Quality = 80 }); - return stream.ToArray(); - } - - /// - /// 向所有连接的客户端广播帧数据 - /// - private async Task BroadcastFrameAsync(byte[] frameData, CancellationToken cancellationToken) - { - if (frameData == null || frameData.Length == 0) - { - _logger.LogWarning("尝试广播空帧数据"); - return; - } - - // 准备MJPEG帧数据 - var mjpegFrameHeader = $"--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: {frameData.Length}\r\n\r\n"; - var headerBytes = Encoding.ASCII.GetBytes(mjpegFrameHeader); - - var clientsToRemove = new List(); - var clientsToProcess = new List(); - - // 获取当前连接的客户端列表 - lock (_clientsLock) - { - clientsToProcess.AddRange(_activeClients); - } - - if (clientsToProcess.Count == 0) - { - return; // 没有活跃客户端 - } - - // 向每个活跃的客户端发送帧 - foreach (var client in clientsToProcess) - { - try - { - // 发送帧头部 - await client.OutputStream.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken); - - // 发送JPEG数据 - await client.OutputStream.WriteAsync(frameData, 0, frameData.Length, cancellationToken); - - // 发送结尾换行符 - await client.OutputStream.WriteAsync(Encoding.ASCII.GetBytes("\r\n"), 0, 2, cancellationToken); - - // 确保数据立即发送 - await client.OutputStream.FlushAsync(cancellationToken); - - if (_frameCounter % 30 == 0) // 每秒记录一次日志 - { - _logger.LogDebug("已向客户端 {ClientId} 发送第 {FrameNumber} 帧,大小:{Size} 字节", - client.OutputStream.GetHashCode(), _frameCounter, frameData.Length); - } - } - catch (Exception ex) - { - _logger.LogDebug("发送帧数据时出错: {Error}", ex.Message); - clientsToRemove.Add(client); - } - } - - // 移除断开连接的客户端 - if (clientsToRemove.Count > 0) - { - lock (_clientsLock) - { - foreach (var client in clientsToRemove) - { - _activeClients.Remove(client); - try { client.Close(); } - catch { /* 忽略关闭错误 */ } - } - } - - _logger.LogInformation("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}", - clientsToRemove.Count, _activeClients.Count); - } - } - - /// - /// 获取连接的客户端端点列表 - /// - public List GetConnectedClientEndpoints() - { - List endpoints = new List(); - - lock (_clientsLock) - { - foreach (var client in _activeClients) - { - endpoints.Add($"Client-{client.OutputStream?.GetHashCode() ?? 0}"); - } - } - - return endpoints; - } - - /// - /// 获取服务状态信息 - /// - public object GetServiceStatus() - { - return new - { - IsRunning = _httpListener?.IsListening ?? false, - ServerPort = _serverPort, - FrameRate = _frameRate, - Resolution = $"{_frameWidth}x{_frameHeight}", - ConnectedClients = ConnectedClientsCount, - ClientEndpoints = GetConnectedClientEndpoints() - }; - } - - /// - /// 停止 HTTP 视频流服务 - /// - public override async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("正在停止 HTTP 视频流服务..."); - - if (_httpListener != null && _httpListener.IsListening) - { - _httpListener.Stop(); - _httpListener.Close(); - } - - // 关闭所有客户端连接 - lock (_clientsLock) - { - foreach (var client in _activeClients) - { - try { client.Close(); } - catch { /* 忽略关闭错误 */ } - } - _activeClients.Clear(); - } - - await base.StopAsync(cancellationToken); - - _logger.LogInformation("HTTP 视频流服务已停止"); - } - - /// - /// 释放资源 - /// - public override void Dispose() - { - if (_httpListener != null) - { - if (_httpListener.IsListening) - { - _httpListener.Stop(); - } - _httpListener.Close(); - } - - lock (_clientsLock) - { - foreach (var client in _activeClients) - { - try { client.Close(); } - catch { /* 忽略关闭错误 */ } - } - _activeClients.Clear(); - } - - base.Dispose(); - } - } -} diff --git a/server/src/Peripherals/CameraClient.cs b/server/src/Peripherals/CameraClient.cs new file mode 100644 index 0000000..e69de29 diff --git a/server/src/JtagClient.cs b/server/src/Peripherals/JtagClient.cs similarity index 99% rename from server/src/JtagClient.cs rename to server/src/Peripherals/JtagClient.cs index 37e0476..14217b0 100644 --- a/server/src/JtagClient.cs +++ b/server/src/Peripherals/JtagClient.cs @@ -1,11 +1,10 @@ using System.Collections; using System.Net; -using BsdlParser; using DotNext; using Newtonsoft.Json; using WebProtocol; -namespace JtagClient; +namespace Peripherals.JtagClient; /// /// Global Constant Jtag Address diff --git a/server/src/RemoteUpdateClient.cs b/server/src/Peripherals/RemoteUpdateClient.cs similarity index 99% rename from server/src/RemoteUpdateClient.cs rename to server/src/Peripherals/RemoteUpdateClient.cs index 88bbb9d..ccfd8dd 100644 --- a/server/src/RemoteUpdateClient.cs +++ b/server/src/Peripherals/RemoteUpdateClient.cs @@ -1,6 +1,7 @@ using System.Net; using DotNext; -namespace RemoteUpdateClient; + +namespace Peripherals.RemoteUpdateClient; static class RemoteUpdaterAddr { diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs new file mode 100644 index 0000000..c35fcc6 --- /dev/null +++ b/server/src/Services/HttpVideoStreamService.cs @@ -0,0 +1,575 @@ +using System.Net; +using System.Text; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; + +namespace server.Services; +/// +/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页 +/// 简化版本实现,先建立基础框架 +/// +public class HttpVideoStreamService : BackgroundService +{ + private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private HttpListener? _httpListener; + private readonly int _serverPort = 8080; + private readonly int _frameRate = 30; // 30 FPS + private readonly int _frameWidth = 640; + private readonly int _frameHeight = 480; + + // 模拟 FPGA 图像数据 + private int _frameCounter = 0; + private readonly List _activeClients = new List(); + private readonly object _clientsLock = new object(); + + /// + /// 获取当前连接的客户端数量 + /// + public int ConnectedClientsCount + { + get + { + lock (_clientsLock) + { + return _activeClients.Count; + } + } + } + + /// + /// 获取服务端口 + /// + public int ServerPort => _serverPort; + + /// + /// 获取帧宽度 + /// + public int FrameWidth => _frameWidth; + + /// + /// 获取帧高度 + /// + public int FrameHeight => _frameHeight; + + /// + /// 获取帧率 + /// + public int FrameRate => _frameRate; + + /// + /// 初始化 HttpVideoStreamService + /// + public HttpVideoStreamService() + { + } + + /// + /// 执行 HTTP 视频流服务 + /// + /// 取消令牌 + /// 任务 + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + logger.Info("启动 HTTP 视频流服务,端口: {Port}", _serverPort); + // 创建 HTTP 监听器 + _httpListener = new HttpListener(); + _httpListener.Prefixes.Add($"http://localhost:{_serverPort}/"); + _httpListener.Start(); + + logger.Info("HTTP 视频流服务已启动,监听端口: {Port}", _serverPort); + + // 开始接受客户端连接 + _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken); + + // 开始生成视频帧 + await GenerateVideoFrames(stoppingToken); + } + catch (HttpListenerException ex) + { + logger.Error(ex, "HTTP 视频流服务启动失败,请确保您有管理员权限或使用netsh配置URL前缀权限"); + } + catch (Exception ex) + { + logger.Error(ex, "HTTP 视频流服务启动失败"); + } + } + + private async Task AcceptClientsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && _httpListener != null && _httpListener.IsListening) + { + try + { + // 等待客户端连接 + var context = await _httpListener.GetContextAsync(); + var request = context.Request; + var response = context.Response; + + logger.Info("新HTTP客户端连接: {RemoteEndPoint}", request.RemoteEndPoint); + // 处理不同的请求路径 + var requestPath = request.Url?.AbsolutePath ?? "/"; + + if (requestPath == "/video-stream") + { + // MJPEG 流请求 + _ = Task.Run(() => HandleMjpegStreamAsync(response, cancellationToken), cancellationToken); + } + else if (requestPath == "/snapshot") + { + // 单帧图像请求 + await HandleSnapshotRequestAsync(response, cancellationToken); + } + else if (requestPath == "/video-feed.html") + { + // HTML页面请求 + await SendVideoHtmlPageAsync(response); + } + else + { + // 默认返回简单的HTML页面,提供链接到视频页面 + await SendIndexHtmlPageAsync(response); + } + } + catch (HttpListenerException) + { + // HTTP监听器可能已停止 + break; + } + catch (ObjectDisposedException) + { + // 对象可能已被释放 + break; + } + catch (Exception ex) + { + logger.Error(ex, "接受HTTP客户端连接时发生错误"); + } + } + } + + private async Task HandleMjpegStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken) + { + try + { + // 设置MJPEG流的响应头 + 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"); + + // 跟踪活跃的客户端 + lock (_clientsLock) + { + _activeClients.Add(response); + } + + logger.Debug("已启动MJPEG流,客户端: {RemoteEndPoint}", response.OutputStream?.GetHashCode() ?? 0); + + // 保持连接直到取消或出错 + try + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken); // 简单的保活循环 + } + } + catch (TaskCanceledException) + { + // 预期的取消 + } + + logger.Debug("MJPEG流已结束,客户端: {ClientId}", response.OutputStream?.GetHashCode() ?? 0); + } + catch (Exception ex) + { + logger.Error(ex, "处理MJPEG流时出错"); + } + finally + { + lock (_clientsLock) + { + _activeClients.Remove(response); + } + + try + { + response.Close(); + } + catch + { + // 忽略关闭时的错误 + } + } + } + + private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, CancellationToken cancellationToken) + { + try + { + // 获取当前帧 + var imageData = await GetFPGAImageData(); + var jpegData = ConvertToJpeg(imageData); + + // 设置响应头 + response.ContentType = "image/jpeg"; + response.ContentLength64 = jpegData.Length; + response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); + + // 发送JPEG数据 + await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); + await response.OutputStream.FlushAsync(cancellationToken); + + logger.Debug("已发送快照图像,大小:{Size} 字节", jpegData.Length); + } + catch (Exception ex) + { + logger.Error(ex, "处理快照请求时出错"); + } + finally + { + 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(); + } + + private async Task GenerateVideoFrames(CancellationToken cancellationToken) + { + var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + // 从 FPGA 获取图像数据(模拟) + var imageData = await GetFPGAImageData(); + + // 将图像数据转换为 JPEG + var jpegData = ConvertToJpeg(imageData); + + // 向所有连接的客户端发送帧 + await BroadcastFrameAsync(jpegData, cancellationToken); + + _frameCounter++; + + // 等待下一帧 + await Task.Delay(frameInterval, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + logger.Error(ex, "生成视频帧时发生错误"); + await Task.Delay(1000, cancellationToken); // 错误恢复延迟 + } + } + } + + /// + /// 模拟从 FPGA 获取图像数据的函数 + /// 实际实现时,这里应该通过 UDP 连接读取 FPGA 特定地址范围的数据 + /// + private async Task GetFPGAImageData() + { + // 模拟异步 FPGA 数据读取 + await Task.Delay(1); + + // 简化的模拟图像数据生成 + var random = new Random(_frameCounter); + var imageData = new byte[_frameWidth * _frameHeight * 3]; // RGB24 格式 + + // 生成简单的彩色噪声图案 + for (int i = 0; i < imageData.Length; i += 3) + { + // 基于帧计数器和位置生成颜色 + var baseColor = (_frameCounter + i / 3) % 256; + imageData[i] = (byte)((baseColor + random.Next(0, 50)) % 256); // R + imageData[i + 1] = (byte)((baseColor * 2 + random.Next(0, 50)) % 256); // G + imageData[i + 2] = (byte)((baseColor * 3 + random.Next(0, 50)) % 256); // B + } + + if (_frameCounter % 30 == 0) // 每秒更新一次日志 + { + logger.Debug("生成第 {FrameNumber} 帧", _frameCounter); + } + + return imageData; + } + + /// + /// 将 RGB 图像数据转换为 JPEG 格式 + /// + private byte[] ConvertToJpeg(byte[] rgbData) + { + using var image = new Image(_frameWidth, _frameHeight); + + // 将 RGB 数据复制到 ImageSharp 图像 + for (int y = 0; y < _frameHeight; y++) + { + for (int x = 0; x < _frameWidth; x++) + { + int index = (y * _frameWidth + x) * 3; + var pixel = new Rgb24(rgbData[index], rgbData[index + 1], rgbData[index + 2]); + image[x, y] = pixel; + } + } + + using var stream = new MemoryStream(); + image.SaveAsJpeg(stream, new JpegEncoder { Quality = 80 }); + return stream.ToArray(); + } + + /// + /// 向所有连接的客户端广播帧数据 + /// + private async Task BroadcastFrameAsync(byte[] frameData, CancellationToken cancellationToken) + { + if (frameData == null || frameData.Length == 0) + { + logger.Warn("尝试广播空帧数据"); + return; + } + + // 准备MJPEG帧数据 + var mjpegFrameHeader = $"--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: {frameData.Length}\r\n\r\n"; + var headerBytes = Encoding.ASCII.GetBytes(mjpegFrameHeader); + + var clientsToRemove = new List(); + var clientsToProcess = new List(); + + // 获取当前连接的客户端列表 + lock (_clientsLock) + { + clientsToProcess.AddRange(_activeClients); + } + + if (clientsToProcess.Count == 0) + { + return; // 没有活跃客户端 + } + + // 向每个活跃的客户端发送帧 + foreach (var client in clientsToProcess) + { + try + { + // 发送帧头部 + await client.OutputStream.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken); + + // 发送JPEG数据 + await client.OutputStream.WriteAsync(frameData, 0, frameData.Length, cancellationToken); + + // 发送结尾换行符 + await client.OutputStream.WriteAsync(Encoding.ASCII.GetBytes("\r\n"), 0, 2, cancellationToken); + + // 确保数据立即发送 + await client.OutputStream.FlushAsync(cancellationToken); + + if (_frameCounter % 30 == 0) // 每秒记录一次日志 + { + logger.Debug("已向客户端 {ClientId} 发送第 {FrameNumber} 帧,大小:{Size} 字节", + client.OutputStream.GetHashCode(), _frameCounter, frameData.Length); + } + } + catch (Exception ex) + { + logger.Debug("发送帧数据时出错: {Error}", ex.Message); + clientsToRemove.Add(client); + } + } + + // 移除断开连接的客户端 + if (clientsToRemove.Count > 0) + { + lock (_clientsLock) + { + foreach (var client in clientsToRemove) + { + _activeClients.Remove(client); + try { client.Close(); } + catch { /* 忽略关闭错误 */ } + } + } + + logger.Info("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}", + clientsToRemove.Count, _activeClients.Count); + } + } + + /// + /// 获取连接的客户端端点列表 + /// + public List GetConnectedClientEndpoints() + { + List endpoints = new List(); + + lock (_clientsLock) + { + foreach (var client in _activeClients) + { + endpoints.Add($"Client-{client.OutputStream?.GetHashCode() ?? 0}"); + } + } + + return endpoints; + } + + /// + /// 获取服务状态信息 + /// + public object GetServiceStatus() + { + return new + { + IsRunning = _httpListener?.IsListening ?? false, + ServerPort = _serverPort, + FrameRate = _frameRate, + Resolution = $"{_frameWidth}x{_frameHeight}", + ConnectedClients = ConnectedClientsCount, + ClientEndpoints = GetConnectedClientEndpoints() + }; + } + + /// + /// 停止 HTTP 视频流服务 + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + logger.Info("正在停止 HTTP 视频流服务..."); + + if (_httpListener != null && _httpListener.IsListening) + { + _httpListener.Stop(); + _httpListener.Close(); + } + + // 关闭所有客户端连接 + lock (_clientsLock) + { + foreach (var client in _activeClients) + { + try { client.Close(); } + catch { /* 忽略关闭错误 */ } + } + _activeClients.Clear(); + } + + await base.StopAsync(cancellationToken); + + logger.Info("HTTP 视频流服务已停止"); + } + + /// + /// 释放资源 + /// + public override void Dispose() + { + if (_httpListener != null) + { + if (_httpListener.IsListening) + { + _httpListener.Stop(); + } + _httpListener.Close(); + } + + lock (_clientsLock) + { + foreach (var client in _activeClients) + { + try { client.Close(); } + catch { /* 忽略关闭错误 */ } + } + _activeClients.Clear(); + } + + base.Dispose(); + } +}