using System.Net; using DotNext; using WebProtocol; namespace Peripherals.HdmiInClient; static class HdmiInAddr { public const UInt32 BASE = 0xA000_0000; 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 { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); readonly int timeout = 500; readonly int taskID; readonly int port; readonly string address; private IPEndPoint ep; public int Width { get; private set; } public int Height { get; private set; } public int FrameLength => Width * Height * 3 / 4; /// /// 初始化HDMI输入客户端 /// /// HDMI输入设备IP地址 /// HDMI输入设备端口 /// 任务ID /// 超时时间(毫秒) public HdmiIn(string address, int port, int taskID, int timeout = 500) { if (timeout < 0) throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); this.address = address; this.port = port; this.ep = new IPEndPoint(IPAddress.Parse(address), port); this.taskID = taskID; this.timeout = timeout; } public async ValueTask> Init(bool enable = true) { { 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> 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}"); return new(ret.Error); } if (!ret.Value) { logger.Error($"HdmiIn_CTRL write returned false for HdmiIn at {this.address}:{this.port}"); return false; } return true; } /// /// 读取一帧图像数据 /// /// 包含图像数据的字节数组 public async ValueTask> ReadFrame() { // 只在第一次或出错时清除UDP缓冲区,避免每帧都清除造成延迟 // MsgBus.UDPServer.ClearUDPData(this.address, this.taskID); logger.Trace($"Reading frame from HdmiIn {this.address}"); // 使用UDPClientPool读取图像帧数据 var result = await UDPClientPool.ReadAddr4BytesAsync( this.ep, this.taskID, // taskID HdmiInAddr.ADDR_HDMI_WD_START, FrameLength, // 使用当前分辨率的动态大小 BurstType.FixedBurst, this.timeout); if (!result.IsSuccessful) { logger.Error($"Failed to read frame from HdmiIn {this.address}:{this.port}, error: {result.Error}"); // 读取失败时清除缓冲区,为下次读取做准备 try { MsgBus.UDPServer.ClearUDPData(this.address, this.taskID); } catch (Exception ex) { logger.Warn($"Failed to clear UDP data after read error: {ex.Message}"); } return new(result.Error); } logger.Trace($"Successfully read frame from HdmiIn {this.address}:{this.port}, data length: {result.Value.Length} bytes"); return result.Value; } public async ValueTask> GetMJpegFrame() { // 从HDMI读取RGB24数据 var readStartTime = DateTime.UtcNow; var frameResult = await ReadFrame(); var readEndTime = DateTime.UtcNow; var readTime = (readEndTime - readStartTime).TotalMilliseconds; if (!frameResult.IsSuccessful || frameResult.Value == null) { logger.Warn("HDMI帧读取失败或为空"); return Optional<(byte[] header, byte[] data, byte[] footer)>.None; } var rgb24Data = frameResult.Value; // 验证数据长度是否正确 (RGB24为每像素2字节) var expectedLength = Width * Height * 2; if (rgb24Data.Length != expectedLength) { logger.Warn("HDMI数据长度不匹配,期望: {Expected}, 实际: {Actual}", expectedLength, rgb24Data.Length); } // 将RGB24转换为JPEG(参考Camera版本的处理) var jpegStartTime = DateTime.UtcNow; 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 Optional<(byte[] header, byte[] data, byte[] footer)>.None; } var jpegData = jpegResult.Value; var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length); var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter(); return (mjpegFrameHeader, jpegData, mjpegFrameFooter); } public async ValueTask> CheckHdmiIsReady() { 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; } public async ValueTask> GetHdmiResolution() { 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> 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; } }