Thursday, September 5, 2013

Take a photo from Javascript

Finally it is 2013 and you can take photos from your browser without requiring flash.

The API that makes it possible is getUserMedia ( http://caniuse.com/#search=getUserMedia ) and it is available in all modern browsers (requiring some vendor-prefixes, though).

For demo: use this fiddle

The code I paste here, takes a photo, dump it to a canvas and tries to upload it to a (non-existent) server.
- The key here is that both the captured image AND the uploaded image does not need to be the same size (you usually don't want to upload very big files). That is controlled via the OUTPUT_RATIO constant: the final size is given by the output canvas.
- Neither the video or the output are required to be shown, however is good to give visual feedback to users.
- NOTE: Chrome does not allow local files to get access to getUserMedia. You can use fiddle to make the test yourself.
- At the present, the api still needs to be prefixed depending on the browsers:
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;



<!DOCTYPE html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <title>getUserApi</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
        <style type="text/css">
        video {
          background: rgba(255,255,255,0.5);
          border: 1px solid #ccc;
        }
        </style>
    </head>
    <body>
        <div id='text'>
            <p>You must grant access to the Camera first.</p>
            <p>Prompt would be shown above this lines, next to the address bar.</p>
            <button type='button' id='button'>Take photo</button>
        </div>
        <video id='video' width="640" height="480"></video>
        <canvas id='canvas' style="display:none;"></canvas>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.js"></script>
        <script>
        (function(window) {

          var nav = window.navigator,
            doc = window.document,
            //some browsers behave differently
            is_webkit = nav.webkitGetUserMedia,
            is_mozilla = nav.mozGetUserMedia,
            showSnapshot = true,
            showVideo = true,
            OUTPUT_RATIO = 0.5, //the output is X times the captured image (ex: we upload small photos)

            source,
            video,
            canvas,
            button,
            ctx,
            localMediaStream;

          var
              initCamera = function() {
              video = document.getElementById('video'),
              canvas = document.getElementById('canvas'),
              button = document.getElementById('button'),
              ctx = canvas.getContext('2d');

              //make canvas and video the same dimensions
              canvas.width = video.width * OUTPUT_RATIO | 0;
              canvas.height = video.height * OUTPUT_RATIO | 0;

              //turn canvas to visible
              canvas.style.display = showSnapshot ? '' : 'none';
              video.style.display = showVideo ? '' : 'none';
              (button || video).addEventListener('click', takeSnapshot, false); //addEventListener: IE9+ Opera7+ Safari, FFox, Chrome

              // if (is_webkit){
              //   nav.getUserMedia('video', onSuccess, onError);
              // }else{
              nav.getUserMedia({
                video: true
              }, onSuccess, onError);
              // }

            },
            onError = function(e) {
              alert('Camera permission rejected!', e);
            },
            onSuccess = function(stream) {
                if (is_mozilla) {
                  source = window.URL.createObjectURL(stream);
                } else if (is_webkit) {
                  source = window.webkitURL.createObjectURL(stream);
                } else {
                  source = stream;
                }

                video.src = source;
                video.play();
                localMediaStream = stream;
            }, stopCamera = function(){
              localMediaStream.stop();
              video.style.display =  canvas.style.display = 'none';
              localMediaStream = canvas = ctx = null;
              button
            }, takeSnapshot = function() {
            if (localMediaStream) {
              ctx.drawImage(video, 0, 0, (video.width * OUTPUT_RATIO) | 0, (video.height * OUTPUT_RATIO) | 0);
              uploadSnapshot();
            }
          }, uploadSnapshot = function(){
              var dataUrl;

            try {
                dataUrl = canvas.toDataURL('image/jpeg', 1).split(',')[1];
            } catch(e) {
                dataUrl = canvas.toDataURL().split(',')[1];
            }
            $.ajax({
                url: "localhost:3000/uploadTest",
                type: "POST",
                data: {imagedata : dataUrl}, //in the server file.write(Base64.decode64(imagedata)) , https://gist.github.com/pierrevalade/397615
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                success: function () {
                    alert('Image Uploaded!!');
                },
                error: function () {
                    alert("There was some error while uploading Image");
                }
            });
          };


            //some browsers use prefixes
          nav.getUserMedia = nav.getUserMedia || nav.webkitGetUserMedia || nav.mozGetUserMedia || nav.msGetUserMedia;

          if (nav.getUserMedia) {
            initCamera();
          } else {
            alert("Your browser does not support getUserMedia()")
          }
        }(this));
        </script>
    </body>
</html> 

Bonus

For large images you can save some bytes by sending the image as a blob instead of a base64 encoding.

First, encode the dataUrl as a blob

function dataURItoBlob(dataURI, dataTYPE) {
  var binary = atob(dataURI), array = [];
  for(var i = 0; i < binary.length; i++) array.push(binary.charCodeAt(i));
  return new Blob([new Uint8Array(array)], {type: dataTYPE});
}

Then, you have 2 options:

- using the FormData api

function uploadWithFormData(dataUrl){
  // Get our file
  var file = dataURItoBlob(dataUrl, 'image/jpeg'),
  fd = new FormData();
  // Append our Canvas image file to the form data
  fd.append("imageNameHere", file);
  // And send it
  $.ajax({
     url: "/server",
     type: "POST",
     data: fd,
     processData: false,
     contentType: false,
  });
}

- or using the XHR

function uploadWithXHR(dataUrl) {
  var file = dataURItoBlob(dataUrl, 'image/jpeg'),
  xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  //add the headers you need
  // xhr.setRequestHeader("Cache-Control", "no-cache");
  // xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
  // xhr.setRequestHeader("X-File-Name", file.name || file.fileName || 'image.jpg');
  // xhr.setRequestHeader("X-File-Size", file.size || file.fileSize);
  // xhr.setRequestHeader("X-File-Type", file.type);
  // xhr.setRequestHeader("Content-Type", options.type);
  // xhr.setRequestHeader("Accept","application/json, text/javascript, */*; q=0.01");
  xhr.send(file);
}