Skip to content

Commit

Permalink
fix(FullDemo): hidden device ID dropdown
Browse files Browse the repository at this point in the history
In some browsers the the dropdown to select a camera on the "full demo"
is invisible:

* In Firefox: when listing available cameras with `enumerateDevices`
  the `label` field seems to be an empty string. In that case the dropdown
  does not render because all selectable options are empty strings.
  Fixing that by also including the device ID.

* On iOS: we can't invoke `enumerateDevices` before the user has given
  camera access permission. Otherwise we always get an empty array (no error).
  Fixing that by waiting for `QrcodeStream` to request permissions.

Also added logging statements for all kinds of state transitions.
  • Loading branch information
gruhn committed May 21, 2024
1 parent d6b0727 commit f762513
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 30 deletions.
61 changes: 37 additions & 24 deletions docs/.vitepress/components/demos/FullDemo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@
wide-angle, infrared, desk-view). The one picked by default is sometimes not the best choice.
If you want fine-grained control, which camera is used, you can enumerate all installed
cameras and then pick the one you need based on it's device ID:
</p>

<select v-model="selectedDevice">
<option
v-for="device in devices"
:key="device.label"
:value="device"
>
{{ device.label }}
</option>
</select>
<p
class="error"
v-if="availableDevices === null"
>
No cameras on this device
</p>

<select
v-model="selectedDevice"
v-else
>
<option
v-for="device in availableDevices"
:key="device.deviceId"
:value="device"
>
{{ device.label }} (ID: {{ device.deviceId }})
</option>
</select>

<p>
Detected codes are visually highlighted in real-time. Use the following dropdown to change the
flavor:
Expand Down Expand Up @@ -59,25 +69,19 @@

<div>
<qrcode-stream
:constraints="{ deviceId: selectedDevice.deviceId }"
:constraints="constraints"
:track="trackFunctionSelected.value"
:formats="selectedBarcodeFormats"
@error="onError"
@detect="onDetect"
v-if="selectedDevice !== null"
@camera-on="onCameraReady"
/>
<p
v-else
class="error"
>
No cameras on this device
</p>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
import { QrcodeStream } from '../../../../src'
/*** detection handling ***/
Expand All @@ -92,15 +96,23 @@ function onDetect(detectedCodes) {
/*** select camera ***/
const selectedDevice = ref(null)
const devices = ref([])
onMounted(async () => {
devices.value = (await navigator.mediaDevices.enumerateDevices()).filter(
const availableDevices = ref(null)
async function onCameraReady() {
// NOTE: on iOS we can't invoke `enumerateDevices` before the user has given
// camera access permission. `QrcodeStream` internally takes care of
// requesting the permissions. The `camera-on` event should guarantee that this
// has happened.
availableDevices.value = (await navigator.mediaDevices.enumerateDevices()).filter(
({ kind }) => kind === 'videoinput'
)
}
if (devices.value.length > 0) {
selectedDevice.value = devices.value[0]
const constraints = computed(() => {
if (selectedDevice.value === null) {
return { facingMode: 'environment' }
} else {
return { deviceId: selectedDevice.value.deviceId }
}
})
Expand Down Expand Up @@ -226,5 +238,6 @@ function onError(err) {
.barcode-format-checkbox {
margin-right: 10px;
white-space: nowrap;
display: inline-block;
}
</style>
9 changes: 4 additions & 5 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { defineConfig } from 'vitepress'
import { withPwa } from '@vite-pwa/vitepress'

const { VITEPRESS_BASE } = process.env

if (VITEPRESS_BASE === undefined) {
throw new Error('env var VITEPRESS_BASE is undefined')
if (process.env.VITEPRESS_BASE === undefined) {
console.warn('env var VITEPRESS_BASE is undefined. Defaulting to: /vue-qrcode-reader/')
}
const { VITEPRESS_BASE } = process.env ?? '/vue-qrcode-reader/'

export default withPwa(
defineConfig({
Expand Down Expand Up @@ -69,7 +68,7 @@ export default withPwa(
{
text: 'Decode by Upload',
link: '/demos/Upload'
},
}
],
'/api/': [
{
Expand Down
11 changes: 11 additions & 0 deletions src/misc/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ async function runStartTask(
constraints: MediaTrackConstraints,
torch: boolean
): Promise<StartTaskResult> {
console.debug(
'[vue-qrcode-reader] starting camera with constraints: ',
JSON.stringify(constraints)
)

// 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.
Expand All @@ -47,6 +52,7 @@ async function runStartTask(
// is not available during SSR. So we lazily apply this shim at runtime.
shimGetUserMedia()

console.debug('[vue-qrcode-reader] calling getUserMedia')
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: constraints
Expand All @@ -69,6 +75,7 @@ async function runStartTask(
// unless video is explictly triggered by play()
videoEl.play()

console.debug('[vue-qrcode-reader] waiting for video element to load')
await Promise.race([
eventOn(videoEl, 'loadeddata'),

Expand All @@ -82,6 +89,7 @@ async function runStartTask(
throw new StreamLoadTimeoutError()
})
])
console.debug('[vue-qrcode-reader] video element loaded')

// According to: https://oberhofer.co/mediastreamtrack-and-its-capabilities/#queryingcapabilities
// On some devices, getCapabilities only returns a non-empty object after
Expand All @@ -98,6 +106,7 @@ async function runStartTask(
isTorchOn = true
}

console.debug('[vue-qrcode-reader] camera ready')
return {
type: 'start',
data: {
Expand Down Expand Up @@ -174,6 +183,8 @@ async function runStopTask(
stream: MediaStream,
isTorchOn: boolean
): Promise<StopTaskResult> {
console.debug('[vue-qrcode-reader] stopping camera')

videoEl.src = ''
videoEl.srcObject = null
videoEl.load()
Expand Down
5 changes: 4 additions & 1 deletion src/misc/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ export const keepScanning = async (
formats: BarcodeFormat[]
}
) => {
console.debug('[vue-qrcode-reader] start scanning')
barcodeDetector = new BarcodeDetector({ formats })

const processFrame =
(state: { lastScanned: number; contentBefore: string[]; lastScanHadContent: boolean }) =>
async (timeNow: number) => {
if (videoElement.readyState > 1) {
if (videoElement.readyState === 0) {
console.debug('[vue-qrcode-reader] stop scanning: video element readyState is 0')
} else {
const { lastScanned, contentBefore, lastScanHadContent } = state

// Scanning is expensive and we don't need to scan camera frames with
Expand Down

0 comments on commit f762513

Please sign in to comment.