
本文介绍如何利用 Fabric.js 将未保存的实时摄像头流作为画布背景,同时支持添加、拖拽、编辑 Fabric 对象(如圆形、矩形等),避免传统 setBackgroundImage 因依赖 URL 或已加载 <img> 元素而失效的问题。核心方案是通过自定义 Fabric 对象或重写渲染逻辑,在每一帧中动态绘制视频帧。
本文介绍如何利用 fabric.js 将未保存的实时摄像头画面设为画布背景并叠加可编辑对象,避免传统 setbackgroundimage 因依赖 url 或已加载 元素而失效的问题。核心方案是通过自定义 fabric 对象或重写渲染逻辑,在每一帧中动态绘制视频帧。
Fabric.js 默认的 setBackgroundImage() 方法要求传入一个已加载完成的图像资源(如 <img> 元素或有效 URL),而摄像头捕获的帧是动态、未持久化的 HTMLVideoElement 流——它无法直接转为 Image 实例供 Fabric 加载,因此调用 setBackgroundImage() 或 fabric.Image.fromURL() 均会失败或空白。根本解法是绕过“背景图”机制,将视频帧作为画布底层内容,在 Fabric 渲染流程中主动绘制。
✅ 推荐方案:自定义 Fabric 对象实现动态视频背景
创建继承自 fabric.Object 的自定义类(如 fabricBGStream),重写其 render() 方法,在每次 Fabric 渲染时调用 ctx.drawImage(video, ...) 绘制当前视频帧。该对象不参与交互、不响应事件,仅作视觉底图:
const video = document.getElementById('img-stream');
const canvas = new fabric.Canvas('img-canvas', {
width: window.innerWidth,
height: window.innerHeight,
});
// 自定义视频背景对象
const fabricBGStream = fabric.util.createClass(fabric.Object, {
type: 'fabricBGStream',
initialize() {
this.callSuper('initialize', {});
},
render(ctx) {
ctx.save();
// 关键:按画布视口偏移校准绘制位置(适配 zoom/pan)
const { viewportTransform } = canvas;
const offsetX = -viewportTransform[4];
const offsetY = -viewportTransform[5];
ctx.drawImage(
video,
0, 0, video.videoWidth, video.videoHeight, // 源区域
offsetX, offsetY, canvas.width, canvas.height // 目标区域
);
ctx.restore();
}
});
// 添加背景对象(必须置于最底层)
const bg = new fabricBGStream();
canvas.add(bg);
canvas.sendToBack(bg); // 确保在所有其他对象之下⚠️ 注意:video.videoWidth/video.videoHeight 在 loadedmetadata 事件后才可用。务必在 video.onloadedmetadata 或 video.readyState >= 2 后初始化 Fabric 并添加背景对象,否则尺寸为 0。
✅ 启动摄像头与持续渲染
Fabric 本身不监听视频帧变化,需手动触发 canvas.renderAll()。推荐使用 requestAnimationFrame 在视频播放时持续渲染:
let animationId;
function animate() {
canvas.renderAll();
animationId = requestAnimationFrame(animate);
}
video.addEventListener('play', () => {
animate();
});
video.addEventListener('pause', () => {
cancelAnimationFrame(animationId);
});
// 初始化摄像头
if (navigator.mediaDevices?.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => {
video.srcObject = stream;
video.play();
})
.catch(err => console.error('摄像头访问被拒绝:', err));
}✅ 添加可编辑对象(无背景丢失)
此时再添加任何 Fabric 对象(如圆形、文字、路径)均能正常显示在视频背景之上,且支持拖拽、缩放、选中等全部交互:
// 示例:添加一个居中红色圆圈
canvas.add(new fabric.Circle({
left: canvas.width / 2,
top: canvas.height / 2,
radius: 30,
fill: 'rgba(255,0,0,0.3)',
stroke: 'red',
strokeWidth: 2,
selectable: true,
evented: true
}));
// 示例:添加文本
canvas.add(new fabric.Text('Hello Webcam!', {
left: 50,
top: 50,
fontSize: 24,
fill: 'white',
fontWeight: 'bold'
}));❌ 不推荐方案:重写 _renderBackground
虽然可通过覆盖私有方法 canvas._renderBackground 实现类似效果:
canvas._renderBackground = function(ctx) {
if (this.backgroundColor) {
ctx.fillStyle = this.backgroundColor;
ctx.fillRect(0, 0, this.width, this.height);
}
if (video.readyState === video.HAVE_ENOUGH_DATA) {
ctx.drawImage(video, 0, 0, this.width, this.height);
}
};但该方式直接修改 Fabric 内部方法,违反封装原则,易受版本升级破坏,且无法精确控制绘制时机与坐标映射,故仅作技术参考,生产环境应避免。
✅ 总结与最佳实践
- 核心原则:视频流 ≠ 静态图片,必须在渲染循环中动态绘制;
- 关键步骤:① 创建自定义 fabric.Object 类;② 在 render() 中调用 drawImage(video, ...);③ 使用 requestAnimationFrame 驱动持续渲染;④ 确保视频元数据就绪后再初始化 Fabric;
- 性能提示:若需高清输出,建议设置 canvas.width/height 与视频分辨率一致(而非 CSS 缩放),避免浏览器插值失真;
- 扩展性:此模式同样适用于 <video> 播放本地文件、WebRTC 远程流等场景,无需修改逻辑。
至此,你已拥有一套稳定、可维护、支持实时标注的 Fabric.js 摄像头画布方案。


















