基于ASP.NET Core的视频接收、预处理与客户端推流方案咨询
视频监控服务器实现方案
已实现进展
- 使用RtspClientSharp获取RTSP视频流,通过基于FFMpeg的FrameDecoderCore将H.264原始帧解码为Bitmap;
- 基于ML.Net搭建了YoloV4 ONNX目标检测模型。
核心问题与解决方案
问题1:无延迟视频接收下的目标检测及ONNX预测模型构建
无延迟处理方案
当前FrameReceived事件同步处理解码逻辑,会阻塞RTSP流接收。必须解耦流接收与帧处理:
- 用
Channel<T>创建异步有界队列,将解码后的DecodedFrame存入队列,后台独立任务消费队列执行目标检测; - 禁止在
FrameReceived回调中做任何耗时操作(包括检测、Bitmap转换后的复杂处理),确保RTSP客户端持续接收流,避免丢帧。
ONNX预测模型优化
- 简化模型结构:用Netron工具打开原始ONNX模型,删除训练相关节点(损失计算、优化器等),导出仅保留推理链路的轻量模型;
- ML.Net加载时明确输入输出约束:
var mlContext = new MLContext(); var model = mlContext.Model.Load("yolov4-infer-only.onnx", out _); // 定义与模型匹配的输入输出类型,确保Bitmap预处理(缩放、归一化)符合模型要求 var predictionEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(model);
- 将Bitmap预处理(如缩放至416x416、像素归一化)与模型推理放在同一个异步任务中,减少上下文切换开销。
问题2:处理后Bitmap转回视频流及目标触发录像
Bitmap转视频流
使用FFMpeg封装库(如Xabe.FFmpeg或FFmpeg.AutoGen)实现编码:
- 初始化编码器,配置分辨率、帧率、码率等参数(需与原始流匹配或按需调整);
- 将Bitmap转换为FFmpeg支持的YUV420P格式,逐帧送入编码器生成H.264原始帧;
- 编码后的帧可存入缓冲区,用于推流或本地文件写入。
目标触发录像
- 维护一个固定长度的帧缓存队列(比如缓存最近5秒的帧);
- 检测到目标时,从缓存中取出触发前的历史帧,拼接后续检测到目标的帧,用FFmpeg编码为MP4文件保存;
- 目标消失后立即停止录像,避免无效存储占用磁盘空间。
问题3:按需向Angular/React客户端推流
gRPC推流优化(替代Bitmap直推)
直接推送Bitmap效率极低,建议推送压缩后的帧数据:
- 定义gRPC服务协议:
service VideoStream { rpc GetStream (StreamRequest) returns (stream FramePacket); } message StreamRequest { bool requireProcessed = 1; // 标记是否需要经过检测处理的流 } message FramePacket { bytes data = 1; // H.264原始帧或JPEG压缩后的Bitmap数据 int64 timestamp = 2; }
- 后端根据客户端请求,选择推送原始编码帧或处理后编码帧;
- 前端接收后:
- 若为H.264流,用
hls.js或mpegts.js解析,直接在<video>标签播放; - 若为JPEG序列,用
requestAnimationFrame循环更新<canvas>,模拟视频播放。
- 若为H.264流,用
低延迟替代方案:WebRTC
如果追求极致低延迟,WebRTC比gRPC更适合:
- 后端用
Microsoft.MixedReality.WebRTC搭建WebRTC服务,将处理后的视频流通过WebRTC推送到前端; - 前端直接通过WebRTC API接收流,在
<video>标签播放,延迟可控制在100ms以内。
优化后的服务代码示例
using System.Threading.Channels; public class CCTVService : BackgroundService { private readonly RtspClient _rtspClient; private readonly ILogger<CCTVService> _logger; private readonly Channel<DecodedFrame> _frameQueue; private readonly PredictionEngine<ModelInput, ModelOutput> _predictionEngine; private const int streamWidth = 480; private const int streamHeight = 640; private static readonly FrameDecoder FrameDecoder = new FrameDecoder(); private static readonly FrameTransformer FrameTransformer = new FrameTransformer(streamWidth, streamHeight); public CCTVService(ILogger<CCTVService> logger) { _logger = logger; _rtspClient = new RtspClient(new ConnectionParameters(new Uri("rtsp://192.168.0.99:5540/ch0"))); // 创建有界队列,避免内存溢出,满时丢弃最旧帧 _frameQueue = Channel.CreateBounded<DecodedFrame>(new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.DropOldest }); // 初始化预测引擎 var mlContext = new MLContext(); var model = mlContext.Model.Load("yolov4-infer-only.onnx", out _); _predictionEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(model); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 启动后台帧处理任务 _ = ProcessFramesAsync(stoppingToken); using (_rtspClient) { try { await _rtspClient.ConnectAsync(stoppingToken); _logger.LogInformation("已连接到RTSP流"); } catch (RtspClientException ex) { _logger.LogError(ex, "RTSP连接失败"); return; } _rtspClient.FrameReceived += async (_, rawFrame) => { if (rawFrame is not RawVideoFrame rawVideoFrame) return; var decodedFrame = FrameDecoder.TryDecode(rawVideoFrame); if (decodedFrame == null) return; // 异步写入队列,不阻塞RTSP接收线程 await _frameQueue.Writer.WriteAsync(decodedFrame, stoppingToken); }; try { await _rtspClient.ReceiveAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "RTSP流接收异常"); } } } private async Task ProcessFramesAsync(CancellationToken stoppingToken) { await foreach (var frame in _frameQueue.Reader.ReadAllAsync(stoppingToken)) { try { using var bitmap = FrameTransformer.TransformToBitmap(frame); // 执行目标检测 var input = new ModelInput { Image = bitmap }; var detectionResult = _predictionEngine.Predict(input); // 后续处理:编码推流、触发录像等 await HandleProcessedFrameAsync(bitmap, detectionResult, stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "帧处理失败"); } } } private async Task HandleProcessedFrameAsync(Bitmap bitmap, ModelOutput result, CancellationToken ct) { // 这里实现编码、推流、录像逻辑 } } // 模型输入输出定义 public class ModelInput { [ImageType(416, 416)] public Bitmap Image { get; set; } } public class ModelOutput { [ColumnName("output")] public float[] Predictions { get; set; } }
内容的提问来源于stack exchange,提问作者Ruh Roh Raggy
相关产品推荐
相关产品推荐

