import { useState } from 'react'; import { type ViewProps } from 'react-native'; import { runAtTargetFps, useFrameProcessor, type CameraProps, type Frame, } from 'react-native-vision-camera'; import { Worklets, useSharedValue } from 'react-native-worklets-core'; import { ScanBarcodesOptions, scanCodes } from 'src/module'; import type { Barcode, BarcodeType, Highlight, PointMapperFn, Rect, Size, } from 'src/types'; import { computeHighlights } from '..'; import { useLatestSharedValue } from './useLatestSharedValue'; type ResizeMode = NonNullable; export type UseBarcodeScannerOptions = { barcodeTypes?: BarcodeType[]; regionOfInterest?: Rect; fps?: number; onBarcodeScanned?: (barcodes: Barcode[], frame: Frame) => void; pointMapper?: PointMapperFn; disableHighlighting?: boolean; resizeMode?: ResizeMode; scanMode?: 'continuous' | 'once'; isMountedRef?: { value: boolean }; }; export const useBarcodeScanner = ({ barcodeTypes, regionOfInterest, onBarcodeScanned, pointMapper, disableHighlighting, resizeMode = 'cover', scanMode = 'continuous', isMountedRef, fps = 30, }: UseBarcodeScannerOptions) => { // Layout of the component const layoutRef = useSharedValue({ width: 0, height: 0 }); const onLayout: ViewProps['onLayout'] = (event) => { const { width, height } = event.nativeEvent.layout; layoutRef.value = { width, height }; }; const resizeModeRef = useLatestSharedValue(resizeMode); // Barcode highlights related state const barcodesRef = useSharedValue([]); // Barcode highlights related state const [highlights, setHighlights] = useState([]); const lastHighlightsCount = useSharedValue(0); const setHighlightsJS = Worklets.createRunOnJS(setHighlights); const pixelFormat: CameraProps['pixelFormat'] = 'yuv'; const frameProcessor = useFrameProcessor( (frame) => { 'worklet'; if (isMountedRef && isMountedRef.value === false) { return; } runAtTargetFps(fps, () => { 'worklet'; const { value: layout } = layoutRef; const { value: prevBarcodes } = barcodesRef; const { value: resizeMode } = resizeModeRef; const { width, height, orientation } = frame; // Call the native barcode scanner const options: ScanBarcodesOptions = {}; if (barcodeTypes !== undefined) { options.barcodeTypes = barcodeTypes; } if (regionOfInterest !== undefined) { const { x, y, width, height } = regionOfInterest; options.regionOfInterest = [x, y, width, height]; } const barcodes = scanCodes(frame, options); if (barcodes.length > 0) { // If the scanMode is "continuous", we stream all the barcodes responses if (scanMode === 'continuous') { onBarcodeScanned && onBarcodeScanned(barcodes, frame); // If the scanMode is "once", we only call the callback if the barcodes have actually changed } else if (scanMode === 'once') { const hasChanged = prevBarcodes.length !== barcodes.length || JSON.stringify(prevBarcodes.map(({ value }) => value)) !== JSON.stringify(barcodes.map(({ value }) => value)); if (hasChanged) { onBarcodeScanned && onBarcodeScanned(barcodes, frame); } } barcodesRef.value = barcodes; } if (disableHighlighting !== true && resizeMode !== undefined) { const highlights = computeHighlights( barcodes, { width, height, orientation }, // "serialized" frame layout, resizeMode, pointMapper, ); // Spare a re-render if the highlights are both empty if (lastHighlightsCount.value === 0 && highlights.length === 0) { return; } lastHighlightsCount.value = highlights.length; setHighlightsJS(highlights); } }); }, [layoutRef, resizeModeRef, disableHighlighting], ); return { props: { pixelFormat, frameProcessor, onLayout, }, highlights, }; };