feat: 完成jpeg读取后端
This commit is contained in:
		@@ -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");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="jpegData">原始 JPEG 数据(可能缺少完整头部)</param>
 | 
			
		||||
    /// <param name="width">图像宽度</param>
 | 
			
		||||
    /// <param name="height">图像高度</param>
 | 
			
		||||
    /// <param name="quality">JPEG质量(1-100,默认80)</param>
 | 
			
		||||
    /// <returns>完整的 JPEG 图片数据</returns>
 | 
			
		||||
    public static Result<byte[]> 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<Rgb24>(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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 从 JPEG 数据生成 MJPEG 帧数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="jpegData">完整的 JPEG 数据</param>
 | 
			
		||||
    /// <param name="boundary">边界字符串(默认为"--boundary")</param>
 | 
			
		||||
    /// <returns>MJPEG 帧数据</returns>
 | 
			
		||||
    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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 创建完整的 MJPEG 帧数据
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="jpegData">JPEG数据</param>
 | 
			
		||||
    /// <param name="boundary">边界字符串(默认为"--boundary")</param>
 | 
			
		||||
    /// <returns>完整的MJPEG帧数据</returns>
 | 
			
		||||
    public static Result<byte[]> 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)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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<HdmiVideoStreamClient?> 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帧时发生错误");
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user