You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor+SignalR+.NET6 WebAPI推视频流Canvas无法逐帧更新问题

Canvas无法逐帧更新的解决方案

问题根因

你当前的实现存在两个核心问题导致画面卡在第一帧:

  1. JS侧drawPixels函数在给img.src赋值后就同步返回,C#端await cameraStreamInterop.DrawPixelsAsync实际只等待了函数同步执行的部分,没有等待图片解码、Canvas绘制完成,导致帧接收速度远快于浏览器实际绘制速度,大量图片解码回调排队阻塞渲染线程,最终只有第一帧能正常绘制,后续帧的onload虽然触发但无法正常更新画布。
  2. 每次绘制都新建Image实例,会触发浏览器频繁GC和解码器初始化,额外消耗性能;同时没有帧丢弃逻辑,一旦解码速度跟不上发送速度,帧队列会持续堆积,延迟越来越高。

另外你在Blazor端每次收到帧都调用StateHasChanged()是完全多余的:Canvas绘制是JS侧直接操作DOM,Blazor组件本身没有任何状态变化,频繁调用组件重渲染只会占用调度线程资源,拖慢帧处理速度,直接删除这行代码即可。

修复代码

修改JS侧的绘制逻辑,复用Canvas上下文和Image实例,加入帧丢弃和渲染队列控制,确保函数真正等待绘制完成再返回:

// 缓存每个Canvas对应的上下文、Image实例和渲染状态,避免重复创建
const canvasRenderCache = new Map();

export async function drawPixels(cameraId, imageBytes) {
    let renderContext = canvasRenderCache.get(cameraId);
    if (!renderContext) {
        const canvas = document.getElementById(cameraId);
        if (!canvas) throw new Error(`ID为${cameraId}的Canvas元素不存在`);
        const ctx = canvas.getContext("2d");
        const img = new Image();
        renderContext = {
            canvas,
            ctx,
            img,
            isRendering: false,
            pendingFrame: null
        };
        canvasRenderCache.set(cameraId, renderContext);
    }

    // 始终只保留最新一帧,旧帧直接丢弃,避免队列堆积
    renderContext.pendingFrame = imageBytes;
    // 已有待渲染任务时直接返回,不重复触发渲染
    if (renderContext.isRendering) return;

    renderContext.isRendering = true;
    // 对齐浏览器渲染帧率,避免不必要的重绘
    await new Promise(resolve => requestAnimationFrame(resolve));

    return new Promise((resolve, reject) => {
        const renderCurrentFrame = () => {
            renderContext.img.onload = null;
            renderContext.img.onerror = null;
            renderContext.ctx.drawImage(
                renderContext.img, 
                0, 0, 
                renderContext.canvas.width, 
                renderContext.canvas.height
            );
            renderContext.isRendering = false;

            // 如果渲染期间收到新帧,立刻触发下一轮渲染
            if (renderContext.pendingFrame) {
                const nextFrame = renderContext.pendingFrame;
                renderContext.pendingFrame = null;
                renderContext.isRendering = true;
                renderContext.img.src = `data:image/jpeg;base64,${nextFrame}`;
                renderContext.img.onload = renderCurrentFrame;
                renderContext.img.onerror = err => {
                    renderContext.isRendering = false;
                    reject(err);
                };
            } else {
                resolve();
            }
        };

        const firstFrame = renderContext.pendingFrame;
        renderContext.pendingFrame = null;
        renderContext.img.src = `data:image/jpeg;base64,${firstFrame}`;
        renderContext.img.onload = renderCurrentFrame;
        renderContext.img.onerror = err => {
            renderContext.isRendering = false;
            reject(err);
        };
    });
}

额外优化建议:不要在C#端做byte[]转Base64的操作,直接通过JS互操作传递原始字节数组,JS侧接收Uint8Array后通过URL.createObjectURL(new Blob([frameBytes], {type: 'image/jpeg'}))生成图片地址,能减少30%左右的转码开销和序列化体积。


SignalR逐帧传输视频的合理性与优化方案

方案合理性判断

这个方案仅适合小并发、内网、低延迟要求不极端的场景,公网、高并发场景下不是合理实践,核心原因如下:

  • 逐帧传输JPG图像没有帧间压缩,相同画质下码率是H.264/H.265编码视频的5~10倍,带宽成本极高
  • SignalR定位是实时消息推送框架,不是流媒体传输协议,没有内置拥塞控制、流控、关键帧请求、丢包重传策略等流媒体必备能力,公网环境下很容易出现卡顿、花屏、延迟累积的问题
  • 逐帧发送会产生大量网络小包,网络有效载荷率低,进一步浪费带宽

优化方案

根据场景可以选择以下优化方向:

  1. 优先替换为标准流媒体方案:服务端将摄像头流编码为H.264/H.265后,通过WebRTC、HTTP-FLV、HLS等标准流媒体协议输出,前端直接使用hls.js、flv.js或原生video标签播放,不需要自行处理帧传输和绘制,编码效率、兼容性、延迟表现都远好于自定义SignalR方案。
  2. 如果必须使用SignalR传输,做以下调整降低开销:
    • 启用MessagePack序列化协议,相比JSON可以减少30%~50%的消息体积
    • 服务端增加帧丢弃逻辑:当客户端接收缓冲区有积压时,直接丢弃旧的非关键帧,只发送最新帧,避免延迟累积
    • 取消C#端Base64转码,直接传递原始二进制数据,JS侧用Blob URL代替Base64加载图片
    • 调整SignalR WebSocket缓冲区配置,关闭不必要的协议日志,降低框架开销
    • 不要逐帧发送,可以将2~3帧合并为一个消息批量发送,降低协议头开销,注意控制合并后的总延迟不要超过100ms
  3. 并发量超过10路以上的场景,不要用SignalR承载视频流传输,建议使用专门的流媒体服务器处理流转推和分发。

内容的提问来源于stack exchange,提问作者Ties Mulder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 13:42:21