剪辑通过画布运行的视频的最有效方法是什么

时间:2017-02-09 21:45:06

标签: javascript canvas html5-canvas html5-video

我遇到需要剪辑图片或视频的情况。图像或视频需要能够重叠。我们最初使用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();
        });
    }
}

您将观察到浏览器立即放慢速度。我不建议长时间查看该代码,因为各地的事情都会变得非常缓慢。

我正在使用"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>
&#13;
&#13;
&#13;

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

The desired effect is an image in one container, a video in another, and they're both cropped

1 个答案:

答案 0 :(得分:1)

从比较我的代码到其他人的代码如何在这种情况下工作,我发现这个缺陷是我用drawFrame()方法实际将图像从视频绘制到画布中。

有两个基本问题:

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

    通过使用toggle变量仅在循环运行时每次绘制图像来解决问题1。

    通过在循环外部裁剪图像来解决问题2。

    这两项更改在页面上的其他元素如何加载,动画和响应用户方面产生了明显的差异。

    现在看来显而易见,但剪切视频中的每一帧比剪切画布要昂贵得多。

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