feat: 更新api,并更新了串流页面
This commit is contained in:
		@@ -14,6 +14,11 @@ public class TutorialController : ControllerBase
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
    private readonly IWebHostEnvironment _environment;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="environment">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    public TutorialController(IWebHostEnvironment environment)
 | 
			
		||||
    {
 | 
			
		||||
        _environment = environment;
 | 
			
		||||
 
 | 
			
		||||
@@ -109,6 +109,7 @@ public class UDPController : ControllerBase
 | 
			
		||||
    /// 获取指定IP地址接收的数据列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="address">IP地址</param>
 | 
			
		||||
    /// <param name="taskID">任务ID</param>
 | 
			
		||||
    [HttpGet("GetRecvDataArray")]
 | 
			
		||||
    [ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using Microsoft.AspNetCore.Cors;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 视频流控制器,支持动态配置摄像头连接
 | 
			
		||||
@@ -17,10 +17,16 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class CameraConfigRequest
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 摄像头地址
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [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; } = "";
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 摄像头端口
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Required]
 | 
			
		||||
        [Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
 | 
			
		||||
        public int Port { get; set; }
 | 
			
		||||
@@ -54,21 +60,11 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
            var status = _videoStreamService.GetServiceStatus();
 | 
			
		||||
 | 
			
		||||
            // 转换为小写首字母的JSON属性(符合前端惯例)
 | 
			
		||||
            return TypedResults.Ok(new
 | 
			
		||||
            {
 | 
			
		||||
                isRunning = true, // HTTP视频流服务作为后台服务始终运行
 | 
			
		||||
                serverPort = _videoStreamService.ServerPort,
 | 
			
		||||
                streamUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
 | 
			
		||||
                mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
 | 
			
		||||
                snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
 | 
			
		||||
                connectedClients = _videoStreamService.ConnectedClientsCount,
 | 
			
		||||
                clientEndpoints = _videoStreamService.GetConnectedClientEndpoints(),
 | 
			
		||||
                cameraStatus = _videoStreamService.GetCameraStatus()
 | 
			
		||||
            });
 | 
			
		||||
            return TypedResults.Ok(status);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "获取 HTTP 视频流服务状态失败"); 
 | 
			
		||||
            logger.Error(ex, "获取 HTTP 视频流服务状态失败");
 | 
			
		||||
            return TypedResults.InternalServerError(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -95,13 +91,11 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
                htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
 | 
			
		||||
                mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
 | 
			
		||||
                snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
 | 
			
		||||
                cameraAddress = _videoStreamService.CameraAddress,
 | 
			
		||||
                cameraPort = _videoStreamService.CameraPort
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "获取 HTTP 视频流信息失败"); 
 | 
			
		||||
            logger.Error(ex, "获取 HTTP 视频流信息失败");
 | 
			
		||||
            return TypedResults.InternalServerError(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -123,7 +117,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
            logger.Info("配置摄像头连接: {Address}:{Port}", config.Address, config.Port);
 | 
			
		||||
 | 
			
		||||
            var success = await _videoStreamService.ConfigureCameraAsync(config.Address, config.Port);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            if (success)
 | 
			
		||||
            {
 | 
			
		||||
                return TypedResults.Ok(new
 | 
			
		||||
@@ -166,7 +160,7 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info("获取摄像头配置");
 | 
			
		||||
            var cameraStatus = _videoStreamService.GetCameraStatus();
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            return TypedResults.Ok(new
 | 
			
		||||
            {
 | 
			
		||||
                address = _videoStreamService.CameraAddress,
 | 
			
		||||
@@ -183,35 +177,23 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 测试摄像头连接
 | 
			
		||||
    /// 控制 HTTP 视频流服务开关
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>连接测试结果</returns>
 | 
			
		||||
    [HttpPost("TestCameraConnection")]
 | 
			
		||||
    /// <param name="enabled">是否启用服务</param>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [HttpPost("SetEnabled")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public async Task<IResult> TestCameraConnection()
 | 
			
		||||
    public IResult SetEnabled([FromQuery] bool enabled)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        logger.Info("设置视频流服务开关: {Enabled}", enabled);
 | 
			
		||||
        _videoStreamService.Enabled = enabled;
 | 
			
		||||
        return TypedResults.Ok(new
 | 
			
		||||
        {
 | 
			
		||||
            logger.Info("测试摄像头连接");
 | 
			
		||||
            
 | 
			
		||||
            var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync();
 | 
			
		||||
            
 | 
			
		||||
            return TypedResults.Ok(new
 | 
			
		||||
            {
 | 
			
		||||
                success = isSuccess,
 | 
			
		||||
                message = message,
 | 
			
		||||
                cameraAddress = _videoStreamService.CameraAddress,
 | 
			
		||||
                cameraPort = _videoStreamService.CameraPort,
 | 
			
		||||
                timestamp = DateTime.Now
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "测试摄像头连接失败");
 | 
			
		||||
            return TypedResults.InternalServerError(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
            success = true,
 | 
			
		||||
            enabled = _videoStreamService.Enabled
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -229,16 +211,29 @@ public class VideoStreamController : ControllerBase
 | 
			
		||||
            logger.Info("测试 HTTP 视频流连接");
 | 
			
		||||
 | 
			
		||||
            // 尝试通过HTTP请求检查视频流服务是否可访问
 | 
			
		||||
            bool isConnected = false;
 | 
			
		||||
            using (var httpClient = new HttpClient())
 | 
			
		||||
            {
 | 
			
		||||
                httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
 | 
			
		||||
                var response = await httpClient.GetAsync($"http://localhost:{_videoStreamService.ServerPort}/");
 | 
			
		||||
 | 
			
		||||
                // 只要能连接上就认为成功,不管返回状态
 | 
			
		||||
                bool isConnected = response.IsSuccessStatusCode;
 | 
			
		||||
 | 
			
		||||
                return TypedResults.Ok(isConnected);
 | 
			
		||||
                isConnected = response.IsSuccessStatusCode;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.Info("测试摄像头连接");
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ class Camera
 | 
			
		||||
    public async ValueTask<Result<bool>> Init()
 | 
			
		||||
    {
 | 
			
		||||
        var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.timeout);
 | 
			
		||||
        var ret = await i2c.WriteData(0x78, new byte[] { 0x08, 0x30, 0x02 }, Peripherals.I2cClient.I2cProtocol.I2c);
 | 
			
		||||
        var ret = await i2c.WriteData(0x78, new byte[] { 0x30, 0x08, 0x02 }, Peripherals.I2cClient.I2cProtocol.I2c);
 | 
			
		||||
        if (!ret.IsSuccessful)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error($"I2C write failed during camera initialization for {this.address}:{this.port}, error: {ret.Error}");
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,73 @@ using Peripherals.CameraClient; // 添加摄像头客户端引用
 | 
			
		||||
 | 
			
		||||
namespace server.Services;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 表示摄像头连接状态信息
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class CameraStatus
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 摄像头的IP地址
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Address { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 摄像头的端口号
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int Port { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 是否已配置摄像头
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsConfigured { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 摄像头连接字符串(IP:端口)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ConnectionString { get; set; } = string.Empty;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// 表示视频流服务的运行状态
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class ServiceStatus
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 服务是否正在运行
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsRunning { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 服务监听的端口号
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int ServerPort { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 视频流的帧率(FPS)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int FrameRate { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 视频分辨率(如 640x480)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Resolution { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前连接的客户端数量
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int ConnectedClients { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 当前连接的客户端端点列表
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public List<string> ClientEndpoints { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 摄像头连接状态信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public CameraStatus CameraStatus { get; set; } = new();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
 | 
			
		||||
/// 支持动态配置摄像头地址和端口
 | 
			
		||||
@@ -28,6 +95,11 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    private readonly List<HttpListenerResponse> _activeClients = new List<HttpListenerResponse>();
 | 
			
		||||
    private readonly object _clientsLock = new object();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取 / 设置视频流服务是否启用
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Enabled { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取当前连接的客户端数量
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -94,7 +166,7 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await Task.Run(() =>
 | 
			
		||||
            await Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                lock (_cameraLock)
 | 
			
		||||
                {
 | 
			
		||||
@@ -122,6 +194,22 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
 | 
			
		||||
                    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;
 | 
			
		||||
        }
 | 
			
		||||
@@ -176,18 +264,15 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    /// 获取摄像头连接状态
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>连接状态信息</returns>
 | 
			
		||||
    public object GetCameraStatus()
 | 
			
		||||
    public CameraStatus GetCameraStatus()
 | 
			
		||||
    {
 | 
			
		||||
        lock (_cameraLock)
 | 
			
		||||
        return new CameraStatus
 | 
			
		||||
        {
 | 
			
		||||
            return new
 | 
			
		||||
            {
 | 
			
		||||
                Address = _cameraAddress,
 | 
			
		||||
                Port = _cameraPort,
 | 
			
		||||
                IsConfigured = _camera != null,
 | 
			
		||||
                ConnectionString = $"{_cameraAddress}:{_cameraPort}"
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
            Address = _cameraAddress,
 | 
			
		||||
            Port = _cameraPort,
 | 
			
		||||
            IsConfigured = _camera != null,
 | 
			
		||||
            ConnectionString = $"{_cameraAddress}:{_cameraPort}"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -215,7 +300,17 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
            _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken);
 | 
			
		||||
 | 
			
		||||
            // 开始生成视频帧
 | 
			
		||||
            await GenerateVideoFrames(stoppingToken);
 | 
			
		||||
            while (!stoppingToken.IsCancellationRequested)
 | 
			
		||||
            {
 | 
			
		||||
                if (Enabled)
 | 
			
		||||
                {
 | 
			
		||||
                    await GenerateVideoFrames(stoppingToken);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(500, stoppingToken);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (HttpListenerException ex)
 | 
			
		||||
        {
 | 
			
		||||
@@ -660,13 +755,13 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取服务状态信息
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public object GetServiceStatus()
 | 
			
		||||
    public ServiceStatus GetServiceStatus()
 | 
			
		||||
    {
 | 
			
		||||
        var cameraStatus = GetCameraStatus();
 | 
			
		||||
 | 
			
		||||
        return new
 | 
			
		||||
        return new ServiceStatus
 | 
			
		||||
        {
 | 
			
		||||
            IsRunning = _httpListener?.IsListening ?? false,
 | 
			
		||||
            IsRunning = (_httpListener?.IsListening ?? false) && Enabled,
 | 
			
		||||
            ServerPort = _serverPort,
 | 
			
		||||
            FrameRate = _frameRate,
 | 
			
		||||
            Resolution = $"{_frameWidth}x{_frameHeight}",
 | 
			
		||||
@@ -683,6 +778,8 @@ public class HttpVideoStreamService : BackgroundService
 | 
			
		||||
    {
 | 
			
		||||
        logger.Info("正在停止 HTTP 视频流服务...");
 | 
			
		||||
 | 
			
		||||
        Enabled = false;
 | 
			
		||||
 | 
			
		||||
        if (_httpListener != null && _httpListener.IsListening)
 | 
			
		||||
        {
 | 
			
		||||
            _httpListener.Stop();
 | 
			
		||||
 
 | 
			
		||||
@@ -216,11 +216,16 @@ export class VideoStreamClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 测试摄像头连接
 | 
			
		||||
     * @return 连接测试结果
 | 
			
		||||
     * 控制 HTTP 视频流服务开关
 | 
			
		||||
     * @param enabled (optional) 是否启用服务
 | 
			
		||||
     * @return 操作结果
 | 
			
		||||
     */
 | 
			
		||||
    testCameraConnection(): Promise<any> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/VideoStream/TestCameraConnection";
 | 
			
		||||
    setEnabled(enabled: boolean | undefined): Promise<any> {
 | 
			
		||||
        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_: RequestInit = {
 | 
			
		||||
@@ -231,11 +236,11 @@ export class VideoStreamClient {
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.http.fetch(url_, options_).then((_response: Response) => {
 | 
			
		||||
            return this.processTestCameraConnection(_response);
 | 
			
		||||
            return this.processSetEnabled(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processTestCameraConnection(response: Response): Promise<any> {
 | 
			
		||||
    protected processSetEnabled(response: Response): Promise<any> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
@@ -2541,4 +2546,4 @@ function throwException(message: string, status: number, response: string, heade
 | 
			
		||||
        throw result;
 | 
			
		||||
    else
 | 
			
		||||
        throw new ApiException(message, status, response, headers, null);
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -149,69 +149,40 @@
 | 
			
		||||
                </svg>
 | 
			
		||||
                摄像头配置
 | 
			
		||||
              </h3>
 | 
			
		||||
              <div class="flex flex-row justify-between items-center gap-4">
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label">
 | 
			
		||||
                    <span class="label-text">IP地址</span>
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    v-model="cameraConfig.address"
 | 
			
		||||
                    placeholder="例如: 192.168.1.100"
 | 
			
		||||
                    class="input input-bordered input-sm"
 | 
			
		||||
              
 | 
			
		||||
              <div class="flex flex-row justify-around gap-4">
 | 
			
		||||
                <div class="grow">
 | 
			
		||||
                  <IpInputField
 | 
			
		||||
                    v-model="tempCameraConfig.address"
 | 
			
		||||
                    required
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="form-control">
 | 
			
		||||
                  <label class="label">
 | 
			
		||||
                    <span class="label-text">端口号</span>
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="number"
 | 
			
		||||
                    v-model="cameraConfig.port"
 | 
			
		||||
                    placeholder="例如: 8080"
 | 
			
		||||
                    class="input input-bordered input-sm"
 | 
			
		||||
 | 
			
		||||
                <div class="grow">
 | 
			
		||||
                  <PortInputField
 | 
			
		||||
                    v-model="tempCameraConfig.port"
 | 
			
		||||
                    required
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div class="card-actions justify-end mt-4">
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-primary btn-sm"
 | 
			
		||||
                  @click="confirmCameraConfig"
 | 
			
		||||
                  :disabled="configuring"
 | 
			
		||||
                  class="btn btn-ghost"
 | 
			
		||||
                  @click="resetCameraConfig"
 | 
			
		||||
                  :disabled="isDefaultCamera"
 | 
			
		||||
                >
 | 
			
		||||
                  <svg
 | 
			
		||||
                    v-if="configuring"
 | 
			
		||||
                    class="animate-spin h-4 w-4 mr-1"
 | 
			
		||||
                    fill="none"
 | 
			
		||||
                    viewBox="0 0 24 24"
 | 
			
		||||
                  >
 | 
			
		||||
                    <circle
 | 
			
		||||
                      class="opacity-25"
 | 
			
		||||
                      cx="12"
 | 
			
		||||
                      cy="12"
 | 
			
		||||
                      r="10"
 | 
			
		||||
                      stroke="currentColor"
 | 
			
		||||
                      stroke-width="4"
 | 
			
		||||
                    ></circle>
 | 
			
		||||
                    <path
 | 
			
		||||
                      class="opacity-75"
 | 
			
		||||
                      fill="currentColor"
 | 
			
		||||
                      d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
 | 
			
		||||
                    ></path>
 | 
			
		||||
                  </svg>
 | 
			
		||||
                  <svg
 | 
			
		||||
                    v-else
 | 
			
		||||
                    class="w-4 h-4 mr-1"
 | 
			
		||||
                    fill="none"
 | 
			
		||||
                    stroke="currentColor"
 | 
			
		||||
                    viewBox="0 0 24 24"
 | 
			
		||||
                  >
 | 
			
		||||
                    <path
 | 
			
		||||
                      stroke-linecap="round"
 | 
			
		||||
                      stroke-linejoin="round"
 | 
			
		||||
                      stroke-width="2"
 | 
			
		||||
                      d="M5 13l4 4L19 7"
 | 
			
		||||
                    />
 | 
			
		||||
                  </svg>
 | 
			
		||||
                  {{ configuring ? "配置中..." : "确认" }}
 | 
			
		||||
                  <RotateCcw class="w-4 h-4" />
 | 
			
		||||
                  重置
 | 
			
		||||
                </button>
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-primary"
 | 
			
		||||
                  @click="confirmCameraConfig"
 | 
			
		||||
                  :disabled="!isValidCameraConfig || !hasChangesCamera"
 | 
			
		||||
                  :class="{ loading: configuring }"
 | 
			
		||||
                >
 | 
			
		||||
                  <Save class="w-4 h-4" v-if="!configuring" />
 | 
			
		||||
                  {{ configuring ? "配置中..." : "保存配置" }}
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@@ -540,8 +511,15 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, onUnmounted } from "vue";
 | 
			
		||||
import { ref, computed, reactive, watch, onMounted, onUnmounted } from "vue";
 | 
			
		||||
import { useStorage } from "@vueuse/core";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
import {
 | 
			
		||||
  Save,
 | 
			
		||||
  RotateCcw,
 | 
			
		||||
} from "lucide-vue-next";
 | 
			
		||||
import { VideoStreamClient, CameraConfigRequest } from "@/APIClient";
 | 
			
		||||
import { IpInputField, PortInputField } from "@/components/InputField";
 | 
			
		||||
 | 
			
		||||
// 状态管理
 | 
			
		||||
const loading = ref(false);
 | 
			
		||||
@@ -573,12 +551,92 @@ const streamInfo = ref({
 | 
			
		||||
  snapshotUrl: "",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 摄像头配置
 | 
			
		||||
const cameraConfig = ref({
 | 
			
		||||
// 摄像头配置类型定义
 | 
			
		||||
const cameraConfigSchema = z.object({
 | 
			
		||||
  address: z
 | 
			
		||||
    .string()
 | 
			
		||||
    .ip({ version: "v4", message: "请输入有效的IPv4地址" })
 | 
			
		||||
    .min(1, "请输入IP地址"),
 | 
			
		||||
  port: z
 | 
			
		||||
    .number()
 | 
			
		||||
    .int("端口必须是整数")
 | 
			
		||||
    .min(1, "端口必须大于0")
 | 
			
		||||
    .max(65535, "端口必须小于等于65535"),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
type CameraConfig = z.infer<typeof cameraConfigSchema>;
 | 
			
		||||
 | 
			
		||||
// 默认摄像头配置
 | 
			
		||||
const defaultCameraConfig: CameraConfig = {
 | 
			
		||||
  address: "192.168.1.100",
 | 
			
		||||
  port: 8080,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 使用 VueUse 存储摄像头配置
 | 
			
		||||
const cameraConfig = useStorage<CameraConfig>(
 | 
			
		||||
  "camera-config",
 | 
			
		||||
  defaultCameraConfig,
 | 
			
		||||
  localStorage,
 | 
			
		||||
  {
 | 
			
		||||
    serializer: {
 | 
			
		||||
      read: (value: string) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const parsed = JSON.parse(value);
 | 
			
		||||
          const result = cameraConfigSchema.safeParse(parsed);
 | 
			
		||||
          return result.success ? result.data : defaultCameraConfig;
 | 
			
		||||
        } catch {
 | 
			
		||||
          return defaultCameraConfig;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      write: (value: CameraConfig) => JSON.stringify(value),
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// 临时摄像头配置(用于编辑)
 | 
			
		||||
const tempCameraConfig = reactive<CameraConfig>({
 | 
			
		||||
  address: cameraConfig.value.address,
 | 
			
		||||
  port: cameraConfig.value.port,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 摄像头配置验证
 | 
			
		||||
const isValidCameraConfig = computed(() => {
 | 
			
		||||
  return tempCameraConfig.address && tempCameraConfig.port && 
 | 
			
		||||
         tempCameraConfig.port >= 1 && tempCameraConfig.port <= 65535 &&
 | 
			
		||||
         /^(\d{1,3}\.){3}\d{1,3}$/.test(tempCameraConfig.address);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 检查摄像头配置是否有更改
 | 
			
		||||
const hasChangesCamera = computed(() => {
 | 
			
		||||
  return (
 | 
			
		||||
    tempCameraConfig.address !== cameraConfig.value.address || 
 | 
			
		||||
    tempCameraConfig.port !== cameraConfig.value.port
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const isDefaultCamera = computed(() => {
 | 
			
		||||
  return (
 | 
			
		||||
    defaultCameraConfig.address === tempCameraConfig.address && 
 | 
			
		||||
    defaultCameraConfig.port === tempCameraConfig.port
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 重置摄像头配置
 | 
			
		||||
const resetCameraConfig = () => {
 | 
			
		||||
  tempCameraConfig.address = defaultCameraConfig.address;
 | 
			
		||||
  tempCameraConfig.port = defaultCameraConfig.port;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 监听存储的摄像头配置变化,同步到临时配置
 | 
			
		||||
watch(
 | 
			
		||||
  cameraConfig,
 | 
			
		||||
  (newConfig) => {
 | 
			
		||||
    tempCameraConfig.address = newConfig.address;
 | 
			
		||||
    tempCameraConfig.port = newConfig.port;
 | 
			
		||||
  },
 | 
			
		||||
  { deep: true },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const currentVideoSource = ref("");
 | 
			
		||||
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
 | 
			
		||||
 | 
			
		||||
@@ -605,8 +663,11 @@ const loadCameraConfig = async () => {
 | 
			
		||||
 | 
			
		||||
    const config = await videoClient.getCameraConfig();
 | 
			
		||||
    if (config && config.address && config.port) {
 | 
			
		||||
      cameraConfig.value.address = config.address;
 | 
			
		||||
      cameraConfig.value.port = config.port;
 | 
			
		||||
      // 更新存储的配置
 | 
			
		||||
      cameraConfig.value = {
 | 
			
		||||
        address: config.address,
 | 
			
		||||
        port: config.port,
 | 
			
		||||
      };
 | 
			
		||||
      addLog("success", `摄像头配置加载成功: ${config.address}:${config.port}`);
 | 
			
		||||
    } else {
 | 
			
		||||
      addLog("warning", "未找到保存的摄像头配置,使用默认值");
 | 
			
		||||
@@ -619,20 +680,30 @@ const loadCameraConfig = async () => {
 | 
			
		||||
 | 
			
		||||
// 确认摄像头配置
 | 
			
		||||
const confirmCameraConfig = async () => {
 | 
			
		||||
  if (!cameraConfig.value.address || !cameraConfig.value.port) {
 | 
			
		||||
    addLog("error", "请填写完整的摄像头IP地址和端口号");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!isValidCameraConfig.value) return;
 | 
			
		||||
 | 
			
		||||
  configuring.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    addLog(
 | 
			
		||||
      "info",
 | 
			
		||||
      `正在配置摄像头: ${cameraConfig.value.address}:${cameraConfig.value.port}`,
 | 
			
		||||
      `正在配置摄像头: ${tempCameraConfig.address}:${tempCameraConfig.port}`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 模拟保存延迟
 | 
			
		||||
    await new Promise((resolve) => setTimeout(resolve, 500));
 | 
			
		||||
 | 
			
		||||
    // 保存配置
 | 
			
		||||
    cameraConfig.value = {
 | 
			
		||||
      address: tempCameraConfig.address,
 | 
			
		||||
      port: tempCameraConfig.port,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 使用新配置调用API
 | 
			
		||||
    const result = await videoClient.configureCamera(
 | 
			
		||||
      CameraConfigRequest.fromJS(cameraConfig.value),
 | 
			
		||||
      CameraConfigRequest.fromJS({
 | 
			
		||||
        address: tempCameraConfig.address,
 | 
			
		||||
        port: tempCameraConfig.port,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (result) {
 | 
			
		||||
@@ -650,35 +721,6 @@ const confirmCameraConfig = async () => {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 测试摄像头连接
 | 
			
		||||
const testCameraConnection = async () => {
 | 
			
		||||
  if (!cameraConfig.value.address || !cameraConfig.value.port) {
 | 
			
		||||
    addLog("error", "请先配置摄像头IP地址和端口号");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  testingCamera.value = true;
 | 
			
		||||
  try {
 | 
			
		||||
    addLog(
 | 
			
		||||
      "info",
 | 
			
		||||
      `正在测试摄像头连接: ${cameraConfig.value.address}:${cameraConfig.value.port}`,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const result = await videoClient.testCameraConnection();
 | 
			
		||||
 | 
			
		||||
    if (result && result.success) {
 | 
			
		||||
      addLog("success", "摄像头连接测试成功");
 | 
			
		||||
    } else {
 | 
			
		||||
      addLog("error", `摄像头连接测试失败: ${result?.message || "未知错误"}`);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    addLog("error", `摄像头连接测试失败: ${error}`);
 | 
			
		||||
    console.error("摄像头连接测试失败:", error);
 | 
			
		||||
  } finally {
 | 
			
		||||
    testingCamera.value = false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 格式化时间
 | 
			
		||||
const formatTime = (time: Date) => {
 | 
			
		||||
  return time.toLocaleTimeString();
 | 
			
		||||
@@ -823,6 +865,7 @@ const startStream = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    addLog("info", "正在启动视频流...");
 | 
			
		||||
    videoStatus.value = "正在连接视频流...";
 | 
			
		||||
    videoClient.setEnabled(true);
 | 
			
		||||
 | 
			
		||||
    // 刷新状态
 | 
			
		||||
    await refreshStatus();
 | 
			
		||||
@@ -846,6 +889,7 @@ const startStream = async () => {
 | 
			
		||||
const stopStream = () => {
 | 
			
		||||
  try {
 | 
			
		||||
    addLog("info", "正在停止视频流...");
 | 
			
		||||
    videoClient.setEnabled(false);
 | 
			
		||||
 | 
			
		||||
    // 清除视频源
 | 
			
		||||
    currentVideoSource.value = "";
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user