feat: 支持实际摄像头视频流

This commit is contained in:
2025-07-03 17:51:12 +08:00
parent 178ac0de67
commit e84a784517
4 changed files with 1323 additions and 438 deletions

View File

@@ -1,15 +1,12 @@
using System.Net;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using Peripherals.CameraClient; // 添加摄像头客户端引用
namespace server.Services;
/// <summary>
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
/// 简化版本实现,先建立基础框架
/// 支持动态配置摄像头地址和端口
/// </summary>
public class HttpVideoStreamService : BackgroundService
{
@@ -22,8 +19,9 @@ public class HttpVideoStreamService : BackgroundService
// 摄像头客户端
private Camera? _camera;
private readonly string _cameraAddress = "192.168.1.100"; // 根据实际FPGA地址配置
private readonly int _cameraPort = 8888; // 根据实际端口配置
private string _cameraAddress = "192.168.1.100"; // 默认FPGA地址
private int _cameraPort = 8888; // 默认端口
private readonly object _cameraLock = new object();
// 模拟 FPGA 图像数据
private int _frameCounter = 0;
@@ -33,16 +31,7 @@ public class HttpVideoStreamService : BackgroundService
/// <summary>
/// 获取当前连接的客户端数量
/// </summary>
public int ConnectedClientsCount
{
get
{
lock (_clientsLock)
{
return _activeClients.Count;
}
}
}
public int ConnectedClientsCount { get { return _activeClients.Count; } }
/// <summary>
/// 获取服务端口
@@ -64,13 +53,141 @@ public class HttpVideoStreamService : BackgroundService
/// </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()
{
// 初始化摄像头客户端
_camera = new Camera(_cameraAddress, _cameraPort);
// 延迟初始化摄像头客户端,直到配置完成
logger.Info("HttpVideoStreamService 初始化完成,默认摄像头地址: {Address}:{Port}", _cameraAddress, _cameraPort);
}
/// <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
{
await Task.Run(() =>
{
lock (_cameraLock)
{
// 如果地址和端口没有变化,直接返回成功
if (_cameraAddress == address && _cameraPort == port && _camera != null)
{
logger.Info("摄像头配置未变化,保持当前连接");
return;
}
// 关闭现有连接
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);
}
});
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 object GetCameraStatus()
{
lock (_cameraLock)
{
return new
{
Address = _cameraAddress,
Port = _cameraPort,
IsConfigured = _camera != null,
ConnectionString = $"{_cameraAddress}:{_cameraPort}"
};
}
}
/// <summary>
@@ -83,6 +200,10 @@ public class HttpVideoStreamService : BackgroundService
try
{
logger.Info("启动 HTTP 视频流服务,端口: {Port}", _serverPort);
// 初始化默认摄像头连接
await ConfigureCameraAsync(_cameraAddress, _cameraPort);
// 创建 HTTP 监听器
_httpListener = new HttpListener();
_httpListener.Prefixes.Add($"http://localhost:{_serverPort}/");
@@ -220,7 +341,18 @@ public class HttpVideoStreamService : BackgroundService
{
// 获取当前帧
var imageData = await GetFPGAImageData();
var jpegData = ConvertToJpeg(imageData);
// 直接使用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";
@@ -338,11 +470,8 @@ public class HttpVideoStreamService : BackgroundService
// 从 FPGA 获取图像数据(模拟)
var imageData = await GetFPGAImageData();
// 将图像数据转换为 JPEG
var jpegData = ConvertToJpeg(imageData);
// 向所有连接的客户端发送帧
await BroadcastFrameAsync(jpegData, cancellationToken);
await BroadcastFrameAsync(imageData, cancellationToken);
_frameCounter++;
@@ -367,7 +496,14 @@ public class HttpVideoStreamService : BackgroundService
/// </summary>
private async Task<byte[]> GetFPGAImageData()
{
if (_camera == null)
Camera? currentCamera = null;
lock (_cameraLock)
{
currentCamera = _camera;
}
if (currentCamera == null)
{
logger.Error("摄像头客户端未初始化");
return new byte[0];
@@ -376,8 +512,8 @@ public class HttpVideoStreamService : BackgroundService
try
{
// 从摄像头读取帧数据
var result = await _camera.ReadFrame();
var result = await currentCamera.ReadFrame();
if (!result.IsSuccessful)
{
logger.Error("读取摄像头帧数据失败: {Error}", result.Error);
@@ -385,11 +521,11 @@ public class HttpVideoStreamService : BackgroundService
}
var rgb565Data = result.Value;
// 验证数据长度是否正确
if (!Common.Image.ValidateImageDataLength(rgb565Data, _frameWidth, _frameHeight, 2))
{
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
_frameWidth * _frameHeight * 2, rgb565Data.Length);
}
@@ -403,7 +539,7 @@ public class HttpVideoStreamService : BackgroundService
if (_frameCounter % 30 == 0) // 每秒更新一次日志
{
logger.Debug("成功获取第 {FrameNumber} 帧RGB565大小: {RGB565Size} 字节, RGB24大小: {RGB24Size} 字节",
logger.Debug("成功获取第 {FrameNumber} 帧RGB565大小: {RGB565Size} 字节, RGB24大小: {RGB24Size} 字节",
_frameCounter, rgb565Data.Length, rgb24Result.Value.Length);
}
@@ -416,18 +552,6 @@ public class HttpVideoStreamService : BackgroundService
}
}
private async Task<byte[]> ConvertToJpeg(byte[] rgbData)
{
var jpegResult = Common.Image.ConvertRGB24ToJpeg(rgbData, _frameWidth, _frameHeight, 80);
if (!jpegResult.IsSuccessful)
{
logger.Error("RGB24转JPEG失败: {Error}", jpegResult.Error);
return new byte[0];
}
return jpegResult.Value;
}
/// <summary>
/// 向所有连接的客户端广播帧数据
/// </summary>
@@ -439,8 +563,18 @@ public class HttpVideoStreamService : BackgroundService
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(frameData.Length);
var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
var clientsToRemove = new List<HttpListenerResponse>();
@@ -466,7 +600,7 @@ public class HttpVideoStreamService : BackgroundService
await client.OutputStream.WriteAsync(mjpegFrameHeader, 0, mjpegFrameHeader.Length, cancellationToken);
// 发送JPEG数据
await client.OutputStream.WriteAsync(frameData, 0, frameData.Length, cancellationToken);
await client.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
// 发送结尾换行符
await client.OutputStream.WriteAsync(mjpegFrameFooter, 0, mjpegFrameFooter.Length, cancellationToken);
@@ -477,7 +611,7 @@ public class HttpVideoStreamService : BackgroundService
if (_frameCounter % 30 == 0) // 每秒记录一次日志
{
logger.Debug("已向客户端 {ClientId} 发送第 {FrameNumber} 帧,大小:{Size} 字节",
client.OutputStream.GetHashCode(), _frameCounter, frameData.Length);
client.OutputStream.GetHashCode(), _frameCounter, jpegData.Length);
}
}
catch (Exception ex)
@@ -528,6 +662,8 @@ public class HttpVideoStreamService : BackgroundService
/// </summary>
public object GetServiceStatus()
{
var cameraStatus = GetCameraStatus();
return new
{
IsRunning = _httpListener?.IsListening ?? false,
@@ -535,7 +671,8 @@ public class HttpVideoStreamService : BackgroundService
FrameRate = _frameRate,
Resolution = $"{_frameWidth}x{_frameHeight}",
ConnectedClients = ConnectedClientsCount,
ClientEndpoints = GetConnectedClientEndpoints()
ClientEndpoints = GetConnectedClientEndpoints(),
CameraStatus = cameraStatus
};
}
@@ -563,6 +700,12 @@ public class HttpVideoStreamService : BackgroundService
_activeClients.Clear();
}
// 关闭摄像头连接
lock (_cameraLock)
{
_camera = null;
}
await base.StopAsync(cancellationToken);
logger.Info("HTTP 视频流服务已停止");
@@ -592,6 +735,11 @@ public class HttpVideoStreamService : BackgroundService
_activeClients.Clear();
}
lock (_cameraLock)
{
_camera = null;
}
base.Dispose();
}
}