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,8 +1,9 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// [TODO:description]
/// 视频流控制器,支持动态配置摄像头连接
/// </summary>
[ApiController]
[Route("api/[controller]")]
@@ -11,6 +12,20 @@ public class VideoStreamController : ControllerBase
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly server.Services.HttpVideoStreamService _videoStreamService;
/// <summary>
/// 摄像头配置请求模型
/// </summary>
public class CameraConfigRequest
{
[Required]
[RegularExpression(@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ErrorMessage = "IP地址")]
public string Address { get; set; } = "";
[Required]
[Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
public int Port { get; set; }
}
/// <summary>
/// 初始化HTTP视频流控制器
/// </summary>
@@ -47,12 +62,14 @@ public class VideoStreamController : ControllerBase
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
connectedClients = _videoStreamService.ConnectedClientsCount,
clientEndpoints = _videoStreamService.GetConnectedClientEndpoints()
clientEndpoints = _videoStreamService.GetConnectedClientEndpoints(),
cameraStatus = _videoStreamService.GetCameraStatus()
});
}
catch (Exception ex)
{
logger.Error(ex, "获取 HTTP 视频流服务状态失败"); return TypedResults.InternalServerError(ex.Message);
logger.Error(ex, "获取 HTTP 视频流服务状态失败");
return TypedResults.InternalServerError(ex.Message);
}
}
@@ -77,12 +94,123 @@ public class VideoStreamController : ControllerBase
format = "MJPEG",
htmlUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-feed.html",
mjpegUrl = $"http://localhost:{_videoStreamService.ServerPort}/video-stream",
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot"
snapshotUrl = $"http://localhost:{_videoStreamService.ServerPort}/snapshot",
cameraAddress = _videoStreamService.CameraAddress,
cameraPort = _videoStreamService.CameraPort
});
}
catch (Exception ex)
{
logger.Error(ex, "获取 HTTP 视频流信息失败"); return TypedResults.InternalServerError(ex.Message);
logger.Error(ex, "获取 HTTP 视频流信息失败");
return TypedResults.InternalServerError(ex.Message);
}
}
/// <summary>
/// 配置摄像头连接参数
/// </summary>
/// <param name="config">摄像头配置</param>
/// <returns>配置结果</returns>
[HttpPost("ConfigureCamera")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async Task<IResult> ConfigureCamera([FromBody] CameraConfigRequest config)
{
try
{
logger.Info("配置摄像头连接: {Address}:{Port}", config.Address, config.Port);
var success = await _videoStreamService.ConfigureCameraAsync(config.Address, config.Port);
if (success)
{
return TypedResults.Ok(new
{
success = true,
message = "摄像头配置成功",
cameraAddress = config.Address,
cameraPort = config.Port
});
}
else
{
return TypedResults.BadRequest(new
{
success = false,
message = "摄像头配置失败",
cameraAddress = config.Address,
cameraPort = config.Port
});
}
}
catch (Exception ex)
{
logger.Error(ex, "配置摄像头连接失败");
return TypedResults.InternalServerError(ex.Message);
}
}
/// <summary>
/// 获取当前摄像头配置
/// </summary>
/// <returns>摄像头配置信息</returns>
[HttpGet("CameraConfig")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public IResult GetCameraConfig()
{
try
{
logger.Info("获取摄像头配置");
var cameraStatus = _videoStreamService.GetCameraStatus();
return TypedResults.Ok(new
{
address = _videoStreamService.CameraAddress,
port = _videoStreamService.CameraPort,
isConfigured = cameraStatus.GetType().GetProperty("IsConfigured")?.GetValue(cameraStatus),
connectionString = $"{_videoStreamService.CameraAddress}:{_videoStreamService.CameraPort}"
});
}
catch (Exception ex)
{
logger.Error(ex, "获取摄像头配置失败");
return TypedResults.InternalServerError(ex.Message);
}
}
/// <summary>
/// 测试摄像头连接
/// </summary>
/// <returns>连接测试结果</returns>
[HttpPost("TestCameraConnection")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async Task<IResult> TestCameraConnection()
{
try
{
logger.Info("测试摄像头连接");
var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync();
return TypedResults.Ok(new
{
success = isSuccess,
message = message,
cameraAddress = _videoStreamService.CameraAddress,
cameraPort = _videoStreamService.CameraPort,
timestamp = DateTime.Now
});
}
catch (Exception ex)
{
logger.Error(ex, "测试摄像头连接失败");
return TypedResults.InternalServerError(ex.Message);
}
}

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();
}
}