FPGA_WebLab/server/src/Services/HttpVideoStreamService.cs

908 lines
29 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.Threading.Tasks;
using Peripherals.CameraClient; // 添加摄像头客户端引用
namespace server.Services;
/// <summary>
/// 表示摄像头连接状态信息
/// </summary>
public class CameraStatus
{
/// <summary>
/// 摄像头的IP地址
/// </summary>
public string Address { get; set; } = string.Empty;
/// <summary>
/// 摄像头的端口号
/// </summary>
public int Port { get; set; }
/// <summary>
/// 是否已配置摄像头
/// </summary>
public bool IsConfigured { get; set; }
/// <summary>
/// 摄像头连接字符串IP:端口)
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
}
/// <summary>
/// 表示视频流服务的运行状态
/// </summary>
public class ServiceStatus
{
/// <summary>
/// 服务是否正在运行
/// </summary>
public bool IsRunning { get; set; }
/// <summary>
/// 服务监听的端口号
/// </summary>
public int ServerPort { get; set; }
/// <summary>
/// 视频流的帧率FPS
/// </summary>
public int FrameRate { get; set; }
/// <summary>
/// 视频分辨率(如 640x480
/// </summary>
public string Resolution { get; set; } = string.Empty;
/// <summary>
/// 当前连接的客户端数量
/// </summary>
public int ConnectedClients { get; set; }
/// <summary>
/// 当前连接的客户端端点列表
/// </summary>
public List<string> ClientEndpoints { get; set; } = new();
/// <summary>
/// 摄像头连接状态信息
/// </summary>
public CameraStatus CameraStatus { get; set; } = new();
}
/// <summary>
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
/// 支持动态配置摄像头地址和端口
/// </summary>
public class HttpVideoStreamService : BackgroundService
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
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 Camera? _camera;
private bool _cameraEnable = false;
private string _cameraAddress = "192.168.1.100"; // 默认FPGA地址
private int _cameraPort = 8888; // 默认端口
private readonly object _cameraLock = new object();
// 模拟 FPGA 图像数据
private int _frameCounter = 0;
private readonly List<HttpListenerResponse> _activeClients = new List<HttpListenerResponse>();
private readonly object _clientsLock = new object();
/// <summary>
/// 获取当前连接的客户端数量
/// </summary>
public int ConnectedClientsCount { get { return _activeClients.Count; } }
/// <summary>
/// 获取服务端口
/// </summary>
public int ServerPort => _serverPort;
/// <summary>
/// 获取帧宽度
/// </summary>
public int FrameWidth => _frameWidth;
/// <summary>
/// 获取帧高度
/// </summary>
public int FrameHeight => _frameHeight;
/// <summary>
/// 获取帧率
/// </summary>
public int FrameRate => _frameRate;
/// <summary>
/// 获取当前摄像头地址
/// </summary>
public string CameraAddress { get { return _cameraAddress; } }
/// <summary>
/// 获取当前摄像头端口
/// </summary>
public int CameraPort { get { return _cameraPort; } }
/// <summary>
/// 初始化 HttpVideoStreamService
/// </summary>
public HttpVideoStreamService()
{
// 延迟初始化摄像头客户端,直到配置完成
logger.Info("HttpVideoStreamService 初始化完成,默认摄像头地址: {Address}:{Port}", _cameraAddress, _cameraPort);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="isEnabled">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async Task SetEnable(bool isEnabled)
{
if (_camera == null)
{
throw new Exception("Please config camera first");
}
_cameraEnable = isEnabled;
await _camera.EnableCamera(_cameraEnable);
}
/// <summary>
/// 配置摄像头连接参数
/// </summary>
/// <param name="address">摄像头IP地址</param>
/// <param name="port">摄像头端口</param>
/// <returns>配置是否成功</returns>
public async Task<bool> 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
{
lock (_cameraLock)
{
// 关闭现有连接
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);
}
// Init Camera
{
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!");
}
}
return true;
}
catch (Exception ex)
{
logger.Error(ex, "配置摄像头连接时发生错误");
return false;
}
}
/// <summary>
/// 测试摄像头连接
/// </summary>
/// <returns>连接测试结果</returns>
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);
}
}
/// <summary>
/// 获取摄像头连接状态
/// </summary>
/// <returns>连接状态信息</returns>
public CameraStatus GetCameraStatus()
{
return new CameraStatus
{
Address = _cameraAddress,
Port = _cameraPort,
IsConfigured = _camera != null,
ConnectionString = $"{_cameraAddress}:{_cameraPort}"
};
}
/// <summary>
/// 执行 HTTP 视频流服务
/// </summary>
/// <param name="stoppingToken">取消令牌</param>
/// <returns>任务</returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
logger.Info("启动 HTTP 视频流服务,端口: {Port}", _serverPort);
// 初始化默认摄像头连接
await ConfigureCameraAsync(_cameraAddress, _cameraPort);
// 创建 HTTP 监听器
_httpListener = new HttpListener();
_httpListener.Prefixes.Add($"http://localhost:{_serverPort}/");
_httpListener.Start();
logger.Info("HTTP 视频流服务已启动,监听端口: {Port}", _serverPort);
// 开始接受客户端连接
_ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken);
// 开始生成视频帧
while (!stoppingToken.IsCancellationRequested)
{
if (_cameraEnable)
{
await GenerateVideoFrames(stoppingToken);
}
else
{
await Task.Delay(500, stoppingToken);
}
}
}
catch (HttpListenerException ex)
{
logger.Error(ex, "HTTP 视频流服务启动失败请确保您有管理员权限或使用netsh配置URL前缀权限");
}
catch (Exception ex)
{
logger.Error(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.Info("新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.Error(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.Debug("已启动MJPEG流客户端: {RemoteEndPoint}", response.OutputStream?.GetHashCode() ?? 0);
// 保持连接直到取消或出错
try
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken); // 简单的保活循环
}
}
catch (TaskCanceledException)
{
// 预期的取消
}
logger.Debug("MJPEG流已结束客户端: {ClientId}", response.OutputStream?.GetHashCode() ?? 0);
}
catch (Exception ex)
{
logger.Error(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();
// 直接使用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";
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.Debug("已发送快照图像,大小:{Size} 字节", jpegData.Length);
}
catch (Exception ex)
{
logger.Error(ex, "处理快照请求时出错");
}
finally
{
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();
}
private async Task GenerateVideoFrames(CancellationToken cancellationToken)
{
var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate);
var lastFrameTime = DateTime.UtcNow;
while (!cancellationToken.IsCancellationRequested && _cameraEnable)
{
try
{
var frameStartTime = DateTime.UtcNow;
// 从 FPGA 获取图像数据
var imageData = await GetFPGAImageData();
var imageAcquireTime = DateTime.UtcNow;
// 如果有图像数据,立即开始广播(不等待)
if (imageData != null && imageData.Length > 0)
{
// 异步广播帧,不阻塞下一帧的获取
_ = Task.Run(async () =>
{
try
{
await BroadcastFrameAsync(imageData, cancellationToken);
}
catch (Exception ex)
{
logger.Error(ex, "异步广播帧时发生错误");
}
}, cancellationToken);
_frameCounter++;
var frameEndTime = DateTime.UtcNow;
var frameProcessingTime = (frameEndTime - frameStartTime).TotalMilliseconds;
var imageAcquireElapsed = (imageAcquireTime - frameStartTime).TotalMilliseconds;
if (_frameCounter % 30 == 0) // 每秒记录一次性能信息
{
logger.Debug("帧 {FrameNumber} 性能统计 - 图像获取: {AcquireTime:F1}ms, 总处理: {ProcessTime:F1}ms",
_frameCounter, imageAcquireElapsed, frameProcessingTime);
}
}
// 动态调整延迟 - 基于实际处理时间
var elapsed = (DateTime.UtcNow - lastFrameTime).TotalMilliseconds;
var targetInterval = frameInterval.TotalMilliseconds;
var remainingDelay = Math.Max(0, targetInterval - elapsed);
if (remainingDelay > 0)
{
await Task.Delay(TimeSpan.FromMilliseconds(remainingDelay), cancellationToken);
}
lastFrameTime = DateTime.UtcNow;
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
logger.Error(ex, "生成视频帧时发生错误");
await Task.Delay(100, cancellationToken); // 减少错误恢复延迟
}
}
}
/// <summary>
/// 从 FPGA 获取图像数据
/// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24
/// </summary>
private async Task<byte[]> GetFPGAImageData()
{
var startTime = DateTime.UtcNow;
Camera? currentCamera = null;
lock (_cameraLock)
{
currentCamera = _camera;
}
if (currentCamera == null)
{
logger.Error("摄像头客户端未初始化");
return new byte[0];
}
try
{
// 从摄像头读取帧数据
var readStartTime = DateTime.UtcNow;
var result = await currentCamera.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, _frameWidth, _frameHeight, 2))
{
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
_frameWidth * _frameHeight * 2, rgb565Data.Length);
}
// 将 RGB565 转换为 RGB24
var convertStartTime = DateTime.UtcNow;
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, _frameWidth, _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];
}
var totalTime = (DateTime.UtcNow - startTime).TotalMilliseconds;
if (_frameCounter % 30 == 0) // 每秒更新一次日志
{
logger.Debug("帧 {FrameNumber} 数据获取性能 - 读取: {ReadTime:F1}ms, 转换: {ConvertTime:F1}ms, 总计: {TotalTime:F1}ms, RGB565: {RGB565Size} 字节, RGB24: {RGB24Size} 字节",
_frameCounter, readTime, convertTime, totalTime, rgb565Data.Length, rgb24Result.Value.Length);
}
return rgb24Result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "获取FPGA图像数据时发生错误");
return new byte[0];
}
}
/// <summary>
/// 向所有连接的客户端广播帧数据
/// </summary>
private async Task BroadcastFrameAsync(byte[] frameData, CancellationToken cancellationToken)
{
if (frameData == null || frameData.Length == 0)
{
logger.Warn("尝试广播空帧数据");
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(jpegData.Length);
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
var clientsToRemove = new List<HttpListenerResponse>();
var clientsToProcess = new List<HttpListenerResponse>();
// 获取当前连接的客户端列表
lock (_clientsLock)
{
clientsToProcess.AddRange(_activeClients);
}
if (clientsToProcess.Count == 0)
{
return; // 没有活跃客户端
}
// 向每个活跃的客户端并行发送帧
var sendTasks = clientsToProcess.Select(async client =>
{
try
{
// 发送帧头部
await client.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken);
// 发送JPEG数据
await client.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
// 发送结尾换行符
await client.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken);
// 确保数据立即发送
await client.OutputStream.FlushAsync(cancellationToken);
return (client, success: true, error: (Exception?)null);
}
catch (Exception ex)
{
return (client, success: false, error: ex);
}
});
// 等待所有发送任务完成
var results = await Task.WhenAll(sendTasks);
// 处理发送结果
foreach (var (client, success, error) in results)
{
if (!success)
{
logger.Debug("发送帧数据时出错: {Error}", error?.Message ?? "未知错误");
clientsToRemove.Add(client);
}
}
if (_frameCounter % 30 == 0 && clientsToProcess.Count > 0) // 每秒记录一次日志
{
logger.Debug("已向 {ClientCount} 个客户端发送第 {FrameNumber} 帧,大小:{Size} 字节",
clientsToProcess.Count, _frameCounter, jpegData.Length);
}
// 移除断开连接的客户端
if (clientsToRemove.Count > 0)
{
lock (_clientsLock)
{
foreach (var client in clientsToRemove)
{
_activeClients.Remove(client);
try { client.Close(); }
catch { /* 忽略关闭错误 */ }
}
}
logger.Info("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}",
clientsToRemove.Count, _activeClients.Count);
}
}
/// <summary>
/// 获取连接的客户端端点列表
/// </summary>
public List<string> GetConnectedClientEndpoints()
{
List<string> endpoints = new List<string>();
lock (_clientsLock)
{
foreach (var client in _activeClients)
{
endpoints.Add($"Client-{client.OutputStream?.GetHashCode() ?? 0}");
}
}
return endpoints;
}
/// <summary>
/// 获取服务状态信息
/// </summary>
public ServiceStatus GetServiceStatus()
{
var cameraStatus = GetCameraStatus();
return new ServiceStatus
{
IsRunning = (_httpListener?.IsListening ?? false) && _cameraEnable,
ServerPort = _serverPort,
FrameRate = _frameRate,
Resolution = $"{_frameWidth}x{_frameHeight}",
ConnectedClients = ConnectedClientsCount,
ClientEndpoints = GetConnectedClientEndpoints(),
CameraStatus = cameraStatus
};
}
/// <summary>
/// 停止 HTTP 视频流服务
/// </summary>
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.Info("正在停止 HTTP 视频流服务...");
_cameraEnable = false;
if (_httpListener != null && _httpListener.IsListening)
{
_httpListener.Stop();
_httpListener.Close();
}
// 关闭所有客户端连接
lock (_clientsLock)
{
foreach (var client in _activeClients)
{
try { client.Close(); }
catch { /* 忽略关闭错误 */ }
}
_activeClients.Clear();
}
// 关闭摄像头连接
lock (_cameraLock)
{
_camera = null;
}
await base.StopAsync(cancellationToken);
logger.Info("HTTP 视频流服务已停止");
}
/// <summary>
/// 释放资源
/// </summary>
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();
}
lock (_cameraLock)
{
_camera = null;
}
base.Dispose();
}
}