Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use qr-scanner #6

Merged
merged 2 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/components/Generate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ const props = withDefaults(defineProps<{
data: Uint8Array
filename?: string
contentType?: string
speed: number
maxScansPerSecond: number
}>(), {
speed: 250,
maxScansPerSecond: 20,
})

const count = ref(0)
Expand All @@ -34,7 +34,7 @@ onMounted(() => {
const now = performance.now()
renderTime.value = now - frame
frame = now
}, () => props.speed)
}, () => 1000 / props.maxScansPerSecond)
})
</script>

Expand Down
127 changes: 65 additions & 62 deletions app/components/Scan.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@
import { binaryToBlock, createDecoder } from '~~/utils/lt-code'
import { readFileHeaderMetaFromBuffer } from '~~/utils/lt-code/binary-meta'
import { toUint8Array } from 'js-base64'
import { scan } from 'qr-scanner-wechat'
import QrScanner from 'qr-scanner'
import { useBytesRate } from '~/composables/timeseries'

const props = withDefaults(defineProps<{
speed?: number
width?: number
height?: number
maxScansPerSecond?: number
}>(), {
speed: 34,
width: 1080,
height: 1080,
maxScansPerSecond: 30,
})

enum CameraSignalStatus {
Expand Down Expand Up @@ -65,8 +61,7 @@ watch(cameras, () => {

// const results = defineModel<Set<string>>('results', { default: new Set() })

let stream: MediaStream | undefined

let qrScanner: QrScanner | undefined
let timestamp = 0
const fps = ref(0)
function setFps() {
Expand All @@ -78,52 +73,75 @@ function setFps() {
const error = ref<any>()
const shutterCount = ref(0)
const video = shallowRef<HTMLVideoElement>()
const videoWidth = ref(0)
const videoHeight = ref(0)

onMounted(async () => {
watch([() => props.width, () => props.height, selectedCamera], () => {
disconnectCamera()
connectCamera()
}, { immediate: true })

useIntervalFn(
async () => {
watch(() => props.maxScansPerSecond, async (maxScansPerSecond) => {
if (qrScanner) {
qrScanner.destroy()
await new Promise(resolve => setTimeout(resolve, 1000))
}
qrScanner = new QrScanner(video.value!, async (result) => {
try {
await scanFrame()
await scanFrame(result)
}
catch (e) {
error.value = e
console.error(e)
}
},
() => props.speed,
)
}, {
maxScansPerSecond,
highlightCodeOutline: false,
highlightScanRegion: true,
calculateScanRegion: ({ videoHeight, videoWidth }) => {
const size = Math.min(videoWidth, videoHeight)
return {
x: size === videoWidth ? 0 : (videoWidth - size) / 2,
y: size === videoHeight ? 0 : (videoHeight - size) / 2,
width: size,
height: size,
}
},
preferredCamera: selectedCamera.value,
onDecodeError(e) {
if (e.toString() !== 'No QR code found')
error.value = e
},
})
selectedCamera.value && qrScanner.setCamera(selectedCamera.value)
qrScanner.setInversionMode('both')
qrScanner.start()
updateCameraStatus()
}, { immediate: true })
watch(selectedCamera, () => {
if (qrScanner && selectedCamera.value) {
qrScanner.setCamera(selectedCamera.value)
qrScanner.start()
}
})
useIntervalFn(() => {
updateCameraStatus()
}, 1000)
})
onUnmounted(() => qrScanner && qrScanner.destroy())

function disconnectCamera() {
stream?.getTracks().forEach(track => track.stop())
stream = undefined
}

async function connectCamera() {
async function updateCameraStatus() {
try {
if (!(navigator && 'mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function')) {
cameraSignalStatus.value = CameraSignalStatus.NotSupported
return
}

cameraSignalStatus.value = CameraSignalStatus.Waiting
videoHeight.value = video.value!.videoHeight
videoWidth.value = video.value!.videoWidth

stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
width: props.width,
height: props.height,
deviceId: selectedCamera.value,
},
})
if (videoWidth.value === 0 || videoHeight.value === 0) {
cameraSignalStatus.value = CameraSignalStatus.Waiting
return
}

video.value!.srcObject = stream
video.value!.play()
cameraSignalStatus.value = CameraSignalStatus.Ready
}
catch (e) {
console.error(e)
Expand Down Expand Up @@ -209,39 +227,24 @@ function toDataURL(data: Uint8Array | string | any, type: string): string {
}
}

async function scanFrame() {
if (cameraSignalStatus.value === CameraSignalStatus.NotGranted
|| cameraSignalStatus.value === CameraSignalStatus.NotSupported) {
return
}

async function scanFrame(result: QrScanner.ScanResult) {
shutterCount.value += 1
const canvas = document.createElement('canvas')
canvas.width = video.value!.videoWidth
canvas.height = video.value!.videoHeight
if (video.value!.videoWidth === 0 || video.value!.videoHeight === 0) {
cameraSignalStatus.value = CameraSignalStatus.Waiting
return
}

cameraSignalStatus.value = CameraSignalStatus.Ready
const ctx = canvas.getContext('2d')!
ctx.drawImage(video.value!, 0, 0, canvas.width, canvas.height)

const result = await scan(canvas)
if (!result.text)
if (!result.data)
return

setFps()
bytesReceived.value += result.text.length
bytesReceived.value += result.data.length
totalValidBytesReceived.value = decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0)

// Do not process the same QR code twice
if (cached.has(result.text))
if (cached.has(result.data))
return
setFps()

error.value = undefined
const binary = toUint8Array(result.text)
const binary = toUint8Array(result.data)
const data = binaryToBlock(binary)
// Data set changed, reset decoder
if (checksum.value !== data.checksum) {
Expand All @@ -258,7 +261,7 @@ async function scanFrame() {
return
}

cached.add(result.text)
cached.add(result.data)
k.value = data.k

data.indices.map(i => pluse(i))
Expand Down Expand Up @@ -385,11 +388,11 @@ function now() {
/>
</div>

<div relative h-full max-h-150 max-w-150 w-full text="10px md:sm">
<div relative max-w-150 w-full text="10px md:sm">
<video
ref="video"
autoplay muted playsinline :controls="false"
aspect-square h-full w-full rounded-lg
:controls="false"
autoplay muted playsinline h-full w-full rounded-lg
/>

<div absolute left-1 top-1 border="~ gray:50 rounded-md" bg-black:75 px2 py1 text-white font-mono shadow>
Expand Down
15 changes: 8 additions & 7 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ enum ReadPhase {
}

const error = ref<any>()
const speed = ref(100)
const fps = ref(20)
const throttledFps = useDebounce(fps, 500)
const readPhase = ref<ReadPhase>(ReadPhase.Idle)

const filename = ref<string | undefined>()
Expand Down Expand Up @@ -58,21 +59,21 @@ async function onFileChange(file?: File) {
</InputFile>
<div w-full inline-flex flex-row items-center>
<span min-w-30>
<span pr-2 text-neutral-400>Speed</span>
<span font-mono>{{ speed.toFixed(0) }}ms</span>
<span pr-2 text-neutral-400>Ideal FPS</span>
<span font-mono>{{ throttledFps.toFixed(0) }}hz</span>
</span>
<InputSlide
v-model="speed"
:min="30"
:max="500"
v-model="throttledFps"
:min="1"
:max="120"
smooth
w-full flex-1
/>
</div>
</div>
<div v-if="readPhase === ReadPhase.Ready && data" h-full w-full flex justify-center>
<Generate
:speed="speed"
:max-scans-per-second="throttledFps"
:data="data"
:filename="filename"
:content-type="contentType"
Expand Down
14 changes: 8 additions & 6 deletions app/pages/scan.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<script lang="ts" setup>
const speed = ref(16)
const fps = ref(30)

const throttledFps = useDebounce(fps, 500)
</script>

<template>
<div px-4 pt-4 space-y-10>
<Scan :speed="speed" />
<Scan :max-scans-per-second="throttledFps" />
<div max-w-150 w-full inline-flex flex-row items-center>
<span min-w-40>
<span pr-2 text-zinc-400>Speed</span>
<span font-mono>{{ speed.toFixed(0) }}ms</span>
<span pr-2 text-zinc-400>Ideal scans</span>
<span font-mono>{{ fps.toFixed(0) }}hz</span>
</span>
<InputSlide
v-model="speed"
v-model="fps"
:min="1"
:max="100"
:max="120"
smooth
w-full flex-1
/>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"dependencies": {
"js-base64": "^3.7.7",
"qr-scanner-wechat": "^0.1.3",
"qr-scanner": "^1.4.2",
"uqr": "^0.1.2"
},
"devDependencies": {
Expand Down
19 changes: 13 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading