5

I need to play videos that have a transparent background on mobile devices using HTML5. On the Windows Phone, I can capture the video tag's current frame and show that in the canvas. But that doesn't work on Android and iOS devices (I think for security reasons?)

My solution was to split up the .flv using FFMPEG and then stitch those frames together into large images, like sprite sheets.

The Problem is that the animation 'hangs' when I switch over to a new frame sheet. I've only checked this visually and through the console (by logging when I change the current sprite sheet row.) I've tested this by seeing how it hangs when I change the sprite sheet, and how it doesn't hang when I just loop the same sheet over and over.

I pre-load all of the images before hand:

var frameImages = [];

for(var i = 0; i < 35; i++)
{
  frameImages.push(new Image());
  frameImages[i].src = 'frame' + i + '.png';

  console.log(frameImages[i].src);

  frameImages[i].onload = function()
  {
    // Notify us that it's been loaded.
    console.log("Image loaded");
  }
}

And then play it like so:

processFrame = function()
{
    outputCanvas.width = outputCanvas.width;
    output.drawImage(frameImages[currentFrame], (curCol*153), (curRow*448), 153, 448, 0, 0, 153, 448);
    curCol += 1;

    if(curCol >= maxCol)
    {
      curCol = 0;
      curRow += 1;

      if(curRow >= maxRow)
      {
        curRow = 0;
        currentFrame++;
      }
    }
  }
}

var mozstart = window.mozAnimationStartTime;

step = function(timestamp) {

  var diff = (new Date().getTime() - start) - time;
  if(diff >= frameDelay)
  {
    processFrame();
    time += frameDelay;
  }
}

I've tried this in Chrome v 23.0.1271.97 m on a Win 7 machine and on a Nexus 7 with Chrome.

See it in action here:
http://savedbythecode.com/spokes/mozanimation.php - This is using mozAnimationStartTime
and http://savedbythecode.com/spokes/newplayer.php - This is using regular JS timers that are adjusted each step (from http://www.sitepoint.com/creating-accurate-timers-in-javascript/)

Any ideas? Was the problem clear enough?

Thanks, Kevin

Banath
  • 345
  • 3
  • 18
  • It seems to work out well enough for the client. – Banath Jan 20 '13 at 01:00
  • 1
    I mean that if it was hurting business, the people that pay him for this wouldn't keep paying him. His business is growing, not shrinking. Most importantly, you have to keep the audience in mind. We're very comfortable with technology, and the people in our generations are too. The businesses my client deals with has a generally older audience that is less comfortable with technology. Sometimes it can even be overwhelming for them. Having somebody on the website talking to them, probably makes them more comfortable and easier to digest the information. Again, it's not my business. – Banath Jan 20 '13 at 03:59
  • 1
    It works OK for me in Chrome, but only after loading all the frames, so you might want to delay the animation until all the sprites are completely loaded. Not sure if you've done any debugging in other browsers, but I get a delay in animation in Firefox (sound is out of sync) and I don't see the animation nor do I hear the sound in Opera. It doesn't work in IE8, but that's to be expected LOL. Maybe use background sound for non-HTML5 compliant IE browsers and make sure non-supported functions are not called there? I can foresee video+audio syncing problems, though. Nice work so far!!! ;) – TildalWave Jan 25 '13 at 16:02
  • Just an off-topic observation, but the bottom part of the presenter is barely moving at all (no pun intended LOL) and I see possible bandwidth savings in this. The bottom half of the animation only really differs frame-by-frame when the presenter 'enters', then she merely wobbles slightly as her hands move. I realize it would mean a lot of additional work that might not have been requested, or indeed paid for, but has possibly around 40% of bandwidth savings there. – TildalWave Jan 25 '13 at 17:16
  • This actually only has to work on Android and iOS devices, the client already has an existing web solution that's similar to my solution for the Windows Phone. So I don't have to worry about supporting IE :). – Banath Jan 25 '13 at 21:23
  • I took the liberty to investigate one of the sprite images used in your animation by using TweakPNG tool, and it looks kinda strange, showing a lot of chunks, indicating they're not progressively encoded (they seem to be what PNG calls 'interlaced'). Could you try re-encoding them progressively (non-interlaced) and see if that helps with firing your onload function when the whole image loads and not the first visible frame of it? It should save some bandwidth as well, making files a tad smaller. You could also use optipng tool for further file size optimizations (~10%). Cheers! – TildalWave Jan 25 '13 at 21:35
  • Thanks, I'll look into doing that. The images were created by PHP, so nothing fancy there. This is a lot along the lines another person was going in: that the problem might actually be due to the system swapping these images in and out of memory? So making the file sizes smaller might fix this problem and make it quicker to load. – Banath Jan 25 '13 at 22:23
  • Please disregard my last suggestion. I've just tested it using one of your sprites and it works as expected, meaning there isn't any problem there even if they're interlaced. Upon further investigating your code, I did notice though that you're not really waiting for all of the images to load, merely adding a line to the console when they load. I'm also getting some errors suggesting other problems in your code but I need to investigate that further, I just wanted to make sure you don't waste your time with image format, as that isn't a problem. TBC – TildalWave Jan 25 '13 at 22:29
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/23393/discussion-between-tildalwave-and-banath) – TildalWave Jan 25 '13 at 22:55

