diff --git a/server/Program.cs b/server/Program.cs index c101ece..1c9b28e 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -2,9 +2,11 @@ using System.Net; using System.Net.Sockets; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using NLog; using NLog.Web; +using server.src; // Early init of NLog to allow startup and exception logging, before host is built var logger = NLog.LogManager.Setup() @@ -36,9 +38,7 @@ try // Configure Newtonsoft.Json options here options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); - - // Add CORS policy + }); // Add CORS policy if (builder.Environment.IsDevelopment()) { builder.Services.AddCors(options => @@ -56,10 +56,7 @@ try .AllowAnyOrigin() .AllowAnyHeader() ); - }); - - // Add Swagger - builder.Services.AddControllers(); + }); // Add Swagger builder.Services.AddOpenApiDocument(options => { options.PostProcess = document => @@ -81,8 +78,10 @@ try // Url = "https://example.com/license" // } }; - }; - }); + }; }); + // 添加 HTTP 视频流服务 - 使用简化的类型引用 + builder.Services.AddSingleton(); + builder.Services.AddHostedService(provider => provider.GetRequiredService()); // Application Settings var app = builder.Build(); diff --git a/server/server.csproj b/server/server.csproj index 3c6200e..17b1ba6 100644 --- a/server/server.csproj +++ b/server/server.csproj @@ -22,9 +22,10 @@ - + + diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs new file mode 100644 index 0000000..35b8e4e --- /dev/null +++ b/server/src/Controllers/VideoStreamController.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; + +namespace server.src.Controllers; /// + /// HTTP 视频流控制器 + /// + [ApiController] + [Route("api/[controller]")] + public class VideoStreamController : ControllerBase + { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly server.src.HttpVideoStreamService _videoStreamService; /// + /// 初始化HTTP视频流控制器 + /// + /// HTTP视频流服务 + public VideoStreamController(server.src.HttpVideoStreamService videoStreamService) + { + logger.Info("创建VideoStreamController,命名空间:{Namespace}", this.GetType().Namespace); + _videoStreamService = videoStreamService; + } /// + /// 获取 HTTP 视频流服务状态 + /// + /// 服务状态信息 + [HttpGet("Status")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] public IResult GetStatus() + { + try + { + logger.Info("GetStatus方法被调用,控制器:{Controller},路径:api/VideoStream/Status", this.GetType().Name); + + // 使用HttpVideoStreamService提供的状态信息 + 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() + }); + } catch (Exception ex) + { + logger.Error(ex, "获取 HTTP 视频流服务状态失败"); return TypedResults.InternalServerError(ex.Message); + } + } + + /// + /// 获取 HTTP 视频流信息 + /// + /// 流信息 + [HttpGet("StreamInfo")] + [EnableCors("Users")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + public IResult GetStreamInfo() + { + try + { + logger.Info("获取 HTTP 视频流信息"); + return TypedResults.Ok(new + { + frameRate = _videoStreamService.FrameRate, + frameWidth = _videoStreamService.FrameWidth, + frameHeight = _videoStreamService.FrameHeight, + format = "MJPEG", + htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html", + mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream", + snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot" + }); + } + catch (Exception ex) + { + logger.Error(ex, "获取 HTTP 视频流信息失败"); return TypedResults.InternalServerError(ex.Message); + } + } + + /// + /// 测试 HTTP 视频流连接 + /// + /// 连接测试结果 + [HttpPost("TestConnection")] + [EnableCors("Users")] + [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] + public async Task TestConnection() + { + try + { + logger.Info("测试 HTTP 视频流连接"); + + // 尝试通过HTTP请求检查视频流服务是否可访问 + 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); + } + } + catch (Exception ex) + { + logger.Error(ex, "HTTP 视频流连接测试失败"); + // 连接失败但不抛出异常,而是返回连接失败的结果 + return TypedResults.Ok(false); + } + } +} \ No newline at end of file diff --git a/server/src/HttpVideoStreamService.cs b/server/src/HttpVideoStreamService.cs new file mode 100644 index 0000000..c7337d1 --- /dev/null +++ b/server/src/HttpVideoStreamService.cs @@ -0,0 +1,583 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using System.Text; + +namespace server.src +{ + /// + /// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页 + /// 简化版本实现,先建立基础框架 + /// + public class HttpVideoStreamService : BackgroundService + { + private readonly ILogger _logger; + private HttpListener? _httpListener; + private readonly int _serverPort = 8080; + private readonly int _frameRate = 30; // 30 FPS + private readonly int _frameWidth = 640; + private readonly int _frameHeight = 480; + + // 模拟 FPGA 图像数据 + private int _frameCounter = 0; + private readonly List _activeClients = new List(); + private readonly object _clientsLock = new object(); + + /// + /// 获取当前连接的客户端数量 + /// + public int ConnectedClientsCount + { + get + { + lock (_clientsLock) + { + return _activeClients.Count; + } + } + } + + /// + /// 获取服务端口 + /// + public int ServerPort => _serverPort; + + /// + /// 获取帧宽度 + /// + public int FrameWidth => _frameWidth; + + /// + /// 获取帧高度 + /// + public int FrameHeight => _frameHeight; + + /// + /// 获取帧率 + /// + public int FrameRate => _frameRate; + + /// + /// 初始化 HttpVideoStreamService + /// + /// 日志记录器 + public HttpVideoStreamService(ILogger logger) + { + _logger = logger; + } + + /// + /// 执行 HTTP 视频流服务 + /// + /// 取消令牌 + /// 任务 + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + _logger.LogInformation("启动 HTTP 视频流服务,端口: {Port}", _serverPort); + // 创建 HTTP 监听器 + _httpListener = new HttpListener(); + _httpListener.Prefixes.Add($"http://localhost:{_serverPort}/"); + _httpListener.Start(); + + _logger.LogInformation("HTTP 视频流服务已启动,监听端口: {Port}", _serverPort); + + // 开始接受客户端连接 + _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken); + + // 开始生成视频帧 + await GenerateVideoFrames(stoppingToken); + } + catch (HttpListenerException ex) + { + _logger.LogError(ex, "HTTP 视频流服务启动失败,请确保您有管理员权限或使用netsh配置URL前缀权限"); + } + catch (Exception ex) + { + _logger.LogError(ex, "HTTP 视频流服务启动失败"); + } + } + + private async Task AcceptClientsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && _httpListener != null && _httpListener.IsListening) + { + try + { + // 等待客户端连接 + var context = await _httpListener.GetContextAsync(); + var request = context.Request; + var response = context.Response; + + _logger.LogInformation("新HTTP客户端连接: {RemoteEndPoint}", request.RemoteEndPoint); + // 处理不同的请求路径 + var requestPath = request.Url?.AbsolutePath ?? "/"; + + if (requestPath == "/video-stream") + { + // MJPEG 流请求 + _ = Task.Run(() => HandleMjpegStreamAsync(response, cancellationToken), cancellationToken); + } + else if (requestPath == "/snapshot") + { + // 单帧图像请求 + await HandleSnapshotRequestAsync(response, cancellationToken); + } + else if (requestPath == "/video-feed.html") + { + // HTML页面请求 + await SendVideoHtmlPageAsync(response); + } + else + { + // 默认返回简单的HTML页面,提供链接到视频页面 + await SendIndexHtmlPageAsync(response); + } + } + catch (HttpListenerException) + { + // HTTP监听器可能已停止 + break; + } + catch (ObjectDisposedException) + { + // 对象可能已被释放 + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "接受HTTP客户端连接时发生错误"); + } + } + } + + private async Task HandleMjpegStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken) + { + try + { + // 设置MJPEG流的响应头 + response.ContentType = "multipart/x-mixed-replace; boundary=--boundary"; + response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); + response.Headers.Add("Pragma", "no-cache"); + response.Headers.Add("Expires", "0"); + + // 跟踪活跃的客户端 + lock (_clientsLock) + { + _activeClients.Add(response); + } + + _logger.LogDebug("已启动MJPEG流,客户端: {RemoteEndPoint}", response.OutputStream?.GetHashCode() ?? 0); + + // 保持连接直到取消或出错 + try + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(100, cancellationToken); // 简单的保活循环 + } + } + catch (TaskCanceledException) + { + // 预期的取消 + } + + _logger.LogDebug("MJPEG流已结束,客户端: {ClientId}", response.OutputStream?.GetHashCode() ?? 0); + } + catch (Exception ex) + { + _logger.LogError(ex, "处理MJPEG流时出错"); + } + finally + { + lock (_clientsLock) + { + _activeClients.Remove(response); + } + + try + { + response.Close(); + } + catch + { + // 忽略关闭时的错误 + } + } + } + + private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, CancellationToken cancellationToken) + { + try + { + // 获取当前帧 + var imageData = await GetFPGAImageData(); + var jpegData = ConvertToJpeg(imageData); + + // 设置响应头 + response.ContentType = "image/jpeg"; + response.ContentLength64 = jpegData.Length; + response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); + + // 发送JPEG数据 + await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); + await response.OutputStream.FlushAsync(cancellationToken); + + _logger.LogDebug("已发送快照图像,大小:{Size} 字节", jpegData.Length); + } + catch (Exception ex) + { + _logger.LogError(ex, "处理快照请求时出错"); + } + finally + { + response.Close(); + } + } + + private async Task SendVideoHtmlPageAsync(HttpListenerResponse response) + { + string html = $@" + + + FPGA 视频流 + + + + +

