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

_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的一个参数为绘制到画板的元素,允许任何的画布图像源,例如:HTMLImageElementSVGImageElementHTMLVideoElementHTMLCanvasElementImageBitmapOffscreenCanvasVideoFrame;所以可以直接渲染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实时播放视频并扣除绿幕,不过还有一些问题和没有详细讲述的点:

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

这些问题有空再详细说明

相关推荐
2301_7969821413 分钟前
网页打开时,下载的文件text/html/重定向类型有什么作用?
前端·html
重生之我在20年代敲代码14 分钟前
HTML讲解(二)head部分
前端·笔记·html·web app
天下无贼!21 分钟前
2024年最新版TypeScript学习笔记——泛型、接口、枚举、自定义类型等知识点
前端·javascript·vue.js·笔记·学习·typescript·html
小白小白从不日白1 小时前
react 高阶组件
前端·javascript·react.js
程序员大金1 小时前
基于SpringBoot+Vue+MySQL的智能物流管理系统
java·javascript·vue.js·spring boot·后端·mysql·mybatis
Mingyueyixi1 小时前
Flutter Spacer引发的The ParentDataWidget Expanded(flex: 1) 惨案
前端·flutter
Rverdoser2 小时前
unocss 一直热更新打印[vite] hot updated: /__uno.css
前端·css
Bang邦3 小时前
使用nvm管理Node.js多版本
前端·node.js·node多版本管理
podoor3 小时前
wordpress不同网站 调用同一数据表
前端·wordpress