Modern mobile phones often have a variety of different cameras installed (e.g. front, rear,
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:
-
+ For more fine-grained control, you can select a camera by device constraints or by the device
+ ID:
-
- No cameras on this device
+
-
-
Detected codes are visually highlighted in real-time. Use the following dropdown to change the
flavor:
@@ -69,7 +59,7 @@
kind === 'videoinput'
- )
+ const devices = await navigator.mediaDevices.enumerateDevices()
+ const videoDevices = devices.filter(({ kind }) => kind === 'videoinput')
+
+ constraintOptions.value = [
+ ...defaultConstraintOptions,
+ ...videoDevices.map(({ deviceId, label }) => ({
+ label: `${label} (ID: ${deviceId})`,
+ constraints: { deviceId }
+ }))
+ ]
+
+ error.value = ''
}
-const constraints = computed(() => {
- if (selectedDevice.value === null) {
- return { facingMode: 'environment' }
- } else {
- return { deviceId: selectedDevice.value.deviceId }
- }
-})
-
/*** track functons ***/
function paintOutline(detectedCodes, ctx) {
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 31ad0017..a31fe98d 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -4,7 +4,7 @@ import { withPwa } from '@vite-pwa/vitepress'
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/'
+const { VITEPRESS_BASE = '/vue-qrcode-reader/' } = process.env
export default withPwa(
defineConfig({
diff --git a/src/misc/camera.ts b/src/misc/camera.ts
index 3d3dac7e..8ba1c7c8 100644
--- a/src/misc/camera.ts
+++ b/src/misc/camera.ts
@@ -1,8 +1,9 @@
import { StreamApiNotSupportedError, InsecureContextError, StreamLoadTimeoutError } from './errors'
import { eventOn, timeout } from './callforth'
import shimGetUserMedia from './shimGetUserMedia'
+import { assertNever } from './util'
-interface StartTaskResult {
+type StartTaskResult = {
type: 'start'
data: {
videoEl: HTMLVideoElement
@@ -13,12 +14,17 @@ interface StartTaskResult {
}
}
-interface StopTaskResult {
+type StopTaskResult = {
type: 'stop'
data: {}
}
-type TaskResult = StartTaskResult | StopTaskResult
+type FailedTask = {
+ type: 'failed'
+ error: Error
+}
+
+type TaskResult = StartTaskResult | StopTaskResult | FailedTask
let taskQueue: Promise = Promise.resolve({ type: 'stop', data: {} })
@@ -132,50 +138,65 @@ export async function start(
}
): Promise> {
// update the task queue synchronously
- taskQueue = taskQueue.then((prevTaskResult) => {
- if (prevTaskResult.type === 'start') {
- // previous task is a start task
- // we'll check if we can reuse the previous result
- const {
- data: {
- videoEl: prevVideoEl,
- stream: prevStream,
- constraints: prevConstraints,
- isTorchOn: prevIsTorchOn
+ taskQueue = taskQueue
+ .then((prevTaskResult) => {
+ if (prevTaskResult.type === 'start') {
+ // previous task is a start task
+ // we'll check if we can reuse the previous result
+ const {
+ data: {
+ videoEl: prevVideoEl,
+ stream: prevStream,
+ constraints: prevConstraints,
+ isTorchOn: prevIsTorchOn
+ }
+ } = prevTaskResult
+ // TODO: Should we keep this object comparison
+ // this code only checks object sameness not equality
+ // deep comparison requires snapshots and value by value check
+ // which seem too much
+ if (
+ !restart &&
+ videoEl === prevVideoEl &&
+ constraints === prevConstraints &&
+ torch === prevIsTorchOn
+ ) {
+ // things didn't change, reuse the previous result
+ return prevTaskResult
}
- } = prevTaskResult
- // TODO: Should we keep this object comparison
- // this code only checks object sameness not equality
- // deep comparison requires snapshots and value by value check
- // which seem too much
- if (
- !restart &&
- videoEl === prevVideoEl &&
- constraints === prevConstraints &&
- torch === prevIsTorchOn
- ) {
- // things didn't change, reuse the previous result
- return prevTaskResult
+ // something changed, restart (stop then start)
+ return runStopTask(prevVideoEl, prevStream, prevIsTorchOn).then(() =>
+ runStartTask(videoEl, constraints, torch)
+ )
+ } else if (prevTaskResult.type === 'stop' || prevTaskResult.type === 'failed') {
+ // previous task is a stop/error task
+ // we can safely start
+ return runStartTask(videoEl, constraints, torch)
}
- // something changed, restart (stop then start)
- return runStopTask(prevVideoEl, prevStream, prevIsTorchOn).then(() =>
- runStartTask(videoEl, constraints, torch)
- )
- }
- // previous task is a stop task
- // we can safely start
- return runStartTask(videoEl, constraints, torch)
- })
+
+ assertNever(prevTaskResult)
+ })
+ .catch((error: Error) => {
+ console.debug(`[vue-qrcode-reader] starting camera failed with "${error}"`)
+ return { type: 'failed', error }
+ })
+
// await the task queue asynchronously
const taskResult = await taskQueue
+
if (taskResult.type === 'stop') {
// we just synchronously updated the task above
// to make the latest task a start task
// so this case shouldn't happen
throw new Error('Something went wrong with the camera task queue (start task).')
+ } else if (taskResult.type === 'failed') {
+ throw taskResult.error
+ } else if (taskResult.type === 'start') {
+ // return the data we want
+ return taskResult.data.capabilities
}
- // return the data we want
- return taskResult.data.capabilities
+
+ assertNever(taskResult)
}
async function runStopTask(
@@ -208,7 +229,7 @@ async function runStopTask(
export async function stop() {
// update the task queue synchronously
taskQueue = taskQueue.then((prevTaskResult) => {
- if (prevTaskResult.type === 'stop') {
+ if (prevTaskResult.type === 'stop' || prevTaskResult.type === 'failed') {
// previous task is a stop task
// no need to stop again
return prevTaskResult
diff --git a/src/misc/util.ts b/src/misc/util.ts
index 890dbed3..d2baf68f 100644
--- a/src/misc/util.ts
+++ b/src/misc/util.ts
@@ -45,3 +45,7 @@ export function assert(condition: boolean, failureMessage?: string): asserts con
throw new Error(failureMessage ?? 'assertion failure')
}
}
+
+export function assertNever(_witness: never): never {
+ throw new Error('this code should be unreachable')
+}