剪辑视频 运行 到 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()
方法中.
有两个基本问题:
- requestAnimationFrame() 运行大约 60fps,因为这是视频,所以不需要超过 30
- 我在
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,他的代码示例帮助我找出了问题所在。
我有一种情况需要剪辑图像或视频。图片或视频需要能够重叠。我们最初使用 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()
方法中.
有两个基本问题:
- requestAnimationFrame() 运行大约 60fps,因为这是视频,所以不需要超过 30
- 我在
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,他的代码示例帮助我找出了问题所在。