@mleonard9/vin-scanner
v1.3.0
Published
High-performance VIN scanner for React Native Vision Camera powered by Google ML Kit barcode + text recognition.
Maintainers
Readme
@mleonard9/vin-scanner
High-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
Requirements
react-native-vision-camera>= 3.9.0react-native-worklets-core>= 0.4.0- iOS 13+ / Android 21+
Installation
yarn add @mleonard9/vin-scanner
# or
npm install @mleonard9/vin-scanner
# iOS
cd ios && pod installUsage
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<VinCandidate[] | null>(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 (
<View style={StyleSheet.absoluteFill}>
<VinScannerCamera
style={StyleSheet.absoluteFill}
device={device}
frameProcessor={frameProcessor}
callback={(event) => setResults(event.candidates)}
/>
</View>
);
}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
AR Overlay with Confidence Scoring
The package includes an optional AR overlay component that renders real-time bounding boxes around detected VINs, color-coded by confidence score.
Installation:
yarn add @shopify/react-native-skia
# or
npm install @shopify/react-native-skiaUsage:
import { VinScannerOverlay } from '@mleonard9/vin-scanner';
export function VinScannerWithOverlay() {
const [candidates, setCandidates] = useState<VinCandidate[]>([]);
const { frameProcessor } = useVinScanner({
onResult: (detectedCandidates) => {
setCandidates(detectedCandidates);
},
});
return (
<View style={StyleSheet.absoluteFill}>
<Camera
device={device}
frameProcessor={frameProcessor}
style={StyleSheet.absoluteFill}
/>
<VinScannerOverlay
candidates={candidates}
colors={{ high: '#00FF00', medium: '#FFFF00', low: '#FF0000' }}
/>
</View>
);
}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
Smart Duplicate Filtering
By default, the scanner uses time-based debouncing to prevent duplicate callbacks for the same VIN:
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.
Performance Telemetry
Every VinScannerEvent includes detailed performance metrics for data-driven optimization:
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
textScanIntervalbased on actual timing - Monitor performance across different devices
- Track improvements after configuration changes
Camera Settings Optimization
Configure camera parameters for device-specific optimization:
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: 30lowLightBoost: Auto-brighten in dark conditions. Default: truevideoStabilizationMode:'off'|'standard'|'cinematic'|'auto'. Default: 'off'
Tip: For auction lanes with good lighting, try fps: 60 and videoStabilizationMode: 'standard' for best results.
Callback payload
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.
Options
| 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 |
Performance
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:
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);
},
});Advanced frame-processor controls
- 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 })ortextScanner.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
BoundingBoxstructures 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.
Hook-only usage
If you prefer to configure react-native-vision-camera yourself, grab the frame processor from the hook:
const { frameProcessor } = useVinScanner({
onResult: (candidates, event) => {
console.log('Current VINs', candidates, event.firstCandidate);
console.log(`Duration: ${event.duration}ms`);
},
});
return (
<Camera
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:
yarn prepare # builds /lib via bob
npm publish --access publicEnsure the authenticated npm user has access to the @mleonard9 scope.
