Detecting Camera Permission Dialog in Browser

September 1st 2017 HTML 5 JavaScript Web Analytics

When trying to capture camera video from browser using MediaDevices.getUserMedia, user consent is required. On first call the browser will show a dialog to the user who can either allow or block access to the camera. It might be useful to have some analytical data for your application on how many users blocked the camera access, but unfortunately there's no direct way to detect from code when the dialog was shown and how the user answered.

Camera permission dialog in Google Chrome

You can get some information when the call to getUserMedia fails with a rejected promise - NotAllowedError indicates that the user has blocked access to the camera:

var successCallback = function(error) {
  // user allowed access to camera
};
var errorCallback = function(error) {
  if (error.name == 'NotAllowedError') {
    // user denied access to camera
  }
};
navigator.mediaDevices.getUserMedia(constraints)
  .then(successCallback, errorCallback);

However, this doesn't necessarily mean that a dialog was shown at all. The same error will still be returned, even if the user blocked access during his previous visit. Similarly, if the call succeeded, we know that the user must have allowed access once, but again he could have already done it previously.

Fortunately we can take advantage of the promise returned by getUserMedia to quite reliably detect when the permission dialog was shown: the promise is only resolved or rejected after the user dismisses the dialog. Based on the difference between the time we call getUserMedia and the time one of the callbacks is called, we can conclude whether there was any user interaction involved. When a dialog is shown, the user must respond to it, which will always take longer than the camera initialization, let alone a direct rejection.

var startTime = Date.now();
var detectPermissionDialog = function(allowed) {
  if (Date.now() - startTime > timeThreshold) {
    // dialog was shown
  }
};
var successCallback = function(error) {
  detectPermissionDialog(true);
};
var errorCallback = function(error) {
  if ((error.name == 'NotAllowedError') ||
    (error.name == 'PermissionDismissedError')) {
    detectPermissionDialog(false);
  }
};
navigator.mediaDevices.getUserMedia(constraints)
  .then(successCallback, errorCallback);

You might have noticed that I added a check for the undocumented error value PermissionDismissedError to the errorCallback. This error is returned by Chrome when the user closes the permission dialog without allowing or blocking access to the camera.

There's still the issue of finding a good value for timeThreshold to differentiate between user interaction and programmatic response. During my testing 1000 ms worked well enough. Although a user could respond faster than that if he already anticipated the dialog, it will usually take him longer than that to notice the dialog and decide how to respond. On the other hand camera initialization normally shouldn't take that long even on slower devices. The detection might not be perfect, but it should work correctly in most cases.

With some consideration, the same approach could be used for other browser permission dialogs. For example, when using Geolocation.getCurrentPosition() to access location information, a dialog will be shown for the user to grant access to his current location. Although the rejection can still be handled the same as with the camera, this does not hold true when access is granted. Since obtaining location information might require the device to get a GPS fix, it could take even longer than user's interaction with the dialog. Hence, for Geolocation we can use this approach only to detect when access was blocked, but not when it was allowed.

Copyright
Creative Commons License