剪辑视频 运行 到 canvas 的最有效方法是什么

What is the most efficient way to clip a video running through canvas

我有一种情况需要剪辑图像或视频。图片或视频需要能够重叠。我们最初使用 SVG 尝试过此操作,但由于各种原因,效果不佳,所以现在我们在 Canvas 中进行了此操作。

这对于图像来说效果很好,但是对于视频来说,浏览器在大约 2 分钟后几乎戛然而止。 (您在示例代码或 link 中看不到的是,我们还在视频不在视图中和选项卡不在视图中时暂停视频。)

这是一个link:http://codepen.io/paceaux/pen/egLOeR

主要关注的是这个方法:

drawFrame () {
    if (this.isVideo && this.media.paused) return false;

    let x = 0;
    let width = this.media.offsetWidth;
    let y = 0;

    this.imageFrames[this.module.dataset.imageFrame](this.backContext);
    this.backContext.drawImage(this.media, x, y, width, this.canvas.height);

    this.context.drawImage(this.backCanvas, 0, 0);

    if (this.isVideo) {
        window.requestAnimationFrame(()=>{
            this.drawFrame();
        });
    }
}

您会发现浏览器速度立即变慢。我不建议看那个 codepen 太久,因为到处都会变得非常慢。

我正在使用 "backCanvas" technique 但这似乎只会让事情变得更糟。

我也尝试过使用 Path2D() 来保存剪辑路径,但这似乎也没什么用。

        wedgeTop: (context, wedgeHeight = defaults.wedgeHeight) => {
        var wedge = new Path2D();

        wedge.moveTo(this.dimensions.width, 0);
        wedge.lineTo(this.dimensions.width, this.dimensions.height);
        wedge.lineTo(0, this.dimensions.height);
        wedge.lineTo(0, wedgeHeight);
        wedge.closePath();
        context.clip(wedge);
    },

还有我遗漏的任何其他优化吗? (保存视频的大小)。

let imageFrames =  function () {
 let defaults = {
  wedgeHeight: 50
 };
 return {
  defaults: defaults,

  //all wedges draw paths clockwise: top right, bottom right, bottom left, top left
  wedgeTop: (context, wedgeHeight = defaults.wedgeHeight) => {
   var wedge = new Path2D();

   wedge.moveTo(this.dimensions.width, 0);
   wedge.lineTo(this.dimensions.width, this.dimensions.height);
   wedge.lineTo(0, this.dimensions.height);
   wedge.lineTo(0, wedgeHeight);
   wedge.closePath();
   context.clip(wedge);
  },

  wedgeTopReverse: (context, wedgeHeight = defaults.wedgeHeight) => {
   var wedge = new Path2D();

   wedge.moveTo(this.dimensions.width, wedgeHeight);
   wedge.lineTo(this.dimensions.width, this.dimensions.height);
   wedge.lineTo(0, this.dimensions.height);
   wedge.lineTo(0, 0);
   wedge.closePath();
   context.clip(wedge);

  },

  wedgeBottom: (context, wedgeHeight = defaults.wedgeHeight) => {
   var wedge = new Path2D();

   wedge.moveTo(this.dimensions.width, 0);
   wedge.lineTo(this.dimensions.width, this.dimensions.height - wedgeHeight);
   wedge.lineTo(0, this.dimensions.height);
   wedge.lineTo(0,0);
   wedge.closePath();
   context.clip(wedge);
  },

  wedgeBottomReverse: (context, wedgeHeight = defaults.wedgeHeight) => {
   var wedge = new Path2D();

   wedge.moveTo(this.dimensions.width, 0);
   wedge.lineTo(this.dimensions.width, this.dimensions.height);
   wedge.lineto(0, this.dimensions.height - wedgeHeight);
   wedge.lineTo(0, 0);
   wedge.closePath();
   context.clip(wedge);
  }
 };
};

class ImageCanvasModule  {
 constructor(module) {
  this.module = module;
  this.imageFrames = imageFrames.call(this);

  if(this.isVideo) {
   /*drawFrame has a check where it'll only draw on reqAnimationFrame if video.paused === false,
   so we need to fire drawFrame on both events because that boolean will be false when it's paused, thus cancelling the animation frame
   */
   this.media.addEventListener('play', ()=>{
    this.drawOnCanvas();
   });

   this.media.addEventListener('pause', ()=> {
    this.drawOnCanvas();
   });
  }
 }

 get isPicture() {
  return (this.module.nodeName === 'PICTURE');
 }

 get isVideo() {
  return (this.module.nodeName === 'VIDEO');
 }

 get media() {
  return this.isPicture ? this.module.querySelector('img') : this.module;
 }

