From 3644c75304847cbaf69de0b426812fe15353710e Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Fri, 15 Aug 2025 21:04:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90jpeg=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Common/Image.cs | 162 +++++++++++++++++- server/src/Peripherals/JpegClient.cs | 6 + .../Services/HttpHdmiVideoStreamService.cs | 99 ++++++++--- 3 files changed, 236 insertions(+), 31 deletions(-) diff --git a/server/src/Common/Image.cs b/server/src/Common/Image.cs index d22d5cf..65cd5ef 100644 --- a/server/src/Common/Image.cs +++ b/server/src/Common/Image.cs @@ -72,7 +72,7 @@ public class Image var b8 = (byte)((b5 * 255) / 31); // 5位扩展到8位 // 存储到 RGB24 数组 - var rgb24Index = (i%2 == 0)?((i+1) * 3):((i-1) * 3); + var rgb24Index = (i % 2 == 0) ? ((i + 1) * 3) : ((i - 1) * 3); rgb24Data[rgb24Index] = r8; // R rgb24Data[rgb24Index + 1] = g8; // G rgb24Data[rgb24Index + 2] = b8; // B @@ -255,13 +255,169 @@ public class Image return Encoding.ASCII.GetBytes("\r\n"); } + /// + /// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片 + /// + /// 原始 JPEG 数据(可能缺少完整头部) + /// 图像宽度 + /// 图像高度 + /// JPEG质量(1-100,默认80) + /// 完整的 JPEG 图片数据 + public static Result CompleteJpegData(byte[] jpegData, int width, int height, int quality = 80) + { + if (jpegData == null) + return new(new ArgumentNullException(nameof(jpegData))); + + if (width <= 0 || height <= 0) + return new(new ArgumentException("Width and height must be positive")); + + if (quality < 1 || quality > 100) + return new(new ArgumentException("Quality must be between 1 and 100")); + + try + { + // 检查是否已经是完整的 JPEG 文件(以 FFD8 开头,FFD9 结尾) + if (jpegData.Length >= 4 && + jpegData[0] == 0xFF && jpegData[1] == 0xD8 && + jpegData[jpegData.Length - 2] == 0xFF && jpegData[jpegData.Length - 1] == 0xD9) + { + // 已经是完整的 JPEG 文件,直接返回 + return jpegData; + } + + // 创建一个临时的 RGB24 图像用于生成 JPEG 头部 + using var tempImage = new SixLabors.ImageSharp.Image(new Configuration + { + + }, width, height); + + // 填充临时图像(使用简单的渐变色作为占位符) + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + tempImage[x, y] = new Rgb24((byte)(x % 256), (byte)(y % 256), 128); + } + } + + using var stream = new MemoryStream(); + tempImage.SaveAsJpeg(stream, new JpegEncoder { Quality = quality }); + var completeJpeg = stream.ToArray(); + + // 如果原始数据看起来是 JPEG 扫描数据,尝试替换扫描数据部分 + if (jpegData.Length > 0) + { + // 查找 JPEG 扫描数据开始位置(SOS 标记 0xFFDA 后) + int sosIndex = -1; + for (int i = 0; i < completeJpeg.Length - 1; i++) + { + if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xDA) + { + // 跳过 SOS 段头部,找到实际扫描数据开始位置 + i += 2; // 跳过 FF DA + if (i < completeJpeg.Length - 1) + { + int segmentLength = (completeJpeg[i] << 8) | completeJpeg[i + 1]; + sosIndex = i + segmentLength; + break; + } + } + } + + // 查找 EOI 标记位置(0xFFD9) + int eoiIndex = -1; + for (int i = completeJpeg.Length - 2; i >= 0; i--) + { + if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xD9) + { + eoiIndex = i; + break; + } + } + + if (sosIndex > 0 && eoiIndex > sosIndex) + { + // 替换扫描数据部分 + var headerLength = sosIndex; + var footerStart = eoiIndex; + var footerLength = completeJpeg.Length - footerStart; + + var newJpegLength = headerLength + jpegData.Length + footerLength; + var newJpegData = new byte[newJpegLength]; + + // 复制头部 + Array.Copy(completeJpeg, 0, newJpegData, 0, headerLength); + + // 复制原始扫描数据 + Array.Copy(jpegData, 0, newJpegData, headerLength, jpegData.Length); + + // 复制尾部 + Array.Copy(completeJpeg, footerStart, newJpegData, headerLength + jpegData.Length, footerLength); + + return newJpegData; + } + } + + // 如果无法智能合并,返回完整的模板 JPEG + return completeJpeg; + } + catch (Exception ex) + { + return new(ex); + } + } + + /// + /// 从 JPEG 数据生成 MJPEG 帧数据 + /// + /// 完整的 JPEG 数据 + /// 边界字符串(默认为"--boundary") + /// MJPEG 帧数据 + public static Result<(byte[] header, byte[] footer, byte[] data)> CreateMjpegFrameFromJpeg( + byte[] jpegData, string boundary = "--boundary") + { + if (jpegData == null) + return new(new ArgumentNullException(nameof(jpegData))); + + // 验证是否为有效的 JPEG 数据 + if (jpegData.Length < 4 || jpegData[0] != 0xFF || jpegData[1] != 0xD8) + { + return new(new ArgumentException("Invalid JPEG data: missing JPEG header")); + } + + try + { + var header = CreateMjpegFrameHeader(jpegData.Length, boundary); + var footer = CreateMjpegFrameFooter(); + + var totalLength = header.Length + jpegData.Length + footer.Length; + var frameData = new byte[totalLength]; + + var offset = 0; + Array.Copy(header, 0, frameData, offset, header.Length); + offset += header.Length; + + Array.Copy(jpegData, 0, frameData, offset, jpegData.Length); + offset += jpegData.Length; + + Array.Copy(footer, 0, frameData, offset, footer.Length); + + return (header, footer, frameData); + } + catch (Exception ex) + { + return new(ex); + } + } + /// /// 创建完整的 MJPEG 帧数据 /// /// JPEG数据 /// 边界字符串(默认为"--boundary") /// 完整的MJPEG帧数据 - public static Result CreateMjpegFrame(byte[] jpegData, string boundary = "--boundary") + public static Result<(byte[] header, byte[] footer, byte[] data)> CreateMjpegFrame( + byte[] jpegData, string boundary = "--boundary") { if (jpegData == null) return new(new ArgumentNullException(nameof(jpegData))); @@ -283,7 +439,7 @@ public class Image Array.Copy(footer, 0, frameData, offset, footer.Length); - return frameData; + return (header, footer, frameData); } catch (Exception ex) { diff --git a/server/src/Peripherals/JpegClient.cs b/server/src/Peripherals/JpegClient.cs index 2be3f7c..1a32c3c 100644 --- a/server/src/Peripherals/JpegClient.cs +++ b/server/src/Peripherals/JpegClient.cs @@ -82,6 +82,9 @@ public class Jpeg readonly string address; private IPEndPoint ep; + public int Width { get; set; } + public int Height { get; set; } + public Jpeg(string address, int port, int taskID, int timeout = 2000) { if (timeout < 0) @@ -206,6 +209,9 @@ public class Jpeg var width = data[0] | (data[1] << 8); var height = data[2] | (data[3] << 8); + this.Width = width; + this.Height = height; + return new((width, height)); } diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs index deb308b..98d839f 100644 --- a/server/src/Services/HttpHdmiVideoStreamService.cs +++ b/server/src/Services/HttpHdmiVideoStreamService.cs @@ -20,6 +20,11 @@ public class HdmiVideoStreamClient public required Jpeg JpegClient { get; set; } public required CancellationTokenSource CTS { get; set; } + + public required int Offset { get; set; } + + public int Width { get; set; } + public int Height { get; set; } } public class HttpHdmiVideoStreamService : BackgroundService @@ -97,14 +102,14 @@ public class HttpHdmiVideoStreamService : BackgroundService var client = _clientDict[key]; client.CTS.Cancel(); - var disableResult = await client.HdmiInClient.EnableTrans(false); - if (disableResult.IsSuccessful) + var disableResult = await client.JpegClient.SetEnable(false); + if (disableResult) { logger.Info("Successfully disabled HDMI transmission"); } else { - logger.Error($"Failed to disable HDMI transmission: {disableResult.Error}"); + logger.Error($"Failed to disable HDMI transmission"); } } catch (Exception ex) @@ -115,7 +120,12 @@ public class HttpHdmiVideoStreamService : BackgroundService private async Task GetOrCreateClientAsync(string boardId) { - if (_clientDict.TryGetValue(boardId, out var client)) return client; + if (_clientDict.TryGetValue(boardId, out var client)) + { + client.Width = client.JpegClient.Width; + client.Height = client.JpegClient.Height; + return client; + } var userManager = new Database.UserManager(); @@ -132,19 +142,20 @@ public class HttpHdmiVideoStreamService : BackgroundService { HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1), JpegClient = new Jpeg(board.IpAddr, board.Port, 1), - CTS = new CancellationTokenSource() + CTS = new CancellationTokenSource(), + Offset = 0 }; // 启用HDMI传输 try { - var hdmiEnableRet = await client.HdmiInClient.EnableTrans(true); - if (!hdmiEnableRet.IsSuccessful) - { - logger.Error($"Failed to enable HDMI transmission for board {boardId}: {hdmiEnableRet.Error}"); - return null; - } - logger.Info($"Successfully enabled HDMI transmission for board {boardId}"); + // var hdmiEnableRet = await client.JpegClient.EnableTrans(true); + // if (!hdmiEnableRet.IsSuccessful) + // { + // logger.Error($"Failed to enable HDMI transmission for board {boardId}: {hdmiEnableRet.Error}"); + // return null; + // } + // logger.Info($"Successfully enabled HDMI transmission for board {boardId}"); var jpegEnableRet = await client.JpegClient.Init(true); if (!jpegEnableRet.IsSuccessful) @@ -153,6 +164,9 @@ public class HttpHdmiVideoStreamService : BackgroundService return null; } logger.Info($"Successfully enabled JPEG transmission for board {boardId}"); + + client.Width = client.JpegClient.Width; + client.Height = client.JpegClient.Height; } catch (Exception ex) { @@ -209,8 +223,8 @@ public class HttpHdmiVideoStreamService : BackgroundService logger.Debug("处理HDMI快照请求"); // 从HDMI读取RGB565数据 - var frameResult = await client.HdmiInClient.ReadFrame(); - if (!frameResult.IsSuccessful || frameResult.Value == null) + var frameResult = await client.JpegClient.GetMultiFrames((uint)client.Offset); + if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0) { logger.Error("HDMI快照获取失败"); response.StatusCode = 500; @@ -220,17 +234,27 @@ public class HttpHdmiVideoStreamService : BackgroundService return; } - var jpegData = frameResult.Value; + var jpegData = frameResult.Value[0]; + var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height); + if (!jpegImage.IsSuccessful) + { + logger.Error("JPEG数据补全失败"); + response.StatusCode = 500; + var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to complete JPEG data"); + await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken); + response.Close(); + return; + } // 设置响应头(参考Camera版本) response.ContentType = "image/jpeg"; - response.ContentLength64 = jpegData.Length; + response.ContentLength64 = jpegImage.Value.Length; response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); - await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); + await response.OutputStream.WriteAsync(jpegImage.Value, 0, jpegImage.Value.Length, cancellationToken); await response.OutputStream.FlushAsync(cancellationToken); - logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegData.Length); + logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegImage.Value.Length); } catch (Exception ex) { @@ -260,13 +284,35 @@ public class HttpHdmiVideoStreamService : BackgroundService while (!cancellationToken.IsCancellationRequested) { - try - { - var frameStartTime = DateTime.UtcNow; + var frameStartTime = DateTime.UtcNow; - var ret = await client.HdmiInClient.GetMJpegFrame(); - if (ret == null) continue; - var frame = ret.Value; + var frameResult = + await client.JpegClient.GetMultiFrames((uint)client.Offset); + if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0) + { + logger.Error("获取HDMI帧失败"); + await Task.Delay(100, cancellationToken); + continue; + } + + foreach (var framebytes in frameResult.Value) + { + var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height); + if (!jpegImage.IsSuccessful) + { + logger.Error("JPEG数据不完整"); + await Task.Delay(100, cancellationToken); + continue; + } + + var frameRet = Common.Image.CreateMjpegFrameFromJpeg(jpegImage.Value); + if (!frameRet.IsSuccessful) + { + logger.Error("创建MJPEG帧失败"); + await Task.Delay(100, cancellationToken); + continue; + } + var frame = frameRet.Value; await response.OutputStream.WriteAsync(frame.header, 0, frame.header.Length, cancellationToken); await response.OutputStream.WriteAsync(frame.data, 0, frame.data.Length, cancellationToken); @@ -283,10 +329,7 @@ public class HttpHdmiVideoStreamService : BackgroundService logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节", frameCounter, totalTime, frame.data.Length); } - } - catch (Exception ex) - { - logger.Error(ex, "处理HDMI帧时发生错误"); + } } }