refactor: 视频流前后端适配
This commit is contained in:
		
							
								
								
									
										3
									
								
								server/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								server/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,6 @@
 | 
			
		||||
# Generate
 | 
			
		||||
obj
 | 
			
		||||
bin
 | 
			
		||||
bitstream
 | 
			
		||||
bsdl
 | 
			
		||||
 | 
			
		||||
data
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using DotNext;
 | 
			
		||||
using server.Services;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 视频流控制器,支持动态配置摄像头连接
 | 
			
		||||
@@ -15,7 +16,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    private readonly server.Services.HttpVideoStreamService _videoStreamService;
 | 
			
		||||
    private readonly HttpVideoStreamService _videoStreamService;
 | 
			
		||||
    private readonly Database.UserManager _userManager;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -24,7 +25,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// <param name="videoStreamService">HTTP视频流服务</param>
 | 
			
		||||
    /// <param name="userManager">用户管理服务</param>
 | 
			
		||||
    public VideoStreamController(
 | 
			
		||||
        server.Services.HttpVideoStreamService videoStreamService, Database.UserManager userManager)
 | 
			
		||||
        HttpVideoStreamService videoStreamService, Database.UserManager userManager)
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info("创建VideoStreamController,命名空间:{Namespace}", this.GetType().Namespace);
 | 
			
		||||
        _videoStreamService = videoStreamService;
 | 
			
		||||
