Skip to content

Commit a0be7fa

Browse files
committed
fix: narrow down camera selection
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
1 parent d327ed8 commit a0be7fa

File tree

3 files changed

+53
-43
lines changed

3 files changed

+53
-43
lines changed

demo.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
99

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

1414
<style>
1515
p {
@@ -33,7 +33,7 @@
3333
{{ errorMessage }}
3434
</p>
3535

36-
<qrcode-stream :torch="true" @init="onInit"></qrcode-stream>
36+
<qrcode-stream @decode="onDecode" @init="onInit"></qrcode-stream>
3737
</div>
3838
</body>
3939
<script>

src/components/QrcodeStream.vue

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -102,34 +102,10 @@ export default {
102102
}
103103
},
104104
105-
constraints() {
106-
const base = {
107-
audio: false,
108-
video: {
109-
width: { min: 360, ideal: 640, max: 1920 },
110-
height: { min: 240, ideal: 480, max: 1080 }
111-
}
112-
};
113-
114-
switch (this.camera) {
115-
case "auto":
116-
base.video.facingMode = { ideal: "environment" };
117-
118-
return base;
119-
case "rear":
120-
base.video.facingMode = { exact: "environment" };
121-
122-
return base;
123-
case "front":
124-
base.video.facingMode = { exact: "user" };
125-
126-
return base;
127-
case "off":
128-
return undefined;
129-
130-
default:
131-
return undefined;
132-
}
105+
facingMode() {
106+
if (this.camera === "front") return "user";
107+
else if (this.camera === "rear") return "environment";
108+
else return undefined;
133109
}
134110
},
135111
@@ -175,20 +151,17 @@ export default {
175151
const promise = (async () => {
176152
this.beforeResetCamera();
177153
178-
if (this.constraints === undefined) {
154+
if (this.camera === "off") {
179155
this.cameraInstance = null;
180156
181157
return {
182158
capabilities: {}
183159
};
184160
} else {
185-
this.cameraInstance = await Camera(
186-
this.constraints,
187-
this.$refs.video,
188-
{
189-
torch: this.torch
190-
}
191-
);
161+
this.cameraInstance = await Camera(this.$refs.video, {
162+
facingMode: this.facingMode,
163+
torch: this.torch
164+
});
192165
193166
const capabilities = this.cameraInstance.getCapabilities();
194167

src/misc/camera.js

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ class Camera {
2424
}
2525
}
2626

27+
const narrowDownFacingMode = async facingMode => {
28+
// Modern phones often have multipe front/rear cameras.
29+
// Sometimes special purpose cameras like the wide-angle camera are picked
30+
// by default. Those are not optimal for scanning QR codes but standard
31+
// media constraints don't allow us to specify which camera we want exactly.
32+
// However, explicitly picking the first entry in the list of all videoinput
33+
// devices for as the default front camera and the last entry as the default
34+
// rear camera seems to be a workaround.
35+
const devices = (await navigator.mediaDevices.enumerateDevices()).filter(
36+
({ kind }) => kind === "videoinput"
37+
);
38+
39+
if (devices.length > 2) {
40+
const frontCamera = devices[0];
41+
const rearCamera = devices[devices.length - 1];
42+
43+
if (facingMode === "front") {
44+
return { deviceId: { exact: frontCamera } };
45+
} else {
46+
return { deviceId: { exact: rearCamera } };
47+
}
48+
} else {
49+
return { facingMode };
50+
}
51+
};
52+
2753
const INSECURE_CONTEXT = window.isSecureContext !== true;
2854

2955
const STREAM_API_NOT_SUPPORTED = !(
@@ -34,13 +60,13 @@ const STREAM_API_NOT_SUPPORTED = !(
3460

3561
let streamApiShimApplied = false;
3662

37-
export default async function(constraints, videoEl, advancedConstraints) {
63+
export default async function(videoEl, { facingMode, torch }) {
3864
// At least in Chrome `navigator.mediaDevices` is undefined when the page is
3965
// loaded using HTTP rather than HTTPS. Thus `STREAM_API_NOT_SUPPORTED` is
4066
// initialized with `false` although the API might actually be supported.
41-
// So although `getUserMedia` already should have a build-in mechanism to
67+
// So although `getUserMedia` already should have a built-in mechanism to
4268
// detect insecure context (by throwing `NotAllowedError`), we have to do a
43-
// manual check before even calling `getUserMedia`.
69+
// manual check before even calling `getUserMedia`.
4470
if (INSECURE_CONTEXT) {
4571
throw new InsecureContextError();
4672
}
@@ -49,11 +75,22 @@ export default async function(constraints, videoEl, advancedConstraints) {
4975
throw new StreamApiNotSupportedError();
5076
}
5177

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

85+
const constraints = {
86+
audio: false,
87+
video: {
88+
width: { min: 360, ideal: 640, max: 1920 },
89+
height: { min: 240, ideal: 480, max: 1080 },
90+
...(await narrowDownFacingMode(facingMode))
91+
}
92+
};
93+
5794
const stream = await navigator.mediaDevices.getUserMedia(constraints);
5895

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

71108
await eventOn(videoEl, "loadeddata");
72109

73-
if (advancedConstraints.torch) {
110+
if (torch) {
74111
const [track] = stream.getVideoTracks();
75112

76113
try {

0 commit comments

Comments
 (0)