Skip to content

Commit b09ded1

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 e40a06a commit b09ded1

File tree

3 files changed

+63
-45
lines changed

3 files changed

+63
-45
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" camera="rear" @init="onInit"></qrcode-stream>
3737
</div>
3838
</body>
3939
<script>

src/components/QrcodeStream.vue

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -100,36 +100,6 @@ export default {
100100
} else {
101101
return this.track;
102102
}
103-
},
104-
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-
}
133103
}
134104
},
135105
@@ -175,20 +145,17 @@ export default {
175145
const promise = (async () => {
176146
this.beforeResetCamera();
177147
178-
if (this.constraints === undefined) {
148+
if (this.camera === "off") {
179149
this.cameraInstance = null;
180150
181151
return {
182152
capabilities: {}
183153
};
184154
} else {
185-
this.cameraInstance = await Camera(
186-
this.constraints,
187-
this.$refs.video,
188-
{
189-
torch: this.torch
190-
}
191-
);
155+
this.cameraInstance = await Camera(this.$refs.video, {
156+
camera: this.camera,
157+
torch: this.torch
158+
});
192159
193160
const capabilities = this.cameraInstance.getCapabilities();
194161

src/misc/camera.js

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

27+
const narrowDownFacingMode = async camera => {
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+
switch (camera) {
44+
case "auto":
45+
return { deviceId: { exact: rearCamera.deviceId } };
46+
case "rear":
47+
return { deviceId: { exact: rearCamera.deviceId } };
48+
case "front":
49+
return { deviceId: { exact: frontCamera.deviceId } };
50+
default:
51+
return undefined;
52+
}
53+
} else {
54+
switch (camera) {
55+
case "auto":
56+
return { facingMode: { ideal: "environment" } };
57+
case "rear":
58+
return { facingMode: { exact: "environment" } };
59+
case "front":
60+
return { facingMode: { exact: "user" } };
61+
default:
62+
return undefined;
63+
}
64+
}
65+
};
66+
2767
const INSECURE_CONTEXT = window.isSecureContext !== true;
2868

2969
const STREAM_API_NOT_SUPPORTED = !(
@@ -34,13 +74,13 @@ const STREAM_API_NOT_SUPPORTED = !(
3474

3575
let streamApiShimApplied = false;
3676

37-
export default async function(constraints, videoEl, advancedConstraints) {
77+
export default async function(videoEl, { camera, torch }) {
3878
// At least in Chrome `navigator.mediaDevices` is undefined when the page is
3979
// loaded using HTTP rather than HTTPS. Thus `STREAM_API_NOT_SUPPORTED` is
4080
// initialized with `false` although the API might actually be supported.
41-
// So although `getUserMedia` already should have a build-in mechanism to
81+
// So although `getUserMedia` already should have a built-in mechanism to
4282
// detect insecure context (by throwing `NotAllowedError`), we have to do a
43-
// manual check before even calling `getUserMedia`.
83+
// manual check before even calling `getUserMedia`.
4484
if (INSECURE_CONTEXT) {
4585
throw new InsecureContextError();
4686
}
@@ -49,11 +89,22 @@ export default async function(constraints, videoEl, advancedConstraints) {
4989
throw new StreamApiNotSupportedError();
5090
}
5191

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

99+
const constraints = {
100+
audio: false,
101+
video: {
102+
width: { min: 360, ideal: 640, max: 1920 },
103+
height: { min: 240, ideal: 480, max: 1080 },
104+
...(await narrowDownFacingMode(camera))
105+
}
106+
};
107+
57108
const stream = await navigator.mediaDevices.getUserMedia(constraints);
58109

59110
if (videoEl.srcObject !== undefined) {
@@ -70,7 +121,7 @@ export default async function(constraints, videoEl, advancedConstraints) {
70121

71122
await eventOn(videoEl, "loadeddata");
72123

73-
if (advancedConstraints.torch) {
124+
if (torch) {
74125
const [track] = stream.getVideoTracks();
75126

76127
try {

0 commit comments

Comments
 (0)