@@ -62,11 +63,11 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// 获取 HTTP 视频流服务状态
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>服务状态信息</returns>
 | 
			
		||||
    [HttpGet("Status")]
 | 
			
		||||
    [HttpGet("ServiceStatus")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(VideoStreamServiceStatus), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IResult GetStatus()
 | 
			
		||||
    public IResult GetServiceStatus()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -85,7 +86,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
 | 
			
		||||
    [HttpGet("MyEndpoint")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(VideoStreamEndpoint), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IResult MyEndpoint()
 | 
			
		||||
    {
 | 
			
		||||
@@ -141,14 +142,14 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPost("DisableTransmission")]
 | 
			
		||||
    public async Task<IActionResult> DisableHdmiTransmission()
 | 
			
		||||
    [HttpPost("SetVideoStreamEnable")]
 | 
			
		||||
    public async Task<IActionResult> SetVideoStreamEnable(bool enable)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var boardId = TryGetBoardId().OrThrow(() => new ArgumentException("Board ID is required"));
 | 
			
		||||
 | 
			
		||||
            await _videoStreamService.DisableHdmiTransmissionAsync(boardId.ToString());
 | 
			
		||||
            await _videoStreamService.SetVideoStreamEnableAsync(boardId.ToString(), enable);
 | 
			
		||||
            return Ok($"HDMI transmission for board {boardId} disabled.");
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
@@ -210,7 +211,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// <returns>支持的分辨率列表</returns>
 | 
			
		||||
    [HttpGet("SupportedResolutions")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(AvailableResolutionsResponse[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IResult GetSupportedResolutions()
 | 
			
		||||
    {
 | 
			
		||||
@@ -319,6 +320,39 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置摄像头连接参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>配置结果</returns>
 | 
			
		||||
    [HttpPost("ConfigureCamera")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IResult> ConfigureCamera()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
 | 
			
		||||
 | 
			
		||||
            var ret = await _videoStreamService.ConfigureCameraAsync(boardId);
 | 
			
		||||
 | 
			
		||||
            if (ret)
 | 
			
		||||
            {
 | 
			
		||||
                return TypedResults.Ok(new { Message = "配置成功" });
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return TypedResults.BadRequest(new { Message = "配置失败" });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "配置摄像头连接失败");
 | 
			
		||||
            return TypedResults.InternalServerError(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 分辨率配置请求模型
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ namespace server.Services;
 | 
			
		||||
public class VideoStreamClient
 | 
			
		||||
{
 | 
			
		||||
    public string? ClientId { get; set; } = string.Empty;
 | 
			
		||||
    public bool IsEnabled { get; set; } = true;
 | 
			
		||||
    public int FrameWidth { get; set; }
 | 
			
		||||
    public int FrameHeight { get; set; }
 | 
			
		||||
    public int FrameRate { get; set; }
 | 
			
		||||
@@ -35,28 +36,35 @@ public class VideoStreamClient
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 表示摄像头连接状态信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class VideoEndpoint
 | 
			
		||||
public class VideoStreamEndpoint
 | 
			
		||||
{
 | 
			
		||||
    public string BoardId { get; set; } = "";
 | 
			
		||||
    public string MjpegUrl { get; set; } = "";
 | 
			
		||||
    public string VideoUrl { get; set; } = "";
 | 
			
		||||
    public string SnapshotUrl { get; set; } = "";
 | 
			
		||||
    public required string BoardId { get; set; } = "";
 | 
			
		||||
    public required string MjpegUrl { get; set; } = "";
 | 
			
		||||
    public required string VideoUrl { get; set; } = "";
 | 
			
		||||
    public required string SnapshotUrl { get; set; } = "";
 | 
			
		||||
    public required string HtmlUrl { get; set; } = "";
 | 
			
		||||
    public required string UsbCameraUrl { get; set; } = "";
 | 
			
		||||
 | 
			
		||||
    public required bool IsEnabled { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 视频流的帧率(FPS)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int FrameRate { get; set; }
 | 
			
		||||
    public required int FrameRate { get; set; }
 | 
			
		||||
 | 
			
		||||
    public int FrameWidth { get; set; }
 | 
			
		||||
    public int FrameHeight { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 视频分辨率(如 640x480)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Resolution { get; set; } = string.Empty;
 | 
			
		||||
    public string Resolution => $"{FrameWidth}x{FrameHeight}";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 表示视频流服务的运行状态
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ServiceStatus
 | 
			
		||||
public class VideoStreamServiceStatus
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 服务是否正在运行
 | 
			
		||||
@@ -71,7 +79,7 @@ public class ServiceStatus
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前连接的客户端端点列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public List<VideoEndpoint> ClientEndpoints { get; set; } = new();
 | 
			
		||||
    public List<VideoStreamEndpoint> ClientEndpoints { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前连接的客户端数量
 | 
			
		||||
@@ -106,36 +114,6 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
        _userManager = userManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化 HttpVideoStreamService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public override async Task StartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 停止 HTTP 视频流服务
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public override async Task StopAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var clientKey in _clientDict.Keys)
 | 
			
		||||
        {
 | 
			
		||||
            var client = _clientDict[clientKey];
 | 
			
		||||
            client.CTS.Cancel();
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
 | 
			
		||||
            {
 | 
			
		||||
                await client.Camera.EnableHardwareTrans(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        _clientDict.Clear();
 | 
			
		||||
        await base.StopAsync(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<VideoStreamClient> TryGetClient(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
@@ -176,6 +154,36 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
        return client;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 初始化 HttpVideoStreamService
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public override async Task StartAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        _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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 停止 HTTP 视频流服务
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public override async Task StopAsync(CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var clientKey in _clientDict.Keys)
 | 
			
		||||
        {
 | 
			
		||||
            var client = _clientDict[clientKey];
 | 
			
		||||
            client.CTS.Cancel();
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
 | 
			
		||||
            {
 | 
			
		||||
                await client.Camera.EnableHardwareTrans(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        _clientDict.Clear();
 | 
			
		||||
        await base.StopAsync(cancellationToken);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 执行 HTTP 视频流服务
 | 
			
		||||
@@ -254,6 +262,11 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
                // 单帧图像请求
 | 
			
		||||
                await HandleSnapshotRequestAsync(context.Response, client, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
            else if (path == "/html")
 | 
			
		||||
            {
 | 
			
		||||
                // HTML页面请求
 | 
			
		||||
                await SendIndexHtmlPageAsync(context.Response);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // 默认返回简单的HTML页面,提供链接到视频页面
 | 
			
		||||
@@ -668,42 +681,12 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<VideoEndpoint> GetAllVideoEndpoints()
 | 
			
		||||
    {
 | 
			
		||||
        var endpoints = new List<VideoEndpoint>();
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 配置摄像头连接参数
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="boardId">板卡ID</param>
 | 
			
		||||
    /// <returns>配置是否成功</returns>
 | 
			
		||||
    public async Task<bool> ConfigureCameraAsync(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
@@ -711,8 +694,67 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            {
 | 
			
		||||
                var ret = await client.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!");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            {
 | 
			
		||||
                var ret = await client.Camera.ChangeResolution(client.FrameWidth, client.FrameHeight);
 | 
			
		||||
                if (!ret.IsSuccessful)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error(ret.Error);
 | 
			
		||||
                    throw ret.Error;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!ret.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.Error($"Camera Resolution Change Failed!");
 | 
			
		||||
                    throw new Exception($"Camera Resolution Change Failed!");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "配置摄像头连接时发生错误");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task SetVideoStreamEnableAsync(string boardId, bool enable)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
 | 
			
		||||
            if (client.IsEnabled == enable)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            using (await client.Lock.AcquireWriteLockAsync())
 | 
			
		||||
            {
 | 
			
		||||
                if (enable)
 | 
			
		||||
                {
 | 
			
		||||
                    client.CTS = new CancellationTokenSource();
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    client.CTS.Cancel();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var camera = client.Camera;
 | 
			
		||||
                var disableResult = await camera.EnableHardwareTrans(false);
 | 
			
		||||
                var disableResult = await camera.EnableHardwareTrans(enable);
 | 
			
		||||
                if (disableResult.IsSuccessful && disableResult.Value)
 | 
			
		||||
                    logger.Info($"Successfully disabled camera {boardId} hardware transmission");
 | 
			
		||||
                else
 | 
			
		||||
@@ -743,4 +785,41 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public VideoStreamEndpoint GetVideoEndpoint(string boardId)
 | 
			
		||||
    {
 | 
			
		||||
        var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
 | 
			
		||||
 | 
			
		||||
        return new VideoStreamEndpoint
 | 
			
		||||
        {
 | 
			
		||||
            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}",
 | 
			
		||||
            UsbCameraUrl = $"http://{Global.LocalHost}:{_serverPort}/usbCamera?boardId={boardId}",
 | 
			
		||||
            HtmlUrl = $"http://{Global.LocalHost}:{_serverPort}/html?boardId={boardId}",
 | 
			
		||||
            IsEnabled = client.IsEnabled,
 | 
			
		||||
            FrameRate = client.FrameRate
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<VideoStreamEndpoint> GetAllVideoEndpoints()
 | 
			
		||||
    {
 | 
			
		||||
        var endpoints = new List<VideoStreamEndpoint>();
 | 
			
		||||
 | 
			
		||||
        foreach (var boardId in _clientDict.Keys)
 | 
			
		||||
            endpoints.Add(GetVideoEndpoint(boardId));
 | 
			
		||||
 | 
			
		||||
        return endpoints;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public VideoStreamServiceStatus GetServiceStatus()
 | 
			
		||||
    {
 | 
			
		||||
        return new VideoStreamServiceStatus
 | 
			
		||||
        {
 | 
			
		||||
            IsRunning = true,
 | 
			
		||||
            ServerPort = _serverPort,
 | 
			
		||||
            ClientEndpoints = GetAllVideoEndpoints()
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										323
									
								
								src/APIClient.ts
									
									
									
									
									
								
							
							
						
						
									
										323
									
								
								src/APIClient.ts
									
									
									
									
									
								
							@@ -129,8 +129,8 @@ export class VideoStreamClient {
 | 
			
		||||
     * 获取 HTTP 视频流服务状态
 | 
			
		||||
     * @return 服务状态信息
 | 
			
		||||
     */
 | 
			
		||||
    getStatus( cancelToken?: CancelToken): Promise<any> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/Status";
 | 
			
		||||
    getServiceStatus( cancelToken?: CancelToken): Promise<VideoStreamServiceStatus> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/ServiceStatus";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: AxiosRequestConfig = {
 | 
			
		||||
@@ -149,11 +149,11 @@ export class VideoStreamClient {
 | 
			
		||||
                throw _error;
 | 
			
		||||
            }
 | 
			
		||||
        }).then((_response: AxiosResponse) => {
 | 
			
		||||
            return this.processGetStatus(_response);
 | 
			
		||||
            return this.processGetServiceStatus(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processGetStatus(response: AxiosResponse): Promise<any> {
 | 
			
		||||
    protected processGetServiceStatus(response: AxiosResponse): Promise<VideoStreamServiceStatus> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {};
 | 
			
		||||
        if (response.headers && typeof response.headers === "object") {
 | 
			
		||||
@@ -167,9 +167,8 @@ export class VideoStreamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200  = _responseText;
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return Promise.resolve<any>(result200);
 | 
			
		||||
            result200 = VideoStreamServiceStatus.fromJS(resultData200);
 | 
			
		||||
            return Promise.resolve<VideoStreamServiceStatus>(result200);
 | 
			
		||||
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
@@ -182,10 +181,10 @@ export class VideoStreamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<any>(null as any);
 | 
			
		||||
        return Promise.resolve<VideoStreamServiceStatus>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    myEndpoint( cancelToken?: CancelToken): Promise<any> {
 | 
			
		||||
    myEndpoint( cancelToken?: CancelToken): Promise<VideoStreamEndpoint> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/MyEndpoint";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
@@ -209,7 +208,7 @@ export class VideoStreamClient {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processMyEndpoint(response: AxiosResponse): Promise<any> {
 | 
			
		||||
    protected processMyEndpoint(response: AxiosResponse): Promise<VideoStreamEndpoint> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {};
 | 
			
		||||
        if (response.headers && typeof response.headers === "object") {
 | 
			
		||||
@@ -223,9 +222,8 @@ export class VideoStreamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200  = _responseText;
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return Promise.resolve<any>(result200);
 | 
			
		||||
            result200 = VideoStreamEndpoint.fromJS(resultData200);
 | 
			
		||||
            return Promise.resolve<VideoStreamEndpoint>(result200);
 | 
			
		||||
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
@@ -238,7 +236,7 @@ export class VideoStreamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<any>(null as any);
 | 
			
		||||
        return Promise.resolve<VideoStreamEndpoint>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -301,8 +299,12 @@ export class VideoStreamClient {
 | 
			
		||||
        return Promise.resolve<boolean>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disableHdmiTransmission( cancelToken?: CancelToken): Promise<FileResponse | null> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/DisableTransmission";
 | 
			
		||||
    setVideoStreamEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<FileResponse | null> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/SetVideoStreamEnable?";
 | 
			
		||||
        if (enable === null)
 | 
			
		||||
            throw new Error("The parameter 'enable' cannot be null.");
 | 
			
		||||
        else if (enable !== undefined)
 | 
			
		||||
            url_ += "enable=" + encodeURIComponent("" + enable) + "&";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: AxiosRequestConfig = {
 | 
			
		||||
@@ -322,11 +324,11 @@ export class VideoStreamClient {
 | 
			
		||||
                throw _error;
 | 
			
		||||
            }
 | 
			
		||||
        }).then((_response: AxiosResponse) => {
 | 
			
		||||
            return this.processDisableHdmiTransmission(_response);
 | 
			
		||||
            return this.processSetVideoStreamEnable(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processDisableHdmiTransmission(response: AxiosResponse): Promise<FileResponse | null> {
 | 
			
		||||
    protected processSetVideoStreamEnable(response: AxiosResponse): Promise<FileResponse | null> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {};
 | 
			
		||||
        if (response.headers && typeof response.headers === "object") {
 | 
			
		||||
@@ -432,7 +434,7 @@ export class VideoStreamClient {
 | 
			
		||||
     * 获取支持的分辨率列表
 | 
			
		||||
     * @return 支持的分辨率列表
 | 
			
		||||
     */
 | 
			
		||||
    getSupportedResolutions( cancelToken?: CancelToken): Promise<any> {
 | 
			
		||||
    getSupportedResolutions( cancelToken?: CancelToken): Promise<AvailableResolutionsResponse[]> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/SupportedResolutions";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
@@ -456,7 +458,7 @@ export class VideoStreamClient {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processGetSupportedResolutions(response: AxiosResponse): Promise<any> {
 | 
			
		||||
    protected processGetSupportedResolutions(response: AxiosResponse): Promise<AvailableResolutionsResponse[]> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {};
 | 
			
		||||
        if (response.headers && typeof response.headers === "object") {
 | 
			
		||||
@@ -470,9 +472,15 @@ export class VideoStreamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200  = _responseText;
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return Promise.resolve<any>(result200);
 | 
			
		||||
            if (Array.isArray(resultData200)) {
 | 
			
		||||
                result200 = [] as any;
 | 
			
		||||
                for (let item of resultData200)
 | 
			
		||||
                    result200!.push(AvailableResolutionsResponse.fromJS(item));
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                result200 = <any>null;
 | 
			
		||||
            }
 | 
			
		||||
            return Promise.resolve<AvailableResolutionsResponse[]>(result200);
 | 
			
		||||
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
@@ -486,7 +494,7 @@ export class VideoStreamClient {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<any>(null as any);
 | 
			
		||||
        return Promise.resolve<AvailableResolutionsResponse[]>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -626,6 +634,74 @@ export class VideoStreamClient {
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<any>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 配置摄像头连接参数
 | 
			
		||||
     * @return 配置结果
 | 
			
		||||
     */
 | 
			
		||||
    configureCamera( cancelToken?: CancelToken): Promise<any> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/ConfigureCamera";
 | 
			
		||||
        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.processConfigureCamera(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processConfigureCamera(response: AxiosResponse): Promise<any> {
 | 
			
		||||
        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 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return Promise.resolve<any>(result200);
 | 
			
		||||
 | 
			
		||||
        } else if (status === 400) {
 | 
			
		||||
            const _responseText = response.data;
 | 
			
		||||
            let result400: any = null;
 | 
			
		||||
            let resultData400  = _responseText;
 | 
			
		||||
                result400 = resultData400 !== undefined ? resultData400 : <any>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<any>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class BsdlParserClient {
 | 
			
		||||
@@ -6927,6 +7003,157 @@ export class UDPClient {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 表示视频流服务的运行状态 */
 | 
			
		||||
export class VideoStreamServiceStatus implements IVideoStreamServiceStatus {
 | 
			
		||||
    /** 服务是否正在运行 */
 | 
			
		||||
    isRunning!: boolean;
 | 
			
		||||
    /** 服务监听的端口号 */
 | 
			
		||||
    serverPort!: number;
 | 
			
		||||
    /** 当前连接的客户端端点列表 */
 | 
			
		||||
    clientEndpoints!: VideoStreamEndpoint[];
 | 
			
		||||
    /** 当前连接的客户端数量 */
 | 
			
		||||
    connectedClientsNum!: number;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IVideoStreamServiceStatus) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
                    (<any>this)[property] = (<any>data)[property];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!data) {
 | 
			
		||||
            this.clientEndpoints = [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init(_data?: any) {
 | 
			
		||||
        if (_data) {
 | 
			
		||||
            this.isRunning = _data["isRunning"];
 | 
			
		||||
            this.serverPort = _data["serverPort"];
 | 
			
		||||
            if (Array.isArray(_data["clientEndpoints"])) {
 | 
			
		||||
                this.clientEndpoints = [] as any;
 | 
			
		||||
                for (let item of _data["clientEndpoints"])
 | 
			
		||||
                    this.clientEndpoints!.push(VideoStreamEndpoint.fromJS(item));
 | 
			
		||||
            }
 | 
			
		||||
            this.connectedClientsNum = _data["connectedClientsNum"];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromJS(data: any): VideoStreamServiceStatus {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        let result = new VideoStreamServiceStatus();
 | 
			
		||||
        result.init(data);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toJSON(data?: any) {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        data["isRunning"] = this.isRunning;
 | 
			
		||||
        data["serverPort"] = this.serverPort;
 | 
			
		||||
        if (Array.isArray(this.clientEndpoints)) {
 | 
			
		||||
            data["clientEndpoints"] = [];
 | 
			
		||||
            for (let item of this.clientEndpoints)
 | 
			
		||||
                data["clientEndpoints"].push(item ? item.toJSON() : <any>undefined);
 | 
			
		||||
        }
 | 
			
		||||
        data["connectedClientsNum"] = this.connectedClientsNum;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 表示视频流服务的运行状态 */
 | 
			
		||||
export interface IVideoStreamServiceStatus {
 | 
			
		||||
    /** 服务是否正在运行 */
 | 
			
		||||
    isRunning: boolean;
 | 
			
		||||
    /** 服务监听的端口号 */
 | 
			
		||||
    serverPort: number;
 | 
			
		||||
    /** 当前连接的客户端端点列表 */
 | 
			
		||||
    clientEndpoints: VideoStreamEndpoint[];
 | 
			
		||||
    /** 当前连接的客户端数量 */
 | 
			
		||||
    connectedClientsNum: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 表示摄像头连接状态信息 */
 | 
			
		||||
export class VideoStreamEndpoint implements IVideoStreamEndpoint {
 | 
			
		||||
    boardId!: string;
 | 
			
		||||
    mjpegUrl!: string;
 | 
			
		||||
    videoUrl!: string;
 | 
			
		||||
    snapshotUrl!: string;
 | 
			
		||||
    htmlUrl!: string;
 | 
			
		||||
    usbCameraUrl!: string;
 | 
			
		||||
    isEnabled!: boolean;
 | 
			
		||||
    /** 视频流的帧率(FPS) */
 | 
			
		||||
    frameRate!: number;
 | 
			
		||||
    frameWidth!: number;
 | 
			
		||||
    frameHeight!: number;
 | 
			
		||||
    /** 视频分辨率(如 640x480) */
 | 
			
		||||
    resolution!: string;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IVideoStreamEndpoint) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
                    (<any>this)[property] = (<any>data)[property];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init(_data?: any) {
 | 
			
		||||
        if (_data) {
 | 
			
		||||
            this.boardId = _data["boardId"];
 | 
			
		||||
            this.mjpegUrl = _data["mjpegUrl"];
 | 
			
		||||
            this.videoUrl = _data["videoUrl"];
 | 
			
		||||
            this.snapshotUrl = _data["snapshotUrl"];
 | 
			
		||||
            this.htmlUrl = _data["htmlUrl"];
 | 
			
		||||
            this.usbCameraUrl = _data["usbCameraUrl"];
 | 
			
		||||
            this.isEnabled = _data["isEnabled"];
 | 
			
		||||
            this.frameRate = _data["frameRate"];
 | 
			
		||||
            this.frameWidth = _data["frameWidth"];
 | 
			
		||||
            this.frameHeight = _data["frameHeight"];
 | 
			
		||||
            this.resolution = _data["resolution"];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromJS(data: any): VideoStreamEndpoint {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        let result = new VideoStreamEndpoint();
 | 
			
		||||
        result.init(data);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toJSON(data?: any) {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        data["boardId"] = this.boardId;
 | 
			
		||||
        data["mjpegUrl"] = this.mjpegUrl;
 | 
			
		||||
        data["videoUrl"] = this.videoUrl;
 | 
			
		||||
        data["snapshotUrl"] = this.snapshotUrl;
 | 
			
		||||
        data["htmlUrl"] = this.htmlUrl;
 | 
			
		||||
        data["usbCameraUrl"] = this.usbCameraUrl;
 | 
			
		||||
        data["isEnabled"] = this.isEnabled;
 | 
			
		||||
        data["frameRate"] = this.frameRate;
 | 
			
		||||
        data["frameWidth"] = this.frameWidth;
 | 
			
		||||
        data["frameHeight"] = this.frameHeight;
 | 
			
		||||
        data["resolution"] = this.resolution;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 表示摄像头连接状态信息 */
 | 
			
		||||
export interface IVideoStreamEndpoint {
 | 
			
		||||
    boardId: string;
 | 
			
		||||
    mjpegUrl: string;
 | 
			
		||||
    videoUrl: string;
 | 
			
		||||
    snapshotUrl: string;
 | 
			
		||||
    htmlUrl: string;
 | 
			
		||||
    usbCameraUrl: string;
 | 
			
		||||
    isEnabled: boolean;
 | 
			
		||||
    /** 视频流的帧率(FPS) */
 | 
			
		||||
    frameRate: number;
 | 
			
		||||
    frameWidth: number;
 | 
			
		||||
    frameHeight: number;
 | 
			
		||||
    /** 视频分辨率(如 640x480) */
 | 
			
		||||
    resolution: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Exception implements IException {
 | 
			
		||||
    declare message: string;
 | 
			
		||||
    innerException?: Exception | undefined;
 | 
			
		||||
@@ -7021,6 +7248,54 @@ export interface IResolutionConfigRequest {
 | 
			
		||||
    height: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AvailableResolutionsResponse implements IAvailableResolutionsResponse {
 | 
			
		||||
    width!: number;
 | 
			
		||||
    height!: number;
 | 
			
		||||
    name!: string;
 | 
			
		||||
    value!: string;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IAvailableResolutionsResponse) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
                    (<any>this)[property] = (<any>data)[property];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init(_data?: any) {
 | 
			
		||||
        if (_data) {
 | 
			
		||||
            this.width = _data["width"];
 | 
			
		||||
            this.height = _data["height"];
 | 
			
		||||
            this.name = _data["name"];
 | 
			
		||||
            this.value = _data["value"];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromJS(data: any): AvailableResolutionsResponse {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        let result = new AvailableResolutionsResponse();
 | 
			
		||||
        result.init(data);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toJSON(data?: any) {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        data["width"] = this.width;
 | 
			
		||||
        data["height"] = this.height;
 | 
			
		||||
        data["name"] = this.name;
 | 
			
		||||
        data["value"] = this.value;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IAvailableResolutionsResponse {
 | 
			
		||||
    width: number;
 | 
			
		||||
    height: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    value: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ProblemDetails implements IProblemDetails {
 | 
			
		||||
    type?: string | undefined;
 | 
			
		||||
    title?: string | undefined;
 | 
			
		||||
 
 | 
			
		||||
@@ -87,9 +87,12 @@ import type { HubConnection } from "@microsoft/signalr";
 | 
			
		||||
import type {
 | 
			
		||||
  IProgressHub,
 | 
			
		||||
  IProgressReceiver,
 | 
			
		||||
} from "@/TypedSignalR.Client/server.Hubs";
 | 
			
		||||
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
 | 
			
		||||
import { ProgressStatus } from "@/server.Hubs";
 | 
			
		||||
} from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
 | 
			
		||||
import {
 | 
			
		||||
  getHubProxyFactory,
 | 
			
		||||
  getReceiverRegister,
 | 
			
		||||
} from "@/utils/signalR/TypedSignalR.Client";
 | 
			
		||||
import { ProgressStatus } from "@/utils/signalR/server.Hubs";
 | 
			
		||||
import { useRequiredInjection } from "@/utils/Common";
 | 
			
		||||
import { useAlertStore } from "./Alert";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,12 @@ import { useDialogStore } from "./dialog";
 | 
			
		||||
import { toFileParameterOrUndefined } from "@/utils/Common";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
 | 
			
		||||
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
 | 
			
		||||
import {
 | 
			
		||||
  getHubProxyFactory,
 | 
			
		||||
  getReceiverRegister,
 | 
			
		||||
} from "@/utils/signalR/TypedSignalR.Client";
 | 
			
		||||
import type { ResourceInfo } from "@/APIClient";
 | 
			
		||||
import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs";
 | 
			
		||||
import type { IJtagHub } from "@/utils/signalR/TypedSignalR.Client/server.Hubs";
 | 
			
		||||
 | 
			
		||||
export const useEquipments = defineStore("equipments", () => {
 | 
			
		||||
  // Global Stores
 | 
			
		||||
 
 | 
			
		||||
@@ -8,20 +8,31 @@
 | 
			
		||||
          控制面板
 | 
			
		||||
        </h2>
 | 
			
		||||
 | 
			
		||||
        <div class="grid grid-cols-1  gap-4"
 | 
			
		||||
          :class="{ 'md:grid-cols-3': streamType === 'usbCamera', 'md:grid-cols-4': streamType === 'videoStream' }">
 | 
			
		||||
        <div
 | 
			
		||||
          class="grid grid-cols-1 gap-4"
 | 
			
		||||
          :class="{
 | 
			
		||||
            'md:grid-cols-3': streamType === 'usbCamera',
 | 
			
		||||
            'md:grid-cols-4': streamType === 'videoStream',
 | 
			
		||||
          }"
 | 
			
		||||
        >
 | 
			
		||||
          <!-- 服务状态 -->
 | 
			
		||||
          <div class="stats shadow">
 | 
			
		||||
            <div class="stat bg-base-100">
 | 
			
		||||
              <div class="stat-figure text-primary">
 | 
			
		||||
                <div class="badge" :class="statusInfo.isRunning ? 'badge-success' : 'badge-error'
 | 
			
		||||
                  ">
 | 
			
		||||
                  {{ statusInfo.isRunning ? "运行中" : "已停止" }}
 | 
			
		||||
                <div
 | 
			
		||||
                  class="badge"
 | 
			
		||||
                  :class="
 | 
			
		||||
                    videoStreamInfo.isRunning ? 'badge-success' : 'badge-error'
 | 
			
		||||
                  "
 | 
			
		||||
                >
 | 
			
		||||
                  {{ videoStreamInfo.isRunning ? "运行中" : "已停止" }}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="stat-title">服务状态</div>
 | 
			
		||||
              <div class="stat-value text-primary">HTTP</div>
 | 
			
		||||
              <div class="stat-desc">端口: {{ statusInfo.serverPort }}</div>
 | 
			
		||||
              <div class="stat-desc">
 | 
			
		||||
                端口: {{ videoStreamInfo.serverPort }}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@@ -33,9 +44,11 @@
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="stat-title">视频规格</div>
 | 
			
		||||
              <div class="stat-value text-secondary">
 | 
			
		||||
                {{ streamInfo.frameWidth }}×{{ streamInfo.frameHeight }}
 | 
			
		||||
                {{ videoStreamInfo.frameWidth }}×{{
 | 
			
		||||
                  videoStreamInfo.frameHeight
 | 
			
		||||
                }}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="stat-desc">{{ streamInfo.frameRate }} FPS</div>
 | 
			
		||||
              <div class="stat-desc">{{ videoStreamInfo.frameRate }} FPS</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
@@ -47,17 +60,31 @@
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="stat-title">分辨率设置</div>
 | 
			
		||||
              <div class="stat-value text-sm">
 | 
			
		||||
                <select class="select select-sm select-bordered max-w-xs" v-model="selectedResolution"
 | 
			
		||||
                  @change="changeResolution" :disabled="changingResolution">
 | 
			
		||||
                  <option v-for="res in supportedResolutions" :key="`${res.width}x${res.height}`" :value="res">
 | 
			
		||||
                <select
 | 
			
		||||
                  class="select select-sm select-bordered max-w-xs"
 | 
			
		||||
                  v-model="selectedResolution"
 | 
			
		||||
                  @change="changeResolution"
 | 
			
		||||
                  :disabled="changingResolution"
 | 
			
		||||
                >
 | 
			
		||||
                  <option
 | 
			
		||||
                    v-for="res in supportedResolutions"
 | 
			
		||||
                    :key="`${res.width}x${res.height}`"
 | 
			
		||||
                    :value="res"
 | 
			
		||||
                  >
 | 
			
		||||
                    {{ res.width }}×{{ res.height }}
 | 
			
		||||
                  </option>
 | 
			
		||||
                </select>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="stat-desc">
 | 
			
		||||
                <button class="btn btn-xs btn-outline btn-info mt-1" @click="refreshResolutions"
 | 
			
		||||
                  :disabled="loadingResolutions">
 | 
			
		||||
                  <RefreshCw v-if="loadingResolutions" class="animate-spin h-3 w-3" />
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-xs btn-outline btn-info mt-1"
 | 
			
		||||
                  @click="refreshResolutions"
 | 
			
		||||
                  :disabled="loadingResolutions"
 | 
			
		||||
                >
 | 
			
		||||
                  <RefreshCw
 | 
			
		||||
                    v-if="loadingResolutions"
 | 
			
		||||
                    class="animate-spin h-3 w-3"
 | 
			
		||||
                  />
 | 
			
		||||
                  {{ loadingResolutions ? "刷新中..." : "刷新" }}
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -72,22 +99,34 @@
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="stat-title">连接数</div>
 | 
			
		||||
              <div class="stat-value text-accent">
 | 
			
		||||
                {{ statusInfo.connectedClients }}
 | 
			
		||||
                {{ videoStreamInfo.connectedClients }}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="stat-desc">
 | 
			
		||||
                <div class="dropdown dropdown-hover dropdown-top">
 | 
			
		||||
                  <div tabindex="0" role="button" class="text-xs underline cursor-help">
 | 
			
		||||
                  <div
 | 
			
		||||
                    tabindex="0"
 | 
			
		||||
                    role="button"
 | 
			
		||||
                    class="text-xs underline cursor-help"
 | 
			
		||||
                  >
 | 
			
		||||
                    查看客户端
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <ul tabindex="0"
 | 
			
		||||
                    class="dropdown-content z-20 menu p-2 shadow bg-base-200 rounded-box w-64 max-h-48 overflow-y-auto">
 | 
			
		||||
                    <li v-for="(client, index) in statusInfo.clientEndpoints" :key="index" class="text-xs">
 | 
			
		||||
                  <ul
 | 
			
		||||
                    tabindex="0"
 | 
			
		||||
                    class="dropdown-content z-20 menu p-2 shadow bg-base-200 rounded-box w-64 max-h-48 overflow-y-auto"
 | 
			
		||||
                  >
 | 
			
		||||
                    <li
 | 
			
		||||
                      v-for="(client, index) in videoStreamInfo.clientEndpoints"
 | 
			
		||||
                      :key="index"
 | 
			
		||||
                      class="text-xs"
 | 
			
		||||
                    >
 | 
			
		||||
                      <a class="break-all">{{ client }}</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li v-if="
 | 
			
		||||
                      !statusInfo.clientEndpoints ||
 | 
			
		||||
                      statusInfo.clientEndpoints.length === 0
 | 
			
		||||
                    ">
 | 
			
		||||
                    <li
 | 
			
		||||
                      v-if="
 | 
			
		||||
                        !videoStreamInfo.clientEndpoints ||
 | 
			
		||||
                        videoStreamInfo.clientEndpoints.length === 0
 | 
			
		||||
                      "
 | 
			
		||||
                    >
 | 
			
		||||
                      <a class="text-xs opacity-50">无活跃连接</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                  </ul>
 | 
			
		||||
@@ -99,21 +138,41 @@
 | 
			
		||||
 | 
			
		||||
        <!-- 操作按钮 -->
 | 
			
		||||
        <div class="card-actions justify-end mt-4">
 | 
			
		||||
          <button class="btn btn-outline btn-warning mr-2" @click="toggleStreamType" :disabled="isSwitchingStreamType">
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-outline btn-warning mr-2"
 | 
			
		||||
            @click="toggleStreamType"
 | 
			
		||||
            :disabled="isSwitchingStreamType"
 | 
			
		||||
          >
 | 
			
		||||
            <SwitchCamera class="h-4 w-4 mr-2" />
 | 
			
		||||
            {{ streamType === 'usbCamera' ? '切换到视频流' : '切换到USB摄像头' }}
 | 
			
		||||
            {{
 | 
			
		||||
              streamType === "usbCamera" ? "切换到视频流" : "切换到USB摄像头"
 | 
			
		||||
            }}
 | 
			
		||||
          </button>
 | 
			
		||||
          <button v-show="streamType === 'videoStream'" class="btn btn-outline btn-primary" @click="configCamera" :disabled="configing">
 | 
			
		||||
          <button
 | 
			
		||||
            v-show="streamType === 'videoStream'"
 | 
			
		||||
            class="btn btn-outline btn-primary"
 | 
			
		||||
            @click="configCamera"
 | 
			
		||||
            :disabled="configing"
 | 
			
		||||
          >
 | 
			
		||||
            <RefreshCw v-if="configing" class="animate-spin h-4 w-4 mr-2" />
 | 
			
		||||
            <CogIcon v-else class="h-4 w-4 mr-2" />
 | 
			
		||||
            {{ configing ? "配置中..." : "配置摄像头" }}
 | 
			
		||||
          </button>
 | 
			
		||||
          <button class="btn btn-outline btn-primary" @click="refreshStatus" :disabled="loading">
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-outline btn-primary"
 | 
			
		||||
            @click="refreshStatus"
 | 
			
		||||
            :disabled="loading"
 | 
			
		||||
          >
 | 
			
		||||
            <RefreshCw v-if="loading" class="animate-spin h-4 w-4 mr-2" />
 | 
			
		||||
            <RefreshCw v-else class="h-4 w-4 mr-2" />
 | 
			
		||||
            {{ loading ? "刷新中..." : "刷新状态" }}
 | 
			
		||||
          </button>
 | 
			
		||||
          <button v-show="streamType === 'videoStream'" class="btn btn-primary" @click="testConnection" :disabled="testing">
 | 
			
		||||
          <button
 | 
			
		||||
            v-show="streamType === 'videoStream'"
 | 
			
		||||
            class="btn btn-primary"
 | 
			
		||||
            @click="testConnection"
 | 
			
		||||
            :disabled="testing"
 | 
			
		||||
          >
 | 
			
		||||
            <RefreshCw v-if="testing" class="animate-spin h-4 w-4 mr-2" />
 | 
			
		||||
            <TestTube v-else class="h-4 w-4 mr-2" />
 | 
			
		||||
            {{ testing ? "测试中..." : "测试连接" }}
 | 
			
		||||
@@ -130,24 +189,42 @@
 | 
			
		||||
          视频预览
 | 
			
		||||
        </h2>
 | 
			
		||||
 | 
			
		||||
        <div class="relative bg-black rounded-lg overflow-hidden cursor-pointer" :class="[
 | 
			
		||||
          focusAnimationClass,
 | 
			
		||||
          { 'cursor-not-allowed': !isPlaying || hasVideoError }
 | 
			
		||||
        ]" style="aspect-ratio: 4/3" @click="handleVideoClick">
 | 
			
		||||
        <div
 | 
			
		||||
          class="relative bg-black rounded-lg overflow-hidden cursor-pointer"
 | 
			
		||||
          :class="[
 | 
			
		||||
            focusAnimationClass,
 | 
			
		||||
            { 'cursor-not-allowed': !isPlaying || hasVideoError },
 | 
			
		||||
          ]"
 | 
			
		||||
          style="aspect-ratio: 4/3"
 | 
			
		||||
          @click="handleVideoClick"
 | 
			
		||||
        >
 | 
			
		||||
          <!-- 视频播放器 - 使用img标签直接显示MJPEG流 -->
 | 
			
		||||
          <div v-show="isPlaying" class="w-full h-full flex items-center justify-center">
 | 
			
		||||
            <img :src="currentVideoSource" alt="视频流" class="max-w-full max-h-full object-contain"
 | 
			
		||||
              @error="handleVideoError" @load="handleVideoLoad" />
 | 
			
		||||
          <div
 | 
			
		||||
            v-show="isPlaying"
 | 
			
		||||
            class="w-full h-full flex items-center justify-center"
 | 
			
		||||
          >
 | 
			
		||||
            <img
 | 
			
		||||
              :src="currentVideoSource"
 | 
			
		||||
              alt="视频流"
 | 
			
		||||
              class="max-w-full max-h-full object-contain"
 | 
			
		||||
              @error="handleVideoError"
 | 
			
		||||
              @load="handleVideoLoad"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 对焦提示 -->
 | 
			
		||||
          <div v-if="isPlaying && !hasVideoError"
 | 
			
		||||
            class="absolute top-4 right-4 text-white text-sm bg-black bg-opacity-50 px-2 py-1 rounded">
 | 
			
		||||
            {{ isFocusing ? '对焦中...' : '点击画面对焦' }}
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="isPlaying && !hasVideoError"
 | 
			
		||||
            class="absolute top-4 right-4 text-white text-sm bg-black bg-opacity-50 px-2 py-1 rounded"
 | 
			
		||||
          >
 | 
			
		||||
            {{ isFocusing ? "对焦中..." : "点击画面对焦" }}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 错误信息显示 -->
 | 
			
		||||
          <div v-if="hasVideoError" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70">
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="hasVideoError"
 | 
			
		||||
            class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="card bg-error text-white shadow-lg w-full max-w-lg">
 | 
			
		||||
              <div class="card-body">
 | 
			
		||||
                <h3 class="card-title flex items-center gap-2">
 | 
			
		||||
@@ -158,10 +235,13 @@
 | 
			
		||||
                <ul class="list-disc list-inside">
 | 
			
		||||
                  <li>视频流服务是否已启动</li>
 | 
			
		||||
                  <li>网络连接是否正常</li>
 | 
			
		||||
                  <li>端口 {{ statusInfo.serverPort }} 是否可访问</li>
 | 
			
		||||
                  <li>端口 {{ videoStreamInfo.serverPort }} 是否可访问</li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                <div class="card-actions justify-end mt-2">
 | 
			
		||||
                  <button class="btn btn-sm btn-outline btn-primary" @click="tryReconnect">
 | 
			
		||||
                  <button
 | 
			
		||||
                    class="btn btn-sm btn-outline btn-primary"
 | 
			
		||||
                    @click="tryReconnect"
 | 
			
		||||
                  >
 | 
			
		||||
                    重试连接
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -170,8 +250,10 @@
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 占位符 -->
 | 
			
		||||
          <div v-show="!isPlaying && !hasVideoError"
 | 
			
		||||
            class="absolute inset-0 flex items-center justify-center text-white">
 | 
			
		||||
          <div
 | 
			
		||||
            v-show="!isPlaying && !hasVideoError"
 | 
			
		||||
            class="absolute inset-0 flex items-center justify-center text-white"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="text-center">
 | 
			
		||||
              <Video class="w-16 h-16 mx-auto mb-4 opacity-50" />
 | 
			
		||||
              <p class="text-lg opacity-75">{{ videoStatus }}</p>
 | 
			
		||||
@@ -187,18 +269,25 @@
 | 
			
		||||
          <div class="text-sm text-base-content/70">
 | 
			
		||||
            流地址:
 | 
			
		||||
            <code class="bg-base-300 px-2 py-1 rounded">{{
 | 
			
		||||
              streamInfo.mjpegUrl
 | 
			
		||||
              videoStreamInfo.mjpegUrl
 | 
			
		||||
            }}</code>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="space-x-2">
 | 
			
		||||
            <div class="dropdown dropdown-hover dropdown-top dropdown-end">
 | 
			
		||||
              <div tabindex="0" role="button" class="btn btn-sm btn-outline btn-accent">
 | 
			
		||||
              <div
 | 
			
		||||
                tabindex="0"
 | 
			
		||||
                role="button"
 | 
			
		||||
                class="btn btn-sm btn-outline btn-accent"
 | 
			
		||||
              >
 | 
			
		||||
                <MoreHorizontal class="w-4 h-4 mr-1" />
 | 
			
		||||
                更多功能
 | 
			
		||||
              </div>
 | 
			
		||||
              <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
 | 
			
		||||
              <ul
 | 
			
		||||
                tabindex="0"
 | 
			
		||||
                class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52"
 | 
			
		||||
              >
 | 
			
		||||
                <li>
 | 
			
		||||
                  <a @click="openInNewTab(streamInfo.htmlUrl)">
 | 
			
		||||
                  <a @click="openInNewTab(videoStreamInfo.htmlUrl)">
 | 
			
		||||
                    <ExternalLink class="w-4 h-4" />
 | 
			
		||||
                    在新标签打开视频页面
 | 
			
		||||
                  </a>
 | 
			
		||||
@@ -210,18 +299,26 @@
 | 
			
		||||
                  </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li>
 | 
			
		||||
                  <a @click="copyToClipboard(streamInfo.mjpegUrl)">
 | 
			
		||||
                  <a @click="copyToClipboard(videoStreamInfo.mjpegUrl)">
 | 
			
		||||
                    <Copy class="w-4 h-4" />
 | 
			
		||||
                    复制MJPEG地址
 | 
			
		||||
                  </a>
 | 
			
		||||
                </li>
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
            <button class="btn btn-success btn-sm" @click="startStream" :disabled="isPlaying">
 | 
			
		||||
            <button
 | 
			
		||||
              class="btn btn-success btn-sm"
 | 
			
		||||
              @click="startStream"
 | 
			
		||||
              :disabled="isPlaying"
 | 
			
		||||
            >
 | 
			
		||||
              <Play class="w-4 h-4 mr-1" />
 | 
			
		||||
              播放视频流
 | 
			
		||||
            </button>
 | 
			
		||||
            <button class="btn btn-error btn-sm" @click="stopStream" :disabled="!isPlaying">
 | 
			
		||||
            <button
 | 
			
		||||
              class="btn btn-error btn-sm"
 | 
			
		||||
              @click="stopStream"
 | 
			
		||||
              :disabled="!isPlaying"
 | 
			
		||||
            >
 | 
			
		||||
              <Square class="w-4 h-4 mr-1" />
 | 
			
		||||
              停止视频流
 | 
			
		||||
            </button>
 | 
			
		||||
@@ -239,11 +336,20 @@
 | 
			
		||||
        </h2>
 | 
			
		||||
 | 
			
		||||
        <div class="bg-base-300 rounded-lg p-4 h-48 overflow-y-auto">
 | 
			
		||||
          <div v-for="(log, index) in logs" :key="index" class="text-sm font-mono mb-1">
 | 
			
		||||
            <span class="text-base-content/50">[{{ formatTime(log.time) }}]</span>
 | 
			
		||||
          <div
 | 
			
		||||
            v-for="(log, index) in logs"
 | 
			
		||||
            :key="index"
 | 
			
		||||
            class="text-sm font-mono mb-1"
 | 
			
		||||
          >
 | 
			
		||||
            <span class="text-base-content/50"
 | 
			
		||||
              >[{{ formatTime(log.time) }}]</span
 | 
			
		||||
            >
 | 
			
		||||
            <span :class="getLogClass(log.level)">{{ log.message }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div v-if="logs.length === 0" class="text-base-content/50 text-center py-8">
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="logs.length === 0"
 | 
			
		||||
            class="text-base-content/50 text-center py-8"
 | 
			
		||||
          >
 | 
			
		||||
            暂无日志记录
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -277,8 +383,9 @@ import {
 | 
			
		||||
  MoreHorizontal,
 | 
			
		||||
  SwitchCamera,
 | 
			
		||||
} from "lucide-vue-next";
 | 
			
		||||
import { VideoStreamClient, CameraConfigRequest, ResolutionConfigRequest, StreamInfoResult } from "@/APIClient";
 | 
			
		||||
import { VideoStreamClient, ResolutionConfigRequest } from "@/APIClient";
 | 
			
		||||
import { useEquipments } from "@/stores/equipments";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
 | 
			
		||||
const eqps = useEquipments();
 | 
			
		||||
 | 
			
		||||
@@ -291,12 +398,12 @@ const hasVideoError = ref(false);
 | 
			
		||||
const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频');
 | 
			
		||||
 | 
			
		||||
// 视频流类型切换相关
 | 
			
		||||
const streamType = ref<'usbCamera' | 'videoStream'>('videoStream');
 | 
			
		||||
const streamType = ref<"usbCamera" | "videoStream">("videoStream");
 | 
			
		||||
const isSwitchingStreamType = ref(false);
 | 
			
		||||
 | 
			
		||||
// 对焦相关状态
 | 
			
		||||
const isFocusing = ref(false);
 | 
			
		||||
const focusAnimationClass = ref('');
 | 
			
		||||
const focusAnimationClass = ref("");
 | 
			
		||||
 | 
			
		||||
// 分辨率相关状态
 | 
			
		||||
const changingResolution = ref(false);
 | 
			
		||||
@@ -304,36 +411,29 @@ const loadingResolutions = ref(false);
 | 
			
		||||
const selectedResolution = ref({ width: 640, height: 480 });
 | 
			
		||||
const supportedResolutions = ref([
 | 
			
		||||
  { width: 640, height: 480 },
 | 
			
		||||
  { width: 1280, height: 720 }
 | 
			
		||||
  { width: 1280, height: 720 },
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 数据
 | 
			
		||||
const statusInfo = ref({
 | 
			
		||||
const videoStreamInfo = ref({
 | 
			
		||||
  frameWidth: 640,
 | 
			
		||||
  frameHeight: 480,
 | 
			
		||||
  frameRate: 30,
 | 
			
		||||
  isRunning: false,
 | 
			
		||||
  serverPort: 8080,
 | 
			
		||||
  streamUrl: "",
 | 
			
		||||
  mjpegUrl: "",
 | 
			
		||||
  snapshotUrl: "",
 | 
			
		||||
  htmlUrl: "",
 | 
			
		||||
  usbCameraUrl: "",
 | 
			
		||||
  connectedClients: 0,
 | 
			
		||||
  clientEndpoints: [] as string[],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const streamInfo = ref<StreamInfoResult>(new StreamInfoResult({
 | 
			
		||||
  frameRate: 30,
 | 
			
		||||
  frameWidth: 640,
 | 
			
		||||
  frameHeight: 480,
 | 
			
		||||
  format: "MJPEG",
 | 
			
		||||
  htmlUrl: "",
 | 
			
		||||
  mjpegUrl: "",
 | 
			
		||||
  snapshotUrl: "",
 | 
			
		||||
  usbCameraUrl: "",
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const currentVideoSource = ref("");
 | 
			
		||||
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
 | 
			
		||||
 | 
			
		||||
// API 客户端
 | 
			
		||||
const videoClient = new VideoStreamClient();
 | 
			
		||||
const videoClient = AuthManager.createAuthenticatedVideoStreamClient();
 | 
			
		||||
 | 
			
		||||
// 添加日志
 | 
			
		||||
const addLog = (level: string, message: string) => {
 | 
			
		||||
@@ -397,16 +497,23 @@ const toggleStreamType = async () => {
 | 
			
		||||
  isSwitchingStreamType.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    // 这里假设后端有API: setStreamType(type: string)
 | 
			
		||||
    addLog('info', `正在切换视频流类型到${streamType.value === 'usbCamera' ? '视频流' : 'USB摄像头'}...`);
 | 
			
		||||
    addLog(
 | 
			
		||||
      "info",
 | 
			
		||||
      `正在切换视频流类型到${streamType.value === "usbCamera" ? "视频流" : "USB摄像头"}...`,
 | 
			
		||||
    );
 | 
			
		||||
    refreshStatus();
 | 
			
		||||
 | 
			
		||||
    // 设置视频源
 | 
			
		||||
    streamType.value = streamType.value === 'usbCamera' ? 'videoStream' : 'usbCamera';
 | 
			
		||||
    addLog('success', `已切换到${streamType.value === 'usbCamera' ? 'USB摄像头' : '视频流'}`);
 | 
			
		||||
    streamType.value =
 | 
			
		||||
      streamType.value === "usbCamera" ? "videoStream" : "usbCamera";
 | 
			
		||||
    addLog(
 | 
			
		||||
      "success",
 | 
			
		||||
      `已切换到${streamType.value === "usbCamera" ? "USB摄像头" : "视频流"}`,
 | 
			
		||||
    );
 | 
			
		||||
    stopStream();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    addLog('error', `切换视频流类型失败: ${error}`);
 | 
			
		||||
    console.error('切换视频流类型失败:', error);
 | 
			
		||||
    addLog("error", `切换视频流类型失败: ${error}`);
 | 
			
		||||
    console.error("切换视频流类型失败:", error);
 | 
			
		||||
  } finally {
 | 
			
		||||
    isSwitchingStreamType.value = false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -418,7 +525,7 @@ const takeSnapshot = async () => {
 | 
			
		||||
    addLog("info", "正在获取快照...");
 | 
			
		||||
 | 
			
		||||
    // 使用当前的快照URL
 | 
			
		||||
    const snapshotUrl = streamInfo.value.snapshotUrl;
 | 
			
		||||
    const snapshotUrl = videoStreamInfo.value.snapshotUrl;
 | 
			
		||||
    if (!snapshotUrl) {
 | 
			
		||||
      addLog("error", "快照URL不可用");
 | 
			
		||||
      return;
 | 
			
		||||
@@ -446,17 +553,14 @@ async function configCamera() {
 | 
			
		||||
  configing.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    addLog("info", "正在配置并初始化摄像头...");
 | 
			
		||||
    const boardconfig = new CameraConfigRequest({
 | 
			
		||||
      address: eqps.boardAddr,
 | 
			
		||||
      port: eqps.boardPort,
 | 
			
		||||
    });
 | 
			
		||||
    await videoClient.configureCamera(boardconfig);
 | 
			
		||||
    await videoClient.configureCamera();
 | 
			
		||||
 | 
			
		||||
    const status = await videoClient.getCameraConfig();
 | 
			
		||||
    if (status.isConfigured) {
 | 
			
		||||
    const ret = await videoClient.testConnection();
 | 
			
		||||
 | 
			
		||||
    if (ret) {
 | 
			
		||||
      addLog("success", "摄像头已配置并初始化");
 | 
			
		||||
    } else {
 | 
			
		||||
      addLog("error", "摄像头配置失败,请检查地址和端口");
 | 
			
		||||
      addLog("error", "摄像头配置失败");
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    addLog("error", `摄像头配置失败: ${error}`);
 | 
			
		||||
@@ -473,11 +577,23 @@ const refreshStatus = async () => {
 | 
			
		||||
    addLog("info", "正在获取服务状态...");
 | 
			
		||||
 | 
			
		||||
    // 使用新的API方法名称
 | 
			
		||||
    const status = await videoClient.getStatus();
 | 
			
		||||
    statusInfo.value = status;
 | 
			
		||||
 | 
			
		||||
    const info = await videoClient.getStreamInfo();
 | 
			
		||||
    streamInfo.value = info;
 | 
			
		||||
    const serviceStatus = await videoClient.getServiceStatus();
 | 
			
		||||
    const endpointInfo = await videoClient.myEndpoint();
 | 
			
		||||
    videoStreamInfo.value = {
 | 
			
		||||
      frameWidth: endpointInfo.frameWidth,
 | 
			
		||||
      frameHeight: endpointInfo.frameHeight,
 | 
			
		||||
      frameRate: endpointInfo.frameRate,
 | 
			
		||||
      isRunning: serviceStatus.isRunning,
 | 
			
		||||
      serverPort: serviceStatus.serverPort,
 | 
			
		||||
      mjpegUrl: endpointInfo.mjpegUrl,
 | 
			
		||||
      snapshotUrl: endpointInfo.snapshotUrl,
 | 
			
		||||
      htmlUrl: endpointInfo.htmlUrl,
 | 
			
		||||
      usbCameraUrl: endpointInfo.usbCameraUrl,
 | 
			
		||||
      connectedClients: serviceStatus.connectedClientsNum,
 | 
			
		||||
      clientEndpoints: serviceStatus.clientEndpoints.map(
 | 
			
		||||
        (ep) => `${ep.boardId}`,
 | 
			
		||||
      ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    addLog("success", "服务状态获取成功");
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
@@ -527,9 +643,6 @@ const handleVideoLoad = () => {
 | 
			
		||||
const tryReconnect = () => {
 | 
			
		||||
  addLog("info", "尝试重新连接视频流...");
 | 
			
		||||
  hasVideoError.value = false;
 | 
			
		||||
 | 
			
		||||
  // 重新设置视频源,添加时间戳避免缓存问题
 | 
			
		||||
  currentVideoSource.value = `${streamInfo.value.mjpegUrl}?t=${new Date().getTime()}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 执行对焦
 | 
			
		||||
@@ -538,41 +651,41 @@ const performFocus = async () => {
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    isFocusing.value = true;
 | 
			
		||||
    focusAnimationClass.value = 'focus-starting';
 | 
			
		||||
    focusAnimationClass.value = "focus-starting";
 | 
			
		||||
    addLog("info", "正在执行自动对焦...");
 | 
			
		||||
 | 
			
		||||
    // 调用对焦API
 | 
			
		||||
    const response = await fetch('/api/VideoStream/Focus');
 | 
			
		||||
    const response = await fetch("/api/VideoStream/Focus");
 | 
			
		||||
    const result = await response.json();
 | 
			
		||||
 | 
			
		||||
    if (result.success) {
 | 
			
		||||
      // 对焦成功动画
 | 
			
		||||
      focusAnimationClass.value = 'focus-success';
 | 
			
		||||
      focusAnimationClass.value = "focus-success";
 | 
			
		||||
      addLog("success", "自动对焦执行成功");
 | 
			
		||||
 | 
			
		||||
      // 2秒后消失
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        focusAnimationClass.value = '';
 | 
			
		||||
        focusAnimationClass.value = "";
 | 
			
		||||
      }, 2000);
 | 
			
		||||
    } else {
 | 
			
		||||
      // 对焦失败动画
 | 
			
		||||
      focusAnimationClass.value = 'focus-error';
 | 
			
		||||
      addLog("error", `自动对焦执行失败: ${result.message || '未知错误'}`);
 | 
			
		||||
      focusAnimationClass.value = "focus-error";
 | 
			
		||||
      addLog("error", `自动对焦执行失败: ${result.message || "未知错误"}`);
 | 
			
		||||
 | 
			
		||||
      // 2秒后消失
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        focusAnimationClass.value = '';
 | 
			
		||||
        focusAnimationClass.value = "";
 | 
			
		||||
      }, 2000);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    // 对焦失败动画
 | 
			
		||||
    focusAnimationClass.value = 'focus-error';
 | 
			
		||||
    focusAnimationClass.value = "focus-error";
 | 
			
		||||
    addLog("error", `自动对焦执行失败: ${error}`);
 | 
			
		||||
    console.error("自动对焦执行失败:", error);
 | 
			
		||||
 | 
			
		||||
    // 2秒后消失
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      focusAnimationClass.value = '';
 | 
			
		||||
      focusAnimationClass.value = "";
 | 
			
		||||
    }, 2000);
 | 
			
		||||
  } finally {
 | 
			
		||||
    // 1秒后重置对焦状态
 | 
			
		||||
@@ -598,13 +711,16 @@ const startStream = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    addLog("info", "正在启动视频流...");
 | 
			
		||||
    videoStatus.value = "正在连接视频流...";
 | 
			
		||||
    videoClient.setEnabled(true);
 | 
			
		||||
    videoClient.setVideoStreamEnable(true);
 | 
			
		||||
 | 
			
		||||
    // 刷新状态
 | 
			
		||||
    await refreshStatus();
 | 
			
		||||
 | 
			
		||||
    // 设置视频源
 | 
			
		||||
    currentVideoSource.value = streamType.value === 'usbCamera' ? streamInfo.value.usbCameraUrl : streamInfo.value.mjpegUrl;
 | 
			
		||||
    currentVideoSource.value =
 | 
			
		||||
      streamType.value === "usbCamera"
 | 
			
		||||
        ? videoStreamInfo.value.usbCameraUrl
 | 
			
		||||
        : videoStreamInfo.value.mjpegUrl;
 | 
			
		||||
 | 
			
		||||
    // 设置播放状态
 | 
			
		||||
    isPlaying.value = true;
 | 
			
		||||
@@ -625,12 +741,18 @@ const refreshResolutions = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    addLog("info", "正在获取支持的分辨率列表...");
 | 
			
		||||
    const resolutions = await videoClient.getSupportedResolutions();
 | 
			
		||||
    supportedResolutions.value = resolutions.resolutions;
 | 
			
		||||
    supportedResolutions.value = resolutions.map((resolution) => ({
 | 
			
		||||
      width: resolution.width,
 | 
			
		||||
      height: resolution.height,
 | 
			
		||||
    }));
 | 
			
		||||
    console.log("支持的分辨率列表:", supportedResolutions.value);
 | 
			
		||||
 | 
			
		||||
    // 获取当前分辨率
 | 
			
		||||
    const currentRes = await videoClient.getCurrentResolution();
 | 
			
		||||
    selectedResolution.value = currentRes;
 | 
			
		||||
    const endpointInfo = await videoClient.myEndpoint();
 | 
			
		||||
    selectedResolution.value = {
 | 
			
		||||
      width: endpointInfo.frameWidth,
 | 
			
		||||
      height: endpointInfo.frameHeight,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    addLog("success", "分辨率列表获取成功");
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
@@ -649,18 +771,21 @@ const changeResolution = async () => {
 | 
			
		||||
  const wasPlaying = isPlaying.value;
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    addLog("info", `正在切换分辨率到 ${selectedResolution.value.width}×${selectedResolution.value.height}...`);
 | 
			
		||||
    addLog(
 | 
			
		||||
      "info",
 | 
			
		||||
      `正在切换分辨率到 ${selectedResolution.value.width}×${selectedResolution.value.height}...`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 如果正在播放,先停止视频流
 | 
			
		||||
    if (wasPlaying) {
 | 
			
		||||
      stopStream();
 | 
			
		||||
      await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
 | 
			
		||||
      await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1秒
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 设置新分辨率
 | 
			
		||||
    const resolutionRequest = new ResolutionConfigRequest({
 | 
			
		||||
      width: selectedResolution.value.width,
 | 
			
		||||
      height: selectedResolution.value.height
 | 
			
		||||
      height: selectedResolution.value.height,
 | 
			
		||||
    });
 | 
			
		||||
    const success = await videoClient.setResolution(resolutionRequest);
 | 
			
		||||
 | 
			
		||||
@@ -670,11 +795,14 @@ const changeResolution = async () => {
 | 
			
		||||
 | 
			
		||||
      // 如果之前在播放,重新启动视频流
 | 
			
		||||
      if (wasPlaying) {
 | 
			
		||||
        await new Promise(resolve => setTimeout(resolve, 500)); // 短暂延迟
 | 
			
		||||
        await new Promise((resolve) => setTimeout(resolve, 500)); // 短暂延迟
 | 
			
		||||
        await startStream();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      addLog("success", `分辨率已切换到 ${selectedResolution.value.width}×${selectedResolution.value.height}`);
 | 
			
		||||
      addLog(
 | 
			
		||||
        "success",
 | 
			
		||||
        `分辨率已切换到 ${selectedResolution.value.width}×${selectedResolution.value.height}`,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      addLog("error", "分辨率切换失败");
 | 
			
		||||
    }
 | 
			
		||||
@@ -690,7 +818,7 @@ const changeResolution = async () => {
 | 
			
		||||
const stopStream = () => {
 | 
			
		||||
  try {
 | 
			
		||||
    addLog("info", "正在停止视频流...");
 | 
			
		||||
    videoClient.setEnabled(false);
 | 
			
		||||
    videoClient.setVideoStreamEnable(false);
 | 
			
		||||
 | 
			
		||||
    // 清除视频源
 | 
			
		||||
    currentVideoSource.value = "";
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user