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(); } }