Query Camera and Mic Permissions in JavaScript

This article provides a practical overview of checking and requesting camera and microphone permissions in web browsers using modern JavaScript. It explains how to check existing access rights non-intrusively with the Permissions API, how to request direct stream access using navigator.mediaDevices.getUserMedia(), and how to listen for changes to user permission states in real time.

Checking Permission State with the Permissions API

The standard method to inspect camera and microphone permissions without triggering a user prompt is using the navigator.permissions.query() method.

async function checkMediaPermissions() {
  try {
    const cameraPermission = await navigator.permissions.query({ name: 'camera' });
    const micPermission = await navigator.permissions.query({ name: 'microphone' });

    console.log(`Camera status: ${cameraPermission.state}`);
    console.log(`Microphone status: ${micPermission.state}`);

    return {
      camera: cameraPermission.state,
      microphone: micPermission.state
    };
  } catch (error) {
    console.warn('Permissions API query not supported for media devices:', error);
    return null;
  }
}

The state property of the returned PermissionStatus object will return one of three values: - granted: The user has previously granted permission. Hardware can be accessed without a prompt. - prompt: The user has not yet decided. Accessing the device will display a permission prompt. - denied: The user has explicitly blocked access, or access is disabled at the system/browser level.

Requesting Access Using the MediaDevices API

The navigator.mediaDevices.getUserMedia() method is used to directly prompt the user and obtain the media stream. If the permission state is prompt, executing this method will open the browser’s permission dialog.

async function requestMediaAccess(enableVideo = true, enableAudio = true) {
  const constraints = {
    video: enableVideo,
    audio: enableAudio
  };

  try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    console.log('Access granted. Stream acquired:', stream);
    
    // Stop tracks if you only needed to confirm permissions
    // stream.getTracks().forEach(track => track.stop());
    
    return stream;
  } catch (error) {
    if (error.name === 'NotAllowedError') {
      console.error('Permission denied by the user or system policy.');
    } else if (error.name === 'NotFoundError') {
      console.error('No compatible camera or microphone found on this device.');
    } else {
      console.error('Error accessing media devices:', error);
    }
    return null;
  }
}

Listening for Permission Changes

Permission states can change dynamically if a user updates their browser settings while on the page. You can listen to the change event on the PermissionStatus object to update application UI accordingly.

async function monitorCameraPermission() {
  try {
    const status = await navigator.permissions.query({ name: 'camera' });

    status.addEventListener('change', () => {
      console.log(`Camera permission changed to: ${status.state}`);
      if (status.state === 'granted') {
        // Enable video features
      } else if (status.state === 'denied') {
        // Disable video features and alert the user
      }
    });
  } catch (error) {
    console.warn('Permission change listener could not be established:', error);
  }
}

Browser Compatibility and Fallback Handling

While navigator.mediaDevices.getUserMedia is widely supported across all modern browsers, navigator.permissions.query({ name: 'camera' }) has limitations in specific browsers (such as older versions of Safari).

To ensure complete cross-browser coverage, write your code to attempt navigator.permissions.query first. If it is unsupported or fails, fall back to invoking navigator.mediaDevices.getUserMedia() directly and handle the promise rejection with appropriate error handlers for NotAllowedError.