sozsoft-platform/ui/src/components/shared/BarcodeScanner/useBarcodeReader.ts
2026-09-21 18:01:40 +03:00

123 lines
3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }
}