GetOrCreateClientAsync(string boardId, int initWidth, int initHeight)
{
if (_clientDict.TryGetValue(boardId, out var client))
{
// 可在此处做分辨率/Camera等配置更新
return client;
}
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 Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
var ret = await camera.Init();
if (!ret.IsSuccessful || !ret.Value)
{
logger.Error("Camera Init Failed!");
return null;
}
client = new VideoStreamClient(boardId, initWidth, initHeight, camera);
_clientDict[boardId] = client;
return client;
}
///
/// 初始化 HttpVideoStreamService
///
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);
}
///
/// 停止 HTTP 视频流服务
///
public override async Task StopAsync(CancellationToken cancellationToken)
{
foreach (var clientKey in _clientDict.Keys)
{
var client = _clientDict[clientKey];
client.CTS.Cancel();
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
await client.Camera.EnableHardwareTrans(false);
}
}
_clientDict.Clear();
await base.StopAsync(cancellationToken);
}
///
/// 执行 HTTP 视频流服务
///
/// 取消令牌
/// 任务
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["board"];
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;
if (string.IsNullOrEmpty(boardId))
{
await SendErrorAsync(context.Response, "Missing clientId");
return;
}
var client = await GetOrCreateClientAsync(boardId, width, height);
if (client == null)
{
await SendErrorAsync(context.Response, "Invalid clientId or camera not available");
return;
}
var clientToken = client.CTS.Token;
try
{
logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint);
if (path == "/video-stream")
{
// MJPEG 流请求(FPGA)
await HandleMjpegStreamAsync(context.Response, client, cancellationToken);
}
#if USB_CAMERA
else if (requestPath == "/usb-camera")
{
// USB Camera MJPEG流请求
await HandleUsbCameraStreamAsync(response, cancellationToken);
}
#endif
else if (path == "/snapshot")
{
// 单帧图像请求
await HandleSnapshotRequestAsync(context.Response, client, cancellationToken);
}
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流处理
#if USB_CAMERA
private async Task HandleUsbCameraStreamAsync(HttpListenerResponse response, CancellationToken cancellationToken)
{
try
{
lock (_usbCameraLock)
{
if (_usbCamera == null)
{
_usbCamera = new VideoCapture(1);
_usbCamera.Fps = _frameRate;
_usbCamera.FrameWidth = _frameWidth;
_usbCamera.FrameHeight = _frameHeight;
_usbCameraEnable = _usbCamera.IsOpened();
}
}
if (!_usbCameraEnable || _usbCamera == null || !_usbCamera.IsOpened())
{
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");
using (var mat = new Mat())
{
while (!cancellationToken.IsCancellationRequested)
{
bool grabbed;
lock (_usbCameraLock)
{
grabbed = _usbCamera.Read(mat);
}
if (!grabbed || mat.Empty())
{
await Task.Delay(50, cancellationToken);
continue;
}
// 编码为JPEG
byte[]? jpegData = null;
try
{
jpegData = mat.ToBytes(".jpg", new int[] { (int)ImwriteFlags.JpegQuality, 80 });
}
catch (Exception ex)
{
logger.Error(ex, "USB Camera帧编码JPEG失败");
continue;
}
if (jpegData == null)
continue;
// MJPEG帧头
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);
await Task.Delay(1000 / _frameRate, cancellationToken);
}
}
}
catch (Exception ex)
{
logger.Error(ex, "USB Camera MJPEG流处理异常");
}
finally
{
try { response.Close(); } catch { }
}
}
#endif
private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
// 读取 Camera 快照,返回 JPEG
var frameResult = await client.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");
while (!cancellationToken.IsCancellationRequested)
{
var frameResult = await client.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 = $@"
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();
}
///
/// 从 FPGA 获取图像数据
/// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24
///
private async Task GetFPGAImageData(
VideoStreamClient client, CancellationToken cancellationToken = default)
{
try
{
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
// 从摄像头读取帧数据
var readStartTime = DateTime.UtcNow;
var result = await client.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];
}
}
///
/// 设置视频流分辨率
///
/// 板卡ID
/// 宽度
/// 高度
/// 超时时间(毫秒)
/// 取消令牌
/// 设置结果
public async Task> 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 = client.Camera;
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));
}
}
///
/// 初始化摄像头自动对焦功能
///
/// 初始化结果
public async Task 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 result = await client.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;
}
}
///
/// 执行摄像头自动对焦
///
/// 对焦结果
public async Task PerformAutoFocusAsync(
string boardId, int timeout = 1000, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
logger.Info($"Board{boardId}开始执行摄像头自动对焦");
var result = await client.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;
}
}
///
/// 配置摄像头连接参数
///
/// 板卡ID
/// 配置是否成功
public async Task ConfigureCameraAsync(string boardId)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
using (await client.Lock.AcquireWriteLockAsync())
{
var ret = await client.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())
{
var ret = await client.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 = new CancellationTokenSource();
}
else
{
client.CTS.Cancel();
}
var camera = client.Camera;
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 HDMI transmission for camera {boardId}");
}
}
public async ValueTask 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 = TryGetClient(boardId).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 GetAllVideoEndpoints()
{
var endpoints = new List();
foreach (var boardId in _clientDict.Keys)
endpoints.Add(GetVideoEndpoint(boardId));
return endpoints;
}
public VideoStreamServiceStatus GetServiceStatus()
{
return new VideoStreamServiceStatus
{
IsRunning = true,
ServerPort = _serverPort,
ClientEndpoints = GetAllVideoEndpoints()
};
}
}