sozsoft-platform/ui/src/components/shared/BarcodeScanner/useBarcodeReader.ts

124 lines
3 KiB
TypeScript
Raw Normal View History

2026-09-21 15:01:40 +00:00
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 }
}