124 lines
3 KiB
TypeScript
124 lines
3 KiB
TypeScript
|
|
import { useEffect, useRef, useState } from 'react'
|
|||
|
|
import {
|
|||
|
|
createBarcodeDecoder,
|
|||
|
|
openCameraStream,
|
|||
|
|
stopCameraStream,
|
|||
|
|
vibrateOnScan,
|
|||
|
|
} from '@/utils/barcode'
|
|||
|
|
import { isCameraAvailable, isCameraBlockedByStandaloneIos } from '@/utils/device'
|
|||
|
|
|
|||
|
|
export type BarcodeReaderStatus =
|
|||
|
|
| 'starting'
|
|||
|
|
| 'scanning'
|
|||
|
|
| 'denied'
|
|||
|
|
| 'insecure'
|
|||
|
|
| 'standaloneIos'
|
|||
|
|
| 'error'
|
|||
|
|
|
|||
|
|
/** Kare çözme aralığı. Her karede çözmek mobilde pili hızla tüketir. */
|
|||
|
|
const DECODE_INTERVAL_MS = 180
|
|||
|
|
|
|||
|
|
/** Aynı kodun arka arkaya birden çok kez okunmasını engelleyen pencere. */
|
|||
|
|
const DUPLICATE_WINDOW_MS = 1500
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Kamerayı açar, kareleri çözer ve okunan kodu bildirir.
|
|||
|
|
*
|
|||
|
|
* `onDetect` her okumada yeniden bağlanabildiği için ref üzerinden tutulur;
|
|||
|
|
* aksi hâlde her render kamerayı kapatıp yeniden açardı.
|
|||
|
|
*/
|
|||
|
|
export const useBarcodeReader = ({
|
|||
|
|
active,
|
|||
|
|
onDetect,
|
|||
|
|
}: {
|
|||
|
|
active: boolean
|
|||
|
|
onDetect: (code: string) => void
|
|||
|
|
}) => {
|
|||
|
|
const videoRef = useRef<HTMLVideoElement>(null)
|
|||
|
|
const onDetectRef = useRef(onDetect)
|
|||
|
|
const [status, setStatus] = useState<BarcodeReaderStatus>('starting')
|
|||
|
|
|
|||
|
|
onDetectRef.current = onDetect
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!active) return
|
|||
|
|
|
|||
|
|
if (isCameraBlockedByStandaloneIos()) {
|
|||
|
|
setStatus('standaloneIos')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isCameraAvailable()) {
|
|||
|
|
setStatus('insecure')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const video = videoRef.current
|
|||
|
|
let stream: MediaStream | undefined
|
|||
|
|
let timer: number | undefined
|
|||
|
|
let cancelled = false
|
|||
|
|
let lastCode = ''
|
|||
|
|
let lastCodeAt = 0
|
|||
|
|
|
|||
|
|
const start = async () => {
|
|||
|
|
setStatus('starting')
|
|||
|
|
try {
|
|||
|
|
const [cameraStream, decode] = await Promise.all([
|
|||
|
|
openCameraStream(),
|
|||
|
|
createBarcodeDecoder(),
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
if (cancelled) {
|
|||
|
|
stopCameraStream(cameraStream)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!video) return
|
|||
|
|
stream = cameraStream
|
|||
|
|
|
|||
|
|
video.srcObject = cameraStream
|
|||
|
|
await video.play()
|
|||
|
|
setStatus('scanning')
|
|||
|
|
|
|||
|
|
const tick = async () => {
|
|||
|
|
if (cancelled) return
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const code = await decode(video)
|
|||
|
|
const now = Date.now()
|
|||
|
|
if (code && (code !== lastCode || now - lastCodeAt > DUPLICATE_WINDOW_MS)) {
|
|||
|
|
lastCode = code
|
|||
|
|
lastCodeAt = now
|
|||
|
|
vibrateOnScan()
|
|||
|
|
onDetectRef.current(code)
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
// Tek bir karenin çözülememesi tarama döngüsünü durdurmaz.
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!cancelled) {
|
|||
|
|
timer = window.setTimeout(tick, DECODE_INTERVAL_MS)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tick()
|
|||
|
|
} catch (error) {
|
|||
|
|
if (cancelled) return
|
|||
|
|
const name = (error as DOMException)?.name
|
|||
|
|
setStatus(name === 'NotAllowedError' || name === 'SecurityError' ? 'denied' : 'error')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
start()
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
cancelled = true
|
|||
|
|
if (timer) window.clearTimeout(timer)
|
|||
|
|
stopCameraStream(stream)
|
|||
|
|
if (video) video.srcObject = null
|
|||
|
|
}
|
|||
|
|
}, [active])
|
|||
|
|
|
|||
|
|
return { videoRef, status }
|
|||
|
|
}
|