using System.Net; using System.Collections.Concurrent; using Peripherals.HdmiInClient; namespace server.Services; public class HdmiVideoStreamEndpoint { public string BoardId { get; set; } = ""; public string MjpegUrl { get; set; } = ""; public string VideoUrl { get; set; } = ""; public string SnapshotUrl { get; set; } = ""; } public class HttpHdmiVideoStreamService : BackgroundService { private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private HttpListener? _httpListener; private readonly int _serverPort = 4322; private readonly ConcurrentDictionary _hdmiInDict = new(); private readonly ConcurrentDictionary _hdmiInCtsDict = new(); public override async Task StartAsync(CancellationToken cancellationToken) { _httpListener = new HttpListener(); _httpListener.Prefixes.Add($"http://*:{_serverPort}/"); _httpListener.Start(); logger.Info($"HDMI Video Stream Service started on port {_serverPort}"); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { while (!stoppingToken.IsCancellationRequested) { HttpListenerContext? context = null; try { logger.Debug("Waiting for HTTP request..."); context = await _httpListener.GetContextAsync(); logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}"); } catch (ObjectDisposedException) { // Listener closed, exit loop break; } catch (HttpListenerException) { // Listener closed, exit loop break; } catch (Exception ex) { logger.Error(ex, "Error in GetContextAsync"); break; } if (context != null) _ = HandleRequestAsync(context, stoppingToken); } } finally { _httpListener?.Close(); logger.Info("HDMI Video Stream Service stopped."); } } public override async Task StopAsync(CancellationToken cancellationToken) { logger.Info("Stopping HDMI Video Stream Service..."); // 禁用所有活跃的HDMI传输 var disableTasks = new List(); foreach (var hdmiKey in _hdmiInDict.Keys) { disableTasks.Add(DisableHdmiTransmissionAsync(hdmiKey)); } // 等待所有禁用操作完成 await Task.WhenAll(disableTasks); // 清空字典 _hdmiInDict.Clear(); _hdmiInCtsDict.Clear(); _httpListener?.Close(); // 立即关闭监听器,唤醒阻塞 await base.StopAsync(cancellationToken); } public async Task DisableHdmiTransmissionAsync(string key) { try { var cts = _hdmiInCtsDict[key]; cts.Cancel(); var hdmiIn = _hdmiInDict[key]; var disableResult = await hdmiIn.EnableTrans(false); if (disableResult.IsSuccessful) { logger.Info("Successfully disabled HDMI transmission"); } else { logger.Error($"Failed to disable HDMI transmission: {disableResult.Error}"); } } catch (Exception ex) { logger.Error(ex, "Exception occurred while disabling HDMI transmission"); } } // 获取/创建 HdmiIn 实例 private async Task GetOrCreateHdmiInAsync(string boardId) { if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn)) { try { var enableResult = await hdmiIn.EnableTrans(true); if (!enableResult.IsSuccessful) { logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}"); return null; } logger.Info($"Successfully enabled HDMI transmission for board {boardId}"); } catch (Exception ex) { logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}"); return null; } _hdmiInDict[boardId] = hdmiIn; _hdmiInCtsDict[boardId] = new CancellationTokenSource(); return hdmiIn; } var db = new Database.AppDataConnection(); if (db == null) { logger.Error("Failed to create HdmiIn instance"); return null; } var boardRet = db.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; hdmiIn = new HdmiIn(board.IpAddr, board.Port, 0); // taskID 可根据实际需求调整 // 启用HDMI传输 try { var enableResult = await hdmiIn.EnableTrans(true); if (!enableResult.IsSuccessful) { logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}"); return null; } logger.Info($"Successfully enabled HDMI transmission for board {boardId}"); } catch (Exception ex) { logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}"); return null; } _hdmiInDict[boardId] = hdmiIn; _hdmiInCtsDict[boardId] = new CancellationTokenSource(); return hdmiIn; } 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 boardId"); return; } var hdmiIn = await GetOrCreateHdmiInAsync(boardId); if (hdmiIn == null) { await SendErrorAsync(context.Response, "Invalid boardId or board not available"); return; } var hdmiInToken = _hdmiInCtsDict[boardId].Token; if (hdmiInToken == null) { await SendErrorAsync(context.Response, "HDMI input is not available"); return; } if (path == "/snapshot") { await HandleSnapshotRequestAsync(context.Response, hdmiIn, hdmiInToken); } else if (path == "/mjpeg") { await HandleMjpegStreamAsync(context.Response, hdmiIn, hdmiInToken); } else if (path == "/video") { await SendVideoHtmlPageAsync(context.Response, boardId); } else { await SendIndexHtmlPageAsync(context.Response, boardId); } } private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken) { try { logger.Debug("处理HDMI快照请求"); const int frameWidth = 960; // HDMI输入分辨率 const int frameHeight = 540; // 从HDMI读取RGB565数据 var frameResult = await hdmiIn.ReadFrame(); if (!frameResult.IsSuccessful || frameResult.Value == null) { logger.Error("HDMI快照获取失败"); response.StatusCode = 500; var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI snapshot"); await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken); response.Close(); return; } var rgb565Data = frameResult.Value; // 验证数据长度 var expectedLength = frameWidth * frameHeight * 2; if (rgb565Data.Length != expectedLength) { logger.Warn("HDMI快照数据长度不匹配,期望: {Expected}, 实际: {Actual}", expectedLength, rgb565Data.Length); } // 将RGB565转换为RGB24 var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, frameWidth, frameHeight, isLittleEndian: false); if (!rgb24Result.IsSuccessful) { logger.Error("HDMI快照RGB565转RGB24失败: {Error}", rgb24Result.Error); response.StatusCode = 500; var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to process HDMI snapshot"); await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken); response.Close(); return; } // 将RGB24转换为JPEG var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Result.Value, frameWidth, frameHeight, 80); if (!jpegResult.IsSuccessful) { logger.Error("HDMI快照RGB24转JPEG失败: {Error}", jpegResult.Error); response.StatusCode = 500; var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to encode HDMI snapshot"); await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken); response.Close(); return; } var jpegData = jpegResult.Value; // 设置响应头(参考Camera版本) response.ContentType = "image/jpeg"; response.ContentLength64 = jpegData.Length; response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); await response.OutputStream.FlushAsync(cancellationToken); logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegData.Length); } catch (Exception ex) { logger.Error(ex, "处理HDMI快照请求时出错"); response.StatusCode = 500; } finally { response.Close(); } } private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken) { try { // 设置MJPEG流的响应头(参考Camera版本) 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.Debug("开始HDMI MJPEG流传输"); int frameCounter = 0; const int frameWidth = 960; // HDMI输入分辨率 const int frameHeight = 540; while (!cancellationToken.IsCancellationRequested) { try { var frameStartTime = DateTime.UtcNow; // 从HDMI读取RGB565数据 var readStartTime = DateTime.UtcNow; var frameResult = await hdmiIn.ReadFrame(); var readEndTime = DateTime.UtcNow; var readTime = (readEndTime - readStartTime).TotalMilliseconds; if (!frameResult.IsSuccessful || frameResult.Value == null) { logger.Warn("HDMI帧读取失败或为空"); continue; } var rgb565Data = frameResult.Value; // 验证数据长度是否正确 (RGB565为每像素2字节) var expectedLength = frameWidth * frameHeight * 2; if (rgb565Data.Length != expectedLength) { logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}", expectedLength, rgb565Data.Length); } // 将RGB565转换为RGB24(参考Camera版本的处理) var convertStartTime = DateTime.UtcNow; var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, frameWidth, frameHeight, isLittleEndian: false); var convertEndTime = DateTime.UtcNow; var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds; if (!rgb24Result.IsSuccessful) { logger.Error("HDMI RGB565转RGB24失败: {Error}", rgb24Result.Error); continue; } // 将RGB24转换为JPEG(参考Camera版本的处理) var jpegStartTime = DateTime.UtcNow; var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Result.Value, frameWidth, frameHeight, 80); var jpegEndTime = DateTime.UtcNow; var jpegTime = (jpegEndTime - jpegStartTime).TotalMilliseconds; if (!jpegResult.IsSuccessful) { logger.Error("HDMI RGB24转JPEG失败: {Error}", jpegResult.Error); continue; } var jpegData = jpegResult.Value; // 发送MJPEG帧(使用Camera版本的格式) var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length); var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter(); await response.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken); await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); await response.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken); await response.OutputStream.FlushAsync(cancellationToken); frameCounter++; var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds; // 性能统计日志(每30帧记录一次) if (frameCounter % 30 == 0) { logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 读取: {ReadTime:F1}ms, RGB转换: {ConvertTime:F1}ms, JPEG转换: {JpegTime:F1}ms, 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节", frameCounter, readTime, convertTime, jpegTime, totalTime, jpegData.Length); } } catch (Exception ex) { logger.Error(ex, "处理HDMI帧时发生错误"); } } } catch (Exception ex) { logger.Error(ex, "HDMI MJPEG流处理异常"); } finally { try { // 停止传输时禁用HDMI传输 await hdmiIn.EnableTrans(false); logger.Info("已禁用HDMI传输"); } catch (Exception ex) { logger.Error(ex, "禁用HDMI传输时出错"); } try { response.Close(); } catch { // 忽略关闭时的错误 } logger.Debug("HDMI MJPEG流连接已关闭"); } } private async Task SendVideoHtmlPageAsync(HttpListenerResponse response, string boardId) { string html = $@"

HDMI Video Stream for Board {boardId}

"; response.ContentType = "text/html"; await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html)); response.Close(); } private async Task SendIndexHtmlPageAsync(HttpListenerResponse response, string boardId) { string html = $@"

Welcome to HDMI Video Stream Service

View Video Stream for Board {boardId} "; response.ContentType = "text/html"; await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html)); response.Close(); } private async Task SendErrorAsync(HttpListenerResponse response, string message) { response.StatusCode = 400; await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message)); response.Close(); } /// /// 获取所有可用的HDMI视频流终端点 /// /// 返回所有可用的HDMI视频流终端点列表 public List? GetAllVideoEndpoints() { var db = new Database.AppDataConnection(); var boards = db?.GetAllBoard(); if (boards == null) return null; var endpoints = new List(); foreach (var board in boards) { endpoints.Add(new HdmiVideoStreamEndpoint { BoardId = board.ID.ToString(), MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={board.ID}", VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={board.ID}", SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={board.ID}" }); } return endpoints; } /// /// 获取指定板卡ID的HDMI视频流终端点 /// /// 板卡ID /// 返回指定板卡的HDMI视频流终端点 public HdmiVideoStreamEndpoint GetVideoEndpoint(string boardId) { return new HdmiVideoStreamEndpoint { 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}" }; } }