Getting access to a phone’s camera from a web page

For a web application I’m developing, I wanted to be able to allow mobile users to seamlessly push a button on the web page, take a photo, and post that photo to the page. Since the W3C draft specification for media capture isn’t yet implemented by any mobile browser except perhaps some Opera versions, it’s necessary to use a native application to get access to the camera. But I really wanted the lightest weight possible application, as I’m targeting tablets, phones, and computers for this site. I didn’t want to end up having to develop a different version for each device.

Enter phone gap! Phone gap is a wrapper application framework that lets you code mobile applications in HTML and javascript. It exposes javascript APIs to hardware features like the camrea. But I didn’t want to have to recompile every time I made a change to the code — really, I just want a thin wrapper around the page to add the one missing feature (camera access). Could my phonegap app be as simple as this?

<!-- phonegap html file for app -->
<html>
  <head>
    <!-- phonegap shim -->
    <script type="text/javascript" charset="utf-8" src="cordova-1.6.0.js"></script>
  </head>
  <body>
    &lt;iframe src="http://myapp.com"></iframe>
  </body>
</html>

(the &lt; is a result of wordpress.com’s apparent inability to render a literal iframe tag, even when properly escaped.)

The problem is that the iframe and the phonegap app’s page run on different domains, and thus they can’t see each other. The inner iframe can’t trigger a camera event on the outer frame directly.

Stack Overflow commenters alluded vaguely that it might be possible to do this with cross-domain messaging. Several hours later, here’s how, in detail!

Cross-document messaging

The main difficulty with accessing the camera from within an iframe in a PhoneGap application is that the document inside the iframe (which contains your remote webpage) has a different origin from the local web page (which has the phone gap shim). Consequently, the remote page can’t access navigator.camera. Cross-document messaging makes it possible for them to communicate even so. Here’s a decent writeup on the topic.

Basically, the parent document can send messages to the iframe (if it’s listening) like this:

iframe.contentWindow.postMessage({data: "stuff"}, "http://iframepage.com")

Replace "iframepage.com" with the URL for the page the iframe is accessing. The iframe can talk to the parent document (the phonegap window which has access to the camera) like this:

iframe.parent.postMessage({stuff: "rad"}, "file://")

Yes, that’s right — the PhoneGap’s page identifies as "file://", with no other path.

Listening for messages is fairly straight-forward. In both the phonegap file and the remote webpage, listen for cross-document messages by attaching an event listener to window:

window.addEventListener("message", function(event) {
  // Check that the message is coming from an expected sender --
  // "file://" if you're in the iframe, or your remote URL for
  // the phonegap file.
  if (event.origin == url) {
    // do something with the message
  }
});

Putting it all together

So the plan is to have the phonegap page do nothing other than announce to the iframe that a camera is available, and respond if the camera is found. The only additional wrinkle is just delaying your events until both the phonegap page and the iframe have loaded.

Putting it all together, here is the entire phonegap html page:

<!DOCTYPE HTML>
<html>
<head>
<title>My app</title>
<script charset="utf-8" type="text/javascript" src="cordova-1.6.0.js"></script><script type="text/javascript">// <![CDATA[
  document.addEventListener("deviceready",function() {
    var iframe = document.getElementById('iframe');
    var url = "http://example.com";
    // Announce that we have a camera.
    iframe.addEventListener("load", function(event) {
      iframe.contentWindow.postMessage({
        cameraEnabled: navigator.camera != null && navigator.camera.getPicture != null
      }, url);
    }, false);
    // Listen for requests to use it.
    window.addEventListener("message", function(event) {
      if (event.origin == url) {
        if (event.data == "camera") {
          navigator.camera.getPicture(function(imageData) {
            iframe.contentWindow.postMessage({
              image: imageData
            }, url);
          }, function(message) {
            iframe.contentWindow.postMessage({
              error: message
            }, url);
          }, {
            quality: 50,
            destinationType: Camera.DestinationType.DATA_URL,
            targetWidth: 640,
            targetHeight: 640
          });
        }
      }
    }, false);
    iframe.src= url;
  }, false);
// ]]></script>
<style type='text/css'>
  body,html,iframe {
    margin: 0; padding: 0; border: none; width: 100%; height: 100%;
  }
</style>
</head>
<body>
  &lt;iframe src='' id='iframe'></iframe>
</body>
</html>

In the remote webpage, you can seamlessly handle devices that have cameras or that don’t:

var cameraAvailable = false;

window.addEventListener('message', function (event) {
  if (event.origin == "file://") {
    if (event.data.cameraEnabled) {
      cameraAvailable = true;
    } else if (event.data.image) {
      image = $("<img/>");
      $("#app").prepend(image)
      image.attr("src", "data:image/jpg;base64," + event.data.image);
    } else if (event.error) {
      alert("Error! " + event.error);
    }
  }
}, false);
// When you want to get a picture:
if (cameraAvailable) {
    window.parent.postMessage("camera", "file://");
}

