diff --git a/server/src/Peripherals/HdmiInClient.cs b/server/src/Peripherals/HdmiInClient.cs index 9b5080c..493da7e 100644 --- a/server/src/Peripherals/HdmiInClient.cs +++ b/server/src/Peripherals/HdmiInClient.cs @@ -12,7 +12,7 @@ static class HdmiInAddr public const UInt32 HdmiIn_READFIFO = BASE + 0x1; } -class HdmiIn +public class HdmiIn { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); diff --git a/server/src/Peripherals/JpegClient.cs b/server/src/Peripherals/JpegClient.cs index 870c441..2be3f7c 100644 --- a/server/src/Peripherals/JpegClient.cs +++ b/server/src/Peripherals/JpegClient.cs @@ -7,14 +7,28 @@ namespace Peripherals.JpegClient; static class JpegAddr { const UInt32 BASE = 0x0000_0000; - public const UInt32 ENABLE = BASE + 0x0; - public const UInt32 FRAME_NUM = BASE + 0x1; - public const UInt32 FRAME_INFO = BASE + 0x2; - public const UInt32 FRAME_SAMPLE_RATE = BASE + 0x3; - public const UInt32 FRAME_DATA_MAX_POINTER = BASE + 0x4; - public const UInt32 DDR_FRAME_DATA_ADDR = 0x0000_0000; - public const UInt32 DDR_FRAME_DATA_MAX_ADDR = 0x8000_0000; + public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0; + public const UInt32 CAPTURE_WR_CTRL = BASE + 0x1; + + public const UInt32 START_WR_ADDR0 = BASE + 0x2; + public const UInt32 END_WR_ADDR0 = BASE + 0x3; + public const UInt32 START_WR_ADDR1 = BASE + 0x4; + public const UInt32 END_WR_ADDR1 = BASE + 0x5; + public const UInt32 START_RD_ADDR0 = BASE + 0x6; + public const UInt32 END_RD_ADDR0 = BASE + 0x7; + + public const UInt32 HDMI_NOT_READY = BASE + 0x8; + public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x9; + + public const UInt32 JPEG_HEIGHT_WIDTH = BASE + 0xA; + public const UInt32 JPEG_ADD_NEED_FRAME_NUM = BASE + 0xB; + public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC; + public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD; + + public const UInt32 ADDR_HDMI_WD_START = 0x4000_0000; + public const UInt32 ADDR_JPEG_START = 0x8000_0000; + public const UInt32 ADDR_JPEG_END = 0xA000_0000; } public class JpegInfo @@ -79,39 +93,248 @@ public class Jpeg 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 SetEnable(true); + else return true; + } + public async ValueTask SetEnable(bool enable) { - var ret = await UDPClientPool.WriteAddr( - this.ep, this.taskID, JpegAddr.ENABLE, Convert.ToUInt32(enable), this.timeout); + if (enable) + { + var ret = await UDPClientPool.WriteAddrSeq( + this.ep, + this.taskID, + [JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL], + [0b11, 0b01], + this.timeout + ); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set JPEG enable: {ret.Error}"); + return false; + } + return ret.Value; + } + else + { + var ret = await UDPClientPool.WriteAddrSeq( + this.ep, + this.taskID, + [JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL], + [0b00, 0b00], + this.timeout + ); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set JPEG disable: {ret.Error}"); + return false; + } + return ret.Value; + } + } + + public async ValueTask> CheckHdmiIsReady() + { + var ret = await UDPClientPool.ReadAddrWithWait( + this.ep, this.taskID, JpegAddr.HDMI_NOT_READY, 0b01, 0b01, 100, this.timeout); if (!ret.IsSuccessful) { - logger.Error($"Failed to set JPEG enable: {ret.Error}"); - return false; + logger.Error($"Failed to check HDMI status: {ret.Error}"); + return new(ret.Error); } return ret.Value; } - public async ValueTask SetSampleRate(uint rate) + public async ValueTask> GetHdmiResolution() { - var ret = await UDPClientPool.WriteAddr( - this.ep, this.taskID, JpegAddr.FRAME_SAMPLE_RATE, rate, this.timeout); + var ret = await UDPClientPool.ReadAddr( + this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, 0, this.timeout); if (!ret.IsSuccessful) { - logger.Error($"Failed to set JPEG sample rate: {ret.Error}"); - return false; + logger.Error($"Failed to get HDMI resolution: {ret.Error}"); + return new(ret.Error); } - return ret.Value; + + 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[0] | (data[1] << 8); + var height = data[2] | (data[3] << 8); + return new((width, height)); } - public async ValueTask SetSampleRate(JpegSampleRate rate) + public async ValueTask> ConnectJpeg2Hdmi(int width, int height) { - return await SetSampleRate((uint)rate); + 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 / 4); + + { + var ret = await UDPClientPool.WriteAddr( + this.ep, this.taskID, JpegAddr.START_WR_ADDR0, JpegAddr.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, JpegAddr.END_WR_ADDR0, + JpegAddr.ADDR_HDMI_WD_START + frameSize, 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; + } + } + + { + var ret = await UDPClientPool.WriteAddr( + this.ep, this.taskID, JpegAddr.START_RD_ADDR0, JpegAddr.ADDR_HDMI_WD_START, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set jpeg input start address: {ret.Error}"); + return new(ret.Error); + } + if (!ret.Value) + { + logger.Error($"Failed to set jpeg input address"); + return false; + } + } + + { + var ret = await UDPClientPool.WriteAddr( + this.ep, this.taskID, JpegAddr.END_RD_ADDR0, + JpegAddr.ADDR_HDMI_WD_START + frameSize, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set jpeg input end address: {ret.Error}"); + return new(ret.Error); + } + if (!ret.Value) + { + logger.Error($"Failed to set jpeg input end address"); + return false; + } + } + + { + var ret = await UDPClientPool.WriteAddr( + this.ep, this.taskID, JpegAddr.START_WR_ADDR1, JpegAddr.ADDR_JPEG_START, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set jpeg output start address: {ret.Error}"); + return new(ret.Error); + } + if (!ret.Value) + { + logger.Error($"Failed to set jpeg output start address"); + return false; + } + } + + { + var ret = await UDPClientPool.WriteAddr( + this.ep, this.taskID, JpegAddr.END_WR_ADDR1, JpegAddr.ADDR_JPEG_END, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set jpeg output end address: {ret.Error}"); + return new(ret.Error); + } + if (!ret.Value) + { + logger.Error($"Failed to set jpeg output end address"); + return false; + } + } + + return true; } + // public async ValueTask SetSampleRate(uint rate) + // { + // var ret = await UDPClientPool.WriteAddr( + // this.ep, this.taskID, JpegAddr.FRAME_SAMPLE_RATE, rate, this.timeout); + // if (!ret.IsSuccessful) + // { + // logger.Error($"Failed to set JPEG sample rate: {ret.Error}"); + // return false; + // } + // return ret.Value; + // } + + // public async ValueTask SetSampleRate(JpegSampleRate rate) + // { + // return await SetSampleRate((uint)rate); + // } + public async ValueTask GetFrameNumber() { var ret = await UDPClientPool.ReadAddrByte( - this.ep, this.taskID, JpegAddr.FRAME_NUM, this.timeout); + this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to get JPEG frame number: {ret.Error}"); @@ -122,7 +345,7 @@ public class Jpeg public async ValueTask>> GetFrameInfo(int num) { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, JpegAddr.FRAME_INFO, num, this.timeout); + var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, JpegAddr.JPEG_FIFO_FRAME_INFO, num, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to get JPEG frame info: {ret.Error}"); @@ -150,10 +373,10 @@ public class Jpeg return new(infos); } - public async ValueTask UpdatePointer(uint cnt) + public async ValueTask AddFrameNum2Process(uint cnt) { var ret = await UDPClientPool.WriteAddr( - this.ep, this.taskID, JpegAddr.FRAME_DATA_MAX_POINTER, cnt, this.timeout); + this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to update pointer: {ret.Error}"); @@ -171,13 +394,16 @@ public class Jpeg } MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port); - var firstReadLength = (int)(Math.Min(length, JpegAddr.DDR_FRAME_DATA_MAX_ADDR - offset)); + var firstReadLength = (int)(Math.Min( + length, + JpegAddr.ADDR_JPEG_END - JpegAddr.ADDR_JPEG_START - offset + )); var secondReadLength = (int)(length - firstReadLength); var dataBytes = new byte[length]; { var ret = await UDPClientPool.ReadAddr4Bytes( - this.ep, this.taskID, JpegAddr.DDR_FRAME_DATA_ADDR + offset, firstReadLength, this.timeout); + this.ep, this.taskID, JpegAddr.ADDR_JPEG_START + offset, firstReadLength, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to get JPEG frame data: {ret.Error}"); @@ -194,7 +420,7 @@ public class Jpeg if (secondReadLength > 0) { var ret = await UDPClientPool.ReadAddr4Bytes( - this.ep, this.taskID, JpegAddr.DDR_FRAME_DATA_ADDR, secondReadLength, this.timeout); + this.ep, this.taskID, JpegAddr.ADDR_JPEG_START, secondReadLength, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to get JPEG frame data: {ret.Error}"); @@ -239,7 +465,7 @@ public class Jpeg } { - var ret = await UpdatePointer((uint)sizes.Length); + var ret = await AddFrameNum2Process((uint)sizes.Length); if (!ret) logger.Error($"Failed to update pointer"); } diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs index 56ac0d8..883137b 100644 --- a/server/src/Services/HttpHdmiVideoStreamService.cs +++ b/server/src/Services/HttpHdmiVideoStreamService.cs @@ -1,6 +1,7 @@ using System.Net; using System.Collections.Concurrent; using Peripherals.HdmiInClient; +using Peripherals.JpegClient; namespace server.Services; @@ -12,6 +13,15 @@ public class HdmiVideoStreamEndpoint public string SnapshotUrl { get; set; } = ""; } +public class HdmiVideoStreamClient +{ + public required HdmiIn HdmiInClient { get; set; } + + public required Jpeg JpegClient { get; set; } + + public required CancellationTokenSource CTS { get; set; } +} + public class HttpHdmiVideoStreamService : BackgroundService { private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); @@ -20,8 +30,7 @@ public class HttpHdmiVideoStreamService : BackgroundService private HttpListener? _httpListener; private readonly int _serverPort = 4322; - private readonly ConcurrentDictionary _hdmiInDict = new(); - private readonly ConcurrentDictionary _hdmiInCtsDict = new(); + private readonly ConcurrentDictionary _clientDict = new(); public HttpHdmiVideoStreamService(IServiceProvider serviceProvider) { @@ -75,7 +84,7 @@ public class HttpHdmiVideoStreamService : BackgroundService // 禁用所有活跃的HDMI传输 var disableTasks = new List(); - foreach (var hdmiKey in _hdmiInDict.Keys) + foreach (var hdmiKey in _clientDict.Keys) { disableTasks.Add(DisableHdmiTransmissionAsync(hdmiKey)); } @@ -84,8 +93,7 @@ public class HttpHdmiVideoStreamService : BackgroundService await Task.WhenAll(disableTasks); // 清空字典 - _hdmiInDict.Clear(); - _hdmiInCtsDict.Clear(); + _clientDict.Clear(); _httpListener?.Close(); // 立即关闭监听器,唤醒阻塞 await base.StopAsync(cancellationToken); @@ -95,11 +103,10 @@ public class HttpHdmiVideoStreamService : BackgroundService { try { - var cts = _hdmiInCtsDict[key]; - cts.Cancel(); + var client = _clientDict[key]; + client.CTS.Cancel(); - var hdmiIn = _hdmiInDict[key]; - var disableResult = await hdmiIn.EnableTrans(false); + var disableResult = await client.HdmiInClient.EnableTrans(false); if (disableResult.IsSuccessful) { logger.Info("Successfully disabled HDMI transmission"); @@ -115,31 +122,9 @@ public class HttpHdmiVideoStreamService : BackgroundService } } - // 获取/创建 HdmiIn 实例 - private async Task GetOrCreateHdmiInAsync(string boardId) + private async Task GetOrCreateClientAsync(string boardId) { - if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn)) - { - 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; - _hdmiInCtsDict[boardId] = new CancellationTokenSource(); - return hdmiIn; - } + if (_clientDict.TryGetValue(boardId, out var client)) return client; using var scope = _serviceProvider.CreateScope(); var userManager = scope.ServiceProvider.GetRequiredService(); @@ -153,18 +138,31 @@ public class HttpHdmiVideoStreamService : BackgroundService var board = boardRet.Value.Value; - hdmiIn = new HdmiIn(board.IpAddr, board.Port, 0); // taskID 可根据实际需求调整 + client = new HdmiVideoStreamClient() + { + HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 1), + JpegClient = new Jpeg(board.IpAddr, board.Port, 1), + CTS = new CancellationTokenSource() + }; // 启用HDMI传输 try { - var enableResult = await hdmiIn.EnableTrans(true); - if (!enableResult.IsSuccessful) + var hdmiEnableRet = await client.HdmiInClient.EnableTrans(true); + if (!hdmiEnableRet.IsSuccessful) { - logger.Error($"Failed to enable HDMI transmission for board {boardId}: {enableResult.Error}"); + 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) + { + logger.Error($"Failed to enable JPEG transmission for board {boardId}: {jpegEnableRet.Error}"); + return null; + } + logger.Info($"Successfully enabled JPEG transmission for board {boardId}"); } catch (Exception ex) { @@ -172,9 +170,8 @@ public class HttpHdmiVideoStreamService : BackgroundService return null; } - _hdmiInDict[boardId] = hdmiIn; - _hdmiInCtsDict[boardId] = new CancellationTokenSource(); - return hdmiIn; + _clientDict[boardId] = client; + return client; } private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken) @@ -187,14 +184,14 @@ public class HttpHdmiVideoStreamService : BackgroundService return; } - var hdmiIn = await GetOrCreateHdmiInAsync(boardId); - if (hdmiIn == null) + var client = await GetOrCreateClientAsync(boardId); + if (client == null) { await SendErrorAsync(context.Response, "Invalid boardId or board not available"); return; } - var hdmiInToken = _hdmiInCtsDict[boardId].Token; + var hdmiInToken = _clientDict[boardId].CTS.Token; if (hdmiInToken == null) { await SendErrorAsync(context.Response, "HDMI input is not available"); @@ -203,11 +200,11 @@ public class HttpHdmiVideoStreamService : BackgroundService if (path == "/snapshot") { - await HandleSnapshotRequestAsync(context.Response, hdmiIn, hdmiInToken); + await HandleSnapshotRequestAsync(context.Response, client, hdmiInToken); } else if (path == "/mjpeg") { - await HandleMjpegStreamAsync(context.Response, hdmiIn, hdmiInToken); + await HandleMjpegStreamAsync(context.Response, client, hdmiInToken); } else if (path == "/video") { @@ -219,14 +216,15 @@ public class HttpHdmiVideoStreamService : BackgroundService } } - private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken) + private async Task HandleSnapshotRequestAsync( + HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken) { try { logger.Debug("处理HDMI快照请求"); // 从HDMI读取RGB565数据 - var frameResult = await hdmiIn.ReadFrame(); + var frameResult = await client.HdmiInClient.ReadFrame(); if (!frameResult.IsSuccessful || frameResult.Value == null) { logger.Error("HDMI快照获取失败"); @@ -260,7 +258,8 @@ public class HttpHdmiVideoStreamService : BackgroundService } } - private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken) + private async Task HandleMjpegStreamAsync( + HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken) { try { @@ -280,7 +279,7 @@ public class HttpHdmiVideoStreamService : BackgroundService { var frameStartTime = DateTime.UtcNow; - var ret = await hdmiIn.GetMJpegFrame(); + var ret = await client.HdmiInClient.GetMJpegFrame(); if (ret == null) continue; var frame = ret.Value; @@ -315,7 +314,7 @@ public class HttpHdmiVideoStreamService : BackgroundService try { // 停止传输时禁用HDMI传输 - await hdmiIn.EnableTrans(false); + await client.HdmiInClient.EnableTrans(false); logger.Info("已禁用HDMI传输"); } catch (Exception ex) diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index 582d7d9..665109a 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -331,7 +331,9 @@ public class UDPClientPool /// 超时时间(毫秒) /// 校验结果,true表示在超时前数据匹配期望值 public static async ValueTask> ReadAddrWithWait( - IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int waittime = 100, int timeout = 1000) + IPEndPoint endPoint, int taskID, uint devAddr, + UInt32 result, UInt32 resultMask, + int waittime = 100, int timeout = 1000) { var address = endPoint.Address.ToString();