Skip to content

Commit 5e0108c

Browse files
authored
refactor: use qr-scanner (#6)
1 parent 9f16138 commit 5e0108c

File tree

6 files changed

+98
-85
lines changed

6 files changed

+98
-85
lines changed

app/components/Generate.vue

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ const props = withDefaults(defineProps<{
88
data: Uint8Array
99
filename?: string
1010
contentType?: string
11-
speed: number
11+
maxScansPerSecond: number
1212
}>(), {
13-
speed: 250,
13+
maxScansPerSecond: 20,
1414
})
1515
1616
const count = ref(0)
@@ -34,7 +34,7 @@ onMounted(() => {
3434
const now = performance.now()
3535
renderTime.value = now - frame
3636
frame = now
37-
}, () => props.speed)
37+
}, () => 1000 / props.maxScansPerSecond)
3838
})
3939
</script>
4040

app/components/Scan.vue

+65-62
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22
import { binaryToBlock, createDecoder } from '~~/utils/lt-code'
33
import { readFileHeaderMetaFromBuffer } from '~~/utils/lt-code/binary-meta'
44
import { toUint8Array } from 'js-base64'
5-
import { scan } from 'qr-scanner-wechat'
5+
import QrScanner from 'qr-scanner'
66
import { useBytesRate } from '~/composables/timeseries'
77
88
const props = withDefaults(defineProps<{
9-
speed?: number
10-
width?: number
11-
height?: number
9+
maxScansPerSecond?: number
1210
}>(), {
13-
speed: 34,
14-
width: 1080,
15-
height: 1080,
11+
maxScansPerSecond: 30,
1612
})
1713
1814
enum CameraSignalStatus {
@@ -65,8 +61,7 @@ watch(cameras, () => {
6561
6662
// const results = defineModel<Set<string>>('results', { default: new Set() })
6763
68-
let stream: MediaStream | undefined
69-
64+
let qrScanner: QrScanner | undefined
7065
let timestamp = 0
7166
const fps = ref(0)
7267
function setFps() {
@@ -78,52 +73,75 @@ function setFps() {
7873
const error = ref<any>()
7974
const shutterCount = ref(0)
8075
const video = shallowRef<HTMLVideoElement>()
76+
const videoWidth = ref(0)
77+
const videoHeight = ref(0)
8178
8279
onMounted(async () => {
83-
watch([() => props.width, () => props.height, selectedCamera], () => {
84-
disconnectCamera()
85-
connectCamera()
86-
}, { immediate: true })
87-
88-
useIntervalFn(
89-
async () => {
80+
watch(() => props.maxScansPerSecond, async (maxScansPerSecond) => {
81+
if (qrScanner) {
82+
qrScanner.destroy()
83+
await new Promise(resolve => setTimeout(resolve, 1000))
84+
}
85+
qrScanner = new QrScanner(video.value!, async (result) => {
9086
try {
91-
await scanFrame()
87+
await scanFrame(result)
9288
}
9389
catch (e) {
9490
error.value = e
9591
console.error(e)
9692
}
97-
},
98-
() => props.speed,
99-
)
93+
}, {
94+
maxScansPerSecond,
95+
highlightCodeOutline: false,
96+
highlightScanRegion: true,
97+
calculateScanRegion: ({ videoHeight, videoWidth }) => {
98+
const size = Math.min(videoWidth, videoHeight)
99+
return {
100+
x: size === videoWidth ? 0 : (videoWidth - size) / 2,
101+
y: size === videoHeight ? 0 : (videoHeight - size) / 2,
102+
width: size,
103+
height: size,
104+
}
105+
},
106+
preferredCamera: selectedCamera.value,
107+
onDecodeError(e) {
108+
if (e.toString() !== 'No QR code found')
109+
error.value = e
110+
},
111+
})
112+
selectedCamera.value && qrScanner.setCamera(selectedCamera.value)
113+
qrScanner.setInversionMode('both')
114+
qrScanner.start()
115+
updateCameraStatus()
116+
}, { immediate: true })
117+
watch(selectedCamera, () => {
118+
if (qrScanner && selectedCamera.value) {
119+
qrScanner.setCamera(selectedCamera.value)
120+
qrScanner.start()
121+
}
122+
})
123+
useIntervalFn(() => {
124+
updateCameraStatus()
125+
}, 1000)
100126
})
127+
onUnmounted(() => qrScanner && qrScanner.destroy())
101128
102-
function disconnectCamera() {
103-
stream?.getTracks().forEach(track => track.stop())
104-
stream = undefined
105-
}
106-
107-
async function connectCamera() {
129+
async function updateCameraStatus() {
108130
try {
109131
if (!(navigator && 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function')) {
110132
cameraSignalStatus.value = CameraSignalStatus.NotSupported
111133
return
112134
}
113135
114-
cameraSignalStatus.value = CameraSignalStatus.Waiting
136+
videoHeight.value = video.value!.videoHeight
137+
videoWidth.value = video.value!.videoWidth
115138
116-
stream = await navigator.mediaDevices.getUserMedia({
117-
audio: false,
118-
video: {
119-
width: props.width,
120-
height: props.height,
121-
deviceId: selectedCamera.value,
122-
},
123-
})
139+
if (videoWidth.value === 0 || videoHeight.value === 0) {
140+
cameraSignalStatus.value = CameraSignalStatus.Waiting
141+
return
142+
}
124143
125-
video.value!.srcObject = stream
126-
video.value!.play()
144+
cameraSignalStatus.value = CameraSignalStatus.Ready
127145
}
128146
catch (e) {
129147
console.error(e)
@@ -209,39 +227,24 @@ function toDataURL(data: Uint8Array | string | any, type: string): string {
209227
}
210228
}
211229
212-
async function scanFrame() {
213-
if (cameraSignalStatus.value === CameraSignalStatus.NotGranted
214-
|| cameraSignalStatus.value === CameraSignalStatus.NotSupported) {
215-
return
216-
}
217-
230+
async function scanFrame(result: QrScanner.ScanResult) {
218231
shutterCount.value += 1
219-
const canvas = document.createElement('canvas')
220-
canvas.width = video.value!.videoWidth
221-
canvas.height = video.value!.videoHeight
222-
if (video.value!.videoWidth === 0 || video.value!.videoHeight === 0) {
223-
cameraSignalStatus.value = CameraSignalStatus.Waiting
224-
return
225-
}
226232
227233
cameraSignalStatus.value = CameraSignalStatus.Ready
228-
const ctx = canvas.getContext('2d')!
229-
ctx.drawImage(video.value!, 0, 0, canvas.width, canvas.height)
230234
231-
const result = await scan(canvas)
232-
if (!result.text)
235+
if (!result.data)
233236
return
234237
235-
setFps()
236-
bytesReceived.value += result.text.length
238+
bytesReceived.value += result.data.length
237239
totalValidBytesReceived.value = decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0)
238240
239241
// Do not process the same QR code twice
240-
if (cached.has(result.text))
242+
if (cached.has(result.data))
241243
return
244+
setFps()
242245
243246
error.value = undefined
244-
const binary = toUint8Array(result.text)
247+
const binary = toUint8Array(result.data)
245248
const data = binaryToBlock(binary)
246249
// Data set changed, reset decoder
247250
if (checksum.value !== data.checksum) {
@@ -258,7 +261,7 @@ async function scanFrame() {
258261
return
259262
}
260263
261-
cached.add(result.text)
264+
cached.add(result.data)
262265
k.value = data.k
263266
264267
data.indices.map(i => pluse(i))
@@ -385,11 +388,11 @@ function now() {
385388
/>
386389
</div>
387390

388-
<div relative h-full max-h-150 max-w-150 w-full text="10px md:sm">
391+
<div relative max-w-150 w-full text="10px md:sm">
389392
<video
390393
ref="video"
391-
autoplay muted playsinline :controls="false"
392-
aspect-square h-full w-full rounded-lg
394+
:controls="false"
395+
autoplay muted playsinline h-full w-full rounded-lg
393396
/>
394397

395398
<div absolute left-1 top-1 border="~ gray:50 rounded-md" bg-black:75 px2 py1 text-white font-mono shadow>

app/pages/index.vue

+8-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ enum ReadPhase {
99
}
1010
1111
const error = ref<any>()
12-
const speed = ref(100)
12+
const fps = ref(20)
13+
const throttledFps = useDebounce(fps, 500)
1314
const readPhase = ref<ReadPhase>(ReadPhase.Idle)
1415
1516
const filename = ref<string | undefined>()
@@ -58,21 +59,21 @@ async function onFileChange(file?: File) {
5859
</InputFile>
5960
<div w-full inline-flex flex-row items-center>
6061
<span min-w-30>
61-
<span pr-2 text-neutral-400>Speed</span>
62-
<span font-mono>{{ speed.toFixed(0) }}ms</span>
62+
<span pr-2 text-neutral-400>Ideal FPS</span>
63+
<span font-mono>{{ throttledFps.toFixed(0) }}hz</span>
6364
</span>
6465
<InputSlide
65-
v-model="speed"
66-
:min="30"
67-
:max="500"
66+
v-model="throttledFps"
67+
:min="1"
68+
:max="120"
6869
smooth
6970
w-full flex-1
7071
/>
7172
</div>
7273
</div>
7374
<div v-if="readPhase === ReadPhase.Ready && data" h-full w-full flex justify-center>
7475
<Generate
75-
:speed="speed"
76+
:max-scans-per-second="throttledFps"
7677
:data="data"
7778
:filename="filename"
7879
:content-type="contentType"

app/pages/scan.vue

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
<script lang="ts" setup>
2-
const speed = ref(16)
2+
const fps = ref(30)
3+
4+
const throttledFps = useDebounce(fps, 500)
35
</script>
46

57
<template>
68
<div px-4 pt-4 space-y-10>
7-
<Scan :speed="speed" />
9+
<Scan :max-scans-per-second="throttledFps" />
810
<div max-w-150 w-full inline-flex flex-row items-center>
911
<span min-w-40>
10-
<span pr-2 text-zinc-400>Speed</span>
11-
<span font-mono>{{ speed.toFixed(0) }}ms</span>
12+
<span pr-2 text-zinc-400>Ideal scans</span>
13+
<span font-mono>{{ fps.toFixed(0) }}hz</span>
1214
</span>
1315
<InputSlide
14-
v-model="speed"
16+
v-model="fps"
1517
:min="1"
16-
:max="100"
18+
:max="120"
1719
smooth
1820
w-full flex-1
1921
/>

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
"dependencies": {
1818
"js-base64": "^3.7.7",
19-
"qr-scanner-wechat": "^0.1.3",
19+
"qr-scanner": "^1.4.2",
2020
"uqr": "^0.1.2"
2121
},
2222
"devDependencies": {

pnpm-lock.yaml

+13-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)