加载中...

Fabric.js实时播放视频并扣除绿幕

Fabric.js实时播放视频并扣除绿幕

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方法,源码调用流程如下:

New Image

_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.drawImagectx.drawImage的一个参数为绘制到画板的元素,允许任何的画布图像源,例如:HTMLImageElementSVGImageElementHTMLVideoElementHTMLCanvasElementImageBitmapOffscreenCanvas 或 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渲染到画布上的内容就是经过滤镜处理之后的内容。

完整调用流程如下: New Image

第三步、扣除视频绿幕

将之前的两个步骤的代码组合在一起,理论上我们就能够去除视频的绿幕了,但是实际上并没有那么简单,想要实现去除视频的绿幕,我们还需要做额外的设置。

因为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); });

GPU内存过高

实际运行中,修改cacheKey的方式存在着巨大的问题,会导致Chrome GPU占用内存过高,32G的内存也没能扛得住几分钟就导致电脑死机了;主要问题是修改cacheKey但是没有清除原本缓解的Texture,所以单纯的修改cacheKey会造成内存使用过高的问题,正确的操作应该是使用fabric.filterBackend.evictCachesForKey(layer.cacheKey)释放内存;

js
代码解读
复制代码
evictCachesForKey: function(cacheKey) { if (this.textureCache[cacheKey]) { this.gl.deleteTexture(this.textureCache[cacheKey]); delete this.textureCache[cacheKey]; } },

evictCachesForKey内部删除了缓存材质,并且清除了缓存信息。完整代码为:

js
代码解读
复制代码
fabric.util.requestAnimFrame(function render() { // 修改cacheKey fabric.filterBackend.evictCachesForKey(video1.cacheKey) // 应用滤镜 video1.applyFilters() canvas.renderAll(); fabric.util.requestAnimFrame(render); });

后记

现在我们就实现了使用Fabric.js实时播放视频并扣除绿幕,不过还有一些问题和没有详细讲述的点:

  1. canvas滤镜和WebGL滤镜在Fabric.js的具体实现逻辑
  2. 现在使用removeColor扣除绿幕,效果并不是很理想,可以自定义实现去除绿幕的滤镜方法

这些问题有空再详细说明