diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs index fa936c3..9a70219 100644 --- a/server/src/Controllers/VideoStreamController.cs +++ b/server/src/Controllers/VideoStreamController.cs @@ -1,8 +1,9 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; /// -/// [TODO:description] +/// 视频流控制器,支持动态配置摄像头连接 /// [ApiController] [Route("api/[controller]")] @@ -11,6 +12,20 @@ public class VideoStreamController : ControllerBase private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private readonly server.Services.HttpVideoStreamService _videoStreamService; + /// + /// 摄像头配置请求模型 + /// + public class CameraConfigRequest + { + [Required] + [RegularExpression(@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ErrorMessage = "请输入有效的IP地址")] + public string Address { get; set; } = ""; + + [Required] + [Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")] + public int Port { get; set; } + } + /// /// 初始化HTTP视频流控制器 /// @@ -47,12 +62,14 @@ public class VideoStreamController : ControllerBase mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream", snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot", connectedClients = _videoStreamService.ConnectedClientsCount, - clientEndpoints = _videoStreamService.GetConnectedClientEndpoints() + clientEndpoints = _videoStreamService.GetConnectedClientEndpoints(), + cameraStatus = _videoStreamService.GetCameraStatus() }); } catch (Exception ex) { - logger.Error(ex, "获取 HTTP 视频流服务状态失败"); return TypedResults.InternalServerError(ex.Message); + logger.Error(ex, "获取 HTTP 视频流服务状态失败"); + return TypedResults.InternalServerError(ex.Message); } } @@ -77,12 +94,123 @@ public class VideoStreamController : ControllerBase format = "MJPEG", htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html", 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) { - logger.Error(ex, "获取 HTTP 视频流信息失败"); return TypedResults.InternalServerError(ex.Message); + logger.Error(ex, "获取 HTTP 视频流信息失败"); + return TypedResults.InternalServerError(ex.Message); + } + } + + /// + /// 配置摄像头连接参数 + /// + /// 摄像头配置 + /// 配置结果 + [HttpPost("ConfigureCamera")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + public async Task ConfigureCamera([FromBody] CameraConfigRequest config) + { + try + { + logger.Info("配置摄像头连接: {Address}:{Port}", config.Address, config.Port); + + var success = await _videoStreamService.ConfigureCameraAsync(config.Address, config.Port); + + if (success) + { + return TypedResults.Ok(new + { + success = true, + message = "摄像头配置成功", + cameraAddress = config.Address, + cameraPort = config.Port + }); + } + else + { + return TypedResults.BadRequest(new + { + success = false, + message = "摄像头配置失败", + cameraAddress = config.Address, + cameraPort = config.Port + }); + } + } + catch (Exception ex) + { + logger.Error(ex, "配置摄像头连接失败"); + return TypedResults.InternalServerError(ex.Message); + } + } + + /// + /// 获取当前摄像头配置 + /// + /// 摄像头配置信息 + [HttpGet("CameraConfig")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + public IResult GetCameraConfig() + { + try + { + logger.Info("获取摄像头配置"); + var cameraStatus = _videoStreamService.GetCameraStatus(); + + return TypedResults.Ok(new + { + address = _videoStreamService.CameraAddress, + port = _videoStreamService.CameraPort, + isConfigured = cameraStatus.GetType().GetProperty("IsConfigured")?.GetValue(cameraStatus), + connectionString = $"{_videoStreamService.CameraAddress}:{_videoStreamService.CameraPort}" + }); + } + catch (Exception ex) + { + logger.Error(ex, "获取摄像头配置失败"); + return TypedResults.InternalServerError(ex.Message); + } + } + + /// + /// 测试摄像头连接 + /// + /// 连接测试结果 + [HttpPost("TestCameraConnection")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + public async Task TestCameraConnection() + { + try + { + 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); } } diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs index 99bbbb3..803a8ee 100644 --- a/server/src/Services/HttpVideoStreamService.cs +++ b/server/src/Services/HttpVideoStreamService.cs @@ -1,15 +1,12 @@ using System.Net; using System.Text; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.PixelFormats; using Peripherals.CameraClient; // 添加摄像头客户端引用 namespace server.Services; /// /// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页 -/// 简化版本实现,先建立基础框架 +/// 支持动态配置摄像头地址和端口 /// public class HttpVideoStreamService : BackgroundService { @@ -22,8 +19,9 @@ public class HttpVideoStreamService : BackgroundService // 摄像头客户端 private Camera? _camera; - private readonly string _cameraAddress = "192.168.1.100"; // 根据实际FPGA地址配置 - private readonly int _cameraPort = 8888; // 根据实际端口配置 + private string _cameraAddress = "192.168.1.100"; // 默认FPGA地址 + private int _cameraPort = 8888; // 默认端口 + private readonly object _cameraLock = new object(); // 模拟 FPGA 图像数据 private int _frameCounter = 0; @@ -33,16 +31,7 @@ public class HttpVideoStreamService : BackgroundService /// /// 获取当前连接的客户端数量 /// - public int ConnectedClientsCount - { - get - { - lock (_clientsLock) - { - return _activeClients.Count; - } - } - } + public int ConnectedClientsCount { get { return _activeClients.Count; } } /// /// 获取服务端口 @@ -64,13 +53,141 @@ public class HttpVideoStreamService : BackgroundService /// public int FrameRate => _frameRate; + /// + /// 获取当前摄像头地址 + /// + public string CameraAddress { get { return _cameraAddress; } } + + /// + /// 获取当前摄像头端口 + /// + public int CameraPort { get { return _cameraPort; } } + /// /// 初始化 HttpVideoStreamService /// public HttpVideoStreamService() { - // 初始化摄像头客户端 - _camera = new Camera(_cameraAddress, _cameraPort); + // 延迟初始化摄像头客户端,直到配置完成 + logger.Info("HttpVideoStreamService 初始化完成,默认摄像头地址: {Address}:{Port}", _cameraAddress, _cameraPort); + } + + /// + /// 配置摄像头连接参数 + /// + /// 摄像头IP地址 + /// 摄像头端口 + /// 配置是否成功 + public async Task ConfigureCameraAsync(string address, int port) + { + if (string.IsNullOrWhiteSpace(address)) + { + logger.Error("摄像头地址不能为空"); + return false; + } + + if (port <= 0 || port > 65535) + { + logger.Error("摄像头端口必须在1-65535范围内"); + return false; + } + + try + { + await Task.Run(() => + { + lock (_cameraLock) + { + // 如果地址和端口没有变化,直接返回成功 + if (_cameraAddress == address && _cameraPort == port && _camera != null) + { + logger.Info("摄像头配置未变化,保持当前连接"); + return; + } + + // 关闭现有连接 + if (_camera != null) + { + logger.Info("关闭现有摄像头连接"); + // Camera doesn't have Dispose method, set to null + _camera = null; + } + + // 更新配置 + _cameraAddress = address; + _cameraPort = port; + + // 创建新的摄像头客户端 + _camera = new Camera(_cameraAddress, _cameraPort); + + logger.Info("摄像头配置已更新: {Address}:{Port}", _cameraAddress, _cameraPort); + } + }); + return true; + } + catch (Exception ex) + { + logger.Error(ex, "配置摄像头连接时发生错误"); + return false; + } + } + + /// + /// 测试摄像头连接 + /// + /// 连接测试结果 + public async Task<(bool IsSuccess, string Message)> TestCameraConnectionAsync() + { + try + { + Camera? testCamera = null; + + lock (_cameraLock) + { + if (_camera == null) + { + return (false, "摄像头未配置"); + } + testCamera = _camera; + } + + // 尝试读取一帧数据来测试连接 + var result = await testCamera.ReadFrame(); + + if (result.IsSuccessful) + { + logger.Info("摄像头连接测试成功: {Address}:{Port}", _cameraAddress, _cameraPort); + return (true, "连接成功"); + } + else + { + logger.Warn("摄像头连接测试失败: {Error}", result.Error); + return (false, result.Error.ToString()); + } + } + catch (Exception ex) + { + logger.Error(ex, "摄像头连接测试出错"); + return (false, ex.Message); + } + } + + /// + /// 获取摄像头连接状态 + /// + /// 连接状态信息 + public object GetCameraStatus() + { + lock (_cameraLock) + { + return new + { + Address = _cameraAddress, + Port = _cameraPort, + IsConfigured = _camera != null, + ConnectionString = $"{_cameraAddress}:{_cameraPort}" + }; + } } /// @@ -83,6 +200,10 @@ public class HttpVideoStreamService : BackgroundService try { logger.Info("启动 HTTP 视频流服务,端口: {Port}", _serverPort); + + // 初始化默认摄像头连接 + await ConfigureCameraAsync(_cameraAddress, _cameraPort); + // 创建 HTTP 监听器 _httpListener = new HttpListener(); _httpListener.Prefixes.Add($"http://localhost:{_serverPort}/"); @@ -220,7 +341,18 @@ public class HttpVideoStreamService : BackgroundService { // 获取当前帧 var imageData = await GetFPGAImageData(); - var jpegData = ConvertToJpeg(imageData); + + // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换 + var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, _frameWidth, _frameHeight, 80); + if (!jpegResult.IsSuccessful) + { + logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); + response.StatusCode = 500; + response.Close(); + return; + } + + var jpegData = jpegResult.Value; // 设置响应头 response.ContentType = "image/jpeg"; @@ -338,11 +470,8 @@ public class HttpVideoStreamService : BackgroundService // 从 FPGA 获取图像数据(模拟) var imageData = await GetFPGAImageData(); - // 将图像数据转换为 JPEG - var jpegData = ConvertToJpeg(imageData); - // 向所有连接的客户端发送帧 - await BroadcastFrameAsync(jpegData, cancellationToken); + await BroadcastFrameAsync(imageData, cancellationToken); _frameCounter++; @@ -367,7 +496,14 @@ public class HttpVideoStreamService : BackgroundService /// private async Task GetFPGAImageData() { - if (_camera == null) + Camera? currentCamera = null; + + lock (_cameraLock) + { + currentCamera = _camera; + } + + if (currentCamera == null) { logger.Error("摄像头客户端未初始化"); return new byte[0]; @@ -376,8 +512,8 @@ public class HttpVideoStreamService : BackgroundService try { // 从摄像头读取帧数据 - var result = await _camera.ReadFrame(); - + var result = await currentCamera.ReadFrame(); + if (!result.IsSuccessful) { logger.Error("读取摄像头帧数据失败: {Error}", result.Error); @@ -385,11 +521,11 @@ public class HttpVideoStreamService : BackgroundService } var rgb565Data = result.Value; - + // 验证数据长度是否正确 if (!Common.Image.ValidateImageDataLength(rgb565Data, _frameWidth, _frameHeight, 2)) { - logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}", + logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}", _frameWidth * _frameHeight * 2, rgb565Data.Length); } @@ -403,7 +539,7 @@ public class HttpVideoStreamService : BackgroundService if (_frameCounter % 30 == 0) // 每秒更新一次日志 { - logger.Debug("成功获取第 {FrameNumber} 帧,RGB565大小: {RGB565Size} 字节, RGB24大小: {RGB24Size} 字节", + logger.Debug("成功获取第 {FrameNumber} 帧,RGB565大小: {RGB565Size} 字节, RGB24大小: {RGB24Size} 字节", _frameCounter, rgb565Data.Length, rgb24Result.Value.Length); } @@ -416,18 +552,6 @@ public class HttpVideoStreamService : BackgroundService } } - private async Task ConvertToJpeg(byte[] rgbData) - { - var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgbData, _frameWidth, _frameHeight, 80); - if (!jpegResult.IsSuccessful) - { - logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); - return new byte[0]; - } - - return jpegResult.Value; - } - /// /// 向所有连接的客户端广播帧数据 /// @@ -439,8 +563,18 @@ public class HttpVideoStreamService : BackgroundService return; } + // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换 + var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, _frameWidth, _frameHeight, 80); + if (!jpegResult.IsSuccessful) + { + logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); + return; + } + + var jpegData = jpegResult.Value; + // 使用Common中的方法准备MJPEG帧数据 - var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(frameData.Length); + var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length); var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter(); var clientsToRemove = new List(); @@ -466,7 +600,7 @@ public class HttpVideoStreamService : BackgroundService await client.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken); // 发送JPEG数据 - await client.OutputStream.WriteAsync(frameData, 0, frameData.Length, cancellationToken); + await client.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); // 发送结尾换行符 await client.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken); @@ -477,7 +611,7 @@ public class HttpVideoStreamService : BackgroundService if (_frameCounter % 30 == 0) // 每秒记录一次日志 { logger.Debug("已向客户端 {ClientId} 发送第 {FrameNumber} 帧,大小:{Size} 字节", - client.OutputStream.GetHashCode(), _frameCounter, frameData.Length); + client.OutputStream.GetHashCode(), _frameCounter, jpegData.Length); } } catch (Exception ex) @@ -528,6 +662,8 @@ public class HttpVideoStreamService : BackgroundService /// public object GetServiceStatus() { + var cameraStatus = GetCameraStatus(); + return new { IsRunning = _httpListener?.IsListening ?? false, @@ -535,7 +671,8 @@ public class HttpVideoStreamService : BackgroundService FrameRate = _frameRate, Resolution = $"{_frameWidth}x{_frameHeight}", ConnectedClients = ConnectedClientsCount, - ClientEndpoints = GetConnectedClientEndpoints() + ClientEndpoints = GetConnectedClientEndpoints(), + CameraStatus = cameraStatus }; } @@ -563,6 +700,12 @@ public class HttpVideoStreamService : BackgroundService _activeClients.Clear(); } + // 关闭摄像头连接 + lock (_cameraLock) + { + _camera = null; + } + await base.StopAsync(cancellationToken); logger.Info("HTTP 视频流服务已停止"); @@ -592,6 +735,11 @@ public class HttpVideoStreamService : BackgroundService _activeClients.Clear(); } + lock (_cameraLock) + { + _camera = null; + } + base.Dispose(); } } diff --git a/src/APIClient.ts b/src/APIClient.ts index 639e779..4d88dbe 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -8,6 +8,306 @@ /* eslint-disable */ // ReSharper disable InconsistentNaming +export class VideoStreamClient { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? "http://localhost:5000"; + } + + /** + * 获取 HTTP 视频流服务状态 + * @return 服务状态信息 + */ + getStatus(): Promise { + let url_ = this.baseUrl + "/api/VideoStream/Status"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetStatus(_response); + }); + } + + protected processGetStatus(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result500 = Exception.fromJS(resultData500); + return throwException("A server side error occurred.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * 获取 HTTP 视频流信息 + * @return 流信息 + */ + getStreamInfo(): Promise { + let url_ = this.baseUrl + "/api/VideoStream/StreamInfo"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetStreamInfo(_response); + }); + } + + protected processGetStreamInfo(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result500 = Exception.fromJS(resultData500); + return throwException("A server side error occurred.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * 配置摄像头连接参数 + * @param config 摄像头配置 + * @return 配置结果 + */ + configureCamera(config: CameraConfigRequest): Promise { + let url_ = this.baseUrl + "/api/VideoStream/ConfigureCamera"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(config); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processConfigureCamera(_response); + }); + } + + protected processConfigureCamera(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 400) { + return response.text().then((_responseText) => { + let result400: any = null; + let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result400 = resultData400 !== undefined ? resultData400 : null; + + return throwException("A server side error occurred.", status, _responseText, _headers, result400); + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result500 = Exception.fromJS(resultData500); + return throwException("A server side error occurred.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * 获取当前摄像头配置 + * @return 摄像头配置信息 + */ + getCameraConfig(): Promise { + let url_ = this.baseUrl + "/api/VideoStream/CameraConfig"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetCameraConfig(_response); + }); + } + + protected processGetCameraConfig(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result500 = Exception.fromJS(resultData500); + return throwException("A server side error occurred.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * 测试摄像头连接 + * @return 连接测试结果 + */ + testCameraConnection(): Promise { + let url_ = this.baseUrl + "/api/VideoStream/TestCameraConnection"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "POST", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processTestCameraConnection(_response); + }); + } + + protected processTestCameraConnection(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result500 = Exception.fromJS(resultData500); + return throwException("A server side error occurred.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + + /** + * 测试 HTTP 视频流连接 + * @return 连接测试结果 + */ + testConnection(): Promise { + let url_ = this.baseUrl + "/api/VideoStream/TestConnection"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "POST", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processTestConnection(_response); + }); + } + + protected processTestConnection(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + let result200: any = null; + let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result200 = resultData200 !== undefined ? resultData200 : null; + + return result200; + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + let result500: any = null; + let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); + result500 = Exception.fromJS(resultData500); + return throwException("A server side error occurred.", status, _responseText, _headers, result500); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } +} + export class BsdlParserClient { private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; private baseUrl: string; @@ -1542,6 +1842,55 @@ export class RemoteUpdateClient { } } +export class TutorialClient { + private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? "http://localhost:5000"; + } + + /** + * 获取所有可用的教程目录 + * @return 教程目录列表 + */ + getTutorials(): Promise { + let url_ = this.baseUrl + "/api/Tutorial"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetTutorials(_response); + }); + } + + protected processGetTutorials(response: Response): Promise { + 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) { + return response.text().then((_responseText) => { + return; + }); + } else if (status === 500) { + return response.text().then((_responseText) => { + return throwException("A server side error occurred.", status, _responseText, _headers); + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } +} + export class UDPClient { private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; private baseUrl: string; @@ -1800,15 +2149,20 @@ export class UDPClient { } /** - * 获取指定IP地址接受的数据列表 + * 获取指定IP地址接收的数据列表 * @param address (optional) IP地址 + * @param taskID (optional) */ - getRecvDataArray(address: string | undefined): Promise { + getRecvDataArray(address: string | undefined, taskID: number | undefined): Promise { let url_ = this.baseUrl + "/api/UDP/GetRecvDataArray?"; if (address === null) throw new Error("The parameter 'address' cannot be null."); else if (address !== undefined) url_ += "address=" + encodeURIComponent("" + address) + "&"; + if (taskID === null) + throw new Error("The parameter 'taskID' cannot be null."); + else if (taskID !== undefined) + url_ += "taskID=" + encodeURIComponent("" + taskID) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { @@ -1853,55 +2207,6 @@ export class UDPClient { } } -export class TutorialClient { - private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; - private baseUrl: string; - protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; - - constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { - this.http = http ? http : window as any; - this.baseUrl = baseUrl ?? "http://localhost:5000"; - } - - /** - * 获取所有可用的教程目录 - * @return 教程目录列表 - */ - getTutorials(): Promise { - let url_ = this.baseUrl + "/api/Tutorial"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "GET", - headers: { - } - }; - - return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processGetTutorials(_response); - }); - } - - protected processGetTutorials(response: Response): Promise { - 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) { - return response.text().then((_responseText) => { - return; - }); - } else if (status === 500) { - return response.text().then((_responseText) => { - return throwException("A server side error occurred.", status, _responseText, _headers); - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - }); - } - return Promise.resolve(null as any); - } -} - export class Exception implements IException { message?: string; innerException?: Exception | undefined; @@ -1950,6 +2255,48 @@ export interface IException { stackTrace?: string | undefined; } +/** 摄像头配置请求模型 */ +export class CameraConfigRequest implements ICameraConfigRequest { + address!: string; + port!: number; + + constructor(data?: ICameraConfigRequest) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.address = _data["address"]; + this.port = _data["port"]; + } + } + + static fromJS(data: any): CameraConfigRequest { + data = typeof data === 'object' ? data : {}; + let result = new CameraConfigRequest(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["address"] = this.address; + data["port"] = this.port; + return data; + } +} + +/** 摄像头配置请求模型 */ +export interface ICameraConfigRequest { + address: string; + port: number; +} + export class SystemException extends Exception implements ISystemException { constructor(data?: ISystemException) { @@ -2091,6 +2438,8 @@ export class UDPData implements IUDPData { address?: string; /** 发送来源的端口号 */ port?: number; + /** 任务ID */ + taskID?: number; /** 接受到的数据 */ data?: string; /** 是否被读取过 */ @@ -2110,6 +2459,7 @@ export class UDPData implements IUDPData { this.dateTime = _data["dateTime"] ? new Date(_data["dateTime"].toString()) : undefined; this.address = _data["address"]; this.port = _data["port"]; + this.taskID = _data["taskID"]; this.data = _data["data"]; this.hasRead = _data["hasRead"]; } @@ -2127,6 +2477,7 @@ export class UDPData implements IUDPData { data["dateTime"] = this.dateTime ? this.dateTime.toISOString() : undefined; data["address"] = this.address; data["port"] = this.port; + data["taskID"] = this.taskID; data["data"] = this.data; data["hasRead"] = this.hasRead; return data; @@ -2141,6 +2492,8 @@ export interface IUDPData { address?: string; /** 发送来源的端口号 */ port?: number; + /** 任务ID */ + taskID?: number; /** 接受到的数据 */ data?: string; /** 是否被读取过 */ @@ -2188,145 +2541,4 @@ function throwException(message: string, status: number, response: string, heade throw result; else throw new ApiException(message, status, response, headers, null); -} - -// VideoStreamClient - 手动添加,用于HTTP视频流控制 -export class VideoStreamClient { - private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; - private baseUrl: string; - protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; - - constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { - this.http = http ? http : window as any; - this.baseUrl = baseUrl ?? "http://localhost:5000"; - } /** - * 获取HTTP视频流服务状态 - * @return HTTP视频流服务状态信息 - */ - status(): Promise { - let url_ = this.baseUrl + "/api/VideoStream/Status"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "GET", - headers: { - "Accept": "application/json" - } - }; - - return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processStatus(_response); - }); - } - - protected processStatus(response: Response): Promise { - 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) { - return response.text().then((_responseText) => { - let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); - return result200; - }); - } else if (status === 500) { - return response.text().then((_responseText) => { - throw new Error(_responseText); - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - throw new Error("未知的响应状态码: " + status + ", 响应文本: " + _responseText); - }); - } - return Promise.resolve(null as any); - } /** - * 获取HTTP视频流信息 - * @return HTTP视频流信息 - */ - streamInfo(): Promise { - let url_ = this.baseUrl + "/api/VideoStream/StreamInfo"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "GET", - headers: { - "Accept": "application/json" - } - }; - - return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processStreamInfo(_response); - }); - } - - protected processStreamInfo(response: Response): Promise { - 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) { - return response.text().then((_responseText) => { - let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); - return result200; - }); - } else if (status === 500) { - return response.text().then((_responseText) => { - throw new Error(_responseText); - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - throw new Error("未知的响应状态码: " + status + ", 响应文本: " + _responseText); - }); - } - return Promise.resolve(null as any); - } /** - * 测试HTTP视频流连接 - * @return 测试结果 - */ - testConnection(): Promise { - let url_ = this.baseUrl + "/api/VideoStream/TestConnection"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "POST", - headers: { - "Accept": "application/json" - } - }; - - return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processTestConnection(_response); - }); - } - - protected processTestConnection(response: Response): Promise { - 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) { - return response.text().then((_responseText) => { - let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); - return result200; - }); - } else if (status === 500) { - return response.text().then((_responseText) => { - throw new Error(_responseText); - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - throw new Error("未知的响应状态码: " + status + ", 响应文本: " + _responseText); - }); - } - return Promise.resolve(false); - } -} +} \ No newline at end of file diff --git a/src/views/VideoStreamView.vue b/src/views/VideoStreamView.vue index ad2cb3c..0fb0b01 100644 --- a/src/views/VideoStreamView.vue +++ b/src/views/VideoStreamView.vue @@ -11,20 +11,34 @@ - - + + 控制面板 - + - - {{ statusInfo.isRunning ? '运行中' : '已停止' }} + + {{ statusInfo.isRunning ? "运行中" : "已停止" }} 服务状态 @@ -37,36 +51,76 @@ - - + + 视频规格 - {{ streamInfo.frameWidth }}×{{ streamInfo.frameHeight }} + + {{ streamInfo.frameWidth }}×{{ streamInfo.frameHeight }} + {{ streamInfo.frameRate }} FPS - + - - + + 连接数 - {{ statusInfo.connectedClients }} + + {{ statusInfo.connectedClients }} + - 查看客户端 - - + + 查看客户端 + + + {{ client }} - + 无活跃连接 @@ -76,29 +130,154 @@ + + + + + + + + + 摄像头配置 + + + + + IP地址 + + + + + + 端口号 + + + + + + + + + + + + {{ configuring ? "配置中..." : "确认" }} + + + + + - - - - + + + - {{ loading ? '刷新中...' : '刷新状态' }} + {{ loading ? "刷新中..." : "刷新状态" }} - - - - + + + - {{ testing ? '测试中...' : '测试连接' }} + {{ testing ? "测试中..." : "测试连接" }} @@ -108,32 +287,61 @@ - - + + 视频预览 - - + + - - + - + - + - - + + 视频流加载失败 @@ -144,24 +352,40 @@ 端口 {{ statusInfo.serverPort }} 是否可访问 - 重试连接 + + 重试连接 + - + - - - + + {{ videoStatus }} - 点击"播放视频流"按钮开始查看实时视频 + + 点击"播放视频流"按钮开始查看实时视频 + @@ -169,41 +393,100 @@ - 流地址: {{ streamInfo.mjpegUrl }} + 流地址: + {{ + streamInfo.mjpegUrl + }} - - - + + + 更多功能 - - 在新标签打开视频页面 + + + 在新标签打开视频页面 + 获取并下载快照 - 复制MJPEG地址 + + 复制MJPEG地址 + - - - - + + + 播放视频流 - - - - + + + 停止视频流 @@ -216,23 +499,41 @@ - - + + 操作日志 - + - - [{{ formatTime(log.time) }}] + + [{{ formatTime(log.time) }}] {{ log.message }} - + 暂无日志记录 - + 清空日志 @@ -245,242 +546,338 @@
{{ videoStatus }}
点击"播放视频流"按钮开始查看实时视频
+ 点击"播放视频流"按钮开始查看实时视频 +
{{ streamInfo.mjpegUrl }}
{{ + streamInfo.mjpegUrl + }}