And that’s it! Not too bad. I don’t know yet whether the resulting app is something the iPhone app store would tolerate, but it flies for Android.

(now if only it were as easy to post html code snippets into wordpress without their getting munged!)

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

14 Responses to Getting access to a phone’s camera from a web page

  1. Thanks for the tutorial, it helped me out a lot!

    One thing to note is that, with the newest version of PhoneGap, you need to “white list” any urls you’re trying to access from the app by adding an entry to the PhoneGap.plist file under “ExternalHosts”. Otherwise, PhoneGap will kick you out to Safari.

    More info here: http://stackoverflow.com/questions/8035701/phonegap-on-iphone-wont-load-external-scripts

  2. I also noticed an error in the javascript for the remote webpage. On line #3, you have “window.addEventListener(‘message’), function (event) {“. There shouldn’t be a closing parentheses after ‘message’.

  3. Thanks, fixed that.

  4. pobrehablador says:

    I’ve tried your code but when i run the phonegap app it opens a new Safari window and it doesn’t display the iframe contents. What’s wrong?

  5. I don’t know. I don’t currently have an iOS device to try it with. Maybe something changed in an upgrade? If you figure it out, let us know how.

  6. Rooter says:

    You just made my day. Thank you so much!

  7. Barrie says:

    does someone know if this app will be published by Apple?

  8. Fil says:

    Thanks, that was just what I was looking for. I couldn’t get the good old way of calling cross-domain functions and this provided an equal solution. Maybe one day windows mobile will support this!

  9. iamsteadman says:

    This is super-useful, thanks very much. Forgive me if someone else hasn’t pointed it out, but I think the error logging stuff might need to be looking for `event.data.error`, rather than `event.error`. (I’ve been testing in the iOS Simulator so come across camera unavailability errors all the time!)

  10. Al says:

    This works brilliantly, thanks. One thing I was wondering – why is the phonegap page origin ‘file://’? It appears to be a phonegap thing although I would expect it to work the same without phonegap but it doesn’t ie. using the same technique without phonegap gives and origin different to ‘file://’.

  11. Martin says:

    Perfect example and it worked at first try (android with PGB). I’m using the exact same scenario. Brilliant brilliant brilliant. The most difficult design part of working with iframe was to remove/hide the iframe’s scrollbar … and still get the touch scroll to work with large documents inside the iframe. But if you’re persistent enough the solution is just round the corner.
    Thanks again.

  12. I found a fix to this problem that doesn’t require the iframe. Added it to your question on stackoverflow. Could you please up vote it so it becomes more visible, other people might need it.

    http://stackoverflow.com/a/26551455/534495

    Thanks.

  13. tkulis says:

    Hi, thanks for tutorial it worked like a charm.But one question though. Have you ever tried to accomplish same thing using CordovaWebView ????If so it would be nice to post some information about it because I haven’t been able to google answer for it.

  14. Anonymous says:

    I found a way to use cordova inside iframes.
    You need to make sure the iframe page is whitelisted though.

    Just add this to the page inside the iframe and all cordova-dependent code works in and outside the iframe:

    parent.document.addEventListener( ‘deviceready’, function()
    {
    function merge( t, s )
    {
    // Do nothing if they’re the same object
    if ( t === s ) return;

    // Loop through source’s own enumerable properties
    Object.keys( s ).forEach( function( key )
    {

    // Get the value
    var val = s[ key ];

    // Is it a non-null object reference?
    if ( val !== null && typeof val === “object” ) {

    // Yes, if it doesn’t exist yet on target, create it
    if ( !t.hasOwnProperty( key ) ) t[ key ] = {};

    // Recurse into that object IF IT DOES NOT CONTAIN CIRCULAR REFERENCES
    if ( !isCyclic( t[ key ] ) && !isCyclic( s[ key ] ) ) merge( t[ key ], s[ key ] );

    // Not a non-null object ref, copy if target doesn’t have it
    }
    else if ( !t.hasOwnProperty( key ) ) t[ key ] = s[ key ];
    } );
    }

    function isCyclic( obj )
    {
    var seenObjects = [];

    function detect( obj )
    {
    if ( obj && typeof obj === ‘object’ ) {
    if ( seenObjects.indexOf( obj ) !== -1 ) return true;
    seenObjects.push( obj );
    for ( var key in obj ) if ( obj.hasOwnProperty( key ) && detect( obj[ key ] ) ) return true;
    }
    return false;
    }

    return detect( obj );
    }

    merge( window, parent );
    } );

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s