All files / app/util webcam.ts

11.9% Statements 5/42
6.45% Branches 2/31
15.38% Functions 2/13
11.11% Lines 4/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115                13x 2x 2x             2x                                                                                                                                                                                                  
 
 
export type FacingMode = 'environment' | 'user';
export interface Camera {
  id: string;
  label: string;
}
 
export async function hasCamera(): Promise<boolean> {
  try {
    return !!(await listCameras(false)).length;
  } catch (e) {
    return false;
  }
}
 
export async function listCameras(requestLabels = false): Promise<Array<Camera>> {
  Eif (!navigator.mediaDevices) return [];
 
  const enumerateCameras = async (): Promise<Array<MediaDeviceInfo>> =>
    (await navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === 'videoinput');
 
  // Note that enumerateDevices can always be called and does not prompt the user for permission.
  // However, enumerateDevices only includes device labels if served via https and an active media stream exists
  // or permission to access the camera was given. Therefore, if we're not getting labels but labels are requested
  // ask for camera permission by opening a stream.
  let openedStream: MediaStream | undefined;
  try {
    if (requestLabels && (await enumerateCameras()).every((camera) => !camera.label)) {
      openedStream = await getWebcam();
    }
  } catch (e) {
    // Fail gracefully, especially if the device has no camera or on mobile when the camera is already in use
    // and some browsers disallow a second stream.
  }
 
  try {
    return (await enumerateCameras()).map((camera, i) => ({
      id: camera.deviceId,
      label: camera.label || (i === 0 ? 'Default Camera' : `Camera ${i + 1}`),
    }));
  } finally {
    // close the stream we just opened for getting camera access for listing the device labels
    if (openedStream) {
      console.warn('Call listCameras after successfully starting a QR scanner to avoid creating '
        + 'a temporary video stream');
      stopVideoStream(openedStream);
    }
  }
}
 
async function getWebcam(constraints?: MediaStreamConstraints) {
  // @ts-ignore
  return navigator.permissions.query({name: 'camera'})
    .catch(err => console.log(err)) // ignore permission errors
    .then(perms => {
      if (perms?.state === 'denied') {
        throw 'permission denied';
      } else {
        return navigator.mediaDevices.getUserMedia(constraints || { video: true });
      }
    });
}
 
export async function getCameraStream(deviceId?: string): Promise<{ stream: MediaStream, facingMode: FacingMode }> {
  if (!navigator.mediaDevices) throw 'Camera not found.';
 
  const constraints: Array<MediaTrackConstraints> = [{
    width: {min: 1024},
    deviceId: deviceId,
  }, {
    width: {min: 768},
    deviceId: deviceId,
  }, {
    width: {min: 1024},
    facingMode: 'environment',
  }, {
    width: {min: 768},
    facingMode: 'environment',
  }];
 
  for (const video of constraints) {
    try {
      const stream = await getWebcam({ video });
      // Try to determine the facing mode from the stream, otherwise use a guess or 'environment' as
      // default. Note that the guess is not always accurate as Safari returns cameras of different facing
      // mode, even for exact facingMode constraints.
      const facingMode = getFacingMode(stream) || 'user';
      return {stream, facingMode};
    } catch (e) { }
  }
 
  const stream = await getWebcam();
  const facingMode = getFacingMode(stream) || 'environment';
  return { stream, facingMode };
}
 
function getFacingMode(videoStream: MediaStream): FacingMode | null {
  const videoTrack = videoStream.getVideoTracks()[0];
  if (!videoTrack) return null; // unknown
  // inspired by https://github.com/JodusNodus/react-qr-reader/blob/master/src/getDeviceId.js#L13
  return /rear|back|environment/i.test(videoTrack.label)
    ? 'environment'
    : /front|user|face/i.test(videoTrack.label)
      ? 'user'
      : null; // unknown
}
 
export function stopVideoStream(stream: MediaStream): void {
  for (const track of stream.getTracks()) {
    track.stop(); //  note that this will also automatically turn the flashlight off
    stream.removeTrack(track);
  }
}