3 Answers3

2

Cool code. Why couldn't you just use a video tag (see option 6) ? Anyway I will just list issues I can think of and hope it helps you :

  1. Change ++ to += 1 : for consistency with your previous code, and because channeling Doug Crockford's benevolent ghost may help to remove glitches from your javascript code.

  2. You stitch together a bunch of images into a single 'sprite sheet'. The code then hangs when you want to go the next of these sheets. So, because your code does not work yet, as a test, remove all the loops cycling through the sub-images in each frame, and just directly output each frame in your frameImages array to canvas at each render to see if it still hangs. The result of this will let you know if your code can get to the next frame in frameImages or if it can't. This could lead you closer to the source of the bug.

  3. Is currentFrame declared with var and in scope of processFrame ? If not, just put var currentFrame = 0; somewhere above where you define function processFrame(...

  4. When you say "and then I play it like so" do you use a promise or equivalently call your animation start function after all the onloads have fired? A way to do this is : You can increment a counter once with each onload that fires and each time check if the counter is equal to the total number of loads needed, and only then call your animation start. My hunch is it is a caching issue as these seem to be the most variable between different browsers due to different internals.

  5. Try clearing the canvas between drawImage calls and when you update currentFrame, since it may help to reset the internal state of the canvas when you change images.

  6. You can do alpha and transparency with HTML5 video tags. Just use some svg transforms. These are easy to do as it says in this SO answer

Community
  • 1
  • 1
Cris Stringfellow
  • 3,714
  • 26
  • 48
  • The video requires a transparent background, so I couldn't use a video tag. Why shouldn't I use increment? I don't understand #2. I'll get back to you on 3 and 4 later. Is not a caching issue at least in the sense of downloading images. Why clear the canvas after drawing..? – Banath Jan 26 '13 at 10:38
  • @Banath -- Okay, I struggled to word #2 I will try again. Why clear the canvas? Well, I recall I may possibly have gotten rid of some javascript graphics glitches in the past by simply clearing the canvas. Because, as you know, the canvas is a state machine, and renders are called as part of a stepping through states. This certainly applies to drawing vertexes and shapes, I seem to recall it also applies to drawImage. Internally, canvas render ops are lumped together, and then executed in composite for efficiency, but the exact details of this are tuned. – Cris Stringfellow Jan 26 '13 at 12:42
  • @Banath -- Re caching : How can you be sure? If you simply tested by outputting "Image loaded" that is not sufficient. I know because I have done that, too. You have to execute all renders, and then start your animation, if you want to eliminate the possibility it is caching. The only strict way to do this (and even onload is unreliable, but still) is to keep a check of how many images have fired onload, and then call-through to your start animation when all images are loaded. Call through from inside the onload handler. Do you understand what I mean? – Cris Stringfellow Jan 26 '13 at 12:45
  • @Banath -- Because of the synchronous nature of it, it might be reaching currentFrame == 1 before that image has fully loaded. And if I recall correctly, you will need to load those images into the DOM. Even if this is not strictly correct, maybe try adding the HTMLImageELements into the DOM anyway, just with a visibility hidden class. Then you can trust the onload method more, I suspect. It is likely caching, as these are the trickiest to debug. – Cris Stringfellow Jan 26 '13 at 12:47
  • I think #6, the SVG solution will work the best as it will be the most reliable especially for audio/video sync, even if the transparency cut-out is a little rough. – Banath Jan 26 '13 at 21:39
  • @Banath -- well I hope so. But your hack was cool and you did it well. Thanks for the +50 but did you try any of the other fixes I mentioned, anything work? – Cris Stringfellow Jan 26 '13 at 22:24
  • Thanks Chris. No I didn't try them yet, I'm swamped with other work for the weekend. I awarded you the bounty because yours was the only answer that actually might work, you put time into it, and you showed me SVG clipping, which will hopefully work out. – Banath Jan 26 '13 at 22:31
2

Here's another, less orthodox solution - use the video as your canvas input and use getImageData to green screen each frame.

basically you've got your video tag and your canvas

<video id='source' style='display: none;'>
   <source>whatever.mp4</source>
   <source>whatever.ogg</source>
</video>
<canvas id='screen' width='videoWidth' height='videoHeight></canvas>

then you setup you draw function

var t = null;
document.getElementById('source').addEventListener('playing', function(){
   var vid = this;   
   t = setInterval(function(){
       if (vid.paused || vid.ended){
          clearInterval(t);
          return
       } else {
          drawFrame();
       }
   }, 20)
}, false)

and your draw frame function is

function drawFrame(){
   var ctx = document.getElementById('screen').getContext('2d'),
       vid = document.getElementById('source');

   ctx.drawImage(vid, 0, 0);
   var tolerance = 0.2,
       bgColor = [0, 255, 0], //or whatever 
       imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height),
       len = imageData.data.length;

   for (var i=0; i<len; i+=4){
      var r = imageData.data[i],
          g = imageData.data[i+1],
          b = imageData.data[i+2];

      //There's lots of different color comparing methods, just check google
      var dif = colorCompare([r, g, b], bgColor);
      if (dif < tolerance){
         imageData.data[i+3] = 255;
      }
   }

   ctx.putImageData(imageData, 0, 0);
}

