diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs index c1cc728..fe786b9 100644 --- a/server/src/Controllers/VideoStreamController.cs +++ b/server/src/Controllers/VideoStreamController.cs @@ -1,77 +1,22 @@ using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; +using Database; +using DotNext; /// /// 视频流控制器,支持动态配置摄像头连接 /// [ApiController] +[Authorize] [Route("api/[controller]")] public class VideoStreamController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private readonly server.Services.HttpVideoStreamService _videoStreamService; - /// - /// 视频流信息结构体 - /// - public class StreamInfoResult - { - /// - /// TODO: - /// - public int FrameRate { get; set; } - /// - /// TODO: - /// - public int FrameWidth { get; set; } - /// - /// TODO: - /// - public int FrameHeight { get; set; } - /// - /// TODO: - /// - public string Format { get; set; } = "MJPEG"; - /// - /// TODO: - /// - public string HtmlUrl { get; set; } = ""; - /// - /// TODO: - /// - public string MjpegUrl { get; set; } = ""; - /// - /// TODO: - /// - public string SnapshotUrl { get; set; } = ""; - /// - /// TODO: - /// - public string UsbCameraUrl { get; set; } = ""; - } - - /// - /// 摄像头配置请求模型 - /// - public class CameraConfigRequest - { - /// - /// 摄像头地址 - /// - [Required] - [RegularExpression(@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ErrorMessage = "请输入有效的IP地址")] - public string Address { get; set; } = ""; - - /// - /// 摄像头端口 - /// - [Required] - [Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")] - public int Port { get; set; } - } - /// /// 分辨率配置请求模型 /// @@ -92,6 +37,14 @@ public class VideoStreamController : ControllerBase public int Height { get; set; } } + public class AvailableResolutionsResponse + { + public int Width { get; set; } + public int Height { get; set; } + public string Name { get; set; } = string.Empty; + public string Value => $"{Width}x{Height}"; + } + /// /// 初始化HTTP视频流控制器 /// @@ -102,6 +55,40 @@ public class VideoStreamController : ControllerBase _videoStreamService = videoStreamService; } + private Optional TryGetBoardId() + { + var userName = User.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrEmpty(userName)) + { + logger.Error("User name not found in claims."); + return Optional.None; + } + + var db = new AppDataConnection(); + if (db == null) + { + logger.Error("Database connection failed."); + return Optional.None; + } + + var userRet = db.GetUserByName(userName); + if (!userRet.IsSuccessful || !userRet.Value.HasValue) + { + logger.Error("User not found."); + return Optional.None; + } + + var user = userRet.Value.Value; + var boardId = user.BoardID; + if (boardId == Guid.Empty) + { + logger.Error("No board bound to this user."); + return Optional.None; + } + + return boardId.ToString(); + } + /// /// 获取 HTTP 视频流服务状态 /// @@ -114,8 +101,6 @@ public class VideoStreamController : ControllerBase { try { - logger.Info("GetStatus方法被调用,控制器:{Controller},路径:api/VideoStream/Status", this.GetType().Name); - // 使用HttpVideoStreamService提供的状态信息 var status = _videoStreamService.GetServiceStatus(); @@ -129,101 +114,18 @@ public class VideoStreamController : ControllerBase } } - /// - /// 获取 HTTP 视频流信息 - /// - /// 流信息 - [HttpGet("StreamInfo")] - [EnableCors("Users")] - [ProducesResponseType(typeof(StreamInfoResult), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] - public IResult GetStreamInfo() - { - try - { - logger.Info("获取 HTTP 视频流信息"); - var result = new StreamInfoResult - { - FrameRate = _videoStreamService.FrameRate, - FrameWidth = _videoStreamService.FrameWidth, - FrameHeight = _videoStreamService.FrameHeight, - Format = "MJPEG", - HtmlUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/video-feed.html", - MjpegUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/video-stream", - SnapshotUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/snapshot", - UsbCameraUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/usb-camera" - }; - return TypedResults.Ok(result); - } - catch (Exception ex) - { - logger.Error(ex, "获取 HTTP 视频流信息失败"); - return TypedResults.InternalServerError(ex.Message); - } - } - - /// - /// 配置摄像头连接参数 - /// - /// 摄像头配置 - /// 配置结果 - [HttpPost("ConfigureCamera")] - [EnableCors("Users")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] - public async Task ConfigureCamera([FromBody] CameraConfigRequest config) - { - try - { - logger.Info("配置摄像头连接: {Address}:{Port}", config.Address, config.Port); - - var success = await _videoStreamService.ConfigureCameraAsync(config.Address, config.Port); - - if (success) - { - return TypedResults.Ok(new - { - success = true, - message = "摄像头配置成功", - cameraAddress = config.Address, - cameraPort = config.Port - }); - } - else - { - return TypedResults.BadRequest(new - { - success = false, - message = "摄像头配置失败", - cameraAddress = config.Address, - cameraPort = config.Port - }); - } - } - catch (Exception ex) - { - logger.Error(ex, "配置摄像头连接失败"); - return TypedResults.InternalServerError(ex.Message); - } - } - - /// - /// 获取当前摄像头配置 - /// - /// 摄像头配置信息 - [HttpGet("CameraConfig")] + [HttpGet("MyEndpoint")] [EnableCors("Users")] [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] - public IResult GetCameraConfig() + public IResult MyEndpoint() { try { - logger.Info("获取摄像头配置"); - var cameraStatus = _videoStreamService.GetCameraStatus(); + var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found")); + var endpoint = _videoStreamService.GetVideoEndpoint(boardId); - return TypedResults.Ok(cameraStatus); + return TypedResults.Ok(endpoint); } catch (Exception ex) { @@ -232,22 +134,6 @@ public class VideoStreamController : ControllerBase } } - /// - /// 控制 HTTP 视频流服务开关 - /// - /// 是否启用服务 - /// 操作结果 - [HttpPost("SetEnabled")] - [EnableCors("Users")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] - public async Task SetEnabled([FromQuery] bool enabled) - { - logger.Info("设置视频流服务开关: {Enabled}", enabled); - await _videoStreamService.SetEnable(enabled); - return TypedResults.Ok(); - } - /// /// 测试 HTTP 视频流连接 /// @@ -260,32 +146,23 @@ public class VideoStreamController : ControllerBase { try { - logger.Info("测试 HTTP 视频流连接"); + var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found")); + var endpoint = _videoStreamService.GetVideoEndpoint(boardId); // 尝试通过HTTP请求检查视频流服务是否可访问 bool isConnected = false; using (var httpClient = new HttpClient()) { httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间 - var response = await httpClient.GetAsync($"http://{Global.localhost}:{_videoStreamService.ServerPort}/"); + var response = await httpClient.GetAsync(endpoint.MjpegUrl); // 只要能连接上就认为成功,不管返回状态 isConnected = response.IsSuccessStatusCode; } - logger.Info("测试摄像头连接"); + var ret = await _videoStreamService.TestCameraConnection(boardId); - var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync(); - - return TypedResults.Ok(new - { - isConnected = isConnected, - success = isSuccess, - message = message, - cameraAddress = _videoStreamService.CameraAddress, - cameraPort = _videoStreamService.CameraPort, - timestamp = DateTime.Now - }); + return TypedResults.Ok(ret); } catch (Exception ex) { @@ -295,6 +172,23 @@ public class VideoStreamController : ControllerBase } } + [HttpPost("DisableTransmission")] + public async Task DisableHdmiTransmission() + { + try + { + var boardId = TryGetBoardId().OrThrow(() => new ArgumentException("Board ID is required")); + + await _videoStreamService.DisableHdmiTransmissionAsync(boardId.ToString()); + return Ok($"HDMI transmission for board {boardId} disabled."); + } + catch (Exception ex) + { + logger.Error(ex, $"Failed to disable HDMI transmission for board"); + return StatusCode(500, $"Error disabling HDMI transmission: {ex.Message}"); + } + } + /// /// 设置视频流分辨率 /// @@ -309,16 +203,16 @@ public class VideoStreamController : ControllerBase { try { - logger.Info($"设置视频流分辨率为 {request.Width}x{request.Height}"); + var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found")); - var (isSuccess, message) = await _videoStreamService.SetResolutionAsync(request.Width, request.Height); + var ret = await _videoStreamService.SetResolutionAsync(boardId, request.Width, request.Height); - if (isSuccess) + if (ret.IsSuccessful && ret.Value) { return TypedResults.Ok(new { success = true, - message = message, + message = $"成功设置分辨率为 {request.Width}x{request.Height}", width = request.Width, height = request.Height, timestamp = DateTime.Now @@ -329,7 +223,7 @@ public class VideoStreamController : ControllerBase return TypedResults.BadRequest(new { success = false, - message = message, + message = ret.Error?.ToString() ?? "未知错误", timestamp = DateTime.Now }); } @@ -341,37 +235,6 @@ public class VideoStreamController : ControllerBase } } - /// - /// 获取当前分辨率 - /// - /// 当前分辨率信息 - [HttpGet("Resolution")] - [EnableCors("Users")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IResult GetCurrentResolution() - { - try - { - logger.Info("获取当前视频流分辨率"); - - var (width, height) = _videoStreamService.GetCurrentResolution(); - - return TypedResults.Ok(new - { - width = width, - height = height, - resolution = $"{width}x{height}", - timestamp = DateTime.Now - }); - } - catch (Exception ex) - { - logger.Error(ex, "获取当前分辨率失败"); - return TypedResults.InternalServerError($"获取当前分辨率失败: {ex.Message}"); - } - } - /// /// 获取支持的分辨率列表 /// @@ -382,29 +245,19 @@ public class VideoStreamController : ControllerBase [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IResult GetSupportedResolutions() { - try + // (640, 480, "640x480 (VGA)"), + // (960, 540, "960x540 (qHD)"), + // (1280, 720, "1280x720 (HD)"), + // (1280, 960, "1280x960 (SXGA)"), + // (1920, 1080, "1920x1080 (Full HD)") + return TypedResults.Ok(new AvailableResolutionsResponse[] { - logger.Info("获取支持的分辨率列表"); - - var resolutions = _videoStreamService.GetSupportedResolutions(); - - return TypedResults.Ok(new - { - resolutions = resolutions.Select(r => new - { - width = r.Width, - height = r.Height, - name = r.Name, - value = $"{r.Width}x{r.Height}" - }), - timestamp = DateTime.Now - }); - } - catch (Exception ex) - { - logger.Error(ex, "获取支持的分辨率列表失败"); - return TypedResults.InternalServerError($"获取支持的分辨率列表失败: {ex.Message}"); - } + new AvailableResolutionsResponse { Width = 640, Height = 480, Name = "640x480(VGA)" }, + new AvailableResolutionsResponse { Width = 960, Height = 480, Name = "960x480(qHD)" }, + new AvailableResolutionsResponse { Width = 1280, Height = 720, Name = "1280x720(HD)" }, + new AvailableResolutionsResponse { Width = 1280, Height = 960, Name = "1280x960(SXGA)" }, + new AvailableResolutionsResponse { Width = 1920, Height = 1080, Name = "1920x1080(Full HD)" } + }); } /// @@ -420,9 +273,9 @@ public class VideoStreamController : ControllerBase { try { - logger.Info("收到初始化自动对焦请求"); + var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found")); - var result = await _videoStreamService.InitAutoFocusAsync(); + var result = await _videoStreamService.InitAutoFocusAsync(boardId); if (result) { @@ -465,9 +318,9 @@ public class VideoStreamController : ControllerBase { try { - logger.Info("收到执行自动对焦请求"); + var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found")); - var result = await _videoStreamService.PerformAutoFocusAsync(); + var result = await _videoStreamService.PerformAutoFocusAsync(boardId); if (result) { @@ -496,61 +349,4 @@ public class VideoStreamController : ControllerBase return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}"); } } - - /// - /// 执行一次自动对焦 (GET方式) - /// - /// 对焦结果 - [HttpGet("Focus")] - [EnableCors("Users")] - [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public async Task Focus() - { - try - { - logger.Info("收到执行一次对焦请求 (GET)"); - - // 检查摄像头是否已配置 - if (!_videoStreamService.IsCameraConfigured()) - { - logger.Warn("摄像头未配置,无法执行对焦"); - return TypedResults.BadRequest(new - { - success = false, - message = "摄像头未配置,请先配置摄像头连接", - timestamp = DateTime.Now - }); - } - - var result = await _videoStreamService.PerformAutoFocusAsync(); - - if (result) - { - logger.Info("对焦执行成功"); - return TypedResults.Ok(new - { - success = true, - message = "对焦执行成功", - timestamp = DateTime.Now - }); - } - else - { - logger.Warn("对焦执行失败"); - return TypedResults.BadRequest(new - { - success = false, - message = "对焦执行失败", - timestamp = DateTime.Now - }); - } - } - catch (Exception ex) - { - logger.Error(ex, "执行对焦时发生异常"); - return TypedResults.InternalServerError($"执行对焦失败: {ex.Message}"); - } - } } diff --git a/server/src/Peripherals/CameraClient.cs b/server/src/Peripherals/CameraClient.cs index c201318..7771d1e 100644 --- a/server/src/Peripherals/CameraClient.cs +++ b/server/src/Peripherals/CameraClient.cs @@ -1,6 +1,5 @@ using System.Net; using DotNext; -using Peripherals.PowerClient; using WebProtocol; namespace Peripherals.CameraClient; @@ -16,7 +15,7 @@ static class CameraAddr public const UInt32 CAMERA_POWER = BASE + 0x10; //[0]: rstn, 0 is reset. [8]: power down, 1 is down. } -class Camera +public class Camera { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); @@ -276,7 +275,7 @@ class Camera { var currentAddress = (UInt16)(baseAddress + i - 1); var data = (byte)cmd[i]; - + logger.Debug($"ConfigureRegisters: 写入地址=0x{currentAddress:X4}, 数据=0x{data:X2}"); // 准备I2C数据:16位地址 + 8位数据 @@ -322,14 +321,14 @@ class Camera public async ValueTask> ReadRegister(UInt16 register) { var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.taskID, this.timeout); - + // Convert 16-bit register address to byte array var registerBytes = new byte[] { (byte)(register >> 8), (byte)(register & 0xFF) }; - + var ret = await i2c.ReadData(CAM_I2C_ADDR, registerBytes, 1, CAM_PROTO); if (!ret.IsSuccessful) return new(ret.Error); - + return new Result(ret.Value[0]); } @@ -412,25 +411,25 @@ class Camera [0x3801, unchecked((byte)(hStart & 0xFF))], [0x3802, unchecked((byte)((vStart >> 8) & 0xFF))], [0x3803, unchecked((byte)(vStart & 0xFF))], - + // H_END/V_END [0x3804, unchecked((byte)((hEnd >> 8) & 0xFF))], [0x3805, unchecked((byte)(hEnd & 0xFF))], [0x3806, unchecked((byte)((vEnd >> 8) & 0xFF))], [0x3807, unchecked((byte)(vEnd & 0xFF))], - + // 输出像素个数 [0x3808, unchecked((byte)((dvpHo >> 8) & 0xFF))], [0x3809, unchecked((byte)(dvpHo & 0xFF))], [0x380A, unchecked((byte)((dvpVo >> 8) & 0xFF))], [0x380B, unchecked((byte)(dvpVo & 0xFF))], - + // 总像素 [0x380C, unchecked((byte)((hts >> 8) & 0xFF))], [0x380D, unchecked((byte)(hts & 0xFF))], [0x380E, unchecked((byte)((vts >> 8) & 0xFF))], [0x380F, unchecked((byte)(vts & 0xFF))], - + // H_OFFSET/V_OFFSET [0x3810, unchecked((byte)((hOffset >> 8) & 0xFF))], [0x3811, unchecked((byte)(hOffset & 0xFF))], @@ -521,7 +520,7 @@ class Camera hOffset: 16, vOffset: 4, hWindow: 2624, vWindow: 1456 ); - + } /// @@ -537,7 +536,7 @@ class Camera hOffset: 16, vOffset: 4, hWindow: 2624, vWindow: 1456 ); - + } /// @@ -637,7 +636,7 @@ class Camera [0x3008, 0x42] // 休眠命令 }; - return await ConfigureRegisters(sleepRegisters, customDelayMs: 50); + return await ConfigureRegisters(sleepRegisters, customDelayMs: 50); } /// @@ -1305,7 +1304,7 @@ class Camera UInt16 firmwareAddr = 0x8000; var firmwareCommand = new UInt16[1 + OV5640_AF_FIRMWARE.Length]; firmwareCommand[0] = firmwareAddr; - + // 将固件数据复制到命令数组中 for (int i = 0; i < OV5640_AF_FIRMWARE.Length; i++) { @@ -1425,7 +1424,7 @@ class Camera logger.Error($"自动对焦超时,状态: 0x{readResult.Value:X2}"); return new(new Exception($"自动对焦超时,状态: 0x{readResult.Value:X2}")); } - + await Task.Delay(100); } diff --git a/server/src/Peripherals/DebuggerClient.cs b/server/src/Peripherals/DebuggerClient.cs index c85da01..609b150 100644 --- a/server/src/Peripherals/DebuggerClient.cs +++ b/server/src/Peripherals/DebuggerClient.cs @@ -55,28 +55,28 @@ class DebuggerCmd public const UInt32 ClearSignal = 0xFFFF_FFFF; } -/// +/// /// 信号捕获模式枚举 /// public enum CaptureMode : byte { - /// + /// /// 无捕获模式 /// None = 0, - /// + /// /// 低电平触发模式 /// Logic0 = 1, - /// + /// /// 高电平触发模式 /// Logic1 = 2, - /// + /// /// 上升沿触发模式 /// Rise = 3, - /// + /// /// 下降沿触发模式 /// Fall = 4, @@ -170,7 +170,7 @@ public class DebuggerClient /// 操作结果,成功返回状态标志字节,失败返回错误信息 public async ValueTask> ReadFlag() { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, DebuggerAddr.Signal, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, DebuggerAddr.Signal, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to read flag: {ret.Error}"); diff --git a/server/src/Peripherals/I2cClient.cs b/server/src/Peripherals/I2cClient.cs index 878e891..7ea28f4 100644 --- a/server/src/Peripherals/I2cClient.cs +++ b/server/src/Peripherals/I2cClient.cs @@ -8,8 +8,8 @@ static class I2cAddr const UInt32 Base = 0x6000_0000; - /// - /// 0x0000_0000: + /// + /// 0x0000_0000: /// [7:0] 本次传输的i2c地址(最高位总为0); /// [8] 1为读,0为写; /// [16] 1为SCCB协议,0为I2C协议; @@ -17,45 +17,45 @@ static class I2cAddr /// public const UInt32 BaseConfig = Base + 0x0000_0000; - /// + /// /// 0x0000_0001: /// [15:0] 本次传输的数据量(以字节为单位,0为传1个字节); /// [31:16] 若本次传输为读的DUMMY数据量(字节为单位,0为传1个字节) /// public const UInt32 TranConfig = Base + 0x0000_0001; - /// + /// /// 0x0000_0002: [0] cmd_done; [8] cmd_error; /// public const UInt32 Flag = Base + 0x0000_0002; - /// + /// /// 0x0000_0003: FIFO写入口,仅低8位有效,只写 /// public const UInt32 Write = Base + 0x0000_0003; - /// + /// /// 0x0000_0004: FIFO读出口,仅低8位有效,只读 /// public const UInt32 Read = Base + 0x0000_0004; - /// + /// /// 0x0000_0005: [0] FIFO写入口清空;[8] FIFO读出口清空; /// public const UInt32 Clear = Base + 0x0000_0005; } -/// +/// /// [TODO:Enum] /// public enum I2cProtocol { - /// + /// /// [TODO:Enum] /// I2c = 0, - /// + /// /// [TODO:Enum] /// SCCB = 1 @@ -296,7 +296,7 @@ public class I2c // 读取数据 { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, I2cAddr.Read); + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, I2cAddr.Read); if (!ret.IsSuccessful) { logger.Error($"Failed to read data from I2C FIFO: {ret.Error}"); diff --git a/server/src/Peripherals/JpegClient.cs b/server/src/Peripherals/JpegClient.cs new file mode 100644 index 0000000..870c441 --- /dev/null +++ b/server/src/Peripherals/JpegClient.cs @@ -0,0 +1,281 @@ +using System.Net; +using DotNext; +using Common; + +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 class JpegInfo +{ + public UInt32 Width { get; set; } + public UInt32 Height { get; set; } + public UInt32 Size { get; set; } + + public JpegInfo(UInt32 width, UInt32 height, UInt32 size) + { + Width = width; + Height = height; + Size = size; + } + + public JpegInfo(byte[] data) + { + if (data.Length < 8) + throw new ArgumentException("Invalid data length", nameof(data)); + + Width = ((UInt32)(data[5] << 8 + data[6] & 0xF0)); + Height = ((UInt32)((data[6] & 0x0F) << 4 + data[7])); + Size = Number.BytesToUInt32(data, 0, 4).Value; + } +} + +public enum JpegSampleRate : UInt32 +{ + RATE_1_1 = 0b1111_1111_1111_1111_1111_1111_1111_1111, + RATE_1_2 = 0b1010_1010_1010_1010_1010_1010_1010_1010, + RATE_1_4 = 0b1000_1000_1000_1000_1000_1000_1000_1000, + RATE_3_4 = 0b1110_1110_1110_1110_1110_1110_1110_1110, + RATE_1_8 = 0b1000_0000_1000_0000_1000_0000_1000_0000, + RATE_3_8 = 0b1001_0010_0100_1001_1001_0010_0100_1001, + RATE_7_8 = 0b1111_1110_1111_1110_1111_1110_1111_1110, + RATE_1_16 = 0b1000_0000_0000_0000_1000_0000_0000_0000, + RATE_3_16 = 0b1000_0100_0010_0000_1000_0100_0010_0000, + RATE_5_16 = 0b1001_0001_0010_0010_0100_0100_1000_1001, + RATE_15_16 = 0b1111_1111_1111_1110_1111_1111_1111_1110, + RATE_1_32 = 0b1000_0000_0000_0000_0000_0000_0000_0000, + RATE_31_32 = 0b1111_1111_1111_1111_1111_1111_1111_1110, +} + +public class Jpeg +{ + private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + readonly int timeout = 2000; + readonly int taskID; + readonly int port; + readonly string address; + private IPEndPoint ep; + + public Jpeg(string address, int port, int taskID, int timeout = 2000) + { + if (timeout < 0) + throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); + this.address = address; + this.taskID = taskID; + this.port = port; + this.ep = new IPEndPoint(IPAddress.Parse(address), port); + this.timeout = timeout; + } + + public async ValueTask SetEnable(bool enable) + { + var ret = await UDPClientPool.WriteAddr( + this.ep, this.taskID, JpegAddr.ENABLE, Convert.ToUInt32(enable), this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to set JPEG enable: {ret.Error}"); + return false; + } + return ret.Value; + } + + 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); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to get JPEG frame number: {ret.Error}"); + return 0; + } + return Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty()).Value; + } + + public async ValueTask>> GetFrameInfo(int num) + { + var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, JpegAddr.FRAME_INFO, num, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to get JPEG frame info: {ret.Error}"); + return new(null); + } + + var data = ret.Value.Options.Data; + if (data == null || data.Length == 0) + { + logger.Error($"Data is null or empty"); + return new(null); + } + if (data.Length != num * 2) + { + logger.Error( + $"Data length should be {num * 2} bytes, instead of {data.Length} bytes"); + return new(null); + } + + var infos = new List(); + for (int i = 0; i < num; i++) + { + infos.Add(new JpegInfo(data[i..(i + 1)])); + } + return new(infos); + } + + public async ValueTask UpdatePointer(uint cnt) + { + var ret = await UDPClientPool.WriteAddr( + this.ep, this.taskID, JpegAddr.FRAME_DATA_MAX_POINTER, cnt, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to update pointer: {ret.Error}"); + return false; + } + return ret.Value; + } + + public async ValueTask> GetFrame(uint offset, uint length) + { + if (!MsgBus.IsRunning) + { + logger.Error("Message bus is not running"); + return new(new Exception("Message bus is not running")); + } + MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port); + + var firstReadLength = (int)(Math.Min(length, JpegAddr.DDR_FRAME_DATA_MAX_ADDR - 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); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to get JPEG frame data: {ret.Error}"); + return null; + } + if (ret.Value.Length != firstReadLength) + { + logger.Error($"Data length should be {firstReadLength} bytes, instead of {ret.Value.Length} bytes"); + return null; + } + Buffer.BlockCopy(ret.Value, 0, dataBytes, 0, firstReadLength); + } + + if (secondReadLength > 0) + { + var ret = await UDPClientPool.ReadAddr4Bytes( + this.ep, this.taskID, JpegAddr.DDR_FRAME_DATA_ADDR, secondReadLength, this.timeout); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to get JPEG frame data: {ret.Error}"); + return null; + } + if (ret.Value.Length != secondReadLength) + { + logger.Error($"Data length should be {secondReadLength} bytes, instead of {ret.Value.Length} bytes"); + return null; + } + Buffer.BlockCopy(ret.Value, 0, dataBytes, firstReadLength, secondReadLength); + } + + return dataBytes; + } + + public async ValueTask> GetMultiFrames(uint offset, uint[] sizes) + { + var frames = new List(); + for (int i = 0; i < sizes.Length; i++) + { + var ret = await GetFrame(offset, sizes[i]); + if (!ret.IsSuccessful) + { + logger.Error($"Failed to get JPEG frame {i} data: {ret.Error}"); + continue; + } + if (ret.Value == null) + { + logger.Error($"Frame {i} data is null"); + continue; + } + if (ret.Value.Length != sizes[i]) + { + logger.Error( + $"Frame {i} data length should be {sizes[i]} bytes, instead of {ret.Value.Length} bytes"); + continue; + } + + frames.Add(ret.Value); + offset += sizes[i]; + } + + { + var ret = await UpdatePointer((uint)sizes.Length); + if (!ret) logger.Error($"Failed to update pointer"); + } + + return frames; + } + + public async ValueTask?>> GetMultiFrames(uint offset) + { + if (!MsgBus.IsRunning) + { + logger.Error("Message bus is not running"); + return new(new Exception("Message bus is not running")); + } + MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port); + + var frameNum = await GetFrameNumber(); + if (frameNum == 0) return null; + + List? frameSizes = null; + { + var ret = await GetFrameInfo((int)frameNum); + if (!ret.HasValue || ret.Value.Count == 0) + { + logger.Error($"Failed to get frame info"); + return null; + } + frameSizes = ret.Value.Select(x => x.Size).ToList(); + } + + var frames = await GetMultiFrames(offset, frameSizes.ToArray()); + if (frames.Count == 0) + { + logger.Error($"Failed to get frames"); + return null; + } + + return frames; + } +} diff --git a/server/src/Peripherals/LogicAnalyzerClient.cs b/server/src/Peripherals/LogicAnalyzerClient.cs index 342ff5d..c6764d9 100644 --- a/server/src/Peripherals/LogicAnalyzerClient.cs +++ b/server/src/Peripherals/LogicAnalyzerClient.cs @@ -12,7 +12,7 @@ static class AnalyzerAddr const UInt32 DMA1_BASE = 0x7000_0000; const UInt32 DDR_BASE = 0x0000_0000; - /// + /// /// 0x0000_0000 R/W [ 0] capture on: 置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零。
/// [ 8] capture force: 置1则强制捕获信号,自动置0。
/// [16] capture busy: 1为逻辑分析仪正在捕获信号。
@@ -21,7 +21,7 @@ static class AnalyzerAddr ///
public const UInt32 CAPTURE_MODE = BASE + 0x0000_0000; - /// + /// /// 0x0000_0001 R/W [1:0] global trig mode: 00: 全局与 (&)
/// 01: 全局或 (|)
/// 10: 全局非与(~&)
@@ -29,7 +29,7 @@ static class AnalyzerAddr ///
public const UInt32 GLOBAL_TRIG_MODE = BASE + 0x0000_0001; - /// + /// /// 0x0000_0010 - 0x0000_0017 R/W [5:0] 信号M的触发操作符,共8路
/// [5:3] M's Operator: 000 ==
/// 001 !=
@@ -73,7 +73,7 @@ static class AnalyzerAddr public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014; public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0100_0000; - /// + /// /// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储,得到的32位数据中低八位最先捕获,高八位最后捕获。
/// 共1024个地址,每个地址存储4组,深度为4096。
///
@@ -87,53 +87,53 @@ static class AnalyzerAddr [Flags] public enum CaptureStatus { - /// + /// /// 无状态标志 /// None = 0, - /// + /// /// 捕获使能位,置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零 /// CaptureOn = 1 << 0, // [0] 捕获使能 - /// + /// /// 强制捕获位,置1则强制捕获信号,自动置0 /// CaptureForce = 1 << 8, // [8] 强制捕获 - /// + /// /// 捕获忙碌位,1为逻辑分析仪正在捕获信号 /// CaptureBusy = 1 << 16, // [16] 捕获进行中 - /// + /// /// 捕获完成位,1为逻辑分析仪内存完整存储了此次捕获的信号 /// CaptureDone = 1 << 24 // [24] 捕获完成 } -/// +/// /// 全局触发模式枚举,定义多路信号触发条件的逻辑组合方式 /// public enum GlobalCaptureMode { - /// + /// /// 全局与模式,所有触发条件都必须满足 /// AND = 0b00, - /// + /// /// 全局或模式,任一触发条件满足即可 /// OR = 0b01, - /// + /// /// 全局非与模式,不是所有触发条件都满足 /// NAND = 0b10, - /// + /// /// 全局非或模式,所有触发条件都不满足 /// NOR = 0b11 @@ -144,32 +144,32 @@ public enum GlobalCaptureMode /// public enum AnalyzerClockDiv { - /// + /// /// 1分频 /// DIV1 = 0x0000_0000, - /// + /// /// 2分频 /// DIV2 = 0x0000_0001, - /// + /// /// 4分频 /// DIV4 = 0x0000_0002, - /// + /// /// 8分频 /// DIV8 = 0x0000_0003, - /// + /// /// 16分频 /// DIV16 = 0x0000_0004, - /// + /// /// 32分频 /// DIV32 = 0x0000_0005, @@ -190,27 +190,27 @@ public enum AnalyzerClockDiv /// public enum SignalOperator : byte { - /// + /// /// 等于操作符 /// Equal = 0b000, // == - /// + /// /// 不等于操作符 /// NotEqual = 0b001, // != - /// + /// /// 小于操作符 /// LessThan = 0b010, // < - /// + /// /// 小于等于操作符 /// LessThanOrEqual = 0b011, // <= - /// + /// /// 大于操作符 /// GreaterThan = 0b100, // > - /// + /// /// 大于等于操作符 /// GreaterThanOrEqual = 0b101 // >= @@ -221,35 +221,35 @@ public enum SignalOperator : byte /// public enum SignalValue : byte { - /// + /// /// 逻辑0电平 /// Logic0 = 0b000, // LOGIC 0 - /// + /// /// 逻辑1电平 /// Logic1 = 0b001, // LOGIC 1 - /// + /// /// 不关心该信号状态 /// NotCare = 0b010, // X(not care) - /// + /// /// 上升沿触发 /// Rise = 0b011, // RISE - /// + /// /// 下降沿触发 /// Fall = 0b100, // FALL - /// + /// /// 上升沿或下降沿触发 /// RiseOrFall = 0b101, // RISE OR FALL - /// + /// /// 信号无变化 /// NoChange = 0b110, // NOCHANGE - /// + /// /// 特定数值 /// SomeNumber = 0b111 // SOME NUMBER @@ -260,11 +260,11 @@ public enum SignalValue : byte /// public enum AnalyzerChannelDiv { - /// + /// /// 1路 /// ONE = 0x0000_0000, - /// + /// /// 2路 /// TWO = 0x0000_0001, @@ -366,7 +366,7 @@ public class Analyzer /// 操作结果,成功返回寄存器值,否则返回异常信息 public async ValueTask> ReadCaptureStatus() { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to read capture status: {ret.Error}"); diff --git a/server/src/Peripherals/OscilloscopeClient.cs b/server/src/Peripherals/OscilloscopeClient.cs index 173924e..d3055a1 100644 --- a/server/src/Peripherals/OscilloscopeClient.cs +++ b/server/src/Peripherals/OscilloscopeClient.cs @@ -9,57 +9,57 @@ static class OscilloscopeAddr { const UInt32 BASE = 0x8000_0000; - /// + /// /// 0x0000_0000:R/W[0] wave_run 启动捕获/关闭 /// public const UInt32 START_CAPTURE = BASE + 0x0000_0000; - /// + /// /// 0x0000_0001: R/W[7:0] trig_level 触发电平 /// public const UInt32 TRIG_LEVEL = BASE + 0x0000_0001; - /// + /// /// 0x0000_0002:R/W[0] trig_edge 触发边沿,0-下降沿,1-上升沿 /// public const UInt32 TRIG_EDGE = BASE + 0x0000_0002; - /// + /// /// 0x0000_0003: R/W[9:0] h shift 水平偏移量 /// public const UInt32 H_SHIFT = BASE + 0x0000_0003; - /// + /// /// 0x0000_0004: R/W[9:0] deci rate 抽样率,0—1023 /// public const UInt32 DECI_RATE = BASE + 0x0000_0004; - /// + /// /// 0x0000_0005:R/W[0] ram refresh RAM刷新 /// public const UInt32 RAM_FRESH = BASE + 0x0000_0005; - /// + /// /// 0x0000 0006:R[19: 0] ad_freq AD采样频率 /// public const UInt32 AD_FREQ = BASE + 0x0000_0006; - /// + /// /// Ox0000_0007: R[7:0] ad_vpp AD采样幅度 /// public const UInt32 AD_VPP = BASE + 0x0000_0007; - /// + /// /// 0x0000_0008: R[7:0] ad max AD采样最大值 /// public const UInt32 AD_MAX = BASE + 0x0000_0008; - /// + /// /// 0x0000_0009: R[7:0] ad_min AD采样最小值 /// public const UInt32 AD_MIN = BASE + 0x0000_0009; - /// + /// /// 0x0000_1000-0x0000_13FF:R[7:0] wave_rd_data 共1024个字节 /// public const UInt32 RD_DATA_ADDR = BASE + 0x0000_1000; @@ -232,7 +232,7 @@ class Oscilloscope /// 操作结果,成功返回采样频率值,否则返回异常信息 public async ValueTask> GetADFrequency() { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_FREQ, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_FREQ, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to read AD frequency: {ret.Error}"); @@ -255,7 +255,7 @@ class Oscilloscope /// 操作结果,成功返回采样幅度值,否则返回异常信息 public async ValueTask> GetADVpp() { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_VPP, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_VPP, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to read AD VPP: {ret.Error}"); @@ -275,7 +275,7 @@ class Oscilloscope /// 操作结果,成功返回采样最大值,否则返回异常信息 public async ValueTask> GetADMax() { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_MAX, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_MAX, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to read AD max: {ret.Error}"); @@ -295,7 +295,7 @@ class Oscilloscope /// 操作结果,成功返回采样最小值,否则返回异常信息 public async ValueTask> GetADMin() { - var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_MIN, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_MIN, this.timeout); if (!ret.IsSuccessful) { logger.Error($"Failed to read AD min: {ret.Error}"); diff --git a/server/src/Peripherals/RemoteUpdateClient.cs b/server/src/Peripherals/RemoteUpdateClient.cs index 296afb8..922f3e2 100644 --- a/server/src/Peripherals/RemoteUpdateClient.cs +++ b/server/src/Peripherals/RemoteUpdateClient.cs @@ -7,20 +7,20 @@ static class RemoteUpdaterAddr { public const UInt32 Base = 0x20_00_00_00; - /// + /// /// ADDR: 0X00: 写Flash-读写地址——控制位
/// [31:16]: wr_sector_num
/// [15: 0]: {flash_wr_en,-,-,-, start_wr_sector}
///
public const UInt32 WriteCtrl = Base + 0x00; - /// + /// /// ADDR: 0X01: 写Flash-只写地址——FIFO入口
/// [31:0]: 写比特流数据入口
///
public const UInt32 WriteFIFO = Base + 0x01; - /// + /// /// ADDR: 0X02: 写Flash-只读地址——标志位
/// [31:24]: {-, -, -, -, -, -, -, wr_fifo_full}
/// [23:16]: {-, -, -, -, -, -, -, wr_fifo_empty}
@@ -29,14 +29,14 @@ static class RemoteUpdaterAddr ///
public const UInt32 WriteSign = Base + 0x02; - /// + /// /// ADDR: 0X03: 读Flash-读写地址——控制位1
/// [31:16]: rd_sector_num
/// [15: 0]: {flash_rd_en,-,-,-, start_rd_sub_sector}
///
public const UInt32 ReadCtrl1 = Base + 0x03; - /// + /// /// ADDR: 0X04: 读Flash-读写地址——控制位2
/// [31:24]: { }
/// [23:16]: {-, -, -, -, -, -,{ bs_crc32_ok }}
@@ -45,19 +45,19 @@ static class RemoteUpdaterAddr ///
public const UInt32 ReadCtrl2 = Base + 0x04; - /// + /// /// ADDR: 0X05: 读Flash-只读地址——FIFO出口
/// [31:0]: 读比特流数据出口
///
public const UInt32 ReadFIFO = Base + 0x05; - /// + /// /// ADDR: 0X06: 读Flash-只读地址——CRC校验值
/// [31:0]: CRC校验值 bs_readback_crc
///
public const UInt32 ReadCRC = Base + 0x06; - /// + /// /// ADDR: 0X07: 读Flash-只读地址——标志位
/// [31:24]: {-, -, -, -, -, -, -, rd_fifo_afull}
/// [23:16]: {-, -, -, -, -, -, -, rd_fifo_empty}
@@ -66,14 +66,14 @@ static class RemoteUpdaterAddr ///
public const UInt32 ReadSign = Base + 0x07; - /// + /// /// ADDR: 0X08: 热启动开关-读写地址——控制位
/// [31: 8]: hotreset_addr
/// [ 7: 0]: {-, -, -, -, -, -, -, hotreset_en}
///
public const UInt32 HotResetCtrl = Base + 0x08; - /// + /// /// ADDR: 0X09: 只读地址 版本号
/// [31: 0]: FPGA_VERSION[31:0]
///
@@ -339,7 +339,7 @@ public class RemoteUpdater } { - var ret = await UDPClientPool.ReadAddr(this.ep, 0, RemoteUpdaterAddr.ReadCRC, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, 0, RemoteUpdaterAddr.ReadCRC, this.timeout); if (!ret.IsSuccessful) return new(ret.Error); var bytes = ret.Value.Options.Data; @@ -543,7 +543,7 @@ public class RemoteUpdater logger.Trace("Clear udp data finished"); { - var ret = await UDPClientPool.ReadAddr(this.ep, 0, RemoteUpdaterAddr.Version, this.timeout); + var ret = await UDPClientPool.ReadAddrByte(this.ep, 0, RemoteUpdaterAddr.Version, this.timeout); if (!ret.IsSuccessful) return new(ret.Error); var retData = ret.Value.Options.Data; diff --git a/server/src/Services/HttpHdmiVideoStreamService.cs b/server/src/Services/HttpHdmiVideoStreamService.cs index a19aacf..d6475cf 100644 --- a/server/src/Services/HttpHdmiVideoStreamService.cs +++ b/server/src/Services/HttpHdmiVideoStreamService.cs @@ -23,7 +23,7 @@ public class HttpHdmiVideoStreamService : BackgroundService public override async Task StartAsync(CancellationToken cancellationToken) { _httpListener = new HttpListener(); - _httpListener.Prefixes.Add($"http://*:{_serverPort}/"); + _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/"); _httpListener.Start(); logger.Info($"HDMI Video Stream Service started on port {_serverPort}"); diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs index 16dbd34..f06967e 100644 --- a/server/src/Services/HttpVideoStreamService.cs +++ b/server/src/Services/HttpVideoStreamService.cs @@ -1,6 +1,8 @@ using System.Net; using System.Text; -using Peripherals.CameraClient; // 添加摄像头客户端引用 +using System.Collections.Concurrent; +using DotNext; +using DotNext.Threading; #if USB_CAMERA using OpenCvSharp; @@ -8,30 +10,47 @@ using OpenCvSharp; namespace server.Services; +public class VideoStreamClient +{ + public string? ClientId { get; set; } = string.Empty; + public int FrameWidth { get; set; } + public int FrameHeight { get; set; } + public int FrameRate { get; set; } + public Peripherals.CameraClient.Camera Camera { get; set; } + public CancellationTokenSource CTS { get; set; } + public readonly AsyncReaderWriterLock Lock = new(); + + public VideoStreamClient( + string clientId, int width, int height, Peripherals.CameraClient.Camera camera) + { + ClientId = clientId; + FrameWidth = width; + FrameHeight = height; + FrameRate = 0; + Camera = camera; + CTS = new CancellationTokenSource(); + } +} + /// /// 表示摄像头连接状态信息 /// -public class CameraStatus +public class VideoEndpoint { - /// - /// 摄像头的IP地址 - /// - public string Address { get; set; } = string.Empty; + public string BoardId { get; set; } = ""; + public string MjpegUrl { get; set; } = ""; + public string VideoUrl { get; set; } = ""; + public string SnapshotUrl { get; set; } = ""; /// - /// 摄像头的端口号 + /// 视频流的帧率(FPS) /// - public int Port { get; set; } + public int FrameRate { get; set; } /// - /// 是否已配置摄像头 + /// 视频分辨率(如 640x480) /// - public bool IsConfigured { get; set; } - - /// - /// 摄像头连接字符串(IP:端口) - /// - public string ConnectionString { get; set; } = string.Empty; + public string Resolution { get; set; } = string.Empty; } /// @@ -50,29 +69,14 @@ public class ServiceStatus public int ServerPort { get; set; } /// - /// 视频流的帧率(FPS) + /// 当前连接的客户端端点列表 /// - public int FrameRate { get; set; } - - /// - /// 视频分辨率(如 640x480) - /// - public string Resolution { get; set; } = string.Empty; + public List ClientEndpoints { get; set; } = new(); /// /// 当前连接的客户端数量 /// - public int ConnectedClients { get; set; } - - /// - /// 当前连接的客户端端点列表 - /// - public List ClientEndpoints { get; set; } = new(); - - /// - /// 摄像头连接状态信息 - /// - public CameraStatus CameraStatus { get; set; } = new(); + public int ConnectedClientsNum => ClientEndpoints.Count; } /// @@ -82,21 +86,11 @@ public class ServiceStatus public class HttpVideoStreamService : BackgroundService { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private HttpListener? _httpListener; private readonly int _serverPort = 4321; - private readonly int _frameRate = 30; // 30 FPS - // 动态分辨率配置 - private int _frameWidth = 640; // 默认640x480 - private int _frameHeight = 480; - private readonly object _resolutionLock = new object(); - - // 摄像头客户端 - private Camera? _camera; - private bool _cameraEnable = false; - private string _cameraAddress = "192.168.1.100"; // 默认FPGA地址 - private int _cameraPort = 8888; // 默认端口 - private readonly object _cameraLock = new object(); + private readonly ConcurrentDictionary _clientDict = new(); // USB Camera 相关 #if USB_CAMERA @@ -105,192 +99,83 @@ public class HttpVideoStreamService : BackgroundService private readonly object _usbCameraLock = new object(); #endif - // 模拟 FPGA 图像数据 - private int _frameCounter = 0; - private readonly List _activeClients = new List(); - private readonly object _clientsLock = new object(); - - /// - /// 获取当前连接的客户端数量 - /// - public int ConnectedClientsCount { get { return _activeClients.Count; } } - - /// - /// 获取服务端口 - /// - public int ServerPort => _serverPort; - - /// - /// 获取帧宽度 - /// - public int FrameWidth => _frameWidth; - - /// - /// 获取帧高度 - /// - public int FrameHeight => _frameHeight; - - /// - /// 获取帧率 - /// - public int FrameRate => _frameRate; - - /// - /// 获取当前摄像头地址 - /// - public string CameraAddress { get { return _cameraAddress; } } - - /// - /// 获取当前摄像头端口 - /// - public int CameraPort { get { return _cameraPort; } } - /// /// 初始化 HttpVideoStreamService /// - public HttpVideoStreamService() + public override async Task StartAsync(CancellationToken cancellationToken) { - // 延迟初始化摄像头客户端,直到配置完成 - logger.Info("HttpVideoStreamService 初始化完成,默认摄像头地址: {Address}:{Port}", _cameraAddress, _cameraPort); + _httpListener = new HttpListener(); + _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/"); + _httpListener.Start(); + logger.Info($"Video Stream Service started on port {_serverPort}"); + + await base.StartAsync(cancellationToken); } /// - /// [TODO:description] + /// 停止 HTTP 视频流服务 /// - /// [TODO:parameter] - /// [TODO:return] - public async Task SetEnable(bool isEnabled) + public override async Task StopAsync(CancellationToken cancellationToken) { - if (_camera == null) + foreach (var clientKey in _clientDict.Keys) { - throw new Exception("Please config camera first"); + var client = _clientDict[clientKey]; + client.CTS.Cancel(); + using (await client.Lock.AcquireWriteLockAsync(cancellationToken)) + { + await client.Camera.EnableHardwareTrans(false); + } } - _cameraEnable = isEnabled; - // if (_cameraEnable) await _camera.WakeUp(); - // else await _camera.Sleep(); - await _camera.EnableHardwareTrans(_cameraEnable); + _clientDict.Clear(); + await base.StopAsync(cancellationToken); } - /// - /// 配置摄像头连接参数 - /// - /// 摄像头IP地址 - /// 摄像头端口 - /// 配置是否成功 - public async Task ConfigureCameraAsync(string address, int port) + private Optional TryGetClient(string boardId) { - if (string.IsNullOrWhiteSpace(address)) + if (_clientDict.TryGetValue(boardId, out var client)) { - logger.Error("摄像头地址不能为空"); - return false; - } - - if (port <= 0 || port > 65535) - { - logger.Error("摄像头端口必须在1-65535范围内"); - return false; - } - - try - { - lock (_cameraLock) - { - // 关闭现有连接 - if (_camera != null) - { - logger.Info("关闭现有摄像头连接"); - // Camera doesn't have Dispose method, set to null - _camera = null; - } - - // 更新配置 - _cameraAddress = address; - _cameraPort = port; - - // 创建新的摄像头客户端 - _camera = new Camera(_cameraAddress, _cameraPort); - - logger.Info("摄像头配置已更新: {Address}:{Port}", _cameraAddress, _cameraPort); - } - - // Init Camera - { - var ret = await _camera.Init(); - if (!ret.IsSuccessful) - { - logger.Error(ret.Error); - throw ret.Error; - } - - if (!ret.Value) - { - logger.Error($"Camera Init Failed!"); - throw new Exception($"Camera Init Failed!"); - } - } - return true; - } - catch (Exception ex) - { - logger.Error(ex, "配置摄像头连接时发生错误"); - return false; + return client; } + return null; } - /// - /// 测试摄像头连接 - /// - /// 连接测试结果 - public async Task<(bool IsSuccess, string Message)> TestCameraConnectionAsync() + private async Task GetOrCreateClientAsync(string boardId, int initWidth, int initHeight) { - try + if (_clientDict.TryGetValue(boardId, out var client)) { - Camera? testCamera = null; - - lock (_cameraLock) - { - if (_camera == null) - { - return (false, "摄像头未配置"); - } - testCamera = _camera; - } - - // 尝试读取一帧数据来测试连接 - var result = await testCamera.ReadFrame(); - - if (result.IsSuccessful) - { - logger.Info("摄像头连接测试成功: {Address}:{Port}", _cameraAddress, _cameraPort); - return (true, "连接成功"); - } - else - { - logger.Warn("摄像头连接测试失败: {Error}", result.Error); - return (false, result.Error.ToString()); - } + // 可在此处做分辨率/Camera等配置更新 + return client; } - catch (Exception ex) + + var db = new Database.AppDataConnection(); + if (db == null) { - logger.Error(ex, "摄像头连接测试出错"); - return (false, ex.Message); + logger.Error("Failed to create HdmiIn instance"); + return null; } + + var boardRet = db.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; + + var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port); + var ret = await camera.Init(); + if (!ret.IsSuccessful || !ret.Value) + { + logger.Error("Camera Init Failed!"); + return null; + } + + client = new VideoStreamClient(boardId, initWidth, initHeight, camera); + _clientDict[boardId] = client; + return client; } - /// - /// 获取摄像头连接状态 - /// - /// 连接状态信息 - public CameraStatus GetCameraStatus() - { - return new CameraStatus - { - Address = _cameraAddress, - Port = _cameraPort, - IsConfigured = _camera != null, - ConnectionString = $"{_cameraAddress}:{_cameraPort}" - }; - } /// /// 执行 HTTP 视频流服务 @@ -299,106 +184,95 @@ public class HttpVideoStreamService : BackgroundService /// 任务 protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - try - { - logger.Info("启动 HTTP 视频流服务,端口: {Port}", _serverPort); - - // 初始化默认摄像头连接 - await ConfigureCameraAsync(_cameraAddress, _cameraPort); - - // 创建 HTTP 监听器 - _httpListener = new HttpListener(); - _httpListener.Prefixes.Add($"http://{Global.localhost}:{_serverPort}/"); - _httpListener.Start(); - - logger.Info("HTTP 视频流服务已启动,监听端口: {Port}", _serverPort); - - // 开始接受客户端连接 - _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken); - - // 开始生成视频帧 - while (!stoppingToken.IsCancellationRequested) - { - if (_cameraEnable) - { - await GenerateVideoFrames(stoppingToken); - } - else - { - await Task.Delay(500, stoppingToken); - } - } - } - catch (HttpListenerException ex) - { - logger.Error(ex, "HTTP 视频流服务启动失败,请确保您有管理员权限或使用netsh配置URL前缀权限"); - } - catch (Exception ex) - { - logger.Error(ex, "HTTP 视频流服务启动失败"); - } - } - - private async Task AcceptClientsAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested && _httpListener != null && _httpListener.IsListening) + while (!stoppingToken.IsCancellationRequested) { + if (_httpListener == null) continue; try { - // 等待客户端连接 - var context = await _httpListener.GetContextAsync(); - var request = context.Request; - var response = context.Response; - - logger.Info("新HTTP客户端连接: {RemoteEndPoint}", request.RemoteEndPoint); - // 处理不同的请求路径 - var requestPath = request.Url?.AbsolutePath ?? "/"; - - if (requestPath == "/video-stream") + logger.Debug("Waiting for HTTP request..."); + var contextTask = _httpListener.GetContextAsync(); + var completedTask = await Task.WhenAny(contextTask, Task.Delay(-1, stoppingToken)); + if (completedTask == contextTask) { - // MJPEG 流请求(FPGA) - _ = Task.Run(() => HandleMjpegStreamAsync(response, cancellationToken), cancellationToken); - } -#if USB_CAMERA - else if (requestPath == "/usb-camera") - { - // USB Camera MJPEG流请求 - _ = Task.Run(() => HandleUsbCameraStreamAsync(response, cancellationToken), cancellationToken); - } -#endif - else if (requestPath == "/snapshot") - { - // 单帧图像请求 - await HandleSnapshotRequestAsync(response, cancellationToken); - } - else if (requestPath == "/video-feed.html") - { - // HTML页面请求 - await SendVideoHtmlPageAsync(response); + var context = contextTask.Result; + logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}"); + if (context != null) + _ = HandleRequestAsync(context, stoppingToken); } else { - // 默认返回简单的HTML页面,提供链接到视频页面 - await SendIndexHtmlPageAsync(response); + break; } } - catch (HttpListenerException) - { - // HTTP监听器可能已停止 - break; - } - catch (ObjectDisposedException) - { - // 对象可能已被释放 - break; - } catch (Exception ex) { - logger.Error(ex, "接受HTTP客户端连接时发生错误"); + logger.Error(ex, "Error in GetContextAsync"); + break; } } } + private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken) + { + var path = context.Request.Url?.AbsolutePath ?? "/"; + var boardId = context.Request.QueryString["board"]; + var width = int.TryParse(context.Request.QueryString["width"], out var w) ? w : 640; + var height = int.TryParse(context.Request.QueryString["height"], out var h) ? h : 480; + + if (string.IsNullOrEmpty(boardId)) + { + await SendErrorAsync(context.Response, "Missing clientId"); + return; + } + + var client = await GetOrCreateClientAsync(boardId, width, height); + if (client == null) + { + await SendErrorAsync(context.Response, "Invalid clientId or camera not available"); + return; + } + + var clientToken = client.CTS.Token; + try + { + logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint); + + if (path == "/video-stream") + { + // MJPEG 流请求(FPGA) + await HandleMjpegStreamAsync(context.Response, client, cancellationToken); + } +#if USB_CAMERA + else if (requestPath == "/usb-camera") + { + // USB Camera MJPEG流请求 + await HandleUsbCameraStreamAsync(response, cancellationToken); + } +#endif + else if (path == "/snapshot") + { + // 单帧图像请求 + await HandleSnapshotRequestAsync(context.Response, client, cancellationToken); + } + else + { + // 默认返回简单的HTML页面,提供链接到视频页面 + await SendIndexHtmlPageAsync(context.Response); + } + } + catch (Exception ex) + { + logger.Error(ex, "接受HTTP客户端连接时发生错误"); + } + } + + private async Task SendErrorAsync(HttpListenerResponse response, string message) + { + response.StatusCode = 400; + await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message)); + response.Close(); + } + // USB Camera MJPEG流处理 #if USB_CAMERA private async Task HandleUsbCameraStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken) @@ -480,107 +354,54 @@ public class HttpVideoStreamService : BackgroundService } #endif - private async Task HandleMjpegStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken) + private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken) { - try + // 读取 Camera 快照,返回 JPEG + var frameResult = await client.Camera.ReadFrame(); + if (!frameResult.IsSuccessful || frameResult.Value == null) { - // 设置MJPEG流的响应头 - response.ContentType = "multipart/x-mixed-replace; boundary=--boundary"; - response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); - response.Headers.Add("Pragma", "no-cache"); - response.Headers.Add("Expires", "0"); - - // 跟踪活跃的客户端 - lock (_clientsLock) - { - _activeClients.Add(response); - } - - logger.Debug("已启动MJPEG流,客户端: {RemoteEndPoint}", response.OutputStream?.GetHashCode() ?? 0); - - // 保持连接直到取消或出错 - try - { - while (!cancellationToken.IsCancellationRequested) - { - await Task.Delay(100, cancellationToken); // 简单的保活循环 - } - } - catch (TaskCanceledException) - { - // 预期的取消 - } - - logger.Debug("MJPEG流已结束,客户端: {ClientId}", response.OutputStream?.GetHashCode() ?? 0); + response.StatusCode = 500; + await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("Failed to get snapshot")); + response.Close(); + return; } - catch (Exception ex) + var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80); + if (!jpegResult.IsSuccessful) { - logger.Error(ex, "处理MJPEG流时出错"); - } - finally - { - lock (_clientsLock) - { - _activeClients.Remove(response); - } - - try - { - response.Close(); - } - catch - { - // 忽略关闭时的错误 - } + response.StatusCode = 500; + await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("JPEG conversion failed")); + response.Close(); + return; } + response.ContentType = "image/jpeg"; + response.ContentLength64 = jpegResult.Value.Length; + await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken); + response.Close(); } - private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, CancellationToken cancellationToken) + private async Task HandleMjpegStreamAsync(HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken) { - try + response.ContentType = "multipart/x-mixed-replace; boundary=--boundary"; + response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); + response.Headers.Add("Pragma", "no-cache"); + response.Headers.Add("Expires", "0"); + + while (!cancellationToken.IsCancellationRequested) { - // 获取当前帧 - var imageData = await GetFPGAImageData(); + var frameResult = await client.Camera.ReadFrame(); + if (!frameResult.IsSuccessful || frameResult.Value == null) continue; + var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80); + if (!jpegResult.IsSuccessful) continue; - // 获取当前分辨率 - int currentWidth, currentHeight; - lock (_resolutionLock) - { - currentWidth = _frameWidth; - currentHeight = _frameHeight; - } - - // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换 - var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, currentWidth, currentHeight, 80); - if (!jpegResult.IsSuccessful) - { - logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); - response.StatusCode = 500; - response.Close(); - return; - } - - var jpegData = jpegResult.Value; - - // 设置响应头 - response.ContentType = "image/jpeg"; - response.ContentLength64 = jpegData.Length; - response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); - - // 发送JPEG数据 - await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); + var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegResult.Value.Length + "\r\n\r\n"); + await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken); + await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken); + await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); await response.OutputStream.FlushAsync(cancellationToken); - logger.Debug("已发送快照图像,大小:{Size} 字节", jpegData.Length); - } - catch (Exception ex) - { - logger.Error(ex, "处理快照请求时出错"); - } - finally - { - response.Close(); + await Task.Delay(1000 / client.FrameWidth, cancellationToken); } + response.Close(); } private async Task SendVideoHtmlPageAsync(HttpListenerResponse response) @@ -667,147 +488,52 @@ public class HttpVideoStreamService : BackgroundService response.Close(); } - private async Task GenerateVideoFrames(CancellationToken cancellationToken) - { - var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate); - var lastFrameTime = DateTime.UtcNow; - - while (!cancellationToken.IsCancellationRequested && _cameraEnable) - { - try - { - var frameStartTime = DateTime.UtcNow; - - // 从 FPGA 获取图像数据 - var imageData = await GetFPGAImageData(); - - var imageAcquireTime = DateTime.UtcNow; - - // 如果有图像数据,立即开始广播(不等待) - if (imageData != null && imageData.Length > 0) - { - // 异步广播帧,不阻塞下一帧的获取 - _ = Task.Run(async () => - { - try - { - await BroadcastFrameAsync(imageData, cancellationToken); - } - catch (Exception ex) - { - logger.Error(ex, "异步广播帧时发生错误"); - } - }, cancellationToken); - - _frameCounter++; - - var frameEndTime = DateTime.UtcNow; - var frameProcessingTime = (frameEndTime - frameStartTime).TotalMilliseconds; - var imageAcquireElapsed = (imageAcquireTime - frameStartTime).TotalMilliseconds; - - if (_frameCounter % 30 == 0) // 每秒记录一次性能信息 - { - logger.Debug("帧 {FrameNumber} 性能统计 - 图像获取: {AcquireTime:F1}ms, 总处理: {ProcessTime:F1}ms", - _frameCounter, imageAcquireElapsed, frameProcessingTime); - } - } - - // 动态调整延迟 - 基于实际处理时间 - var elapsed = (DateTime.UtcNow - lastFrameTime).TotalMilliseconds; - var targetInterval = frameInterval.TotalMilliseconds; - var remainingDelay = Math.Max(0, targetInterval - elapsed); - - if (remainingDelay > 0) - { - await Task.Delay(TimeSpan.FromMilliseconds(remainingDelay), cancellationToken); - } - - lastFrameTime = DateTime.UtcNow; - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - logger.Error(ex, "生成视频帧时发生错误"); - await Task.Delay(100, cancellationToken); // 减少错误恢复延迟 - } - } - } - /// /// 从 FPGA 获取图像数据 /// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24 /// - private async Task GetFPGAImageData() + private async Task GetFPGAImageData( + VideoStreamClient client, CancellationToken cancellationToken = default) { - var startTime = DateTime.UtcNow; - Camera? currentCamera = null; - - lock (_cameraLock) - { - currentCamera = _camera; - } - - if (currentCamera == null) - { - logger.Error("摄像头客户端未初始化"); - return new byte[0]; - } - try { - // 获取当前分辨率 - int currentWidth, currentHeight; - lock (_resolutionLock) + using (await client.Lock.AcquireWriteLockAsync(cancellationToken)) { - currentWidth = _frameWidth; - currentHeight = _frameHeight; + // 从摄像头读取帧数据 + var readStartTime = DateTime.UtcNow; + var result = await client.Camera.ReadFrame(); + var readEndTime = DateTime.UtcNow; + var readTime = (readEndTime - readStartTime).TotalMilliseconds; + + if (!result.IsSuccessful) + { + logger.Error("读取摄像头帧数据失败: {Error}", result.Error); + return new byte[0]; + } + + var rgb565Data = result.Value; + + // 验证数据长度是否正确 + if (!Common.Image.ValidateImageDataLength(rgb565Data, client.FrameWidth, client.FrameHeight, 2)) + { + logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}", + client.FrameWidth * client.FrameHeight * 2, rgb565Data.Length); + } + + // 将 RGB565 转换为 RGB24 + var convertStartTime = DateTime.UtcNow; + var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, client.FrameWidth, client.FrameHeight, isLittleEndian: false); + var convertEndTime = DateTime.UtcNow; + var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds; + + if (!rgb24Result.IsSuccessful) + { + logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error); + return new byte[0]; + } + + return rgb24Result.Value; } - - // 从摄像头读取帧数据 - var readStartTime = DateTime.UtcNow; - var result = await currentCamera.ReadFrame(); - var readEndTime = DateTime.UtcNow; - var readTime = (readEndTime - readStartTime).TotalMilliseconds; - - if (!result.IsSuccessful) - { - logger.Error("读取摄像头帧数据失败: {Error}", result.Error); - return new byte[0]; - } - - var rgb565Data = result.Value; - - // 验证数据长度是否正确 - if (!Common.Image.ValidateImageDataLength(rgb565Data, currentWidth, currentHeight, 2)) - { - logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}", - currentWidth * currentHeight * 2, rgb565Data.Length); - } - - // 将 RGB565 转换为 RGB24 - var convertStartTime = DateTime.UtcNow; - var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, currentWidth, currentHeight, isLittleEndian: false); - var convertEndTime = DateTime.UtcNow; - var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds; - - if (!rgb24Result.IsSuccessful) - { - logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error); - return new byte[0]; - } - - var totalTime = (DateTime.UtcNow - startTime).TotalMilliseconds; - - if (_frameCounter % 30 == 0) // 每秒更新一次日志 - { - logger.Debug("帧 {FrameNumber} 数据获取性能 - 读取: {ReadTime:F1}ms, 转换: {ConvertTime:F1}ms, 总计: {TotalTime:F1}ms, RGB565: {RGB565Size} 字节, RGB24: {RGB24Size} 字节", - _frameCounter, readTime, convertTime, totalTime, rgb565Data.Length, rgb24Result.Value.Length); - } - - return rgb24Result.Value; } catch (Exception ex) { @@ -816,282 +542,61 @@ public class HttpVideoStreamService : BackgroundService } } - /// - /// 向所有连接的客户端广播帧数据 - /// - private async Task BroadcastFrameAsync(byte[] frameData, CancellationToken cancellationToken) - { - if (frameData == null || frameData.Length == 0) - { - logger.Warn("尝试广播空帧数据"); - return; - } - - // 获取当前分辨率 - int currentWidth, currentHeight; - lock (_resolutionLock) - { - currentWidth = _frameWidth; - currentHeight = _frameHeight; - } - - // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换 - var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, currentWidth, currentHeight, 80); - if (!jpegResult.IsSuccessful) - { - logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); - return; - } - - var jpegData = jpegResult.Value; - - // 使用Common中的方法准备MJPEG帧数据 - var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length); - var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter(); - - var clientsToRemove = new List(); - var clientsToProcess = new List(); - - // 获取当前连接的客户端列表 - lock (_clientsLock) - { - clientsToProcess.AddRange(_activeClients); - } - - if (clientsToProcess.Count == 0) - { - return; // 没有活跃客户端 - } - - // 向每个活跃的客户端并行发送帧 - var sendTasks = clientsToProcess.Select(async client => - { - try - { - // 发送帧头部 - await client.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken); - - // 发送JPEG数据 - await client.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); - - // 发送结尾换行符 - await client.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken); - - // 确保数据立即发送 - await client.OutputStream.FlushAsync(cancellationToken); - - return (client, success: true, error: (Exception?)null); - } - catch (Exception ex) - { - return (client, success: false, error: ex); - } - }); - - // 等待所有发送任务完成 - var results = await Task.WhenAll(sendTasks); - - // 处理发送结果 - foreach (var (client, success, error) in results) - { - if (!success) - { - logger.Debug("发送帧数据时出错: {Error}", error?.Message ?? "未知错误"); - clientsToRemove.Add(client); - } - } - - if (_frameCounter % 30 == 0 && clientsToProcess.Count > 0) // 每秒记录一次日志 - { - logger.Debug("已向 {ClientCount} 个客户端发送第 {FrameNumber} 帧,大小:{Size} 字节", - clientsToProcess.Count, _frameCounter, jpegData.Length); - } - - // 移除断开连接的客户端 - if (clientsToRemove.Count > 0) - { - lock (_clientsLock) - { - foreach (var client in clientsToRemove) - { - _activeClients.Remove(client); - try { client.Close(); } - catch { /* 忽略关闭错误 */ } - } - } - - logger.Info("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}", - clientsToRemove.Count, _activeClients.Count); - } - } - - /// - /// 获取连接的客户端端点列表 - /// - public List GetConnectedClientEndpoints() - { - List endpoints = new List(); - - lock (_clientsLock) - { - foreach (var client in _activeClients) - { - endpoints.Add($"Client-{client.OutputStream?.GetHashCode() ?? 0}"); - } - } - - return endpoints; - } - - /// - /// 获取服务状态信息 - /// - public ServiceStatus GetServiceStatus() - { - var cameraStatus = GetCameraStatus(); - - return new ServiceStatus - { - IsRunning = (_httpListener?.IsListening ?? false) && _cameraEnable, - ServerPort = _serverPort, - FrameRate = _frameRate, - Resolution = $"{_frameWidth}x{_frameHeight}", - ConnectedClients = ConnectedClientsCount, - ClientEndpoints = GetConnectedClientEndpoints(), - CameraStatus = cameraStatus - }; - } - - - /// - /// 停止 HTTP 视频流服务 - /// - public override async Task StopAsync(CancellationToken cancellationToken) - { - logger.Info("正在停止 HTTP 视频流服务..."); - - _cameraEnable = false; - - if (_httpListener != null && _httpListener.IsListening) - { - _httpListener.Stop(); - _httpListener.Close(); - } - - // 关闭所有客户端连接 - lock (_clientsLock) - { - foreach (var client in _activeClients) - { - try { client.Close(); } - catch { /* 忽略关闭错误 */ } - } - _activeClients.Clear(); - } - - // 关闭摄像头连接 - lock (_cameraLock) - { - _camera = null; - } - - await base.StopAsync(cancellationToken); - - logger.Info("HTTP 视频流服务已停止"); - } - /// /// 设置视频流分辨率 /// + /// 板卡ID /// 宽度 /// 高度 + /// 超时时间(毫秒) + /// 取消令牌 /// 设置结果 - public async Task<(bool IsSuccess, string Message)> SetResolutionAsync(int width, int height) + public async Task> SetResolutionAsync( + string boardId, int width, int height, + int timeout = 100, CancellationToken cancellationToken = default) { try { - logger.Info($"正在设置视频流分辨率为 {width}x{height}"); + var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); - Camera? currentCamera = null; - lock (_cameraLock) + using (await client.Lock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken)) { - currentCamera = _camera; - } + var currentCamera = client.Camera; + if (currentCamera == null) + { + var message = $"获取摄像头失败"; + logger.Error(message); + return new(new Exception(message)); + } - if (currentCamera == null) - { - var message = "摄像头未配置,无法设置分辨率"; - logger.Error(message); - return (false, message); - } + // 设置摄像头分辨率 + var ret = await currentCamera.ChangeResolution(width, height); + if (!ret.IsSuccessful) + { + var message = $"设置摄像头分辨率失败: {ret.Error}"; + logger.Error(message); + return new(new Exception(message)); + } - // 设置摄像头分辨率 - var cameraResult = await currentCamera.ChangeResolution(width, height); - if (!cameraResult.IsSuccessful) - { - var message = $"设置摄像头分辨率失败: {cameraResult.Error}"; - logger.Error(message); - return (false, message); - } + if (!ret.Value) + { + logger.Warn($"设置摄像头分辨率失败"); + return false; + } - // 更新HTTP服务的分辨率配置 - lock (_resolutionLock) - { - _frameWidth = width; - _frameHeight = height; - } + // 更新HTTP服务的分辨率配置 + client.FrameWidth = width; + client.FrameHeight = height; - var successMessage = $"视频流分辨率已成功设置为 {width}x{height}"; - logger.Info(successMessage); - return (true, successMessage); + logger.Info($"视频流分辨率已成功设置为 {width}x{height}"); + return true; + } } catch (Exception ex) { var message = $"设置分辨率时发生错误: {ex.Message}"; logger.Error(ex, message); - return (false, message); - } - } - - /// - /// 获取当前分辨率 - /// - /// 当前分辨率(宽度, 高度) - public (int Width, int Height) GetCurrentResolution() - { - lock (_resolutionLock) - { - return (_frameWidth, _frameHeight); - } - } - - /// - /// 获取支持的分辨率列表 - /// - /// 支持的分辨率列表 - public List<(int Width, int Height, string Name)> GetSupportedResolutions() - { - return new List<(int, int, string)> - { - (640, 480, "640x480 (VGA)"), - (960, 540, "960x540 (qHD)"), - (1280, 720, "1280x720 (HD)"), - (1280, 960, "1280x960 (SXGA)"), - (1920, 1080, "1920x1080 (Full HD)") - }; - } - - #region 自动对焦功能 - - /// - /// 检查摄像头是否已配置 - /// - /// 是否已配置 - public bool IsCameraConfigured() - { - lock (_cameraLock) - { - return _camera != null && !string.IsNullOrEmpty(_cameraAddress); + return new(new Exception(message)); } } @@ -1099,37 +604,33 @@ public class HttpVideoStreamService : BackgroundService /// 初始化摄像头自动对焦功能 /// /// 初始化结果 - public async Task InitAutoFocusAsync() + public async Task InitAutoFocusAsync( + string boardId, int timeout = 1000, CancellationToken cancellationToken = default) { try { - lock (_cameraLock) + var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); + + using (await client.Lock.AcquireWriteLockAsync( + TimeSpan.FromMilliseconds(timeout), cancellationToken)) { - if (_camera == null) + var result = await client.Camera.InitAutoFocus(); + + if (result.IsSuccessful && result.Value) { - logger.Error("摄像头未配置,无法初始化自动对焦"); + logger.Info($"Board{boardId}摄像头自动对焦功能初始化成功"); + return true; + } + else + { + logger.Error($"Board{boardId}摄像头自动对焦功能初始化失败: {result.Error?.Message ?? "未知错误"}"); return false; } } - - logger.Info("开始初始化摄像头自动对焦功能"); - - var result = await _camera!.InitAutoFocus(); - - if (result.IsSuccessful && result.Value) - { - logger.Info("摄像头自动对焦功能初始化成功"); - return true; - } - else - { - logger.Error($"摄像头自动对焦功能初始化失败: {result.Error?.Message ?? "未知错误"}"); - return false; - } } catch (Exception ex) { - logger.Error(ex, "初始化摄像头自动对焦功能时发生异常"); + logger.Error(ex, $"Board{boardId}初始化摄像头自动对焦功能时发生异常"); return false; } } @@ -1138,40 +639,108 @@ public class HttpVideoStreamService : BackgroundService /// 执行摄像头自动对焦 /// /// 对焦结果 - public async Task PerformAutoFocusAsync() + public async Task PerformAutoFocusAsync( + string boardId, int timeout = 1000, CancellationToken cancellationToken = default) { try { - lock (_cameraLock) - { - if (_camera == null) - { - logger.Error("摄像头未配置,无法执行自动对焦"); - return false; - } - } + var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); - logger.Info("开始执行摄像头自动对焦"); + logger.Info($"Board{boardId}开始执行摄像头自动对焦"); - var result = await _camera!.PerformAutoFocus(); + var result = await client.Camera.PerformAutoFocus(); if (result.IsSuccessful && result.Value) { - logger.Info("摄像头自动对焦执行成功"); + logger.Info($"Board{boardId}摄像头自动对焦成功"); return true; } else { - logger.Error($"摄像头自动对焦执行失败: {result.Error?.Message ?? "未知错误"}"); + logger.Error($"Board{boardId}摄像头自动对焦执行失败: {result.Error?.Message ?? "未知错误"}"); return false; } } catch (Exception ex) { - logger.Error(ex, "执行摄像头自动对焦时发生异常"); + logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常"); return false; } } - #endregion + public VideoEndpoint GetVideoEndpoint(string boardId) + { + var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); + + return new VideoEndpoint + { + BoardId = boardId, + MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={boardId}", + VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={boardId}", + SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={boardId}", + Resolution = $"{client.FrameWidth}x{client.FrameHeight}", + FrameRate = client.FrameRate + }; + } + + public List GetAllVideoEndpoints() + { + var endpoints = new List(); + + foreach (var boardId in _clientDict.Keys) + endpoints.Add(GetVideoEndpoint(boardId)); + + return endpoints; + } + + public ServiceStatus GetServiceStatus() + { + return new ServiceStatus + { + IsRunning = true, + ServerPort = _serverPort, + ClientEndpoints = GetAllVideoEndpoints() + }; + } + + public async Task DisableHdmiTransmissionAsync(string boardId) + { + try + { + var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); + + using (await client.Lock.AcquireWriteLockAsync()) + { + var camera = client.Camera; + var disableResult = await camera.EnableHardwareTrans(false); + if (disableResult.IsSuccessful && disableResult.Value) + logger.Info($"Successfully disabled camera {boardId} hardware transmission"); + else + logger.Error($"Failed to disable camera {boardId} hardware transmission: {disableResult.Error}"); + } + } + catch (Exception ex) + { + logger.Error(ex, $"Exception occurred while disabling HDMI transmission for camera {boardId}"); + } + } + + public async ValueTask TestCameraConnection(string boardId) + { + try + { + var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}")); + + var imageData = await GetFPGAImageData(client); + if (imageData == null || imageData.Length == 0) + return false; + + return true; + } + catch (Exception ex) + { + logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常"); + return false; + } + } } diff --git a/server/src/Services/ProgressTrackerService.cs b/server/src/Services/ProgressTrackerService.cs index ebf9982..4fecfca 100644 --- a/server/src/Services/ProgressTrackerService.cs +++ b/server/src/Services/ProgressTrackerService.cs @@ -41,7 +41,7 @@ public class ProgressReporter : ProgressInfo, IProgress private ProgressStatus _status = ProgressStatus.Pending; private string _errorMessage; - public string TaskId { get; set; } = new Guid().ToString(); + public string TaskId { get; set; } = Guid.NewGuid().ToString(); public int ProgressPercent => _progress * 100 / MaxProgress; public ProgressStatus Status => _status; public string ErrorMessage => _errorMessage; diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index 3cc45b1..582d7d9 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -223,22 +223,28 @@ public class UDPClientPool /// IP端点(IP地址与端口) /// 任务ID /// 设备地址 + /// 数据长度(0~255) /// 超时时间(毫秒) /// 读取结果,包含接收到的数据包 public static async ValueTask> ReadAddr( - IPEndPoint endPoint, int taskID, uint devAddr, int timeout = 1000) + IPEndPoint endPoint, int taskID, uint devAddr, int dataLength, int timeout = 1000) { + if (dataLength <= 0) + return new(new ArgumentException("Data length must be greater than 0")); + + if (dataLength > 255) + return new(new ArgumentException("Data length must be less than or equal to 255")); + var ret = false; var opts = new SendAddrPackOptions() { BurstType = BurstType.FixedBurst, - BurstLength = 0, + BurstLength = ((byte)(dataLength - 1)), CommandID = Convert.ToByte(taskID), Address = devAddr, IsWrite = false, }; - // Read Register ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts)); if (!ret) return new(new Exception("Send Address Package Failed!")); @@ -260,6 +266,20 @@ public class UDPClientPool return retPack; } + /// + /// 读取设备地址数据 + /// + /// IP端点(IP地址与端口) + /// 任务ID + /// 设备地址 + /// 超时时间(毫秒) + /// 读取结果,包含接收到的数据包 + public static async ValueTask> ReadAddrByte( + IPEndPoint endPoint, int taskID, uint devAddr, int timeout = 1000) + { + return await ReadAddr(endPoint, taskID, devAddr, 0, timeout); + } + /// /// 读取设备地址数据并校验结果 /// @@ -271,11 +291,11 @@ public class UDPClientPool /// 超时时间(毫秒) /// 校验结果,true表示数据匹配期望值 public static async ValueTask> ReadAddr( - IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000) + IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000) { var address = endPoint.Address.ToString(); - var ret = await ReadAddr(endPoint, taskID, devAddr, timeout); + var ret = await ReadAddrByte(endPoint, taskID, devAddr, timeout); if (!ret.IsSuccessful) return new(ret.Error); if (!ret.Value.IsSuccessful) return new(new Exception($"Read device {address} address {devAddr} failed")); @@ -324,7 +344,7 @@ public class UDPClientPool await Task.Delay(waittime); try { - var ret = await ReadAddr(endPoint, taskID, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds)); + var ret = await ReadAddrByte(endPoint, taskID, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds)); if (!ret.IsSuccessful) return new(ret.Error); if (!ret.Value.IsSuccessful) return new(new Exception($"Read device {address} address {devAddr} failed")); @@ -555,7 +575,7 @@ public class UDPClientPool var resultData = new List(); for (int i = 0; i < length; i++) { - var ret = await ReadAddr(endPoint, taskID, addr[i], timeout); + var ret = await ReadAddrByte(endPoint, taskID, addr[i], timeout); if (!ret.IsSuccessful) { logger.Error($"ReadAddrSeq failed at index {i}: {ret.Error}"); diff --git a/src/APIClient.ts b/src/APIClient.ts index 047d49c..e553bac 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -185,12 +185,8 @@ export class VideoStreamClient { return Promise.resolve(null as any); } - /** - * 获取 HTTP 视频流信息 - * @return 流信息 - */ - getStreamInfo( cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/VideoStream/StreamInfo"; + myEndpoint( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/VideoStream/MyEndpoint"; url_ = url_.replace(/[?&]$/, ""); let options_: AxiosRequestConfig = { @@ -209,208 +205,11 @@ export class VideoStreamClient { throw _error; } }).then((_response: AxiosResponse) => { - return this.processGetStreamInfo(_response); + return this.processMyEndpoint(_response); }); } - protected processGetStreamInfo(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = StreamInfoResult.fromJS(resultData200); - return Promise.resolve(result200); - - } else if (status === 500) { - const _responseText = response.data; - let result500: any = null; - let resultData500 = _responseText; - result500 = Exception.fromJS(resultData500); - return throwException("A server side error occurred.", status, _responseText, _headers, result500); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - - /** - * 配置摄像头连接参数 - * @param config 摄像头配置 - * @return 配置结果 - */ - configureCamera(config: CameraConfigRequest, cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/VideoStream/ConfigureCamera"; - url_ = url_.replace(/[?&]$/, ""); - - const content_ = JSON.stringify(config); - - let options_: AxiosRequestConfig = { - data: content_, - method: "POST", - url: url_, - headers: { - "Content-Type": "application/json", - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processConfigureCamera(_response); - }); - } - - protected processConfigureCamera(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = resultData200 !== undefined ? resultData200 : null; - - return Promise.resolve(result200); - - } else if (status === 400) { - const _responseText = response.data; - let result400: any = null; - let resultData400 = _responseText; - result400 = resultData400 !== undefined ? resultData400 : null; - - return throwException("A server side error occurred.", status, _responseText, _headers, result400); - - } else if (status === 500) { - const _responseText = response.data; - let result500: any = null; - let resultData500 = _responseText; - result500 = Exception.fromJS(resultData500); - return throwException("A server side error occurred.", status, _responseText, _headers, result500); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - - /** - * 获取当前摄像头配置 - * @return 摄像头配置信息 - */ - getCameraConfig( cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/VideoStream/CameraConfig"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: AxiosRequestConfig = { - method: "GET", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processGetCameraConfig(_response); - }); - } - - protected processGetCameraConfig(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = resultData200 !== undefined ? resultData200 : null; - - return Promise.resolve(result200); - - } else if (status === 500) { - const _responseText = response.data; - let result500: any = null; - let resultData500 = _responseText; - result500 = Exception.fromJS(resultData500); - return throwException("A server side error occurred.", status, _responseText, _headers, result500); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - - /** - * 控制 HTTP 视频流服务开关 - * @param enabled (optional) 是否启用服务 - * @return 操作结果 - */ - setEnabled(enabled: boolean | undefined, cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/VideoStream/SetEnabled?"; - if (enabled === null) - throw new Error("The parameter 'enabled' cannot be null."); - else if (enabled !== undefined) - url_ += "enabled=" + encodeURIComponent("" + enabled) + "&"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: AxiosRequestConfig = { - method: "POST", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processSetEnabled(_response); - }); - } - - protected processSetEnabled(response: AxiosResponse): Promise { + protected processMyEndpoint(response: AxiosResponse): Promise { const status = response.status; let _headers: any = {}; if (response.headers && typeof response.headers === "object") { @@ -502,6 +301,59 @@ export class VideoStreamClient { return Promise.resolve(null as any); } + disableHdmiTransmission( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/VideoStream/DisableTransmission"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processDisableHdmiTransmission(_response); + }); + } + + protected processDisableHdmiTransmission(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + /** * 设置视频流分辨率 * @param request 分辨率配置请求 @@ -576,67 +428,6 @@ export class VideoStreamClient { return Promise.resolve(null as any); } - /** - * 获取当前分辨率 - * @return 当前分辨率信息 - */ - getCurrentResolution( cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/VideoStream/Resolution"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: AxiosRequestConfig = { - method: "GET", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processGetCurrentResolution(_response); - }); - } - - protected processGetCurrentResolution(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = resultData200 !== undefined ? resultData200 : null; - - return Promise.resolve(result200); - - } else if (status === 500) { - const _responseText = response.data; - let result500: any = null; - let resultData500 = _responseText; - result500 = resultData500 !== undefined ? resultData500 : null; - - return throwException("A server side error occurred.", status, _responseText, _headers, result500); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } - /** * 获取支持的分辨率列表 * @return 支持的分辨率列表 @@ -835,75 +626,6 @@ export class VideoStreamClient { } return Promise.resolve(null as any); } - - /** - * 执行一次自动对焦 (GET方式) - * @return 对焦结果 - */ - focus( cancelToken?: CancelToken): Promise { - let url_ = this.baseUrl + "/api/VideoStream/Focus"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: AxiosRequestConfig = { - method: "GET", - url: url_, - headers: { - "Accept": "application/json" - }, - cancelToken - }; - - return this.instance.request(options_).catch((_error: any) => { - if (isAxiosError(_error) && _error.response) { - return _error.response; - } else { - throw _error; - } - }).then((_response: AxiosResponse) => { - return this.processFocus(_response); - }); - } - - protected processFocus(response: AxiosResponse): Promise { - const status = response.status; - let _headers: any = {}; - if (response.headers && typeof response.headers === "object") { - for (const k in response.headers) { - if (response.headers.hasOwnProperty(k)) { - _headers[k] = response.headers[k]; - } - } - } - if (status === 200) { - const _responseText = response.data; - let result200: any = null; - let resultData200 = _responseText; - result200 = resultData200 !== undefined ? resultData200 : null; - - return Promise.resolve(result200); - - } else if (status === 400) { - const _responseText = response.data; - let result400: any = null; - let resultData400 = _responseText; - result400 = resultData400 !== undefined ? resultData400 : null; - - return throwException("A server side error occurred.", status, _responseText, _headers, result400); - - } else if (status === 500) { - const _responseText = response.data; - let result500: any = null; - let resultData500 = _responseText; - result500 = resultData500 !== undefined ? resultData500 : null; - - return throwException("A server side error occurred.", status, _responseText, _headers, result500); - - } else if (status !== 200 && status !== 204) { - const _responseText = response.data; - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - } - return Promise.resolve(null as any); - } } export class BsdlParserClient { @@ -7253,134 +6975,6 @@ export interface IException { stackTrace?: string | undefined; } -/** 视频流信息结构体 */ -export class StreamInfoResult implements IStreamInfoResult { - /** TODO: */ - frameRate!: number; - /** TODO: */ - frameWidth!: number; - /** TODO: */ - frameHeight!: number; - /** TODO: */ - format!: string; - /** TODO: */ - htmlUrl!: string; - /** TODO: */ - mjpegUrl!: string; - /** TODO: */ - snapshotUrl!: string; - /** TODO: */ - usbCameraUrl!: string; - - constructor(data?: IStreamInfoResult) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) - (this)[property] = (data)[property]; - } - } - } - - init(_data?: any) { - if (_data) { - this.frameRate = _data["frameRate"]; - this.frameWidth = _data["frameWidth"]; - this.frameHeight = _data["frameHeight"]; - this.format = _data["format"]; - this.htmlUrl = _data["htmlUrl"]; - this.mjpegUrl = _data["mjpegUrl"]; - this.snapshotUrl = _data["snapshotUrl"]; - this.usbCameraUrl = _data["usbCameraUrl"]; - } - } - - static fromJS(data: any): StreamInfoResult { - data = typeof data === 'object' ? data : {}; - let result = new StreamInfoResult(); - result.init(data); - return result; - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {}; - data["frameRate"] = this.frameRate; - data["frameWidth"] = this.frameWidth; - data["frameHeight"] = this.frameHeight; - data["format"] = this.format; - data["htmlUrl"] = this.htmlUrl; - data["mjpegUrl"] = this.mjpegUrl; - data["snapshotUrl"] = this.snapshotUrl; - data["usbCameraUrl"] = this.usbCameraUrl; - return data; - } -} - -/** 视频流信息结构体 */ -export interface IStreamInfoResult { - /** TODO: */ - frameRate: number; - /** TODO: */ - frameWidth: number; - /** TODO: */ - frameHeight: number; - /** TODO: */ - format: string; - /** TODO: */ - htmlUrl: string; - /** TODO: */ - mjpegUrl: string; - /** TODO: */ - snapshotUrl: string; - /** TODO: */ - usbCameraUrl: string; -} - -/** 摄像头配置请求模型 */ -export class CameraConfigRequest implements ICameraConfigRequest { - /** 摄像头地址 */ - address!: string; - /** 摄像头端口 */ - port!: number; - - constructor(data?: ICameraConfigRequest) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) - (this)[property] = (data)[property]; - } - } - } - - init(_data?: any) { - if (_data) { - this.address = _data["address"]; - this.port = _data["port"]; - } - } - - static fromJS(data: any): CameraConfigRequest { - data = typeof data === 'object' ? data : {}; - let result = new CameraConfigRequest(); - result.init(data); - return result; - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {}; - data["address"] = this.address; - data["port"] = this.port; - return data; - } -} - -/** 摄像头配置请求模型 */ -export interface ICameraConfigRequest { - /** 摄像头地址 */ - address: string; - /** 摄像头端口 */ - port: number; -} - /** 分辨率配置请求模型 */ export class ResolutionConfigRequest implements IResolutionConfigRequest { /** 宽度 */ diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index c962b62..0000000 --- a/src/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import './assets/main.css' - -import { createApp } from 'vue' -import { createPinia } from 'pinia' - -import App from '@/App.vue' -import router from './router' - -const app = createApp(App).use(router).use(createPinia()).mount('#app') -