UNPKG

vue3-qrcode-reader

Version:

A set of Vue.js components for detecting and decoding QR codes.

129 lines (110 loc) 4.29 kB
import { StreamApiNotSupportedError, InsecureContextError } from "./errors.js"; import { imageDataFromVideo } from "./image-data.js"; import { eventOn, timeout } from "callforth"; import shimGetUserMedia from "./shimGetUserMedia"; class Camera { constructor(videoEl, stream) { this.videoEl = videoEl; this.stream = stream; } stop() { this.stream.getTracks().forEach(track => track.stop()); } captureFrame() { return imageDataFromVideo(this.videoEl); } getCapabilities() { const [track] = this.stream.getVideoTracks(); // Firefox does not yet support getCapabilities as of August 2020 return track?.getCapabilities?.() ?? {}; } } const narrowDownFacingMode = async camera => { // Modern phones often have multipe front/rear cameras. // Sometimes special purpose cameras like the wide-angle camera are picked // by default. Those are not optimal for scanning QR codes but standard // media constraints don't allow us to specify which camera we want exactly. // However, explicitly picking the first entry in the list of all videoinput // devices for as the default front camera and the last entry as the default // rear camera seems to be a workaround. const devices = (await navigator.mediaDevices.enumerateDevices()).filter( ({ kind }) => kind === "videoinput" ); if (devices.length > 2) { const frontCamera = devices[0]; const rearCamera = devices[devices.length - 1]; switch (camera) { case "auto": return { deviceId: { exact: rearCamera.deviceId } }; case "rear": return { deviceId: { exact: rearCamera.deviceId } }; case "front": return { deviceId: { exact: frontCamera.deviceId } }; default: return undefined; } } else { switch (camera) { case "auto": return { facingMode: { ideal: "environment" } }; case "rear": return { facingMode: { exact: "environment" } }; case "front": return { facingMode: { exact: "user" } }; default: return undefined; } } }; export default async function(videoEl, { camera, torch }) { // At least in Chrome `navigator.mediaDevices` is undefined when the page is // loaded using HTTP rather than HTTPS. Thus `STREAM_API_NOT_SUPPORTED` is // initialized with `false` although the API might actually be supported. // So although `getUserMedia` already should have a built-in mechanism to // detect insecure context (by throwing `NotAllowedError`), we have to do a // manual check before even calling `getUserMedia`. if (window.isSecureContext !== true) { throw new InsecureContextError(); } if (navigator?.mediaDevices?.getUserMedia === undefined) { throw new StreamApiNotSupportedError(); } // This is a browser API only shim. It patches the global window object which // is not available during SSR. So we lazily apply this shim at runtime. await shimGetUserMedia(); const constraints = { audio: false, video: { width: { min: 360, ideal: 640, max: 1920 }, height: { min: 240, ideal: 480, max: 1080 }, ...(await narrowDownFacingMode(camera)) } }; const stream = await navigator.mediaDevices.getUserMedia(constraints); if (videoEl.srcObject !== undefined) { videoEl.srcObject = stream; } else if (videoEl.mozSrcObject !== undefined) { videoEl.mozSrcObject = stream; } else if (window.URL.createObjectURL) { videoEl.src = window.URL.createObjectURL(stream); } else if (window.webkitURL) { videoEl.src = window.webkitURL.createObjectURL(stream); } else { videoEl.src = stream; } await eventOn(videoEl, "loadeddata"); // According to: https://oberhofer.co/mediastreamtrack-and-its-capabilities/#queryingcapabilities // On some devices, getCapabilities only returns a non-empty object after // some delay. There is no appropriate event so we have to add a constant timeout await timeout(500); if (torch) { const [track] = stream.getVideoTracks(); const capabilities = track.getCapabilities(); if (capabilities.torch) { track.applyConstraints({ advanced: [{ torch: true }] }); } else { console.warn("device does not support torch capability"); } } return new Camera(videoEl, stream); }