vision-camera-simple-scanner
Version:
High performance barcode scanner for React Native using VisionCamera, forked from vision-camera-barcode-scanner
137 lines (123 loc) • 4.29 kB
text/typescript
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<CameraProps['resizeMode']>;
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 <Camera /> component
const layoutRef = useSharedValue<Size>({ width: 0, height: 0 });
const onLayout: ViewProps['onLayout'] = (event) => {
const { width, height } = event.nativeEvent.layout;
layoutRef.value = { width, height };
};
const resizeModeRef = useLatestSharedValue<ResizeMode>(resizeMode);
// Barcode highlights related state
const barcodesRef = useSharedValue<Barcode[]>([]);
// Barcode highlights related state
const [highlights, setHighlights] = useState<Highlight[]>([]);
const lastHighlightsCount = useSharedValue<number>(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,
};
};