<canvas> 逐帧视频中的一致 FPS

Consistent FPS in frame by frame video with <canvas>

我正在尝试显示足够精确的视频,以便我可以停止或跳转到特定帧。现在我的方法是在 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()


然后我想到的第一个解决方案是在 setTimeout 之后利用 requestAnimationFrame()(以驯服 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 个 canvases 并在 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(我正在开发)似乎有很多空闲时间:

那我该怎么办?

问题:

您的主要问题是 setTimeout and setInterval 不能保证在指定的延迟时准确触发,但会在延迟后的某个时刻触发。

来自the MDN article on setTimeout(重点是我加的)。

delay is the number of milliseconds (thousandths of a second) that the function call should be delayed by. If omitted, it defaults to 0. The actual delay may be longer; see Notes below.

这里是上面提到的来自MDN的相关注释。

Historically browsers implement setTimeout() "clamping": successive setTimeout() calls with delay smaller than the "minimum delay" limit are forced to use at least the minimum delay. The minimum delay, DOM_MIN_TIMEOUT_VALUE, is 4 ms (stored in a preference in Firefox: dom.min_timeout_value), with a DOM_CLAMP_TIMEOUT_NESTING_LEVEL of 5.

In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.

In addition to "clamping", the timeout can also fire later when the page (or the OS/browser itself) is busy with other tasks.

解决方案:

您最好只使用 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>