3

I'm doing one of them stop-shot-scroll-controlled-playback sites like Sony's Be Moved.

The problem that I'm facing, considering the stop-shot technique, is the time that it takes for image to be rasterized before browser draws it on screen. It takes a lot on mobile. Probably resizing the image takes most of the cpu, but I'm not sure. This is how I show the frames:

<div 
  style="
    position: fixed;
    top:0; right:0; bottom:0; left:0;
    background-image: url(...);
    background-position: center;
    background-size: cover;
  "
></div>

The question:

Is there a way to cache a rasterized version of an image? Maybe canvas supports this? That way, when I decide to show it on screen, it'll be ready.

Right now, this is the only way I know how to cache an image.

var image = new Image();
image.src = '...';
Daniel Birowsky Popeski
  • 8,752
  • 12
  • 60
  • 125
  • This question is already answered in great depth here - http://stackoverflow.com/questions/10240110/how-do-you-cache-an-image-in-javascript?answertab=active#tab-top – Ben Rondeau Apr 19 '15 at 15:31
  • @BenRondeau yet its not – Daniel Birowsky Popeski Apr 19 '15 at 15:34
  • How so? Can you make a codepen or something to demonstrate this? – Ben Rondeau Apr 19 '15 at 15:47
  • A bitmap image is already rasterized by definition, you also mention Sony's site which uses video (+ CSS). Do you mean, how to capture a frame from the video for later usage? –  Apr 19 '15 at 18:25
  • @KenFyrstenberg It IS rasterized, but for a certain pixel map. And then, even if I draw the image in that same pixel map, the image would first need to be decoded. But it's not just that, in practice, all devices have different size, ergo the image needs to get resized after decoding(rerasterized). – Daniel Birowsky Popeski Apr 19 '15 at 18:53
  • @KenFyrstenberg Sony doesn't use video. It uses jpeg frames. Check the network, you'll see. And as much as I preferred video over jpeg frames, I actually went that route initially, but wasn't able to access each frame of it, ergo, bigger problems. But if you have a way to cache video frames individually, YOU ARE THE MAN. – Daniel Birowsky Popeski Apr 19 '15 at 18:55
  • @Birowsky you're right, and I'm a bit surprised.. :) but yes, a jpeg sequence is indeed being used. There is a way, sort of, but it is memory hungry. Give me a sec or three.. –  Apr 19 '15 at 19:14

2 Answers2

3

Ref comments - there is a way to pre-cache video frames. Each frame will use a full memory block for the bitmap (which in any case also is the case with preloaded image sequences).

Cache Process

  • Create an "off-line" video element
  • Set video source with preload set to auto
  • You need to know the frame rate (typical: 30 fps for USA/Japan, 25 fps for Europe), calculate a time delta based on this, ie. 1 / FPS.
  • Use the timeupdate event for every currentTime update as setting current time is asynchronous.

Chose an in-point in the video, cache (this can take a while due to the event cycle), store to a frame buffer using a canvas element for each frame. Then playback the buffer when and as needed (this also gives you the ability to play video backwards as shown below, a feature not yet supported in the browsers).

Example

This example will load a video from net, cache 90 (3 sec @ 30 fps) frames to memory, then play back the sequence ping-pong in the window (the images you see are from the cache obviously):

var canvas = document.querySelector("canvas"),
    ctx = canvas.getContext("2d"),
    video = document.createElement("video"),
    frames = [],
    w = canvas.width, h = canvas.height;

video.addEventListener("canplay", cache);
video.preload = "auto";
video.src = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4";

function cache() {
  this.removeEventListener("canplay", cache);         // remove to avoid recalls

  var fps = 30,                                       // assuming 30 FPS
      delta = 1 / fps,                                // time delta
      count = 0,                                      // current cached frame
      max = fps * 3,                                  // 3 seconds
      div = document.querySelector("div");            // just for info

  this.addEventListener("timeupdate", cacheFrame);    // time update is aync
  this.currentTime = 19;                              // start with initial time
  
  function cacheFrame() {
    div.innerHTML = "Caching frame: " + count;
    
    if (count++ < max) {
      
      // create canvas for frame-buffer;
      var canvas = document.createElement("canvas"),
          ctx = canvas.getContext("2d");

      canvas.width = this.videoWidth;                 // canvas size = video frame
      canvas.height = this.videoHeight;
      
      ctx.drawImage(video, 0, 0);                     // draw current frame
      frames.push(canvas);                            // store frame
      
      this.currentTime += delta;                      // update time, wait..
    }
    else {
      this.removeEventListener("timeupdate", cacheFrame); // remove!!
      play();                                         // play back cached sequence
    }
  }
}

// to demo the cached frames
function play() {
  var current = 0, max = frames.length, dlt = 1,
      div = document.querySelector("div"),
      toggle = false,
      mem = max * video.videoWidth * video.videoHeight * 4; // always RGBA
 
  mem = (mem / 1024) / 1024;                          //mb

  ctx.fillStyle = "red";

  (function loop() {
    toggle = !toggle;                                 // toggle FPS to 30 FPS
    requestAnimationFrame(loop);
    
    if (toggle) {
      div.innerHTML = "Playing frame: " + current + 
                      " (raw mem: " + mem.toFixed(1) + " mb)";

      ctx.drawImage(frames[current], 0, 0, w, h);     // using frame-buffer
      ctx.fillRect(0, 0, current/max * w, 3);
      
      current += dlt;
      if (!current || current === max-1) dlt = -dlt;  // pong-pong
    }
  })();
}
html, body {width:100%;height:100%}
body {margin:0; overflow:hidden;background:#aaa}
div {font:bold 20px monospace;padding:12px;color:#000}
canvas {z-index:-1;position:fixed;left:0;top:0;width:100%;height:100%;min-height:400px}
<div>Pre-loading video... wait for it, wait for it...</div>
<canvas width=600 height=360></canvas>
1

Canvas drawing sources are image & video objects (and some other sources which aren't relevant now). So if your unwanted delay is occurring during the initial download and rendering, then canvas will take longer because the incoming image must first be rendered onto an image object and then again rendered onto the canvas--two steps instead of one.

Your answer is not in the canvas element, so you're back to the usual solution: lessen the quantity of image bits being downloaded by lowering the quality of your images (jpg with less quality).

You could also (as you've indicated), preload & cache all your images in new Images so they can be used immediately when needed. The usual cost applies: increased memory usage for the cached images and a delay at the start of your app while all required images are downloaded.

markE
  • 102,905
  • 11
  • 164
  • 176
  • If that's the case, then I agree, canvas is out of the question. I just wonder why Sony chose canvas.. Moving on, I don't have problem with initial delay of network call and memory usage, I just want to when I need the image, to have it rasterized at a certain pixel map, ready to quickly draw on screen. Have in mind that I have heavily compressed all jpeg frames. Which might also require the extra CPU for decoding. Again, I don't have a problem doing all this initially, I just want to find the way. – Daniel Birowsky Popeski Apr 19 '15 at 19:01