feat: 更新api,并更新了串流页面
This commit is contained in:
parent
67bdec8570
commit
443aea5e3e
|
@ -14,6 +14,11 @@ public class TutorialController : ControllerBase
|
||||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||||
private readonly IWebHostEnvironment _environment;
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [TODO:description]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="environment">[TODO:parameter]</param>
|
||||||
|
/// <returns>[TODO:return]</returns>
|
||||||
public TutorialController(IWebHostEnvironment environment)
|
public TutorialController(IWebHostEnvironment environment)
|
||||||
{
|
{
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
|
|
|
@ -109,6 +109,7 @@ public class UDPController : ControllerBase
|
||||||
/// 获取指定IP地址接收的数据列表
|
/// 获取指定IP地址接收的数据列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">IP地址</param>
|
/// <param name="address">IP地址</param>
|
||||||
|
/// <param name="taskID">任务ID</param>
|
||||||
[HttpGet("GetRecvDataArray")]
|
[HttpGet("GetRecvDataArray")]
|
||||||
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 视频流控制器,支持动态配置摄像头连接
|
/// 视频流控制器,支持动态配置摄像头连接
|
||||||
|
@ -17,10 +17,16 @@ public class VideoStreamController : ControllerBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CameraConfigRequest
|
public class CameraConfigRequest
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 摄像头地址
|
||||||
|
/// </summary>
|
||||||
[Required]
|
[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地址")]
|
[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; } = "";
|
public string Address { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 摄像头端口
|
||||||
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
[Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
|
[Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
|
@ -54,17 +60,7 @@ public class VideoStreamController : ControllerBase
|
||||||
var status = _videoStreamService.GetServiceStatus();
|
var status = _videoStreamService.GetServiceStatus();
|
||||||
|
|
||||||
// 转换为小写首字母的JSON属性(符合前端惯例)
|
// 转换为小写首字母的JSON属性(符合前端惯例)
|
||||||
return TypedResults.Ok(new
|
return TypedResults.Ok(status);
|
||||||
{
|
|
||||||
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()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -95,8 +91,6 @@ public class VideoStreamController : ControllerBase
|
||||||
htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
|
htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
|
||||||
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
|
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
|
||||||
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
|
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
|
||||||
cameraAddress = _videoStreamService.CameraAddress,
|
|
||||||
cameraPort = _videoStreamService.CameraPort
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -183,35 +177,23 @@ public class VideoStreamController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试摄像头连接
|
/// 控制 HTTP 视频流服务开关
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>连接测试结果</returns>
|
/// <param name="enabled">是否启用服务</param>
|
||||||
[HttpPost("TestCameraConnection")]
|
/// <returns>操作结果</returns>
|
||||||
|
[HttpPost("SetEnabled")]
|
||||||
[EnableCors("Users")]
|
[EnableCors("Users")]
|
||||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
[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("测试摄像头连接");
|
success = true,
|
||||||
|
enabled = _videoStreamService.Enabled
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -229,16 +211,29 @@ public class VideoStreamController : ControllerBase
|
||||||
logger.Info("测试 HTTP 视频流连接");
|
logger.Info("测试 HTTP 视频流连接");
|
||||||
|
|
||||||
// 尝试通过HTTP请求检查视频流服务是否可访问
|
// 尝试通过HTTP请求检查视频流服务是否可访问
|
||||||
|
bool isConnected = false;
|
||||||
using (var httpClient = new HttpClient())
|
using (var httpClient = new HttpClient())
|
||||||
{
|
{
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
|
httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
|
||||||
var response = await httpClient.GetAsync($"http://localhost:{_videoStreamService.ServerPort}/");
|
var response = await httpClient.GetAsync($"http://localhost:{_videoStreamService.ServerPort}/");
|
||||||
|
|
||||||
// 只要能连接上就认为成功,不管返回状态
|
// 只要能连接上就认为成功,不管返回状态
|
||||||
bool isConnected = response.IsSuccessStatusCode;
|
isConnected = response.IsSuccessStatusCode;
|
||||||
|
|
||||||
return TypedResults.Ok(isConnected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Camera
|
||||||
public async ValueTask<Result<bool>> Init()
|
public async ValueTask<Result<bool>> Init()
|
||||||
{
|
{
|
||||||
var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.timeout);
|
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)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"I2C write failed during camera initialization for {this.address}:{this.port}, error: {ret.Error}");
|
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;
|
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>
|
/// <summary>
|
||||||
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
|
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
|
||||||
/// 支持动态配置摄像头地址和端口
|
/// 支持动态配置摄像头地址和端口
|
||||||
|
@ -28,6 +95,11 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
private readonly List<HttpListenerResponse> _activeClients = new List<HttpListenerResponse>();
|
private readonly List<HttpListenerResponse> _activeClients = new List<HttpListenerResponse>();
|
||||||
private readonly object _clientsLock = new object();
|
private readonly object _clientsLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 / 设置视频流服务是否启用
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前连接的客户端数量
|
/// 获取当前连接的客户端数量
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -94,7 +166,7 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
await Task.Run(async () =>
|
||||||
{
|
{
|
||||||
lock (_cameraLock)
|
lock (_cameraLock)
|
||||||
{
|
{
|
||||||
|
@ -122,6 +194,22 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
|
|
||||||
logger.Info("摄像头配置已更新: {Address}:{Port}", _cameraAddress, _cameraPort);
|
logger.Info("摄像头配置已更新: {Address}:{Port}", _cameraAddress, _cameraPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init Camera
|
||||||
|
{
|
||||||
|
var ret = await _camera.Init();
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error(ret.Error);
|
||||||
|
throw ret.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret.Value)
|
||||||
|
{
|
||||||
|
logger.Error($"Camera Init Failed!");
|
||||||
|
throw new Exception($"Camera Init Failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -176,18 +264,15 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
/// 获取摄像头连接状态
|
/// 获取摄像头连接状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>连接状态信息</returns>
|
/// <returns>连接状态信息</returns>
|
||||||
public object GetCameraStatus()
|
public CameraStatus GetCameraStatus()
|
||||||
{
|
{
|
||||||
lock (_cameraLock)
|
return new CameraStatus
|
||||||
{
|
{
|
||||||
return new
|
Address = _cameraAddress,
|
||||||
{
|
Port = _cameraPort,
|
||||||
Address = _cameraAddress,
|
IsConfigured = _camera != null,
|
||||||
Port = _cameraPort,
|
ConnectionString = $"{_cameraAddress}:{_cameraPort}"
|
||||||
IsConfigured = _camera != null,
|
};
|
||||||
ConnectionString = $"{_cameraAddress}:{_cameraPort}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -215,7 +300,17 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
_ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken);
|
_ = 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)
|
catch (HttpListenerException ex)
|
||||||
{
|
{
|
||||||
|
@ -660,13 +755,13 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取服务状态信息
|
/// 获取服务状态信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object GetServiceStatus()
|
public ServiceStatus GetServiceStatus()
|
||||||
{
|
{
|
||||||
var cameraStatus = GetCameraStatus();
|
var cameraStatus = GetCameraStatus();
|
||||||
|
|
||||||
return new
|
return new ServiceStatus
|
||||||
{
|
{
|
||||||
IsRunning = _httpListener?.IsListening ?? false,
|
IsRunning = (_httpListener?.IsListening ?? false) && Enabled,
|
||||||
ServerPort = _serverPort,
|
ServerPort = _serverPort,
|
||||||
FrameRate = _frameRate,
|
FrameRate = _frameRate,
|
||||||
Resolution = $"{_frameWidth}x{_frameHeight}",
|
Resolution = $"{_frameWidth}x{_frameHeight}",
|
||||||
|
@ -683,6 +778,8 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
{
|
{
|
||||||
logger.Info("正在停止 HTTP 视频流服务...");
|
logger.Info("正在停止 HTTP 视频流服务...");
|
||||||
|
|
||||||
|
Enabled = false;
|
||||||
|
|
||||||
if (_httpListener != null && _httpListener.IsListening)
|
if (_httpListener != null && _httpListener.IsListening)
|
||||||
{
|
{
|
||||||
_httpListener.Stop();
|
_httpListener.Stop();
|
||||||
|
|
|
@ -216,11 +216,16 @@ export class VideoStreamClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试摄像头连接
|
* 控制 HTTP 视频流服务开关
|
||||||
* @return 连接测试结果
|
* @param enabled (optional) 是否启用服务
|
||||||
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
testCameraConnection(): Promise<any> {
|
setEnabled(enabled: boolean | undefined): Promise<any> {
|
||||||
let url_ = this.baseUrl + "/api/VideoStream/TestCameraConnection";
|
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(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
let options_: RequestInit = {
|
let options_: RequestInit = {
|
||||||
|
@ -231,11 +236,11 @@ export class VideoStreamClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
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;
|
const status = response.status;
|
||||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
|
|
|
@ -149,69 +149,40 @@
|
||||||
</svg>
|
</svg>
|
||||||
摄像头配置
|
摄像头配置
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex flex-row justify-between items-center gap-4">
|
|
||||||
<div class="form-control">
|
<div class="flex flex-row justify-around gap-4">
|
||||||
<label class="label">
|
<div class="grow">
|
||||||
<span class="label-text">IP地址</span>
|
<IpInputField
|
||||||
</label>
|
v-model="tempCameraConfig.address"
|
||||||
<input
|
required
|
||||||
type="text"
|
|
||||||
v-model="cameraConfig.address"
|
|
||||||
placeholder="例如: 192.168.1.100"
|
|
||||||
class="input input-bordered input-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
<div class="grow">
|
||||||
<span class="label-text">端口号</span>
|
<PortInputField
|
||||||
</label>
|
v-model="tempCameraConfig.port"
|
||||||
<input
|
required
|
||||||
type="number"
|
|
||||||
v-model="cameraConfig.port"
|
|
||||||
placeholder="例如: 8080"
|
|
||||||
class="input input-bordered input-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-actions justify-end mt-4">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-ghost"
|
||||||
@click="confirmCameraConfig"
|
@click="resetCameraConfig"
|
||||||
:disabled="configuring"
|
:disabled="isDefaultCamera"
|
||||||
>
|
>
|
||||||
<svg
|
<RotateCcw class="w-4 h-4" />
|
||||||
v-if="configuring"
|
重置
|
||||||
class="animate-spin h-4 w-4 mr-1"
|
</button>
|
||||||
fill="none"
|
<button
|
||||||
viewBox="0 0 24 24"
|
class="btn btn-primary"
|
||||||
>
|
@click="confirmCameraConfig"
|
||||||
<circle
|
:disabled="!isValidCameraConfig || !hasChangesCamera"
|
||||||
class="opacity-25"
|
:class="{ loading: configuring }"
|
||||||
cx="12"
|
>
|
||||||
cy="12"
|
<Save class="w-4 h-4" v-if="!configuring" />
|
||||||
r="10"
|
{{ configuring ? "配置中..." : "保存配置" }}
|
||||||
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 ? "配置中..." : "确认" }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -540,8 +511,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { VideoStreamClient, CameraConfigRequest } from "@/APIClient";
|
||||||
|
import { IpInputField, PortInputField } from "@/components/InputField";
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
@ -573,12 +551,92 @@ const streamInfo = ref({
|
||||||
snapshotUrl: "",
|
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",
|
address: "192.168.1.100",
|
||||||
port: 8080,
|
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 currentVideoSource = ref("");
|
||||||
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
|
const logs = ref<Array<{ time: Date; level: string; message: string }>>([]);
|
||||||
|
|
||||||
|
@ -605,8 +663,11 @@ const loadCameraConfig = async () => {
|
||||||
|
|
||||||
const config = await videoClient.getCameraConfig();
|
const config = await videoClient.getCameraConfig();
|
||||||
if (config && config.address && config.port) {
|
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}`);
|
addLog("success", `摄像头配置加载成功: ${config.address}:${config.port}`);
|
||||||
} else {
|
} else {
|
||||||
addLog("warning", "未找到保存的摄像头配置,使用默认值");
|
addLog("warning", "未找到保存的摄像头配置,使用默认值");
|
||||||
|
@ -619,20 +680,30 @@ const loadCameraConfig = async () => {
|
||||||
|
|
||||||
// 确认摄像头配置
|
// 确认摄像头配置
|
||||||
const confirmCameraConfig = async () => {
|
const confirmCameraConfig = async () => {
|
||||||
if (!cameraConfig.value.address || !cameraConfig.value.port) {
|
if (!isValidCameraConfig.value) return;
|
||||||
addLog("error", "请填写完整的摄像头IP地址和端口号");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
configuring.value = true;
|
configuring.value = true;
|
||||||
try {
|
try {
|
||||||
addLog(
|
addLog(
|
||||||
"info",
|
"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(
|
const result = await videoClient.configureCamera(
|
||||||
CameraConfigRequest.fromJS(cameraConfig.value),
|
CameraConfigRequest.fromJS({
|
||||||
|
address: tempCameraConfig.address,
|
||||||
|
port: tempCameraConfig.port,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result) {
|
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) => {
|
const formatTime = (time: Date) => {
|
||||||
return time.toLocaleTimeString();
|
return time.toLocaleTimeString();
|
||||||
|
@ -823,6 +865,7 @@ const startStream = async () => {
|
||||||
try {
|
try {
|
||||||
addLog("info", "正在启动视频流...");
|
addLog("info", "正在启动视频流...");
|
||||||
videoStatus.value = "正在连接视频流...";
|
videoStatus.value = "正在连接视频流...";
|
||||||
|
videoClient.setEnabled(true);
|
||||||
|
|
||||||
// 刷新状态
|
// 刷新状态
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
|
@ -846,6 +889,7 @@ const startStream = async () => {
|
||||||
const stopStream = () => {
|
const stopStream = () => {
|
||||||
try {
|
try {
|
||||||
addLog("info", "正在停止视频流...");
|
addLog("info", "正在停止视频流...");
|
||||||
|
videoClient.setEnabled(false);
|
||||||
|
|
||||||
// 清除视频源
|
// 清除视频源
|
||||||
currentVideoSource.value = "";
|
currentVideoSource.value = "";
|
||||||
|
|
Loading…
Reference in New Issue