FPGA 实时视频流

+
+ +
+
+ + + 状态: 连接中... +
+ + +"; + + response.ContentType = "text/html"; + response.ContentEncoding = Encoding.UTF8; + byte[] buffer = Encoding.UTF8.GetBytes(html); + response.ContentLength64 = buffer.Length; + + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.Close(); + } + + private async Task SendIndexHtmlPageAsync(HttpListenerResponse response) + { + string html = $@" + + + FPGA WebLab 视频服务 + + + + +

FPGA WebLab 视频服务

+ +

HTTP流媒体服务端口: {_serverPort}

+ +"; + + response.ContentType = "text/html"; + response.ContentEncoding = Encoding.UTF8; + byte[] buffer = Encoding.UTF8.GetBytes(html); + response.ContentLength64 = buffer.Length; + + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); + response.Close(); + } + + private async Task GenerateVideoFrames(CancellationToken cancellationToken) + { + var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + // 从 FPGA 获取图像数据(模拟) + var imageData = await GetFPGAImageData(); + + // 将图像数据转换为 JPEG + var jpegData = ConvertToJpeg(imageData); + + // 向所有连接的客户端发送帧 + await BroadcastFrameAsync(jpegData, cancellationToken); + + _frameCounter++; + + // 等待下一帧 + await Task.Delay(frameInterval, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "生成视频帧时发生错误"); + await Task.Delay(1000, cancellationToken); // 错误恢复延迟 + } + } + } + + /// + /// 模拟从 FPGA 获取图像数据的函数 + /// 实际实现时,这里应该通过 UDP 连接读取 FPGA 特定地址范围的数据 + /// + private async Task GetFPGAImageData() + { + // 模拟异步 FPGA 数据读取 + await Task.Delay(1); + + // 简化的模拟图像数据生成 + var random = new Random(_frameCounter); + var imageData = new byte[_frameWidth * _frameHeight * 3]; // RGB24 格式 + + // 生成简单的彩色噪声图案 + for (int i = 0; i < imageData.Length; i += 3) + { + // 基于帧计数器和位置生成颜色 + var baseColor = (_frameCounter + i / 3) % 256; + imageData[i] = (byte)((baseColor + random.Next(0, 50)) % 256); // R + imageData[i + 1] = (byte)((baseColor * 2 + random.Next(0, 50)) % 256); // G + imageData[i + 2] = (byte)((baseColor * 3 + random.Next(0, 50)) % 256); // B + } + + if (_frameCounter % 30 == 0) // 每秒更新一次日志 + { + _logger.LogDebug("生成第 {FrameNumber} 帧", _frameCounter); + } + + return imageData; + } + + /// + /// 将 RGB 图像数据转换为 JPEG 格式 + /// + private byte[] ConvertToJpeg(byte[] rgbData) + { + using var image = new Image(_frameWidth, _frameHeight); + + // 将 RGB 数据复制到 ImageSharp 图像 + for (int y = 0; y < _frameHeight; y++) + { + for (int x = 0; x < _frameWidth; x++) + { + int index = (y * _frameWidth + x) * 3; + var pixel = new Rgb24(rgbData[index], rgbData[index + 1], rgbData[index + 2]); + image[x, y] = pixel; + } + } + + using var stream = new MemoryStream(); + image.SaveAsJpeg(stream, new JpegEncoder { Quality = 80 }); + return stream.ToArray(); + } + + /// + /// 向所有连接的客户端广播帧数据 + /// + private async Task BroadcastFrameAsync(byte[] frameData, CancellationToken cancellationToken) + { + if (frameData == null || frameData.Length == 0) + { + _logger.LogWarning("尝试广播空帧数据"); + return; + } + + // 准备MJPEG帧数据 + var mjpegFrameHeader = $"--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: {frameData.Length}\r\n\r\n"; + var headerBytes = Encoding.ASCII.GetBytes(mjpegFrameHeader); + + var clientsToRemove = new List(); + var clientsToProcess = new List(); + + // 获取当前连接的客户端列表 + lock (_clientsLock) + { + clientsToProcess.AddRange(_activeClients); + } + + if (clientsToProcess.Count == 0) + { + return; // 没有活跃客户端 + } + + // 向每个活跃的客户端发送帧 + foreach (var client in clientsToProcess) + { + try + { + // 发送帧头部 + await client.OutputStream.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken); + + // 发送JPEG数据 + await client.OutputStream.WriteAsync(frameData, 0, frameData.Length, cancellationToken); + + // 发送结尾换行符 + await client.OutputStream.WriteAsync(Encoding.ASCII.GetBytes("\r\n"), 0, 2, cancellationToken); + + // 确保数据立即发送 + await client.OutputStream.FlushAsync(cancellationToken); + + if (_frameCounter % 30 == 0) // 每秒记录一次日志 + { + _logger.LogDebug("已向客户端 {ClientId} 发送第 {FrameNumber} 帧,大小:{Size} 字节", + client.OutputStream.GetHashCode(), _frameCounter, frameData.Length); + } + } + catch (Exception ex) + { + _logger.LogDebug("发送帧数据时出错: {Error}", ex.Message); + clientsToRemove.Add(client); + } + } + + // 移除断开连接的客户端 + if (clientsToRemove.Count > 0) + { + lock (_clientsLock) + { + foreach (var client in clientsToRemove) + { + _activeClients.Remove(client); + try { client.Close(); } + catch { /* 忽略关闭错误 */ } + } + } + + _logger.LogInformation("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}", + clientsToRemove.Count, _activeClients.Count); + } + } + + /// + /// 获取连接的客户端端点列表 + /// + public List GetConnectedClientEndpoints() + { + List endpoints = new List(); + + lock (_clientsLock) + { + foreach (var client in _activeClients) + { + endpoints.Add($"Client-{client.OutputStream?.GetHashCode() ?? 0}"); + } + } + + return endpoints; + } + + /// + /// 获取服务状态信息 + /// + public object GetServiceStatus() + { + return new + { + IsRunning = _httpListener?.IsListening ?? false, + ServerPort = _serverPort, + FrameRate = _frameRate, + Resolution = $"{_frameWidth}x{_frameHeight}", + ConnectedClients = ConnectedClientsCount, + ClientEndpoints = GetConnectedClientEndpoints() + }; + } + + /// + /// 停止 HTTP 视频流服务 + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("正在停止 HTTP 视频流服务..."); + + if (_httpListener != null && _httpListener.IsListening) + { + _httpListener.Stop(); + _httpListener.Close(); + } + + // 关闭所有客户端连接 + lock (_clientsLock) + { + foreach (var client in _activeClients) + { + try { client.Close(); } + catch { /* 忽略关闭错误 */ } + } + _activeClients.Clear(); + } + + await base.StopAsync(cancellationToken); + + _logger.LogInformation("HTTP 视频流服务已停止"); + } + + /// + /// 释放资源 + /// + public override void Dispose() + { + if (_httpListener != null) + { + if (_httpListener.IsListening) + { + _httpListener.Stop(); + } + _httpListener.Close(); + } + + lock (_clientsLock) + { + foreach (var client in _activeClients) + { + try { client.Close(); } + catch { /* 忽略关闭错误 */ } + } + _activeClients.Clear(); + } + + base.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/APIClient.ts b/src/APIClient.ts index 44c8f70..639e779 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -2189,3 +2189,144 @@ function throwException(message: string, status: number, response: string, heade 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); + } +} diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue index 4ceaa05..7332635 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -39,8 +39,7 @@ 工程界面 - -
  • +
  • @@ -49,6 +48,14 @@ 测试功能
  • +
  • + + + + + HTTP视频流 +
  • +
    + +
    +

    HTTP 视频流

    +

    FPGA WebLab 视频流传输功能

    +
    + +
    + +
    +
    +

    + + + + 控制面板 +

    + +
    + +
    +
    +
    +
    + {{ statusInfo.isRunning ? '运行中' : '已停止' }} +
    +
    +
    服务状态
    +
    HTTP
    +
    端口: {{ statusInfo.serverPort }}
    +
    +
    + + +
    +
    +
    + + + +
    +
    视频规格
    +
    {{ streamInfo.frameWidth }}×{{ streamInfo.frameHeight }}
    +
    {{ streamInfo.frameRate }} FPS
    +
    +
    + + +
    +
    +
    + + + +
    +
    连接数
    +
    {{ statusInfo.connectedClients }}
    +
    + +
    +
    +
    +
    + + +
    + + +
    +
    +
    + + +
    +
    +

    + + + + 视频预览 +

    + +
    + +
    + 视频流 +
    + + +
    +
    +
    +

    + + + + 视频流加载失败 +

    +

    无法连接到视频服务器,请检查以下内容:

    +
      +
    • 视频流服务是否已启动
    • +
    • 网络连接是否正常
    • +
    • 端口 {{ statusInfo.serverPort }} 是否可访问
    • +
    +
    + +
    +
    +
    +
    + + +
    +
    + + + +

    {{ videoStatus }}

    +

    点击"播放视频流"按钮开始查看实时视频

    +
    +
    +
    + + +
    +
    + 流地址: {{ streamInfo.mjpegUrl }} +
    +
    + + + +
    +
    +
    +
    + + +
    +
    +

    + + + + 操作日志 +

    + +
    +
    + [{{ formatTime(log.time) }}] + {{ log.message }} +
    +
    + 暂无日志记录 +
    +
    + +
    + +
    +
    +
    +
    +
    + + + + +