From a331494fdec7230a44580345973e858f58e79139 Mon Sep 17 00:00:00 2001 From: alivender <13898766233@163.com> Date: Mon, 4 Aug 2025 16:35:42 +0800 Subject: [PATCH] =?UTF-8?q?add:=20=E5=AE=8C=E5=96=84HDMI=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E5=89=8D=E5=90=8E=E7=AB=AF=EF=BC=8C=E7=8E=B0=E5=9C=A8=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/HttpHdmiVideoStreamService.cs | 270 +++++++++- server/src/UdpClientPool.cs | 3 +- src/utils/AuthManager.ts | 8 +- src/views/Project/BottomBar.vue | 28 +- src/views/Project/HdmiVideoStream.vue | 487 ++++++++++++++++++ 5 files changed, 769 insertions(+), 27 deletions(-) create mode 100644 src/views/Project/HdmiVideoStream.vue diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs index e00879a..3e25d11 100644 --- a/server/src/Services/HttpHdmiVideoStreamService.cs +++ b/server/src/Services/HttpHdmiVideoStreamService.cs @@ -16,7 +16,7 @@ public class HttpHdmiVideoStreamService : BackgroundService { private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private HttpListener? _httpListener; - private readonly int _serverPort = 6666; + private readonly int _serverPort = 4322; private readonly ConcurrentDictionary _hdmiInDict = new(); private bool _isEnabled = true; @@ -39,7 +39,9 @@ public class HttpHdmiVideoStreamService : BackgroundService HttpListenerContext? context = null; try { + logger.Debug("Waiting for HTTP request..."); context = await _httpListener.GetContextAsync(); + logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}"); } catch (ObjectDisposedException) { @@ -71,12 +73,46 @@ public class HttpHdmiVideoStreamService : BackgroundService { logger.Info("Stopping HDMI Video Stream Service..."); _isEnabled = false; + + // 禁用所有活跃的HDMI传输 + var disableTasks = new List(); + foreach (var hdmiIn in _hdmiInDict.Values) + { + disableTasks.Add(DisableHdmiTransmissionAsync(hdmiIn)); + } + + // 等待所有禁用操作完成 + await Task.WhenAll(disableTasks); + + // 清空字典 + _hdmiInDict.Clear(); + _httpListener?.Close(); // 立即关闭监听器,唤醒阻塞 await base.StopAsync(cancellationToken); } + + private async Task DisableHdmiTransmissionAsync(HdmiIn hdmiIn) + { + try + { + 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 HdmiIn? GetOrCreateHdmiIn(string boardId) + private async Task GetOrCreateHdmiInAsync(string boardId) { if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn)) return hdmiIn; @@ -98,6 +134,24 @@ public class HttpHdmiVideoStreamService : BackgroundService 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; return hdmiIn; } @@ -112,7 +166,7 @@ public class HttpHdmiVideoStreamService : BackgroundService return; } - var hdmiIn = GetOrCreateHdmiIn(boardId); + var hdmiIn = await GetOrCreateHdmiInAsync(boardId); if (hdmiIn == null) { await SendErrorAsync(context.Response, "Invalid boardId or board not available"); @@ -139,36 +193,205 @@ public class HttpHdmiVideoStreamService : BackgroundService private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken) { - var frameResult = await hdmiIn.ReadFrame(); - if (frameResult.IsSuccessful) + 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"; - await response.OutputStream.WriteAsync(frameResult.Value, 0, frameResult.Value.Length, cancellationToken); + 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); } - else + catch (Exception ex) { + logger.Error(ex, "处理HDMI快照请求时出错"); response.StatusCode = 500; - await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes("Failed to get snapshot")); } - response.Close(); + finally + { + response.Close(); + } } private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken) { - response.ContentType = "multipart/x-mixed-replace; boundary=--frame"; - while (!cancellationToken.IsCancellationRequested && _isEnabled) + try { - var frameResult = await hdmiIn.ReadFrame(); - if (frameResult.IsSuccessful) + // 设置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 && _isEnabled) { - var header = $"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: {frameResult.Value.Length}\r\n\r\n"; - await response.OutputStream.WriteAsync(System.Text.Encoding.ASCII.GetBytes(header)); - await response.OutputStream.WriteAsync(frameResult.Value, 0, frameResult.Value.Length, cancellationToken); - await response.OutputStream.WriteAsync(System.Text.Encoding.ASCII.GetBytes("\r\n")); + 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帧时发生错误"); + } } - await Task.Delay(33, cancellationToken); // ~30fps } - response.Close(); + 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) @@ -200,6 +423,10 @@ public class HttpHdmiVideoStreamService : BackgroundService response.Close(); } + /// + /// 获取所有可用的HDMI视频流终端点 + /// + /// 返回所有可用的HDMI视频流终端点列表 public List? GetAllVideoEndpoints() { var db = new Database.AppDataConnection(); @@ -221,6 +448,11 @@ public class HttpHdmiVideoStreamService : BackgroundService return endpoints; } + /// + /// 获取指定板卡ID的HDMI视频流终端点 + /// + /// 板卡ID + /// 返回指定板卡的HDMI视频流终端点 public HdmiVideoStreamEndpoint GetVideoEndpoint(string boardId) { return new HdmiVideoStreamEndpoint diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index 4222c9f..2e8e712 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -465,7 +465,8 @@ public class UDPClientPool CommandID = Convert.ToByte(taskID), IsWrite = false, BurstLength = (byte)(currentSegmentSize - 1), - Address = devAddr + (uint)(i * max4BytesPerRead) + Address = (burstType == BurstType.ExtendBurst)?(devAddr + (uint)(i * max4BytesPerRead)):(devAddr), + // Address = devAddr + (uint)(i * max4BytesPerRead), }; pkgList.Add(new SendAddrPackage(opts)); } diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index e402d2c..5e2173e 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -15,6 +15,7 @@ import { DebuggerClient, ExamClient, ResourceClient, + HdmiVideoStreamClient, } from "@/APIClient"; import router from "@/router"; import { HubConnectionBuilder } from "@microsoft/signalr"; @@ -38,7 +39,8 @@ type SupportedClient = | OscilloscopeApiClient | DebuggerClient | ExamClient - | ResourceClient; + | ResourceClient + | HdmiVideoStreamClient; export class AuthManager { // 存储token到localStorage @@ -204,6 +206,10 @@ export class AuthManager { public static createAuthenticatedResourceClient(): ResourceClient { return AuthManager.createAuthenticatedClient(ResourceClient); } + + public static createAuthenticatedHdmiVideoStreamClient(): HdmiVideoStreamClient { + return AuthManager.createAuthenticatedClient(HdmiVideoStreamClient); + } public static createAuthenticatedJtagHubConnection() { const token = this.getToken(); diff --git a/src/views/Project/BottomBar.vue b/src/views/Project/BottomBar.vue index b67577e..7ca8c48 100644 --- a/src/views/Project/BottomBar.vue +++ b/src/views/Project/BottomBar.vue @@ -31,8 +31,8 @@ :checked="checkID === 3" @change="handleTabChange" /> - - 示波器 + + HDMI视频流 + @@ -73,12 +84,15 @@
- +
- +
+ +
+
@@ -94,9 +108,11 @@ import { MinimizeIcon, Binary, Hand, + Monitor, } from "lucide-vue-next"; import { useLocalStorage } from "@vueuse/core"; import VideoStreamView from "@/views/Project/VideoStream.vue"; +import HdmiVideoStreamView from "@/views/Project/HdmiVideoStream.vue"; import OscilloscopeView from "@/views/Project/Oscilloscope.vue"; import LogicAnalyzerView from "@/views/Project/LogicAnalyzer.vue"; import { isNull, toNumber } from "lodash"; diff --git a/src/views/Project/HdmiVideoStream.vue b/src/views/Project/HdmiVideoStream.vue new file mode 100644 index 0000000..944adf2 --- /dev/null +++ b/src/views/Project/HdmiVideoStream.vue @@ -0,0 +1,487 @@ + + + + +