This repository has been archived on 2025-10-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FPGA_WebLab/server/src/Services/HttpVideoStreamService.cs

827 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net;
using System.Text;
using System.Collections.Concurrent;
using DotNext;
using DotNext.Threading;
namespace server.Services;
public class VideoStreamClient
{
public string? ClientId { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public int FrameWidth { get; set; }
public int FrameHeight { get; set; }
public int FrameRate { get; set; }
public AsyncLazy<Peripherals.CameraClient.Camera> Camera { get; set; }
public CancellationTokenSource CTS { get; set; }
public readonly AsyncReaderWriterLock Lock = new();
public VideoStreamClient(
string clientId, int width, int height, AsyncLazy<Peripherals.CameraClient.Camera> camera)
{
ClientId = clientId;
FrameWidth = width;
FrameHeight = height;
FrameRate = 30;
Camera = camera;
CTS = new CancellationTokenSource();
}
}
/// <summary>
/// 表示摄像头连接状态信息
/// </summary>
public class VideoStreamEndpoint
{
public required string BoardId { get; set; } = "";
public required string MjpegUrl { get; set; } = "";
public required string VideoUrl { get; set; } = "";
public required string SnapshotUrl { get; set; } = "";
public required string HtmlUrl { get; set; } = "";
public required string UsbCameraUrl { get; set; } = "";
public required bool IsEnabled { get; set; }
/// <summary>
/// 视频流的帧率FPS
/// </summary>
public required int FrameRate { get; set; }
public int FrameWidth { get; set; }
public int FrameHeight { get; set; }
/// <summary>
/// 视频分辨率(如 640x480
/// </summary>
public string Resolution => $"{FrameWidth}x{FrameHeight}";
}
/// <summary>
/// 表示视频流服务的运行状态
/// </summary>
public class VideoStreamServiceStatus
{
/// <summary>
/// 服务是否正在运行
/// </summary>
public bool IsRunning { get; set; }
/// <summary>
/// 服务监听的端口号
/// </summary>
public int ServerPort { get; set; }
/// <summary>
/// 当前连接的客户端端点列表
/// </summary>
public List<VideoStreamEndpoint> ClientEndpoints { get; set; } = new();
/// <summary>
/// 当前连接的客户端数量
/// </summary>
public int ConnectedClientsNum => ClientEndpoints.Count;
}
/// <summary>
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
/// 支持动态配置摄像头地址和端口
/// </summary>
public class HttpVideoStreamService : BackgroundService
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private HttpListener? _httpListener;
private readonly int _serverPort = 4321;
private readonly ConcurrentDictionary<string, VideoStreamClient> _clientDict = new();
// USB Camera 相关
private AsyncLazy<UsbCameraCapture> _usbCamera = new(async token => await InitializeUsbCamera(token));
private static async Task<UsbCameraCapture> InitializeUsbCamera(CancellationToken token)
{
try
{
var camera = new UsbCameraCapture();
var devices = camera.GetDevices();
for (int i = 0; i < devices.Count; i++)
logger.Info($"Device[{i}]: {devices[i].Name}");
await camera.StartAsync(1, 2592, 1994, 30);
return camera;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to start USB camera");
throw;
}
}
private Optional<VideoStreamClient> TryGetClient(string boardId)
{
return _clientDict.TryGetValue(boardId, out var client) ? client : null;
}
private Optional<VideoStreamClient> GetOrCreateClient(
string boardId, int initWidth, int initHeight)
{
if (_clientDict.TryGetValue(boardId, out var client))
{
// 可在此处做分辨率/Camera等配置更新
return client;
}
var userManager = new Database.UserManager();
var boardRet = userManager.GetBoardByID(Guid.Parse(boardId));
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Failed to get board with ID {boardId}");
return null;
}
var board = boardRet.Value.Value;
var camera = new AsyncLazy<Peripherals.CameraClient.Camera>(async (_) =>
{
var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
var ret = await camera.Init();
if (!ret.IsSuccessful || !ret.Value)
{
logger.Error("Camera Init Failed!");
throw new Exception("Camera Init Failed!");
}
return camera;
});
client = new VideoStreamClient(boardId, initWidth, initHeight, camera);
_clientDict[boardId] = client;
return client;
}
/// <summary>
/// 初始化 HttpVideoStreamService
/// </summary>
public override async Task StartAsync(CancellationToken cancellationToken)
{
_httpListener = new HttpListener();
_httpListener.Prefixes.Add($"http://{Global.LocalHost}:{_serverPort}/");
_httpListener.Start();
logger.Info($"Video Stream Service started on port {_serverPort}");
await base.StartAsync(cancellationToken);
}
/// <summary>
/// 停止 HTTP 视频流服务
/// </summary>
public override async Task StopAsync(CancellationToken cancellationToken)
{
foreach (var clientKey in _clientDict.Keys)
{
var client = _clientDict[clientKey];
client.CTS.Cancel();
if (!client.Camera.IsValueCreated) continue;
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
var camera = await client.Camera.WithCancellation(cancellationToken);
await camera.EnableHardwareTrans(false);
}
}
_clientDict.Clear();
await base.StopAsync(cancellationToken);
}
/// <summary>
/// 执行 HTTP 视频流服务
/// </summary>
/// <param name="stoppingToken">取消令牌</param>
/// <returns>任务</returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_httpListener == null) continue;
try
{
logger.Debug("Waiting for HTTP request...");
var contextTask = _httpListener.GetContextAsync();
var completedTask = await Task.WhenAny(contextTask, Task.Delay(-1, stoppingToken));
if (completedTask == contextTask)
{
var context = contextTask.Result;
logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}");
if (context != null)
_ = HandleRequestAsync(context, stoppingToken);
}
else
{
break;
}
}
catch (Exception ex)
{
logger.Error(ex, "Error in GetContextAsync");
break;
}
}
}
private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
{
var path = context.Request.Url?.AbsolutePath ?? "/";
var boardId = context.Request.QueryString["boardId"];
if (string.IsNullOrEmpty(boardId))
{
await SendErrorAsync(context.Response, "Missing clientId");
return;
}
var width = int.TryParse(context.Request.QueryString["width"], out var w) ? w : 640;
var height = int.TryParse(context.Request.QueryString["height"], out var h) ? h : 480;
var clientOpt = GetOrCreateClient(boardId, width, height);
if (!clientOpt.HasValue)
{
await SendErrorAsync(context.Response, "Invalid clientId or camera not available");
return;
}
var client = clientOpt.Value;
var token = CancellationTokenSource.CreateLinkedTokenSource(
client.CTS.Token, cancellationToken).Token;
try
{
token.ThrowIfCancellationRequested();
logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint);
if (path == "/video")
{
// MJPEG 流请求FPGA
await HandleMjpegStreamAsync(context.Response, client, token);
}
else if (path == "/usbCamera")
{
// USB Camera MJPEG流请求
await HandleUsbCameraStreamAsync(context.Response, client, token);
}
else if (path == "/snapshot")
{
// 单帧图像请求
await HandleSnapshotRequestAsync(context.Response, client, token);
}
else if (path == "/html")
{
// HTML页面请求
await SendIndexHtmlPageAsync(context.Response);
}
else
{
// 默认返回简单的HTML页面提供链接到视频页面
await SendIndexHtmlPageAsync(context.Response);
}
}
catch (Exception ex)
{
logger.Error(ex, "接受HTTP客户端连接时发生错误");
}
}
private async Task SendErrorAsync(HttpListenerResponse response, string message)
{
response.StatusCode = 400;
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
response.Close();
}
// USB Camera MJPEG流处理
private async Task HandleUsbCameraStreamAsync(
HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
var camera = await _usbCamera.WithCancellation(cancellationToken);
Action<byte[]> frameHandler = async (jpegData) =>
{
try
{
var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegData.Length + "\r\n\r\n");
await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); // \r\n
await response.OutputStream.FlushAsync(cancellationToken);
}
catch
{
logger.Error("Error sending MJPEG frame");
}
};
try
{
if (!camera.IsCapturing)
{
logger.Error("USB Camera is not capturing");
response.StatusCode = 500;
await response.OutputStream.FlushAsync(cancellationToken);
response.Close();
return;
}
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");
logger.Info("Start USB Camera MJPEG Stream");
camera.FrameReady += frameHandler;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(-1, cancellationToken);
}
}
catch (OperationCanceledException)
{
logger.Info("USB Camera MJPEG 串流取消");
}
catch (Exception ex)
{
logger.Error(ex, "USB Camera MJPEG流处理异常");
}
finally
{
camera.FrameReady -= frameHandler;
logger.Info("Usb Camera Stream Stopped");
try { response.Close(); } catch { }
}
}
private async Task HandleSnapshotRequestAsync(
HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
// 读取 Camera 快照,返回 JPEG
var camera = await client.Camera.WithCancellation(cancellationToken);
var frameResult = await camera.ReadFrame();
if (!frameResult.IsSuccessful || frameResult.Value == null)
{
response.StatusCode = 500;
await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("Failed to get snapshot"));
response.Close();
return;
}
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80);
if (!jpegResult.IsSuccessful)
{
response.StatusCode = 500;
await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("JPEG conversion failed"));
response.Close();
return;
}
response.ContentType = "image/jpeg";
response.ContentLength64 = jpegResult.Value.Length;
await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken);
response.Close();
}
private async Task HandleMjpegStreamAsync(
HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
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");
var camera = await client.Camera.WithCancellation(cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
var frameResult = await camera.ReadFrame();
if (!frameResult.IsSuccessful || frameResult.Value == null) continue;
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80);
if (!jpegResult.IsSuccessful) continue;
var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegResult.Value.Length + "\r\n\r\n");
await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken);
await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken);
await response.OutputStream.FlushAsync(cancellationToken);
await Task.Delay(1000 / client.FrameWidth, cancellationToken);
}
response.Close();
}
private async Task SendVideoHtmlPageAsync(HttpListenerResponse response)
{
string html = $@"
<!DOCTYPE html>
<html>
<head>
<title>FPGA 视频流</title>
<meta charset=""utf-8"">
<style>
body {{ font-family: Arial, sans-serif; text-align: center; margin: 20px; }}
h1 {{ color: #333; }}
.video-container {{ margin: 20px auto; max-width: 800px; }}
.controls {{ margin: 10px 0; }}
img {{ max-width: 100%; border: 1px solid #ddd; }}
button {{ padding: 8px 16px; margin: 0 5px; cursor: pointer; }}
</style>
</head>
<body>
<h1>FPGA 实时视频流</h1>
<div class=""video-container"">
<img id=""videoStream"" src=""/video-stream"" alt=""FPGA视频流"" />
</div>
<div class=""controls"">
<button onclick=""document.getElementById('videoStream').src='/snapshot?t=' + new Date().getTime()"">刷新快照</button>
<button onclick=""document.getElementById('videoStream').src='/video-stream'"">开始流媒体</button>
<span id=""status"">状态: 连接中...</span>
</div>
<script>
document.getElementById('videoStream').onload = function() {{
document.getElementById('status').textContent = '状态: 已连接';
}};
document.getElementById('videoStream').onerror = function() {{
document.getElementById('status').textContent = '状态: 连接错误';
}};
</script>
</body>
</html>
";
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 = $@"
<!DOCTYPE html>
<html>
<head>
<title>FPGA WebLab 视频服务</title>
<meta charset=""utf-8"">
<style>
body {{ font-family: Arial, sans-serif; text-align: center; margin: 20px; }}
h1 {{ color: #333; }}
.links {{ margin: 20px; }}
a {{ padding: 10px 15px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 4px; margin: 5px; display: inline-block; }}
a:hover {{ background-color: #45a049; }}
</style>
</head>
<body>
<h1>FPGA WebLab 视频服务</h1>
<div class=""links"">
<a href=""/video-feed.html"">观看实时视频</a>
<a href=""/snapshot"" target=""_blank"">获取当前快照</a>
</div>
<p>HTTP流媒体服务端口: {_serverPort}</p>
</body>
</html>
";
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();
}
/// <summary>
/// 从 FPGA 获取图像数据
/// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24
/// </summary>
private async Task<byte[]?> GetFPGAImageData(
VideoStreamClient client, CancellationToken cancellationToken = default)
{
try
{
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
// 从摄像头读取帧数据
var readStartTime = DateTime.UtcNow;
var camera = await client.Camera.WithCancellation(cancellationToken);
var result = await camera.ReadFrame();
var readEndTime = DateTime.UtcNow;
var readTime = (readEndTime - readStartTime).TotalMilliseconds;
if (!result.IsSuccessful)
{
logger.Error("读取摄像头帧数据失败: {Error}", result.Error);
return new byte[0];
}
var rgb565Data = result.Value;
// 验证数据长度是否正确
if (!Common.Image.ValidateImageDataLength(rgb565Data, client.FrameWidth, client.FrameHeight, 2))
{
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
client.FrameWidth * client.FrameHeight * 2, rgb565Data.Length);
}
// 将 RGB565 转换为 RGB24
var convertStartTime = DateTime.UtcNow;
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, client.FrameWidth, client.FrameHeight, isLittleEndian: false);
var convertEndTime = DateTime.UtcNow;
var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
if (!rgb24Result.IsSuccessful)
{
logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error);
return new byte[0];
}
return rgb24Result.Value;
}
}
catch (Exception ex)
{
logger.Error(ex, "获取FPGA图像数据时发生错误");
return new byte[0];
}
}
/// <summary>
/// 设置视频流分辨率
/// </summary>
/// <param name="boardId">板卡ID</param>
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>设置结果</returns>
public async Task<Result<bool>> SetResolutionAsync(
string boardId, int width, int height,
int timeout = 100, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
using (await client.Lock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken))
{
var currentCamera = await client.Camera.WithCancellation(cancellationToken);
if (currentCamera == null)
{
var message = $"获取摄像头失败";
logger.Error(message);
return new(new Exception(message));
}
// 设置摄像头分辨率
var ret = await currentCamera.ChangeResolution(width, height);
if (!ret.IsSuccessful)
{
var message = $"设置摄像头分辨率失败: {ret.Error}";
logger.Error(message);
return new(new Exception(message));
}
if (!ret.Value)
{
logger.Warn($"设置摄像头分辨率失败");
return false;
}
// 更新HTTP服务的分辨率配置
client.FrameWidth = width;
client.FrameHeight = height;
logger.Info($"视频流分辨率已成功设置为 {width}x{height}");
return true;
}
}
catch (Exception ex)
{
var message = $"设置分辨率时发生错误: {ex.Message}";
logger.Error(ex, message);
return new(new Exception(message));
}
}
/// <summary>
/// 初始化摄像头自动对焦功能
/// </summary>
/// <returns>初始化结果</returns>
public async Task<bool> InitAutoFocusAsync(
string boardId, int timeout = 1000, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
using (await client.Lock.AcquireWriteLockAsync(
TimeSpan.FromMilliseconds(timeout), cancellationToken))
{
var camera = await client.Camera.WithCancellation(cancellationToken);
var result = await camera.InitAutoFocus();
if (result.IsSuccessful && result.Value)
{
logger.Info($"Board{boardId}摄像头自动对焦功能初始化成功");
return true;
}
else
{
logger.Error($"Board{boardId}摄像头自动对焦功能初始化失败: {result.Error?.Message ?? ""}");
return false;
}
}
}
catch (Exception ex)
{
logger.Error(ex, $"Board{boardId}初始化摄像头自动对焦功能时发生异常");
return false;
}
}
/// <summary>
/// 执行摄像头自动对焦
/// </summary>
/// <returns>对焦结果</returns>
public async Task<bool> PerformAutoFocusAsync(
string boardId, int timeout = 1000, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
logger.Info($"Board{boardId}开始执行摄像头自动对焦");
var camera = await client.Camera.WithCancellation(cancellationToken);
var result = await camera.PerformAutoFocus();
if (result.IsSuccessful && result.Value)
{
logger.Info($"Board{boardId}摄像头自动对焦成功");
return true;
}
else
{
logger.Error($"Board{boardId}摄像头自动对焦执行失败: {result.Error?.Message ?? ""}");
return false;
}
}
catch (Exception ex)
{
logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常");
return false;
}
}
/// <summary>
/// 配置摄像头连接参数
/// </summary>
/// <param name="boardId">板卡ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>配置是否成功</returns>
public async Task<bool> ConfigureCameraAsync(string boardId, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
var camera = await client.Camera.WithCancellation(cancellationToken);
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
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!");
}
}
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
var ret = await camera.ChangeResolution(client.FrameWidth, client.FrameHeight);
if (!ret.IsSuccessful)
{
logger.Error(ret.Error);
throw ret.Error;
}
if (!ret.Value)
{
logger.Error($"Camera Resolution Change Failed!");
throw new Exception($"Camera Resolution Change Failed!");
}
}
return true;
}
catch (Exception ex)
{
logger.Error(ex, "配置摄像头连接时发生错误");
return false;
}
}
public async Task SetVideoStreamEnableAsync(string boardId, bool enable)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
if (client.IsEnabled == enable)
return;
using (await client.Lock.AcquireWriteLockAsync())
{
if (!enable || client.CTS.IsCancellationRequested)
{
client.CTS.Cancel();
client.CTS = new CancellationTokenSource();
}
if (!client.Camera.IsValueCreated) return;
var camera = await client.Camera.WithCancellation(client.CTS.Token);
var disableResult = await camera.EnableHardwareTrans(enable);
if (disableResult.IsSuccessful && disableResult.Value)
logger.Info($"Successfully disabled camera {boardId} hardware transmission");
else
logger.Error($"Failed to disable camera {boardId} hardware transmission: {disableResult.Error}");
}
}
catch (Exception ex)
{
logger.Error(ex, $"Exception occurred while disabling video transmission for {boardId}");
}
}
public async ValueTask<bool> TestCameraConnection(string boardId)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
var imageData = await GetFPGAImageData(client);
if (imageData == null || imageData.Length == 0)
return false;
return true;
}
catch (Exception ex)
{
logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常");
return false;
}
}
public VideoStreamEndpoint GetVideoEndpoint(string boardId)
{
var client = GetOrCreateClient(boardId, 640, 480).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
return new VideoStreamEndpoint
{
BoardId = boardId,
MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={boardId}",
VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={boardId}",
SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={boardId}",
UsbCameraUrl = $"http://{Global.LocalHost}:{_serverPort}/usbCamera?boardId={boardId}",
HtmlUrl = $"http://{Global.LocalHost}:{_serverPort}/html?boardId={boardId}",
IsEnabled = client.IsEnabled,
FrameRate = client.FrameRate
};
}
public List<VideoStreamEndpoint> GetAllVideoEndpoints()
{
var endpoints = new List<VideoStreamEndpoint>();
foreach (var boardId in _clientDict.Keys)
endpoints.Add(GetVideoEndpoint(boardId));
return endpoints;
}
public VideoStreamServiceStatus GetServiceStatus()
{
return new VideoStreamServiceStatus
{
IsRunning = true,
ServerPort = _serverPort,
ClientEndpoints = GetAllVideoEndpoints()
};
}
}