Skip to content

Commit

Permalink
feat: merge master into vue3-compatibility (#314)
Browse files Browse the repository at this point in the history
* fix(QrcodeStream): iOS 15 won't render camera

When a camera stream is loaded, we assign the stream to a `video`
element via `video.srcObject`. At this point the element is hidden with
`v-show="false"` aka. `display: none`. We do that because at this point
the videos dimensions are not known yet. We have to wait for the
`loadeddata` event first. Only after that event we display the video
element. Otherwise the elements size awkwardly flickers.

However, it appears since iOS 15 all iOS browsers won't properly render
the video element if the `video.srcObject` was assigned *while* the
element was hidden with `display: none`. Using `visibility: hidden`
instead seems to have fixed the problem though.

Issue: #264 #266

* fix(QrcodeStream): black list cameras by label

Modern devices sometimes have multiple rear cameras. Not all are optimal
for scanning QR codes. For example wide angle, infra-red and virtual
cameras. By maintaining a black list of unsuitable cameras with unique
labels we can gradually exlude more and more of them.

Issue: #269 #253

* fix(QrcodeStream): reject infrared cameras

When automatically selecting a camera we want to avoid infrared cameras
since they are not suitable for scanning QR codes. Some infrared camera
labels have the substring "infrared". So we filter by that.

Issue: #269

* docs: update Upload demo

On supporting mobile devices QrcodeCapture does not open the file dialog
by default but starts the camera instead. This is supposed to be a
feature but developers keep being surprised by it or think it's a bug.
Therefore trying to clarify this in the associated demo.

Issue: #167 #211 #232 #272 #278

* feat(ts): specify types in package

* Remove unused obsolete import

DropImageDecodeError was removed in dea620e

Co-authored-by: Niklas Gruhn <niklas@gruhn.me>
Co-authored-by: RobWalker <rob.walker@auctria.com>
  • Loading branch information
3 people authored Sep 19, 2022
1 parent 495cc3a commit 4b8ed6d
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 33 deletions.
19 changes: 19 additions & 0 deletions .github/ISSUE_TEMPLATE/wrong_camera.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: Wrong camera selected
about:
title: ''
labels: ''
assignees: ''

---

If your device defaults to the wrong camera, please [open this demo](https://gruhn.github.io/vue-qrcode-reader/select-camera-demo.html).
You should see a list of all cameras installed on your device.
Copy the list and mark the camera that was picked by default and the camera that should actually be picked.
For example like this:

```
FaceTime HD Camera (Built-in) [DEFAULT]
A different Camera [PREFERRED]
Another different Camera
```
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ Use kebab-case to reference them in your templates:
# Troubleshooting :zap:
### The wrong camera is picked by default. Can I set it explicitly?
Modern devices sometimes have multiple rear cameras.
Not all are optimal for scanning QR codes.
For example wide angle, infrared and virtual cameras.
With the current web API it's hard to pick the right camera automatically.
It's technically possible to let the user make that decision.
For example by displaying list of installed cameras and letting the user select the right one.
However, this is a user experience trade-off.
Native QR code reader applications don't face this trade-off.
That's why we want to find a different solution.
Please create a GitHub issue from the [wrong camera selected](https://github.com/gruhn/vue-qrcode-reader/issues/new?assignees=&labels=&template=wrong_camera.md&title=) template and follow the instructions in the text.
#### I don't see the camera when using `QrcodeStream`.
- Check if it works on the demo page. Especially the [Decode All](https://gruhn.github.io/vue-qrcode-reader/demos/DecodeAll.html) demo, since it renders error messages. If you see errors, consult the docs to understand their meaning.
Expand All @@ -168,7 +181,3 @@ Use kebab-case to reference them in your templates:
<a href="https://browserstack.com">
<img height="38" src="https://raw.githubusercontent.com/gruhn/vue-qrcode-reader/master/.github/browserstack-logo.png" alt="BrowserStack Logo">
</a>
<span>&emsp;&emsp;</span>
<a href="https://travis-ci.org">
<img height="38" src="https://raw.githubusercontent.com/gruhn/vue-qrcode-reader/master/.github/travis-logo.png" alt="Travis-CI Logo">
</a>
23 changes: 21 additions & 2 deletions docs/.vuepress/components/demos/Upload.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
<template>
<div>
<p>
Capture:
<select v-model="selected">
<option v-for="option in options" :key="option.text" :value="option">
{{ option.text }}
</option>
</select>
</p>

<hr/>

<p class="decode-result">Last result: <b>{{ result }}</b></p>

<qrcode-capture @decode="onDecode" />
<qrcode-capture @decode="onDecode" :capture="selected.value" />
</div>
</template>

Expand All @@ -14,8 +25,16 @@ export default {
components: { QrcodeCapture },
data () {
const options = [
{ text: "rear camera (default)", value: "environment" },
{ text: "front camera", value: "user" },
{ text: "force file dialog", value: false },
]
return {
result: ''
result: '',
options,
selected: options[0]
}
},
Expand Down
76 changes: 76 additions & 0 deletions docs/.vuepress/public/select-camera-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="https://cdn.jsdelivr.net/npm/webrtc-adapter@7.6.1/out/adapter.js"></script>
</head>
<body>
cameras: <br>
<ul></ul>
<br>
<video autoplay muted playsinline></video>

<script>
const listEl = document.querySelector("ul")

const renderOptions = (currentDeviceId, devices) => {
listEl.innerHTML = ""

devices
.filter(deviceInfo => deviceInfo.kind === "videoinput")
.map(({ label, deviceId }) => {
const el = document.createElement('li')

if (deviceId == currentDeviceId)
el.innerHTML = `<a href="#" onclick="selectCamera('${deviceId}')">${label}</a> [PREFERRED]`
else
el.innerHTML = `<a href="#" onclick="selectCamera('${deviceId}')">${label}</a>`

return el
})
.forEach(el => listEl.appendChild(el))
}


let stream

const selectCamera = async deviceId => {
try {
console.log(deviceId)

if (stream) {
stream.getTracks().forEach(track => track.stop())
}

const videoConstraints = {};
if (!deviceId) {
videoConstraints.facingMode = 'environment';
} else {
videoConstraints.deviceId = { exact: deviceId };
}

stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: videoConstraints,
})

const videoEl = document.querySelector('video')
videoEl.srcObject = stream

const [ videoTrack ] = stream.getVideoTracks()

renderOptions(
videoTrack.getSettings().deviceId,
await navigator.mediaDevices.enumerateDevices()
)
} catch (error) {
console.error(error)
}
}

selectCamera()
</script>
</body>
</html>
6 changes: 4 additions & 2 deletions docs/demos/Upload.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Decode by Upload

Finally, with `QrcodeCapture` comes another component which allows image scanning via classic file upload.
Nothing is actually uploaded. Everything is happening client-side.

If you are on mobile and your browser supports it,
your are not prompted with a file dialog but with your camera.
you are not prompted with a file dialog but with your camera.
So you can directly take the picture to be uploaded.
Adjust this behavior with the following dropdown:

Note that nothing is actually uploaded. Everything is happening client-side.

<ClientOnly>
<DemoWrapper component="Upload" />
Expand Down
31 changes: 17 additions & 14 deletions src/components/QrcodeStream.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
-->
<video
ref="video"
v-show="shouldScan"
:class="{ 'qrcode-stream-camera--hidden': !shouldScan }"
class="qrcode-stream-camera"
autoplay
muted
Expand Down Expand Up @@ -182,19 +182,6 @@ export default {
}
},
onLocate(location) {
if (this.trackRepaintFunction === undefined || location === null) {
this.clearCanvas(this.$refs.trackingLayer);
} else {
const video = this.$refs.video;
const canvas = this.$refs.trackingLayer;
if (video !== undefined && canvas !== undefined) {
this.repaintTrackingLayer(video, canvas, location);
}
}
},
onLocate(detectedCodes) {
const canvas = this.$refs.trackingLayer;
const video = this.$refs.video;
Expand Down Expand Up @@ -319,4 +306,20 @@ export default {
display: block;
object-fit: cover;
}
/* When a camera stream is loaded, we assign the stream to the `video`
* element via `video.srcObject`. At this point the element used to be
* hidden with `v-show="false"` aka. `display: none`. We do that because
* at this point the videos dimensions are not known yet. We have to
* wait for the `loadeddata` event first. Only after that event we
* display the video element. Otherwise the elements size awkwardly flickers.
*
* However, it appears in iOS 15 all iOS browsers won't properly render
* the video element if the `video.srcObject` was assigned *while* the
* element was hidden with `display: none`. Using `visibility: hidden`
* instead seems to have fixed the problem though.
*/
.qrcode-stream-camera--hidden {
visibility: hidden;
position: absolute;
}
</style>
24 changes: 14 additions & 10 deletions src/misc/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,23 @@ class 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.
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"
);
// Filter some devices, known to be bad choices.
const deviceBlackList = ["OBS Virtual Camera", "OBS-Camera"];

const devices = (await navigator.mediaDevices.enumerateDevices())
.filter(({ kind }) => kind === "videoinput")
.filter(({ label }) => !deviceBlackList.includes(label))
.filter(({ label }) => !label.includes("infrared"));

if (devices.length > 2) {
// 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 good heuristic on some devices.
const frontCamera = devices[0];
const rearCamera = devices[devices.length - 1];

Expand Down
2 changes: 1 addition & 1 deletion src/misc/scanner.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DropImageFetchError, DropImageDecodeError } from "./errors.js";
import { DropImageFetchError } from "./errors.js";
import { eventOn } from "callforth";

const adaptOldFormat = detectedCodes => {
Expand Down

0 comments on commit 4b8ed6d

Please sign in to comment.