context drawImage / canvas toDataURL lagging signi

2019-07-25 23:47发布

问题:

I have a bit of Javascript that runs on an Android tablet, running Opera Mobile 12. The tablet is attached to the wall in an office, so it's used by everyone who works in this office, as a timekeeping system (ie. they use it to clock in/clock out of work). This javascript takes a photo of the user when a certain event is raised, then converts this photo to a data URL so it can be sent to a server. This all runs in the background, though - the video and canvas elements are both set to display:none;.

Here's the code that handles the webcam interaction:

var Webcam = function() {
  var video = document.getElementById('video');
  var stream_webcam = function(stream) {
    video.addEventListener('loadedmetadata', function(){
      video.play();
    }, false);
    video.src = window.URL.createObjectURL(stream);
  }

  // start recording
  if (navigator.getUserMedia)
    navigator.getUserMedia({video: true}, stream_webcam);
  else
    _error("No navigator.getUserMedia support detected!");

  var w = {};

  w.snap = function(callback) {
    var canvas = document.getElementById('canvas');
    canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
    // delay is incurred in writing to canvas on opera mobile
    setTimeout(function() {
      callback(canvas.toDataURL());
    }, 1);
  };

  return w;
}

w.snap(callback) gets called when the user presses a certain button. The problem is that there is a significant lag between the call to drawImage, and an image actually being drawn to the canvas. Here's the problem that occurs:

  • User 1 users the tablet, presses the button, has their photo "snapped" and sent to the server. The server receives a blank, transparent, image. (It's not the same issue as html5 canvas toDataURL returns blank image - the data being sent to the server is substantially less for the first image, because it's blank)
  • A few minutes later, user 2 uses the tablet and presses the button, and has their photo "snapped". The server receives the photo of user 1.
  • Later, user 3 does the same thing, and the server gets a photo of user 2.

So I'm guessing the call to toDataURL() is returning old data because drawImage() doesn't . (I read somewhere that it's async, but I can't confirm and can't find any way of attaching an event to when it finishes drawing.)

I've tried:

  • changing the timeout in snap() to a variety of values. The highest I tried was 2000, but the same issue occurred. (If anyone suggests higher numbers, I'm happy to try them, but I don't want to go much higher because if I go too high, there's potential for user 3 to have their photo taken before I've even processed user 2's photo, which means I might lose it entirely!)
  • having the canvas draw something when the page first loads, but that didn't fix it - when user 1 was photographed, the server received whatever photo was taken when the page loaded, instead of the photo of user 1.
  • "getting" the canvas element again, using getElementById, before calling toDataURL.

A few more notes:

  • This works fine on my computer (macbook air) in Chrome and Opera.
  • I can't reliably replicate it using a debugger (linking to the tablet using Opera Dragonfly).
  • AFAIK Opera Mobile is the only Android browser that supports getUserMedia and can thus take photos.
  • If I remove the display:none; from the video and canvas, it works correctly (on the tablet). The issue only occurs when the video and canvas have display:none; set.

Because the page is a single page app with no scrolling or zooming required, a possible workaround is to move the video and canvas below the page fold, then disable scrolling with javascript (ie. scroll to the top whenever the user scrolls).

But I would rather a proper fix than a workaround, if possible!

回答1:

I don't have a very good solution but I would suggest not handling an invisible canvas through the dom by setting it invisible.

Instead of

 var canvas = document.getElementById('canvas');

Try

var canvas = document.createElement('canvas');
 canvas.width = video.width;
 canvas.height = video.height;
 canvas.getContext('2d').drawImage(video, 0, 0)

This way it's separate from the DOM.

Also Why not use

canvas.toDataURL("image/jpg")?

EDIT: Also, if you're designing it, have you tried the other browsers out there? Whats restricting you to Opera over the other browsers available or using phonegap?

EDIT2: Thinking about it, Canvas also has two other options for getting that photo into place that you would want to look in two. Those two being:

var imgData = canvas.getContext('2d').getImageData(0,0,canvas.width,canvas.height);

-or-

canvas.getContext('2d').putImageData(imgData,0,0);

These two ignore any scaling you've done to the context but I've found that they are much more direct and often faster. They are a solid alternative to toDataURL and drawImage but the image data you put and get from these are encoded as an array in the form:

[r1, b1, g1, a1, r2, b2, g2, a2....]

you can find documentation for them here:

http://www.w3schools.com/tags/canvas_putimagedata.asp http://www.w3schools.com/tags/canvas_getimagedata.asp



回答2:

Some version of Android have problems with toDataUrl(); Maybe this is the solution?

http://code.google.com/p/todataurl-png-js/wiki/FirstSteps

The script: http://todataurl-png-js.googlecode.com/svn/trunk/todataurl.js

Paste this in your head:

<script src="todataurl.js"></script>