feat: 使用DDR读取Hdmi视频流
This commit is contained in:
		@@ -7,8 +7,16 @@ namespace Peripherals.HdmiInClient;
 | 
			
		||||
static class HdmiInAddr
 | 
			
		||||
{
 | 
			
		||||
    public const UInt32 BASE = 0xA000_0000;
 | 
			
		||||
    public const UInt32 HdmiIn_CTRL = BASE + 0x0; //[0]: rstn, 0 is reset.
 | 
			
		||||
    public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 START_WR_ADDR0 = BASE + 0x2;
 | 
			
		||||
    public const UInt32 END_WR_ADDR0 = BASE + 0x3;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 HDMI_NOT_READY = BASE + 0x8;
 | 
			
		||||
    public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x9;
 | 
			
		||||
 | 
			
		||||
    public const UInt32 ADDR_HDMI_WD_START = 0x0400_0000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class HdmiIn
 | 
			
		||||
@@ -21,10 +29,9 @@ public class HdmiIn
 | 
			
		||||
    readonly string address;
 | 
			
		||||
    private IPEndPoint ep;
 | 
			
		||||
 | 
			
		||||
    // 动态分辨率参数
 | 
			
		||||
    private UInt16 _currentWidth = 960;
 | 
			
		||||
    private UInt16 _currentHeight = 540;
 | 
			
		||||
    private UInt32 _currentFrameLength = 960 * 540 * 2 / 4; // RGB565格式,2字节/像素,按4字节对齐
 | 
			
		||||
    public int Width { get; private set; }
 | 
			
		||||
    public int Height { get; private set; }
 | 
			
		||||
    public int FrameLength => Width * Height * 3 / 4;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化HDMI输入客户端
 | 
			
		||||
@@ -44,9 +51,54 @@ public class HdmiIn
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Result<bool>> EnableTrans(bool isEnable)
 | 
			
		||||
    public async ValueTask<Result<bool>> Init(bool enable = true)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.HdmiIn_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await CheckHdmiIsReady();
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to check HDMI ready: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("HDMI not ready");
 | 
			
		||||
                return new(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        int width = -1, height = -1;
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await GetHdmiResolution();
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to get HDMI resolution: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            (width, height) = ret.Value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await ConnectJpeg2Hdmi(width, height);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to connect JPEG to HDMI: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("Failed to connect JPEG to HDMI");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (enable) return await SetTransEnable(true);
 | 
			
		||||
        else return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Result<bool>> SetTransEnable(bool isEnable)
 | 
			
		||||
    {
 | 
			
		||||
        var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.CAPTURE_RD_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
 | 
			
		||||
@@ -75,8 +127,8 @@ public class HdmiIn
 | 
			
		||||
        var result = await UDPClientPool.ReadAddr4BytesAsync(
 | 
			
		||||
            this.ep,
 | 
			
		||||
            this.taskID, // taskID
 | 
			
		||||
            HdmiInAddr.HdmiIn_READFIFO,
 | 
			
		||||
            (int)_currentFrameLength, // 使用当前分辨率的动态大小
 | 
			
		||||
            HdmiInAddr.ADDR_HDMI_WD_START,
 | 
			
		||||
            FrameLength, // 使用当前分辨率的动态大小
 | 
			
		||||
            BurstType.FixedBurst,
 | 
			
		||||
            this.timeout);
 | 
			
		||||
 | 
			
		||||
@@ -99,7 +151,7 @@ public class HdmiIn
 | 
			
		||||
        return result.Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<(byte[] header, byte[] data, byte[] footer)?> GetMJpegFrame()
 | 
			
		||||
    public async ValueTask<Optional<(byte[] header, byte[] data, byte[] footer)>> GetMJpegFrame()
 | 
			
		||||
    {
 | 
			
		||||
        // 从HDMI读取RGB24数据
 | 
			
		||||
        var readStartTime = DateTime.UtcNow;
 | 
			
		||||
@@ -110,13 +162,13 @@ public class HdmiIn
 | 
			
		||||
        if (!frameResult.IsSuccessful || frameResult.Value == null)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn("HDMI帧读取失败或为空");
 | 
			
		||||
            return null;
 | 
			
		||||
            return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var rgb24Data = frameResult.Value;
 | 
			
		||||
 | 
			
		||||
        // 验证数据长度是否正确 (RGB24为每像素2字节)
 | 
			
		||||
        var expectedLength = _currentWidth * _currentHeight * 2;
 | 
			
		||||
        var expectedLength = Width * Height * 2;
 | 
			
		||||
        if (rgb24Data.Length != expectedLength)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}",
 | 
			
		||||
@@ -125,40 +177,105 @@ public class HdmiIn
 | 
			
		||||
 | 
			
		||||
        // 将RGB24转换为JPEG(参考Camera版本的处理)
 | 
			
		||||
        var jpegStartTime = DateTime.UtcNow;
 | 
			
		||||
        var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Data, _currentWidth, _currentHeight, 80);
 | 
			
		||||
        var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgb24Data, Width, Height, 80);
 | 
			
		||||
        var jpegEndTime = DateTime.UtcNow;
 | 
			
		||||
        var jpegTime = (jpegEndTime - jpegStartTime).TotalMilliseconds;
 | 
			
		||||
 | 
			
		||||
        if (!jpegResult.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error("HDMI RGB24转JPEG失败: {Error}", jpegResult.Error);
 | 
			
		||||
            return null;
 | 
			
		||||
            return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var jpegData = jpegResult.Value;
 | 
			
		||||
 | 
			
		||||
        // 发送MJPEG帧(使用Camera版本的格式)
 | 
			
		||||
        var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
 | 
			
		||||
        var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
 | 
			
		||||
 | 
			
		||||
        return (mjpegFrameHeader, jpegData, mjpegFrameFooter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取当前分辨率
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>当前分辨率(宽度, 高度)</returns>
 | 
			
		||||
    public (int Width, int Height) GetCurrentResolution()
 | 
			
		||||
    public async ValueTask<Result<bool>> CheckHdmiIsReady()
 | 
			
		||||
    {
 | 
			
		||||
        return (_currentWidth, _currentHeight);
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddrWithWait(
 | 
			
		||||
            this.ep, this.taskID, HdmiInAddr.HDMI_NOT_READY, 0b00, 0b01, 100, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to check HDMI status: {ret.Error}");
 | 
			
		||||
            return new(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
        return ret.Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取当前帧长度
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>当前帧长度</returns>
 | 
			
		||||
    public UInt32 GetCurrentFrameLength()
 | 
			
		||||
    public async ValueTask<Result<(int, int)>> GetHdmiResolution()
 | 
			
		||||
    {
 | 
			
		||||
        return _currentFrameLength;
 | 
			
		||||
        var ret = await UDPClientPool.ReadAddrByte(
 | 
			
		||||
            this.ep, this.taskID, HdmiInAddr.HDMI_HEIGHT_WIDTH, this.timeout);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Failed to get HDMI resolution: {ret.Error}");
 | 
			
		||||
            return new(ret.Error);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var data = ret.Value.Options.Data;
 | 
			
		||||
        if (data == null || data.Length != 4)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Invalid HDMI resolution data length: {data?.Length ?? 0}");
 | 
			
		||||
            return new(new Exception("Invalid HDMI resolution data length"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var width = data[3] | (data[2] << 8);
 | 
			
		||||
        var height = data[1] | (data[0] << 8);
 | 
			
		||||
        this.Width = width;
 | 
			
		||||
        this.Height = height;
 | 
			
		||||
 | 
			
		||||
        logger.Info($"HDMI resolution: {width}x{height}");
 | 
			
		||||
 | 
			
		||||
        return new((width, height));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask<Result<bool>> ConnectJpeg2Hdmi(int width, int height)
 | 
			
		||||
    {
 | 
			
		||||
        if (width <= 0 || height <= 0)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"Invalid HDMI resolution: {width}x{height}");
 | 
			
		||||
            return new(new ArgumentException("Invalid HDMI resolution"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var frameSize = (UInt32)(width * height);
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, HdmiInAddr.START_WR_ADDR0, HdmiInAddr.ADDR_HDMI_WD_START, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output start address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output start address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            var ret = await UDPClientPool.WriteAddr(
 | 
			
		||||
                this.ep, this.taskID, HdmiInAddr.END_WR_ADDR0,
 | 
			
		||||
                HdmiInAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
 | 
			
		||||
            if (!ret.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output end address: {ret.Error}");
 | 
			
		||||
                return new(ret.Error);
 | 
			
		||||
            }
 | 
			
		||||
            if (!ret.Value)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to set HDMI output address");
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ public class HdmiVideoStreamClient
 | 
			
		||||
{
 | 
			
		||||
    public required HdmiIn HdmiInClient { get; set; }
 | 
			
		||||
 | 
			
		||||
    public required Jpeg JpegClient { get; set; }
 | 
			
		||||
    // public required Jpeg JpegClient { get; set; }
 | 
			
		||||
 | 
			
		||||
    public required CancellationTokenSource CTS { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -102,7 +102,8 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            var client = _clientDict[key];
 | 
			
		||||
            client.CTS.Cancel();
 | 
			
		||||
 | 
			
		||||
            var disableResult = await client.JpegClient.SetEnable(false);
 | 
			
		||||
            // var disableResult = await client.JpegClient.SetEnable(false);
 | 
			
		||||
            var disableResult = await client.HdmiInClient.SetTransEnable(false);
 | 
			
		||||
            if (disableResult)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Info("Successfully disabled HDMI transmission");
 | 
			
		||||
@@ -111,6 +112,8 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to disable HDMI transmission");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            client.CTS = new CancellationTokenSource();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -120,53 +123,51 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
    private async Task<HdmiVideoStreamClient?> GetOrCreateClientAsync(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        if (_clientDict.TryGetValue(boardId, out var 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();
 | 
			
		||||
 | 
			
		||||
            var boardRet = userManager.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;
 | 
			
		||||
 | 
			
		||||
            client = new HdmiVideoStreamClient()
 | 
			
		||||
            {
 | 
			
		||||
                HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1),
 | 
			
		||||
                // JpegClient = new Jpeg(board.IpAddr, board.Port, 1),
 | 
			
		||||
                CTS = new CancellationTokenSource(),
 | 
			
		||||
                Offset = 0
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var userManager = new Database.UserManager();
 | 
			
		||||
 | 
			
		||||
        var boardRet = userManager.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;
 | 
			
		||||
 | 
			
		||||
        client = new HdmiVideoStreamClient()
 | 
			
		||||
        {
 | 
			
		||||
            HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1),
 | 
			
		||||
            JpegClient = new Jpeg(board.IpAddr, board.Port, 1),
 | 
			
		||||
            CTS = new CancellationTokenSource(),
 | 
			
		||||
            Offset = 0
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 启用HDMI传输
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // 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)
 | 
			
		||||
            var hdmiEnableRet = await client.HdmiInClient.Init(true);
 | 
			
		||||
            if (!hdmiEnableRet.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error($"Failed to enable JPEG transmission for board {boardId}: {jpegEnableRet.Error}");
 | 
			
		||||
                logger.Error($"Failed to enable HDMI transmission for board {boardId}: {hdmiEnableRet.Error}");
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            logger.Info($"Successfully enabled JPEG transmission for board {boardId}");
 | 
			
		||||
            logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
 | 
			
		||||
 | 
			
		||||
            client.Width = client.JpegClient.Width;
 | 
			
		||||
            client.Height = client.JpegClient.Height;
 | 
			
		||||
            // var jpegEnableRet = await client.JpegClient.Init(true);
 | 
			
		||||
            // if (!jpegEnableRet.IsSuccessful)
 | 
			
		||||
            // {
 | 
			
		||||
            //     logger.Error($"Failed to enable JPEG transmission for board {boardId}: {jpegEnableRet.Error}");
 | 
			
		||||
            //     return null;
 | 
			
		||||
            // }
 | 
			
		||||
            // logger.Info($"Successfully enabled JPEG transmission for board {boardId}");
 | 
			
		||||
 | 
			
		||||
            client.Width = client.HdmiInClient.Width;
 | 
			
		||||
            client.Height = client.HdmiInClient.Height;
 | 
			
		||||
            // client.Width = client.JpegClient.Width;
 | 
			
		||||
            // client.Height = client.JpegClient.Height;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -195,15 +196,16 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var hdmiInToken = _clientDict[boardId].CTS.Token;
 | 
			
		||||
        var token = CancellationTokenSource.CreateLinkedTokenSource(
 | 
			
		||||
            cancellationToken, client.CTS.Token).Token;
 | 
			
		||||
 | 
			
		||||
        if (path == "/snapshot")
 | 
			
		||||
        {
 | 
			
		||||
            await HandleSnapshotRequestAsync(context.Response, client, hdmiInToken);
 | 
			
		||||
            await HandleSnapshotRequestAsync(context.Response, client, token);
 | 
			
		||||
        }
 | 
			
		||||
        else if (path == "/mjpeg")
 | 
			
		||||
        {
 | 
			
		||||
            await HandleMjpegStreamAsync(context.Response, client, hdmiInToken);
 | 
			
		||||
            await HandleMjpegStreamAsync(context.Response, client, token);
 | 
			
		||||
        }
 | 
			
		||||
        else if (path == "/video")
 | 
			
		||||
        {
 | 
			
		||||
@@ -223,36 +225,47 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            logger.Debug("处理HDMI快照请求");
 | 
			
		||||
 | 
			
		||||
            // 从HDMI读取RGB565数据
 | 
			
		||||
            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;
 | 
			
		||||
                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 frameResult = await client.JpegClient.GetMultiFrames((uint)client.Offset);
 | 
			
		||||
            // if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
 | 
			
		||||
            // {
 | 
			
		||||
            //     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 jpegData = frameResult.Value[0];
 | 
			
		||||
            
 | 
			
		||||
            var quantTableResult = await client.JpegClient.GetQuantizationTable();
 | 
			
		||||
            if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
 | 
			
		||||
            // var jpegData = frameResult.Value[0];
 | 
			
		||||
 | 
			
		||||
            // var quantTableResult = await client.JpegClient.GetQuantizationTable();
 | 
			
		||||
            // if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
 | 
			
		||||
            // {
 | 
			
		||||
            //     logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
 | 
			
		||||
            //     response.StatusCode = 500;
 | 
			
		||||
            //     var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table");
 | 
			
		||||
            //     await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
 | 
			
		||||
            //     response.Close();
 | 
			
		||||
            //     return;
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            // var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height, quantTableResult.Value);
 | 
			
		||||
            // 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;
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            var jpegImage = await client.HdmiInClient.GetMJpegFrame();
 | 
			
		||||
            if (!jpegImage.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
 | 
			
		||||
                logger.Error("获取HDMI MJPEG帧失败");
 | 
			
		||||
                response.StatusCode = 500;
 | 
			
		||||
                var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table");
 | 
			
		||||
                await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
 | 
			
		||||
                response.Close();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height, quantTableResult.Value);
 | 
			
		||||
            if (!jpegImage.IsSuccessful)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("JPEG数据补全失败");
 | 
			
		||||
                response.StatusCode = 500;
 | 
			
		||||
                var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to complete JPEG data");
 | 
			
		||||
                var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI MJPEG frame");
 | 
			
		||||
                await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
 | 
			
		||||
                response.Close();
 | 
			
		||||
                return;
 | 
			
		||||
@@ -260,13 +273,13 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
            // 设置响应头(参考Camera版本)
 | 
			
		||||
            response.ContentType = "image/jpeg";
 | 
			
		||||
            response.ContentLength64 = jpegImage.Value.Length;
 | 
			
		||||
            response.ContentLength64 = jpegImage.Value.data.Length;
 | 
			
		||||
            response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
			
		||||
 | 
			
		||||
            await response.OutputStream.WriteAsync(jpegImage.Value, 0, jpegImage.Value.Length, cancellationToken);
 | 
			
		||||
            await response.OutputStream.WriteAsync(jpegImage.Value.data, 0, jpegImage.Value.data.Length, cancellationToken);
 | 
			
		||||
            await response.OutputStream.FlushAsync(cancellationToken);
 | 
			
		||||
 | 
			
		||||
            logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegImage.Value.Length);
 | 
			
		||||
            logger.Debug("已发送HDMI快照图像,大小:{Size} 字节", jpegImage.Value.data.Length);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -275,6 +288,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            response.StatusCode = 200;
 | 
			
		||||
            response.Close();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -292,17 +306,17 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
            logger.Debug("开始HDMI MJPEG流传输");
 | 
			
		||||
 | 
			
		||||
            var quantTableResult = await client.JpegClient.GetQuantizationTable();
 | 
			
		||||
            if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
 | 
			
		||||
            {
 | 
			
		||||
                logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
 | 
			
		||||
                response.StatusCode = 500;
 | 
			
		||||
                await response.OutputStream.WriteAsync(
 | 
			
		||||
                    System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table"), 0, 0, cancellationToken);
 | 
			
		||||
                response.Close();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            var quantTable = quantTableResult.Value;
 | 
			
		||||
            // var quantTableResult = await client.JpegClient.GetQuantizationTable();
 | 
			
		||||
            // if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
 | 
			
		||||
            // {
 | 
			
		||||
            //     logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
 | 
			
		||||
            //     response.StatusCode = 500;
 | 
			
		||||
            //     await response.OutputStream.WriteAsync(
 | 
			
		||||
            //         System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table"), 0, 0, cancellationToken);
 | 
			
		||||
            //     response.Close();
 | 
			
		||||
            //     return;
 | 
			
		||||
            // }
 | 
			
		||||
            // var quantTable = quantTableResult.Value;
 | 
			
		||||
 | 
			
		||||
            int frameCounter = 0;
 | 
			
		||||
 | 
			
		||||
@@ -310,51 +324,74 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            {
 | 
			
		||||
                var frameStartTime = DateTime.UtcNow;
 | 
			
		||||
 | 
			
		||||
                var frameResult =
 | 
			
		||||
                    await client.JpegClient.GetMultiFrames((uint)client.Offset);
 | 
			
		||||
                if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
 | 
			
		||||
                var frameRet = await client.HdmiInClient.GetMJpegFrame();
 | 
			
		||||
                if (!frameRet.HasValue)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error("获取HDMI帧失败");
 | 
			
		||||
                    await Task.Delay(100, cancellationToken);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                var frame = frameRet.Value;
 | 
			
		||||
 | 
			
		||||
                foreach (var framebytes in frameResult.Value)
 | 
			
		||||
                await response.OutputStream.WriteAsync(frame.header, 0, frame.header.Length, cancellationToken);
 | 
			
		||||
                await response.OutputStream.WriteAsync(frame.data, 0, frame.data.Length, cancellationToken);
 | 
			
		||||
                await response.OutputStream.WriteAsync(frame.footer, 0, frame.footer.Length, cancellationToken);
 | 
			
		||||
                await response.OutputStream.FlushAsync(cancellationToken);
 | 
			
		||||
 | 
			
		||||
                frameCounter++;
 | 
			
		||||
 | 
			
		||||
                var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
 | 
			
		||||
 | 
			
		||||
                // 性能统计日志(每30帧记录一次)
 | 
			
		||||
                if (frameCounter % 30 == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height, quantTable);
 | 
			
		||||
                    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);
 | 
			
		||||
                    await response.OutputStream.WriteAsync(frame.footer, 0, frame.footer.Length, cancellationToken);
 | 
			
		||||
                    await response.OutputStream.FlushAsync(cancellationToken);
 | 
			
		||||
 | 
			
		||||
                    frameCounter++;
 | 
			
		||||
 | 
			
		||||
                    var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
 | 
			
		||||
 | 
			
		||||
                    // 性能统计日志(每30帧记录一次)
 | 
			
		||||
                    if (frameCounter % 30 == 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
 | 
			
		||||
                            frameCounter, totalTime, frame.data.Length);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
 | 
			
		||||
                        frameCounter, totalTime, frame.data.Length);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 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, quantTable);
 | 
			
		||||
                //     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);
 | 
			
		||||
                //     await response.OutputStream.WriteAsync(frame.footer, 0, frame.footer.Length, cancellationToken);
 | 
			
		||||
                //     await response.OutputStream.FlushAsync(cancellationToken);
 | 
			
		||||
 | 
			
		||||
                //     frameCounter++;
 | 
			
		||||
 | 
			
		||||
                //     var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
 | 
			
		||||
 | 
			
		||||
                //     // 性能统计日志(每30帧记录一次)
 | 
			
		||||
                //     if (frameCounter % 30 == 0)
 | 
			
		||||
                //     {
 | 
			
		||||
                //         logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
 | 
			
		||||
                //             frameCounter, totalTime, frame.data.Length);
 | 
			
		||||
                //     }
 | 
			
		||||
 | 
			
		||||
                // }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
@@ -366,7 +403,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // 停止传输时禁用HDMI传输
 | 
			
		||||
                await client.HdmiInClient.EnableTrans(false);
 | 
			
		||||
                await client.HdmiInClient.SetTransEnable(false);
 | 
			
		||||
                logger.Info("已禁用HDMI传输");
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,12 @@
 | 
			
		||||
import { ref, onMounted, onUnmounted } from "vue";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { ExamClient, ResourceClient, type ExamInfo } from "@/APIClient";
 | 
			
		||||
import {
 | 
			
		||||
  ExamClient,
 | 
			
		||||
  ResourceClient,
 | 
			
		||||
  ResourcePurpose,
 | 
			
		||||
  type ExamInfo,
 | 
			
		||||
} from "@/APIClient";
 | 
			
		||||
 | 
			
		||||
// 接口定义
 | 
			
		||||
interface Tutorial {
 | 
			
		||||
@@ -146,7 +151,7 @@ onMounted(async () => {
 | 
			
		||||
        const resourceList = await resourceClient.getResourceList(
 | 
			
		||||
          exam.id,
 | 
			
		||||
          "cover",
 | 
			
		||||
          "template",
 | 
			
		||||
          ResourcePurpose.Template,
 | 
			
		||||
        );
 | 
			
		||||
        if (resourceList && resourceList.length > 0) {
 | 
			
		||||
          // 使用第一个封面资源
 | 
			
		||||
 
 | 
			
		||||
@@ -430,13 +430,13 @@ function startStream() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 停止播放视频流
 | 
			
		||||
function stopStream() {
 | 
			
		||||
async function stopStream() {
 | 
			
		||||
  isPlaying.value = false;
 | 
			
		||||
  currentVideoSource.value = "";
 | 
			
		||||
  videoStatus.value = "已停止播放";
 | 
			
		||||
 | 
			
		||||
  const client = AuthManager.createClient(HdmiVideoStreamClient);
 | 
			
		||||
  client.disableHdmiTransmission();
 | 
			
		||||
  await client.disableHdmiTransmission();
 | 
			
		||||
 | 
			
		||||
  addLog("info", "停止播放HDMI视频流");
 | 
			
		||||
  alert?.info("已停止播放HDMI视频流");
 | 
			
		||||
@@ -467,8 +467,9 @@ function handleVideoClick() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重试连接
 | 
			
		||||
function tryReconnect() {
 | 
			
		||||
async function tryReconnect() {
 | 
			
		||||
  hasVideoError.value = false;
 | 
			
		||||
  await stopStream();
 | 
			
		||||
  if (endpoint.value) {
 | 
			
		||||
    startStream();
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -171,7 +171,12 @@ import { useProvideComponentManager } from "@/components/LabCanvas";
 | 
			
		||||
import { useAlertStore } from "@/components/Alert";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { useEquipments } from "@/stores/equipments";
 | 
			
		||||
import { DataClient, ResourceClient, type Board } from "@/APIClient";
 | 
			
		||||
import {
 | 
			
		||||
  DataClient,
 | 
			
		||||
  ResourceClient,
 | 
			
		||||
  ResourcePurpose,
 | 
			
		||||
  type Board,
 | 
			
		||||
} from "@/APIClient";
 | 
			
		||||
 | 
			
		||||
import { useRoute } from "vue-router";
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
@@ -257,7 +262,11 @@ async function loadDocumentContent() {
 | 
			
		||||
      const client = AuthManager.createClient(ResourceClient);
 | 
			
		||||
 | 
			
		||||
      // 获取markdown类型的模板资源列表
 | 
			
		||||
      const resources = await client.getResourceList(examId, "doc", "template");
 | 
			
		||||
      const resources = await client.getResourceList(
 | 
			
		||||
        examId,
 | 
			
		||||
        "doc",
 | 
			
		||||
        ResourcePurpose.Template,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (resources && resources.length > 0) {
 | 
			
		||||
        // 获取第一个markdown资源
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user