使用<canvas> </canvas>逐帧视频的一致FPS

时间:2015-01-22 15:32:43

标签: javascript video canvas html5-canvas

我试图准确显示一段能够停止或跳转到特定帧的视频。现在我的方法是在画布上逐帧显示视频(我确实有要显示的图像列表,我不必从视频中提取它们)。只要它的一致性和大约30fps,速度并不重要。兼容性有点重要(我们可以忽略IE≤8)。

首先,我预先加载了所有图片:

var all_images_loaded = {};
var all_images_src = ["Continuity_0001.png","Continuity_0002.png", ..., "Continuity_0161.png"];

function init() {
    for (var i = all_images_src.length - 1; i >= 0; i--) {
        var objImage = new Image();
        objImage.onload = imagesLoaded;
        objImage.src = 'Continuity/'+all_images_src[i];
        all_images_loaded[all_images_src[i]] = objImage;
    }
}

var loaded_count = 0;
function imagesLoaded () {
    console.log(loaded_count + " / " + all_images_src.length);
    if(++loaded_count === all_images_src.length) startvid();
}

init();

一旦完成,就会调用函数startvid()


然后我提出的第一个解决方案是在requestAnimationFrame()之后绘制setTimeout(以驯服fps):

var canvas = document.getElementsByTagName('canvas')[0];
var ctx = canvas.getContext("2d");

var video_pointer = 0;
function startvid () {
    video_pointer++;
    if(all_images_src[video_pointer]){
        window.requestAnimationFrame((function (video_pointer) {
            ctx.drawImage(all_images_loaded[all_images_src[video_pointer]], 0, 0);
        }).bind(undefined, video_pointer))
        setTimeout(startvid, 33);
    }
}

但感觉有些缓慢和不规则......


所以第二个解决方案是使用2个画布并在hidden上绘制,然后在适当的时间将其切换到visible

var canvas = document.getElementsByTagName('canvas');
var ctx = [canvas[0].getContext("2d"), canvas[1].getContext("2d")];

var curr_can_is_0 = true;
var video_pointer = 0;
function startvid () {
    video_pointer++;
    curr_can_is_0 = !curr_can_is_0;
    if(all_images_src[video_pointer]){
        ctx[curr_can_is_0?1:0].drawImage(all_images_loaded[all_images_src[video_pointer]], 0, 0);

        window.requestAnimationFrame((function (curr_can_is_0, video_pointer) {
            ctx[curr_can_is_0?0:1].canvas.style.visibility = "visible";
            ctx[curr_can_is_0?1:0].canvas.style.visibility = "hidden";
        }).bind(undefined, curr_can_is_0, video_pointer));

        setTimeout(startvid, 33);
    }
}

但这也感觉缓慢而不规律......


然而,Google Chrome(我正在开发)似乎有足够的空闲时间: Chrome Dev Tools, timeline 1 Chrome Dev Tools, timeline 2 Chrome Dev Tools, timeline 3

那我该怎么办?

1 个答案:

答案 0 :(得分:3)

问题:

您的主要问题是setTimeout并且setInterval无法保证在指定的延迟时间内触发,但在延迟后的某个时间点。

来自the MDN article on setTimeout(我强调)。

  

delay是函数调用应该延迟的毫秒数(千分之一秒)。如果省略,则默认为0. 实际延迟可能更长;见下面的注释。

以下是上述MDN的相关说明。

  

历史上,浏览器实现setTimeout()“clamp”:延迟小于“最小延迟”限制的连续setTimeout()调用被强制使用至少最小延迟。最小延迟DOM_MIN_TIMEOUT_VALUE为4毫秒(存储在Firefox中的首选项:dom.min_timeout_value),DOM_CLAMP_TIMEOUT_NESTING_LEVEL为5。

     

事实上,4毫秒是由HTML5规范指定的,并且在2010年及之后发布的浏览器中是一致的。在(Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2)之前,嵌套超时的最小超时值为10毫秒。

     

除了“钳制”之外,当页面(或操作系统/浏览器本身)忙于执行其他任务时,超时也可能会触发。

解决方案:

最好只使用requestAnimationFrame,并使用传递给回调的timestamp参数在回调中计算视频的增量时间,并从列表中绘制必要的帧。见下面的工作示例。作为奖励,我甚至包括代码以防止重新绘制两次相同的帧。

工作示例:

var start_time = null;
var frame_rate = 30;

var canvas = document.getElementById('video');
var ctx = canvas.getContext('2d');

var all_images_loaded = {};
var all_images_src = (function(frames, fps){//Generate some placeholder images.
	var a = [];
	var zfill = function(s, l) {
		s = '' + s;
		while (s.length < l) {
			s = '0' + s;
		}
		return s;
	}
	for(var i = 0; i < frames; i++) {
		a[i] = 'http://placehold.it/480x270&text=' + zfill(Math.floor(i / fps), 2) + '+:+' + zfill(i % fps, 2)
	}
	return a;
})(161, frame_rate);

var video_duration = (all_images_src.length / frame_rate) * 1000;

function init() {
	for (var i = all_images_src.length - 1; i >= 0; i--) {
		var objImage = new Image();
		objImage.onload = imagesLoaded;
		//objImage.src = 'Continuity/'+all_images_src[i];
		objImage.src = all_images_src[i];
		all_images_loaded[all_images_src[i]] = objImage;
	}
}

var loaded_count = 0;
function imagesLoaded () {
    //console.log(loaded_count + " / " + all_images_src.length);
    if (++loaded_count === all_images_src.length) {
		startvid();
	}
}

function startvid() {
	requestAnimationFrame(draw);
}

var last_frame = null;
function draw(timestamp) {
	//Set the start time on the first call.
	if (!start_time) {
		start_time = timestamp;
	}
	//Find the current time in the video.
	var current_time = (timestamp - start_time);
	//Check that it is less than the end of the video.
	if (current_time < video_duration) {
		//Find the delta of the video completed.
		var delta = current_time / video_duration;
		//Find the frame for that delta.
		var current_frame = Math.floor(all_images_src.length * delta);
		//Only draw this frame if it is different from the last one.
		if (current_frame !== last_frame) {
			ctx.drawImage(all_images_loaded[all_images_src[current_frame]], 0, 0);
			last_frame = current_frame;
		}
		//Continue the animation loop.
		requestAnimationFrame(draw);
	}
}

init();
<canvas id="video" width="480" height="270"></canvas>