Fabric.js 是一个强大的 JavaScript 库,专门用于处理画布(Canvas)元素。它提供了一个简单易用的接口来创建和操作图形和图像。
现在有一个需求,需要使用Fabric.js播放视频,并且要扣除视频中的绿幕。
项目地址:github.com/x007xyz/fab...
线上Demo:fabricjs-demo.videocovert.online/
第一步:使用Fabric.js播放视频
官网提供了播放视频的Demo,官网的代码包含了两部分,一个是使用Video元素播放,一个是从直播流中获取视频播放,我们去除直播流的那一部分代码,就可以得到我们想要的代码了:
js
var canvas = new fabric.Canvas('c');
var video1El = document.getElementById('video1');
video1El.addEventListener('loadeddata', function() {
// 视频正常加载后,再生成 fabric.Image 对象
var video1 = new fabric.Image(video1El);
// 也可以使用setElement()方法,将已经加载好的视频元素传入
canvas.add(video1);
// 视频播放,getElement会获取到video元素
video1.getElement().play();
fabric.util.requestAnimFrame(function render() {
canvas.renderAll();
fabric.util.requestAnimFrame(render);
});
});
使用Video元素时,有几点需要注意的:
- 需要等待Video元素加载完成
- 如果没有在网页上进行过任何操作,是无法自动播放音视频的
- video元素在可视区域外会停止播放
源码解析
Fabric.js能够渲染视频,是因为在requestAnimFrame中不断地调用renderAll,最后调用Image对象的_renderFill方法,源码调用流程如下:
_renderFill方法具体代码:
js
_renderFill: function(ctx) {
var elementToDraw = this._element;
if (!elementToDraw) {
return;
}
var scaleX = this._filterScalingX, scaleY = this._filterScalingY,
w = this.width, h = this.height, min = Math.min, max = Math.max,
// crop values cannot be lesser than 0.
cropX = max(this.cropX, 0), cropY = max(this.cropY, 0),
elWidth = elementToDraw.naturalWidth || elementToDraw.width,
elHeight = elementToDraw.naturalHeight || elementToDraw.height,
sX = cropX * scaleX,
sY = cropY * scaleY,
// the width height cannot exceed element width/height, starting from the crop offset.
sW = min(w * scaleX, elWidth - sX),
sH = min(h * scaleY, elHeight - sY),
x = -w / 2, y = -h / 2,
maxDestW = min(w, elWidth / scaleX - cropX),
maxDestH = min(h, elHeight / scaleY - cropY);
elementToDraw && ctx.drawImage(elementToDraw, sX, sY, sW, sH, x, y, maxDestW, maxDestH);
},
其中最核心的代码是ctx.drawImage
,ctx.drawImage
的一个参数为绘制到画板的元素,允许任何的画布图像源,例如:HTMLImageElement
、SVGImageElement
、HTMLVideoElement
、HTMLCanvasElement
、ImageBitmap
、OffscreenCanvas
或 VideoFrame
;所以可以直接渲染Video元素。
第二步、扣除绿幕
扣除绿幕,我们可以使用Fabric.js的removeColor滤镜实现,Fabric.js使用滤镜我们可以在官方找到相关的Demo,去掉和我们需求不相关的部分,得到如下代码:
js
var canvas = new fabric.Canvas('c');
var imageEl = document.getElementById('image');
// 可以手动设置滤镜类型:canvas或者WebGL
// fabric.filterBackend = new fabric.Canvas2dFilterBackend()
// fabric.filterBackend = new fabric.WebglFilterBackend();
fabric.filterBackend = fabric.initFilterBackend();
imageEl.onload = function () {
const image = new fabric.Image(imageEl)
// 设置removeColor滤镜
image.filters.push(
new fabric.Image.filters.RemoveColor({
distance: 0.15,
color: '#115D1E',
}),
)
// 应用滤镜
image.applyFilters()
}
源码解析
之前我们了解到Fabric.js最终会调用ctx.drawImage
渲染this._element
,如果没有使用滤镜,this._element
会是我们设置的元素,但是如果有应用滤镜,需要调用applyFilters
方法,在applyFilters
方法中会执行:
js
if (!fabric.filterBackend) {
fabric.filterBackend = fabric.initFilterBackend();
}
fabric.filterBackend.applyFilters(filters, this._originalElement, sourceWidth, sourceHeight, this._element, this.cacheKey);
根据filterBackend
设置的滤镜类型找到对应滤镜的applyFilters
并执行,滤镜的applyFilters
方法会对this._element
进行处理,ctx.drawImage
渲染到画布上的内容就是经过滤镜处理之后的内容。
完整调用流程如下:
第三步、扣除视频绿幕
将之前的两个步骤的代码组合在一起,理论上我们就能够去除视频的绿幕了,但是实际上并没有那么简单,想要实现去除视频的绿幕,我们还需要做额外的设置。
因为canvas滤镜和WebGL滤镜实现不同,所以我们将两种滤镜分开讨论
canvas滤镜
在扣除绿幕时,我们添加滤镜之后,会执行applyFilters
方法应用滤镜,生成新的this._element
,所以每次渲染画布时吗,都需要重新调用applyFilters
,才能渲染视频的不同帧,形成播放视频的效果,不然只会播放视频的某一帧。
js
var canvas = new fabric.Canvas('c');
var video1El = document.getElementById('video1');
fabric.filterBackend = new fabric.Canvas2dFilterBackend()
video1El.addEventListener('loadeddata', function() {
// 视频正常加载后,再生成 fabric.Image 对象
var video1 = new fabric.Image(video1El);
// 也可以使用setElement()方法,将已经加载好的视频元素传入
canvas.add(video1);
// 视频播放,getElement会获取到video元素
video1.getElement().play();
fabric.util.requestAnimFrame(function render() {
// 应用滤镜
video1.applyFilters()
canvas.renderAll();
fabric.util.requestAnimFrame(render);
});
});
canvas滤镜可以实现视频扣除绿幕,但是存在很多的性能问题,特别是对于分辨率较大的视频,所有我们还是需要使用WebGL滤镜才能实现比较好的播放效果。
WebGL滤镜
如果只是单纯的将fabric.filterBackend
切换为new fabric.WebglFilterBackend()
,WebGL的removeColor滤镜是无法做到我们想要的效果,视频会被渲染为一张黑色图片或者视频截图。
具体原因我们可以对比下两种滤镜的不同实现:
js
applyFilters: function(filters, sourceElement, sourceWidth, sourceHeight, targetCanvas) {
var ctx = targetCanvas.getContext('2d');
ctx.drawImage(sourceElement, 0, 0, sourceWidth, sourceHeight);
var imageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight);
var originalImageData = ctx.getImageData(0, 0, sourceWidth, sourceHeight);
var pipelineState = {
sourceWidth: sourceWidth,
sourceHeight: sourceHeight,
imageData: imageData,
originalEl: sourceElement,
originalImageData: originalImageData,
canvasEl: targetCanvas,
ctx: ctx,
filterBackend: this,
};
filters.forEach(function(filter) { filter.applyTo(pipelineState); });
if (pipelineState.imageData.width !== sourceWidth || pipelineState.imageData.height !== sourceHeight) {
targetCanvas.width = pipelineState.imageData.width;
targetCanvas.height = pipelineState.imageData.height;
}
ctx.putImageData(pipelineState.imageData, 0, 0);
return pipelineState;
}
canvas滤镜每次调用方法都会将元素转换为imageData数据,然后调用具体滤镜方法处理imageData数据。
js
applyFilters: function(filters, source, width, height, targetCanvas, cacheKey) {
var gl = this.gl;
var cachedTexture;
if (cacheKey) {
cachedTexture = this.getCachedTexture(cacheKey, source);
}
var pipelineState = {
originalWidth: source.width || source.originalWidth,
originalHeight: source.height || source.originalHeight,
sourceWidth: width,
sourceHeight: height,
destinationWidth: width,
destinationHeight: height,
context: gl,
sourceTexture: this.createTexture(gl, width, height, !cachedTexture && source),
targetTexture: this.createTexture(gl, width, height),
originalTexture: cachedTexture ||
this.createTexture(gl, width, height, !cachedTexture && source),
passes: filters.length,
webgl: true,
aPosition: this.aPosition,
programCache: this.programCache,
pass: 0,
filterBackend: this,
targetCanvas: targetCanvas
};
var tempFbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, tempFbo);
filters.forEach(function(filter) { filter && filter.applyTo(pipelineState); });
resizeCanvasIfNeeded(pipelineState);
this.copyGLTo2D(gl, pipelineState);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.deleteTexture(pipelineState.sourceTexture);
gl.deleteTexture(pipelineState.targetTexture);
gl.deleteFramebuffer(tempFbo);
targetCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
return pipelineState;
},
WebGL滤镜会使用元素创建Texture并且对其进行缓存;这就是两者的差异,使用WebGL滤镜,如果只是调用applyFilters
,因为Texture一直是没有改变的,所以得到的结果一直是同一帧的内容。
知道原因之后,我们就可以进行修改了,在渲染方法中,应用滤镜之前,修改cacheKey的值,就可以正常渲染视频了:
js
fabric.util.requestAnimFrame(function render() {
// 修改cacheKey
video1.cacheKey = Date.now() + '' + Math.floor(Math.random() * 100)
// 应用滤镜
video1.applyFilters()
canvas.renderAll();
fabric.util.requestAnimFrame(render);
});
后记
现在我们就实现了使用Fabric.js实时播放视频并扣除绿幕,不过还有一些问题和没有详细讲述的点:
- canvas滤镜和WebGL滤镜在Fabric.js的具体实现逻辑
- 现在使用removeColor扣除绿幕,效果并不是很理想,可以自定义实现去除绿幕的滤镜方法
这些问题有空再详细说明