High-performance VIN scanner for React Native Vision Camera powered by Google ML Kit barcode + text recognition.
npm install @mleonard9/vin-scannerHigh-performance VIN detection for React Native powered by Google ML Kit barcode + text recognition and react-native-vision-camera.
Compiled from a combination of other community frame processing plugins
- react-native-vision-camera >= 3.9.0
- react-native-worklets-core >= 0.4.0
- iOS 13+ / Android 21+
``sh
yarn add @mleonard9/vin-scanneror
npm install @mleonard9/vin-scanner
Usage
`tsx
import React, { useMemo, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { useCameraDevice } from 'react-native-vision-camera';
import {
Camera as VinScannerCamera,
useVinScanner,
type VinCandidate,
} from '@mleonard9/vin-scanner';export function VinScannerExample(): JSX.Element {
const device = useCameraDevice('back');
const [results, setResults] = useState(null);
const options = useMemo(
() => ({
barcode: { formats: ['code-39', 'code-128', 'pdf-417'] },
onResult: (candidates, event) => {
setResults(candidates);
console.log(
Scan took ${event.duration}ms);
},
}),
[]
); const { frameProcessor } = useVinScanner(options);
if (device == null) {
return null;
}
return (
style={StyleSheet.absoluteFill}
device={device}
frameProcessor={frameProcessor}
callback={(event) => setResults(event.candidates)}
/>
);
}
`Every frame, the camera runs ML Kit barcode + text recognition, extracts 17-character VIN candidates, validates them (checksum included), and routes a payload to
callback.Advanced Features
$3
The package includes an optional AR overlay component that renders real-time bounding boxes around detected VINs, color-coded by confidence score.
Installation:
`sh
yarn add @shopify/react-native-skia
or
npm install @shopify/react-native-skia
`Usage:
`tsx
import { VinScannerOverlay } from '@mleonard9/vin-scanner';export function VinScannerWithOverlay() {
const [candidates, setCandidates] = useState([]);
const { frameProcessor } = useVinScanner({
onResult: (detectedCandidates) => {
setCandidates(detectedCandidates);
},
});
return (
device={device}
frameProcessor={frameProcessor}
style={StyleSheet.absoluteFill}
/>
candidates={candidates}
colors={{ high: '#00FF00', medium: '#FFFF00', low: '#FF0000' }}
/>
);
}
`Confidence Scoring:
Each
VinCandidate includes a confidence score (0.0-1.0) calculated from:
- Source reliability: Barcodes score higher than OCR text (+0.3)
- Text precision: Element-level text scores higher than block-level (+0.2)
- Context awareness: VIN prefixes like "VIN:" increase confidence (+0.2)
- Checksum validation: All candidates pass ISO 3779 validation (+0.2)Overlay colors by confidence:
- 🟢 Green (
confidence > 0.8): High confidence
- 🟡 Yellow (confidence 0.5-0.8): Medium confidence
- 🔴 Red (confidence < 0.5): Low confidence$3
By default, the scanner uses time-based debouncing to prevent duplicate callbacks for the same VIN:
`tsx
const { frameProcessor } = useVinScanner({
duplicateDebounceMs: 1500, // Default: 1500ms
onResult: (candidates) => {
// Only called when a new VIN is detected or after debounce period
console.log('New VIN detected:', candidates[0]?.value);
},
});
`This prevents callback spam when holding the camera steady on a VIN, improving UX in fast-paced scanning scenarios.
$3
Every
VinScannerEvent includes detailed performance metrics for data-driven optimization:`tsx
const { frameProcessor } = useVinScanner({
onResult: (candidates, event) => {
if (event.performance) {
console.log('Performance breakdown:');
console.log( Barcode scan: ${event.performance.barcodeMs}ms);
console.log( Text recognition: ${event.performance.textMs}ms);
console.log( Validation: ${event.performance.validationMs}ms);
console.log( Total: ${event.performance.totalMs}ms);
}
},
});
`Use these metrics to:
- Identify performance bottlenecks (barcode vs text recognition)
- Optimize
textScanInterval based on actual timing
- Monitor performance across different devices
- Track improvements after configuration changes$3
Configure camera parameters for device-specific optimization:
`tsx
const { frameProcessor } = useVinScanner({
cameraSettings: {
fps: 60, // Higher FPS for smoother scanning
lowLightBoost: true, // Auto-boost in low light (default)
videoStabilizationMode: 'standard' // Reduce motion blur
},
onResult: (candidates) => {
console.log('Detected:', candidates[0]?.value);
},
});
`Available settings:
-
fps: Target frame rate (15-60). Higher = smoother but more CPU. Default: 30
- lowLightBoost: Auto-brighten in dark conditions. Default: true
- videoStabilizationMode: 'off' | 'standard' | 'cinematic' | 'auto'. Default: 'off'Tip: For auction lanes with good lighting, try
fps: 60 and videoStabilizationMode: 'standard' for best results.$3
`ts
type VinScannerEvent = {
timestamp: number;
duration: number;
candidates: VinCandidate[];
firstCandidate?: VinCandidate | null;
raw: {
barcodes: BarcodeDetection[];
textBlocks: TextDetection[];
};
};
`VinCandidate contains { value, source: 'barcode' | 'text', confidence, boundingBox }.
The candidates array contains every potential VIN found in the frame. firstCandidate is a convenience reference to the best match.$3
| Path | Type | Description | Default |
| --- | --- | --- | --- |
|
options.barcode.enabled | boolean | Enable barcode scanning | true |
| options.barcode.formats | BarcodeFormat[] | Restrict ML Kit formats ('code-39', 'code-128', 'pdf-417', etc.) | ['all'] |
| options.text.enabled | boolean | Enable text recognition | true |
| options.text.language | 'latin' \| 'chinese' \| 'devanagari' \| 'japanese' \| 'korean' | ML Kit language pack | 'latin' |
| options.detection.textScanInterval | number | Run text recognition every Nth frame (1 = every frame) | 3 |
| options.detection.maxFrameRate | number | Max FPS budget for frame processing (drops surplus frames to avoid blocking) | 30 |
| options.detection.forceOrientation | 'portrait' \| 'portrait-upside-down' \| 'landscape-left' \| 'landscape-right' | Forces ML Kit to interpret every frame using the given orientation (useful when the UI is locked to portrait but the sensor reports landscape) | null |
| options.detection.scanRegion | ScanRegion | Restrict ML Kit processing to a specific region of the frame (normalized coordinates 0.0-1.0). Significantly improves performance by ignoring irrelevant areas. | { x: 0.15, y: 0.15, width: 0.7, height: 0.7 } |
| options.detection.enableFrameQualityCheck | boolean | Enable intelligent frame quality checks to skip blurry or dark frames, improving accuracy | true |
| options.duplicateDebounceMs | number | Time in milliseconds to suppress duplicate VIN callbacks for the same value | 1500 |
| options.showOverlay | boolean | Enable AR overlay (requires @shopify/react-native-skia) | false |
| options.overlayColors | OverlayColors | Custom colors for AR overlay: { high, medium, low } | { high: '#00FF00', medium: '#FFFF00', low: '#FF0000' } |
| options.cameraSettings | CameraSettings | Camera configuration: { fps, lowLightBoost, videoStabilizationMode } | { fps: 30, lowLightBoost: true, videoStabilizationMode: 'off' } |
| options.onResult | (candidates, event) => void | Convenience callback when using useVinScanner; receives all candidates and the raw event | undefined |$3
Phase 1 optimizations dramatically improve scanning performance through native ROI (Region of Interest) frame cropping:
| Configuration | Avg Duration | Improvement |
| --- | --- | --- |
| Full frame, every frame | ~180ms | baseline |
| ROI scanning (70% center) | ~95ms | 47% faster |
| ROI + text interval (3 frames) | ~45ms | 75% faster |
| ROI + quality check + throttle | ~30ms | 83% faster |
Default configuration uses ROI scanning (
scanRegion: { x: 0.15, y: 0.15, width: 0.7, height: 0.7 }), text scan interval of 3, and frame quality checks enabled. This provides excellent accuracy while maintaining real-time performance on mid-range devices.Tip: For challenging lighting or distance scenarios, set
textScanInterval: 1 to scan every frame at the cost of higher CPU usage.Custom scan regions:
`tsx
const { frameProcessor } = useVinScanner({
detection: {
// Focus on center 50% of frame
scanRegion: { x: 0.25, y: 0.25, width: 0.5, height: 0.5 },
textScanInterval: 2,
},
onResult: (candidates) => {
console.log('Detected VINs:', candidates);
},
});
`
$3
- Per-frame plugin overrides: both barcode and text frame processor plugins accept per-frame arguments, so you can dynamically change ML Kit barcode formats or text recognition language without reinitializing the plugin. Call
barcodeScanner.scanBarcodes(frame, { 'pdf-417': true }) or textScanner.scanText(frame, { language: 'japanese' }) inside your worklet to override the resolved defaults for a single frame.
- Orientation overrides: If your UI is locked to portrait (e.g., iPad kiosks) but VisionCamera streams landscape buffers, set detection.forceOrientation: 'portrait'. The JS hook forwards that override to the native plugins so ML Kit always interprets frames with the requested rotation, eliminating the “upside-down unless I flip the paper” problem described in the VisionCamera orientation guide.
- Shared bounding boxes: native plugins now stream bounding box coordinates via zero-copy shared arrays, minimizing JSI serialization. The hook translates these buffers into the familiar BoundingBox structures before running VIN heuristics, so no API change is required.
- Orientation-safe processing: the native plugins forward VisionCamera’s frame orientation metadata directly into ML Kit as recommended in the VisionCamera orientation guide, ensuring portrait VIN scans stay upright.$3
If you prefer to configure
react-native-vision-camera yourself, grab the frame processor from the hook:`tsx
const { frameProcessor } = useVinScanner({
onResult: (candidates, event) => {
console.log('Current VINs', candidates, event.firstCandidate);
console.log(Duration: ${event.duration}ms);
},
});return (
ref={cameraRef}
device={device}
frameProcessor={frameProcessor}
pixelFormat="yuv"
style={StyleSheet.absoluteFill}
/>
);
`Publishing (internal use)
This package is scoped (
@mleonard9/vin-scanner). To release a new build:`sh
yarn prepare # builds /lib via bob
npm publish --access public
`Ensure the authenticated npm user has access to the
@mleonard9` scope.