123 lines
3 KiB
TypeScript
123 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 }
|
||
}
|