 get context() {
  return this.canvas.getContext('2d');
 }

 get dimensions() {
  return {
   width: this.module.offsetWidth,
   height: this.module.offsetHeight
  };
 }

 createCanvas () {
  let canvas = document.createElement('canvas');

  this.module.parentNode.insertBefore(canvas, this.module.nextSibling);
  canvas.className = this.module.className;

  this.canvas = canvas;

  this.createBackContext();
 }

 createBackContext () {
  this.backCanvas = document.createElement('canvas');
  this.backContext = this.backCanvas.getContext('2d');

  this.backCanvas.width = this.dimensions.width;
  this.backCanvas.height = this.backCanvas.height;
 }

 sizeCanvas () {
  this.canvas.height = this.dimensions.height;
  this.canvas.width = this.dimensions.width;

  this.backCanvas.height = this.dimensions.height;
  this.backCanvas.width = this.dimensions.width;
 }

 drawFrame () {
  if (this.isVideo && this.media.paused) return false;

  let x = 0;
  let width = this.media.offsetWidth;
  let y = 0;
  
  this.imageFrames[this.module.dataset.imageFrame](this.backContext);
  this.backContext.drawImage(this.media, x, y, width, this.canvas.height);

  this.context.drawImage(this.backCanvas, 0, 0);

  if (this.isVideo) {
   window.requestAnimationFrame(()=>{
    this.drawFrame();
   });
  }
 }

 drawOnCanvas () {
  this.sizeCanvas();
  this.drawFrame();
 }

 hideOriginal () {
  //don't use display: none .... you can't get image dimensions when you do that.
  this.module.style.opacity = 0;
 }
}
console.clear();

window.addEventListener('DOMContentLoaded', ()=> {
 var els = document.querySelectorAll('.canvasify');
 var canvasified = [];

 for (el of els) {
  if (el.dataset.imageFrame) {
   let imageModule = new ImageCanvasModule(el);
   imageModule.createCanvas();
   imageModule.drawOnCanvas();
   imageModule.hideOriginal();
   canvasified.push(imageModule);
  }

 }
 console.log(canvasified);
});
body {
 background-color: #333;
}

.container {
 height: 600px;
 width: 100%;
 position: relative;
 display: flex;
 flex-direction: column;
 justify-content: center;
}
.container + .container {
 margin-top: -150px;
}
.canvasify {
 position:absolute;
 top: 0;
 left: 0;
 right: 0;
 bottom: 0;
 width: 100%;
 z-index: -1;
}
video {
 width: 100%
}

h1 {
 font-size: 2em;
 color: #ddd;
}
<div class="container">
 <img class="canvasify" data-image-frame="wedgeTop" src="http://placekitten.com/1280/500" />
 <h1>Kitty with a clipped top</h1>
</div>


<div class="container">
<video controls muted class="canvasify" loop autoplay data-image-frame="wedgeTop">
 <source src="https://poc5.ssl.cdn.sdlmedia.com/web/635663565028367012PU.mp4">
</video>
 <h1>video with a clipped top that overlaps the image above</h1>
</div>

问题是代码笔(和其他页面 运行 此代码)非常慢。我错过了哪些优化,或者使用不当?

通过比较我的代码与其他人的代码在这种情况下的工作方式,我发现缺陷出在我用来将视频中的图像实际绘制到 canvas 中的 drawFrame() 方法中.

有两个基本问题:

  1. requestAnimationFrame() 运行大约 60fps,因为这是视频,所以不需要超过 30
  2. 我在 drawFrame 的每个实例中绘制剪裁,但我不需要这样做。您可以剪辑 canvas 一次 然后 运行 requestAnimationFrame

所以,新的 drawFrame 方法看起来像这样

    drawFrame () {
    if (this.isVideo && this.media.paused) return false;
    this.imageFrames[this.module.dataset.imageFrame]();

    var _this = this;
    var toggle = false;

    (function loop() {
        toggle= !toggle;

        if (toggle) {
            let x = 0;
            let width = _this.media.offsetWidth;
            let y = 0;

        _this.context.drawImage(_this.media, 0, 0, width, _this.canvas.height);
        }

        if (_this.isVideo) {
            window.requestAnimationFrame(loop);
        }

    })();
}

问题 1 已通过使用 toggle 变量在循环 运行 中每隔一段时间仅绘制一个图像得到解决。

问题 2 已通过在循环外裁剪图像得到解决。

这两项更改对页面上其他元素的加载、动画和响应用户的方式产生了显着差异。

现在看来很明显,但是剪辑视频中的每一帧比剪辑 canvas 的成本要高得多。

非常感谢用户K3N,他的代码示例帮助我找出了问题所在。