【问题标题】:What is the most efficient way to clip a video running through canvas剪辑通过画布运行的视频最有效的方法是什么?
【发布时间】:2017-06-28 02:19:27
【问题描述】:

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

这对于图像来说效果很好,但对于视频来说,浏览器在大约 2 分钟后几乎会突然停止。 (您不会从示例代码或链接中看到,我们还在视频不在视图中以及选项卡不在视图中时暂停视频。)

这是一个链接: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>

问题在于 codepen(以及运行此代码的其他页面)非常慢。我错过了哪些优化,或者使用不正确?

【问题讨论】:

  • “重叠”是什么意思?不确定是什么问题?
  • 问题是页面运行速度极慢。 “重叠”意味着视频中的图像应该被剪裁,并与位于其上方的图像重叠。使其看起来像视频/图像不是矩形,而是以角度切割。
  • 还是没有。我正在寻找您在 codepen 中看到的内容。两个独立的容器。一个可能包含图像。另一个可能包含视频。每个容器都会有视频或图像,并且会以一定角度“剪辑”。来自 HTML 的文本将位于图像/视频之上。因此,一个容器中图像的底部边缘将直观地显示在可能是视频的顶部边缘下方。我不需要关于如何操作的帮助(我已经完成了,并且它有效)我需要帮助以使页面表现更好。
  • 好的...只是检查o_o(我链接的示例代码是否也产生了滞后?)
  • “我需要帮助以使页面性能更好。” 如果只呈现图像和视频,为什么需要canvasrequestAnimationFrame?目前如何衡量绩效?

标签: javascript canvas html5-canvas html5-video


【解决方案1】:

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

有两个基本问题:

  1. requestAnimationFrame() 运行大约 60fps,因为这是视频,所以不需要超过 30 个
  2. 我在drawFrame 的每个实例中都绘制了剪辑,我不需要这样做。您可以剪辑画布一次,然后运行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。

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

现在看起来很明显,但剪裁视频中的每一帧比剪裁 画布 的成本要高得多。

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

【讨论】:

    猜你喜欢
    • 2011-05-06
    • 1970-01-01
    • 2021-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-24
    相关资源
    最近更新 更多