diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs index e133c78..323cc17 100644 --- a/server/src/Controllers/VideoStreamController.cs +++ b/server/src/Controllers/VideoStreamController.cs @@ -33,6 +33,26 @@ public class VideoStreamController : ControllerBase public int Port { get; set; } } + /// + /// 分辨率配置请求模型 + /// + public class ResolutionConfigRequest + { + /// + /// 宽度 + /// + [Required] + [Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")] + public int Width { get; set; } + + /// + /// 高度 + /// + [Required] + [Range(480, 1080, ErrorMessage = "高度必须在480-1080范围内")] + public int Height { get; set; } + } + /// /// 初始化HTTP视频流控制器 /// @@ -233,4 +253,116 @@ public class VideoStreamController : ControllerBase return TypedResults.Ok(false); } } + + /// + /// 设置视频流分辨率 + /// + /// 分辨率配置请求 + /// 设置结果 + [HttpPost("Resolution")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task SetResolution([FromBody] ResolutionConfigRequest request) + { + try + { + logger.Info($"设置视频流分辨率为 {request.Width}x{request.Height}"); + + var (isSuccess, message) = await _videoStreamService.SetResolutionAsync(request.Width, request.Height); + + if (isSuccess) + { + return TypedResults.Ok(new + { + success = true, + message = message, + width = request.Width, + height = request.Height, + timestamp = DateTime.Now + }); + } + else + { + return TypedResults.BadRequest(new + { + success = false, + message = message, + timestamp = DateTime.Now + }); + } + } + catch (Exception ex) + { + logger.Error(ex, $"设置分辨率为 {request.Width}x{request.Height} 失败"); + return TypedResults.InternalServerError($"设置分辨率失败: {ex.Message}"); + } + } + + /// + /// 获取当前分辨率 + /// + /// 当前分辨率信息 + [HttpGet("Resolution")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IResult GetCurrentResolution() + { + try + { + logger.Info("获取当前视频流分辨率"); + + var (width, height) = _videoStreamService.GetCurrentResolution(); + + return TypedResults.Ok(new + { + width = width, + height = height, + resolution = $"{width}x{height}", + timestamp = DateTime.Now + }); + } + catch (Exception ex) + { + logger.Error(ex, "获取当前分辨率失败"); + return TypedResults.InternalServerError($"获取当前分辨率失败: {ex.Message}"); + } + } + + /// + /// 获取支持的分辨率列表 + /// + /// 支持的分辨率列表 + [HttpGet("SupportedResolutions")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IResult GetSupportedResolutions() + { + try + { + logger.Info("获取支持的分辨率列表"); + + var resolutions = _videoStreamService.GetSupportedResolutions(); + + return TypedResults.Ok(new + { + resolutions = resolutions.Select(r => new + { + width = r.Width, + height = r.Height, + name = r.Name, + value = $"{r.Width}x{r.Height}" + }), + timestamp = DateTime.Now + }); + } + catch (Exception ex) + { + logger.Error(ex, "获取支持的分辨率列表失败"); + return TypedResults.InternalServerError($"获取支持的分辨率列表失败: {ex.Message}"); + } + } } diff --git a/server/src/Peripherals/CameraClient.cs b/server/src/Peripherals/CameraClient.cs index 382192f..f280c6f 100644 --- a/server/src/Peripherals/CameraClient.cs +++ b/server/src/Peripherals/CameraClient.cs @@ -26,19 +26,13 @@ class Camera const uint CAM_I2C_ADDR = 0x3C; const Peripherals.I2cClient.I2cProtocol CAM_PROTO = Peripherals.I2cClient.I2cProtocol.SCCB; - const UInt16 H_START = 0; //default: 0 - const UInt16 V_START = 0; //default: 0 - const UInt16 DVPHO = 640; //default: 2592 (0xA20) - const UInt16 DVPVO = 480; //default: 1944 (0x798) - const UInt16 H_END = H_START + 1500 - 1; //default: 2624-1 (0xA3F) - const UInt16 V_END = V_START + 1300 - 1; //default: 1951-1 (0x79F) - const UInt16 HTS = 1700; //default: 2844 (0xB1C) - const UInt16 VTS = 1500; //default: 1968 (0x7B0) - const UInt16 H_OFFSET = 16; //default: 16 (0x10) - const UInt16 V_OFFSET = 4; //default: 4 (0x04) const byte PLL_MUX = 10; const UInt32 FrameAddr = 0x00; - const UInt32 FrameLength = DVPHO * DVPVO * 16 / 32; + + // 动态分辨率参数 + private UInt16 _currentWidth = 640; + private UInt16 _currentHeight = 480; + private UInt32 _currentFrameLength = 640 * 480 * 2 / 4; // RGB565格式,2字节/像素,按4字节对齐 /// @@ -183,8 +177,7 @@ class Camera this.ep, this.taskID, // taskID FrameAddr, - // ((int)FrameLength), - 1280*720/2, + (int)(_currentWidth * _currentHeight * 2), // 使用当前分辨率的动态大小 this.timeout); if (!result.IsSuccessful) @@ -423,6 +416,67 @@ class Camera ); } + /// + /// 切换摄像头分辨率 + /// + /// 宽度 + /// 高度 + /// 配置结果 + public async ValueTask> ChangeResolution(int width, int height) + { + try + { + logger.Info($"正在切换摄像头分辨率到 {width}x{height}"); + + Result result; + switch ($"{width}x{height}") + { + case "640x480": + result = await ConfigureResolution640x480(); + break; + case "1280x720": + result = await ConfigureResolution1280x720(); + break; + default: + logger.Error($"不支持的分辨率: {width}x{height}"); + return new(new ArgumentException($"不支持的分辨率: {width}x{height}")); + } + + if (result.IsSuccessful) + { + _currentWidth = (UInt16)width; + _currentHeight = (UInt16)height; + _currentFrameLength = (UInt32)(width * height * 2 / 4); // RGB565格式,按4字节对齐 + logger.Info($"摄像头分辨率已切换到 {width}x{height}"); + } + + return result; + } + catch (Exception ex) + { + logger.Error(ex, $"切换分辨率到 {width}x{height} 时发生错误"); + return new(ex); + } + } + + /// + /// 获取当前分辨率 + /// + /// 当前分辨率(宽度, 高度) + public (int Width, int Height) GetCurrentResolution() + { + return (_currentWidth, _currentHeight); + } + + /// + /// 获取当前帧长度 + /// + /// 当前帧长度 + public UInt32 GetCurrentFrameLength() + { + return _currentFrameLength; + } + /// /// 复位摄像头 /// diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs index 63187bc..c1eaca5 100644 --- a/server/src/Services/HttpVideoStreamService.cs +++ b/server/src/Services/HttpVideoStreamService.cs @@ -82,8 +82,11 @@ public class HttpVideoStreamService : BackgroundService private HttpListener? _httpListener; private readonly int _serverPort = 8080; private readonly int _frameRate = 30; // 30 FPS - private readonly int _frameWidth = 1280; - private readonly int _frameHeight = 720; + + // 动态分辨率配置 + private int _frameWidth = 640; // 默认640x480 + private int _frameHeight = 480; + private readonly object _resolutionLock = new object(); // 摄像头客户端 private Camera? _camera; @@ -439,8 +442,16 @@ public class HttpVideoStreamService : BackgroundService // 获取当前帧 var imageData = await GetFPGAImageData(); + // 获取当前分辨率 + int currentWidth, currentHeight; + lock (_resolutionLock) + { + currentWidth = _frameWidth; + currentHeight = _frameHeight; + } + // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换 - var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, _frameWidth, _frameHeight, 80); + var jpegResult = Common.Image.ConvertRGB24ToJpeg(imageData, currentWidth, currentHeight, 80); if (!jpegResult.IsSuccessful) { logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); @@ -647,6 +658,14 @@ public class HttpVideoStreamService : BackgroundService try { + // 获取当前分辨率 + int currentWidth, currentHeight; + lock (_resolutionLock) + { + currentWidth = _frameWidth; + currentHeight = _frameHeight; + } + // 从摄像头读取帧数据 var readStartTime = DateTime.UtcNow; var result = await currentCamera.ReadFrame(); @@ -662,15 +681,15 @@ public class HttpVideoStreamService : BackgroundService var rgb565Data = result.Value; // 验证数据长度是否正确 - if (!Common.Image.ValidateImageDataLength(rgb565Data, _frameWidth, _frameHeight, 2)) + if (!Common.Image.ValidateImageDataLength(rgb565Data, currentWidth, currentHeight, 2)) { logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}", - _frameWidth * _frameHeight * 2, rgb565Data.Length); + currentWidth * currentHeight * 2, rgb565Data.Length); } // 将 RGB565 转换为 RGB24 var convertStartTime = DateTime.UtcNow; - var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, _frameWidth, _frameHeight, isLittleEndian: false); + var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, currentWidth, currentHeight, isLittleEndian: false); var convertEndTime = DateTime.UtcNow; var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds; @@ -708,8 +727,16 @@ public class HttpVideoStreamService : BackgroundService return; } + // 获取当前分辨率 + int currentWidth, currentHeight; + lock (_resolutionLock) + { + currentWidth = _frameWidth; + currentHeight = _frameHeight; + } + // 直接使用Common.Image.ConvertRGB24ToJpeg进行转换 - var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, _frameWidth, _frameHeight, 80); + var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameData, currentWidth, currentHeight, 80); if (!jpegResult.IsSuccessful) { logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error); @@ -904,4 +931,102 @@ public class HttpVideoStreamService : BackgroundService base.Dispose(); } + + /// + /// 设置视频流分辨率 + /// + /// 宽度 + /// 高度 + /// 设置结果 + public async Task<(bool IsSuccess, string Message)> SetResolutionAsync(int width, int height) + { + try + { + logger.Info($"正在设置视频流分辨率为 {width}x{height}"); + + // 验证分辨率 + if (!IsSupportedResolution(width, height)) + { + var message = $"不支持的分辨率: {width}x{height},支持的分辨率: 640x480, 1280x720"; + logger.Error(message); + return (false, message); + } + + Camera? currentCamera = null; + lock (_cameraLock) + { + currentCamera = _camera; + } + + if (currentCamera == null) + { + var message = "摄像头未配置,无法设置分辨率"; + logger.Error(message); + return (false, message); + } + + // 设置摄像头分辨率 + var cameraResult = await currentCamera.ChangeResolution(width, height); + if (!cameraResult.IsSuccessful) + { + var message = $"设置摄像头分辨率失败: {cameraResult.Error}"; + logger.Error(message); + return (false, message); + } + + // 更新HTTP服务的分辨率配置 + lock (_resolutionLock) + { + _frameWidth = width; + _frameHeight = height; + } + + var successMessage = $"视频流分辨率已成功设置为 {width}x{height}"; + logger.Info(successMessage); + return (true, successMessage); + } + catch (Exception ex) + { + var message = $"设置分辨率时发生错误: {ex.Message}"; + logger.Error(ex, message); + return (false, message); + } + } + + /// + /// 获取当前分辨率 + /// + /// 当前分辨率(宽度, 高度) + public (int Width, int Height) GetCurrentResolution() + { + lock (_resolutionLock) + { + return (_frameWidth, _frameHeight); + } + } + + /// + /// 检查是否支持该分辨率 + /// + /// 宽度 + /// 高度 + /// 是否支持 + private bool IsSupportedResolution(int width, int height) + { + var resolution = $"{width}x{height}"; + return resolution == "640x480" || resolution == "1280x720"; + } + + /// + /// 获取支持的分辨率列表 + /// + /// 支持的分辨率列表 + public List<(int Width, int Height, string Name)> GetSupportedResolutions() + { + return new List<(int, int, string)> + { + (640, 480, "640x480 (VGA)"), + (1280, 720, "1280x720 (HD)") + }; + } } diff --git a/src/APIClient.ts b/src/APIClient.ts index 22851b1..42bcad8 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -311,6 +311,150 @@ export class VideoStreamClient { } return Promise.resolve(null as any); } + + /** + * 设置视频流分辨率 + * @param width 宽度 + * @param height 高度 + * @return 操作结果 + */ + setResolution(width: number, height: number): Promise { + let url_ = this.baseUrl + "/api/VideoStream/SetResolution"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify({ width: width, height: height }); + + let options_: RequestInit = { + method: "POST", + body: content_, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processSetResolution(_response); + }); + } + + protected processSetResolution(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 当前分辨率信息 + */ + getCurrentResolution(): Promise { + let url_ = this.baseUrl + "/api/VideoStream/GetCurrentResolution"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetCurrentResolution(_response); + }); + } + + protected processGetCurrentResolution(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 支持的分辨率列表 + */ + getSupportedResolutions(): Promise { + let url_ = this.baseUrl + "/api/VideoStream/GetSupportedResolutions"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.http.fetch(url_, options_).then((_response: Response) => { + return this.processGetSupportedResolutions(_response); + }); + } + + protected processGetSupportedResolutions(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 { diff --git a/src/views/Project/VideoStream.vue b/src/views/Project/VideoStream.vue index 764a4e2..2e150bb 100644 --- a/src/views/Project/VideoStream.vue +++ b/src/views/Project/VideoStream.vue @@ -8,7 +8,7 @@ 控制面板 -
+
@@ -42,6 +42,38 @@
+ +
+
+
+ +
+
分辨率设置
+
+ +
+
+ +
+
+
+
@@ -321,6 +353,15 @@ const isPlaying = ref(false); const hasVideoError = ref(false); const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频'); +// 分辨率相关状态 +const changingResolution = ref(false); +const loadingResolutions = ref(false); +const selectedResolution = ref({ width: 640, height: 480 }); +const supportedResolutions = ref([ + { width: 640, height: 480 }, + { width: 1280, height: 720 } +]); + // 数据 const statusInfo = ref({ isRunning: false, @@ -549,6 +590,69 @@ const startStream = async () => { } }; +// 分辨率相关方法 +// 获取支持的分辨率列表 +const refreshResolutions = async () => { + loadingResolutions.value = true; + try { + addLog("info", "正在获取支持的分辨率列表..."); + const resolutions = await videoClient.getSupportedResolutions(); + supportedResolutions.value = resolutions; + + // 获取当前分辨率 + const currentRes = await videoClient.getCurrentResolution(); + selectedResolution.value = currentRes; + + addLog("success", "分辨率列表获取成功"); + } catch (error) { + addLog("error", `获取分辨率列表失败: ${error}`); + console.error("获取分辨率列表失败:", error); + } finally { + loadingResolutions.value = false; + } +}; + +// 切换分辨率 +const changeResolution = async () => { + if (!selectedResolution.value) return; + + changingResolution.value = true; + const wasPlaying = isPlaying.value; + + try { + addLog("info", `正在切换分辨率到 ${selectedResolution.value.width}×${selectedResolution.value.height}...`); + + // 如果正在播放,先停止视频流 + if (wasPlaying) { + stopStream(); + await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒 + } + + // 设置新分辨率 + const success = await videoClient.setResolution(selectedResolution.value.width, selectedResolution.value.height); + + if (success) { + // 刷新流信息 + await refreshStatus(); + + // 如果之前在播放,重新启动视频流 + if (wasPlaying) { + await new Promise(resolve => setTimeout(resolve, 500)); // 短暂延迟 + await startStream(); + } + + addLog("success", `分辨率已切换到 ${selectedResolution.value.width}×${selectedResolution.value.height}`); + } else { + addLog("error", "分辨率切换失败"); + } + } catch (error) { + addLog("error", `分辨率切换失败: ${error}`); + console.error("分辨率切换失败:", error); + } finally { + changingResolution.value = false; + } +}; + // 停止视频流 const stopStream = () => { try { @@ -574,6 +678,7 @@ const stopStream = () => { onMounted(async () => { addLog("info", "HTTP 视频流页面已加载"); await refreshStatus(); + await refreshResolutions(); // 初始化分辨率信息 }); onUnmounted(() => {