Skip to content

Commit

Permalink
fix: narrow down camera selection
Browse files Browse the repository at this point in the history
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, we can obtain a list of all video input devices with

  navigator.mediaDevices.enumerateDevices()

Picking the first entry as the default front camera and the last entry
as the default rear camera seems to be a valid heuristic.

Issue: #179
  • Loading branch information
gruhn committed Jun 12, 2020
1 parent d327ed8 commit a0be7fa
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 43 deletions.
6 changes: 3 additions & 3 deletions demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<script src="https://unpkg.com/vue@2/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue-qrcode-reader@2/dist/vue-qrcode-reader.browser.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vue-qrcode-reader@2/dist/vue-qrcode-reader.css">
<script src="./dist/vue-qrcode-reader.browser.js"></script>
<link rel="stylesheet" href="./dist/vue-qrcode-reader.css">

<style>
p {
Expand All @@ -33,7 +33,7 @@
{{ errorMessage }}
</p>

<qrcode-stream :torch="true" @init="onInit"></qrcode-stream>
<qrcode-stream @decode="onDecode" @init="onInit"></qrcode-stream>
</div>
</body>
<script>
Expand Down
45 changes: 9 additions & 36 deletions src/components/QrcodeStream.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,34 +102,10 @@ export default {
}
},
constraints() {
const base = {
audio: false,
video: {
width: { min: 360, ideal: 640, max: 1920 },
height: { min: 240, ideal: 480, max: 1080 }
}
};
switch (this.camera) {
case "auto":
base.video.facingMode = { ideal: "environment" };
return base;
case "rear":
base.video.facingMode = { exact: "environment" };
return base;
case "front":
base.video.facingMode = { exact: "user" };
return base;
case "off":
return undefined;
default:
return undefined;
}
facingMode() {
if (this.camera === "front") return "user";
else if (this.camera === "rear") return "environment";
else return undefined;
}
},
Expand Down Expand Up @@ -175,20 +151,17 @@ export default {
const promise = (async () => {
this.beforeResetCamera();
if (this.constraints === undefined) {
if (this.camera === "off") {
this.cameraInstance = null;
return {
capabilities: {}
};
} else {
this.cameraInstance = await Camera(
this.constraints,
this.$refs.video,
{
torch: this.torch
}
);
this.cameraInstance = await Camera(this.$refs.video, {
facingMode: this.facingMode,
torch: this.torch
});
const capabilities = this.cameraInstance.getCapabilities();
Expand Down
45 changes: 41 additions & 4 deletions src/misc/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,32 @@ class Camera {
}
}

const narrowDownFacingMode = async facingMode => {
// 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];

if (facingMode === "front") {
return { deviceId: { exact: frontCamera } };
} else {
return { deviceId: { exact: rearCamera } };
}
} else {
return { facingMode };
}
};

const INSECURE_CONTEXT = window.isSecureContext !== true;

const STREAM_API_NOT_SUPPORTED = !(
Expand All @@ -34,13 +60,13 @@ const STREAM_API_NOT_SUPPORTED = !(

let streamApiShimApplied = false;

export default async function(constraints, videoEl, advancedConstraints) {
export default async function(videoEl, { facingMode, 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 build-in mechanism to
// 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`.
// manual check before even calling `getUserMedia`.
if (INSECURE_CONTEXT) {
throw new InsecureContextError();
}
Expand All @@ -49,11 +75,22 @@ export default async function(constraints, videoEl, advancedConstraints) {
throw new StreamApiNotSupportedError();
}

// This is a brower API only shim. It patches the global window object which
// is not available during SSR. So we lazily apply this shim at runtime.
if (streamApiShimApplied === false) {
adapterFactory({ window });
streamApiShimApplied = true;
}

const constraints = {
audio: false,
video: {
width: { min: 360, ideal: 640, max: 1920 },
height: { min: 240, ideal: 480, max: 1080 },
...(await narrowDownFacingMode(facingMode))
}
};

const stream = await navigator.mediaDevices.getUserMedia(constraints);

if (videoEl.srcObject !== undefined) {
Expand All @@ -70,7 +107,7 @@ export default async function(constraints, videoEl, advancedConstraints) {

await eventOn(videoEl, "loadeddata");

if (advancedConstraints.torch) {
if (torch) {
const [track] = stream.getVideoTracks();

try {
Expand Down

0 comments on commit a0be7fa

Please sign in to comment.