From 0547bb5a02af5b7cf1a86d8f291887aced06aa4b Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sat, 9 Aug 2025 14:09:03 +0800 Subject: [PATCH] refactor: video stream service; fix: progress tracker guid --- .../src/Controllers/VideoStreamController.cs | 394 ++---- server/src/Peripherals/CameraClient.cs | 29 +- .../Services/HttpHdmiVideoStreamService.cs | 2 +- server/src/Services/HttpVideoStreamService.cs | 1173 ++++++----------- server/src/Services/ProgressTrackerService.cs | 2 +- src/APIClient.ts | 520 +------- src/main.ts | 10 - 7 files changed, 539 insertions(+), 1591 deletions(-) delete mode 100644 src/main.ts 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/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/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') -