You can play around with different parts of it to achieve the performance level you want. For the relatively small (dimension-wise) video you have performance shouldn't be an issue. You might have to use a buffer canvas to avoid screen flickering, but I doubt it. And you can adjust the tolerance variable to avoid any halo effect you might get.

Not saying it's the best solution, but it's definitely a workable solution, and one that would work if you wanted to change the video by just changing the input video with no extra work required.

hobberwickey
  • 6,118
  • 4
  • 28
  • 29
  • 2
    It's not allowed on android and iOS devices. This is essentially what I did for the windows phone solution. – Banath Jan 26 '13 at 20:01
  • @hobberwickey -- yeah it's a cool idea but damn security restrictions. They really need a teeny bit more discrimination with that security model -- like maybe only refuse to grab video if it is coming from the user device like FaceTime or something...otherwise, what's the security it's breaking?? – Cris Stringfellow Jan 26 '13 at 22:26
  • @hobberwickey -- Didn't have time earlier to find the article, but you're basically doing this video & canvas transparency the hard & worse way. Assuming that you have a video already set up for transparency -> http://jakearchibald.com/scratch/alphavid/ (didn't work in Chrome today; unsure why.) A video is set up for transparency when it looks like -> http://savedbythecode.com/transparentvideo.png where the top half is the video the user should see, and the bottom half has the parts of the video to show in white. Your way would be the most viable if the video isn't set up like this already. – Banath Jan 26 '13 at 22:38
  • @Banath I don't know, they're not really all that different. The only difference is the color comparison which could be rather simplistic if the transparent background were grabbed from video in a pretty consistent way. They both loop through the image data, and mine looks like few drawImage calls. I didn't realize about the security restrictions. That's stupid, why don't they just implement the same cross-origin policy they do for images. – hobberwickey Jan 27 '13 at 00:27
0

Not sure if you've looked at this or not...but have you thought about utilizing the jquery queue? When I load it into my browser, i see that some frames don't show, or blanks out, then comes back, etc, etc. I've used queues before to do synchronous processing of say ajax calls that i wanted to "chain" together in a specific order....so that might be an area you could try to load each "pictured frame" in a synchronous way, ensuring each frames gets a load in the right order?

http://api.jquery.com/queue/

Kevin Mansel
  • 2,351
  • 1
  • 16
  • 15