npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mleonard9/vin-scanner

v1.5.2

Published

High-performance VIN scanner for React Native Vision Camera powered by Google ML Kit barcode + text recognition.

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 >= 4.6.0
  • react-native-worklets-core >= 1.3.3
  • react-native-gesture-handler >= 2.0.0 (for tap-to-focus)
  • react-native-reanimated >= 3.0.0 (for tap-to-focus)
  • iOS 13+ / Android 21+

Compatibility

This package keeps peer dependency ranges broad so consuming apps can control their React Native, VisionCamera, and Worklets versions.

Tested matrix:

| Package | Recommended | | --- | --- | | react-native-vision-camera | 4.7.3 | | react-native-worklets-core | 1.3.3 |

Notes:

  • The package's own dev/test environment stays pinned to the matrix above, but published peer ranges remain open to support newer app stacks.
  • react-native-worklets-core 1.6.x has caused frame-processor HostObject compatibility issues with VisionCamera v4 in some apps.
  • The package includes a frame unwrap compatibility shim to improve compatibility with newer Worklets wrappers.
  • If your app is on newer Worklets or React Native, test carefully and prefer keeping VisionCamera and Worklets aligned to versions already validated in your app.

Installation

yarn add @mleonard9/vin-scanner
# or
npm install @mleonard9/vin-scanner

# If you need a known-good baseline, start with:
# yarn add [email protected] [email protected]

# Optional (for haptic cues)
# yarn add react-native-haptic-feedback

# iOS
cd ios && pod install

Usage

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(
    () => ({
      // Include QR for corner codes on auction tags
      barcode: { formats: ['code-39', 'code-128', 'pdf-417', 'qr'] },
      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.

Camera Gestures

The VIN Scanner camera includes built-in support for intuitive camera controls:

Pinch to Zoom

Pinch-to-zoom is enabled by default. Simply pinch on the camera view to zoom in and out. The zoom gesture is natively implemented by react-native-vision-camera for optimal performance.

Tap to Focus

Tap anywhere on the camera view to focus at that point. This feature requires react-native-gesture-handler and react-native-reanimated:

Installation:

yarn add react-native-gesture-handler react-native-reanimated
# or
npm install react-native-gesture-handler react-native-reanimated

# iOS
cd ios && pod install

Note: These dependencies are likely already installed if you're using React Navigation or other common React Native libraries.

The tap-to-focus functionality works automatically once these dependencies are installed. Simply tap on the camera view where you want to focus, and the camera will adjust both auto-focus (AF) and auto-exposure (AE) for that point.

How it works:

  • Tap on a VIN to focus precisely on that area
  • The camera adjusts focus and exposure automatically
  • Works seamlessly with the pinch-to-zoom gesture
  • No additional configuration required

Advanced Features

AR Overlay (deprecated)

The Skia-based overlay component has been removed while alignment issues are addressed. Default barcode formats are tuned for VIN labels (code-39, code-128, pdf-417) with an automatic fallback to all formats after sustained misses. No Skia install is required.

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 textScanInterval based 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: 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.

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.text.requireConfirmation | boolean | When true, text VINs are held until you confirm; barcodes still emit immediately | false | | options.text.pendingTtlMs | number | Auto-dismiss pending text VINs after this many ms (when requireConfirmation is true) | 5000 | | 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 | Deprecated; when false it disables minLuma/minSharpness gates | true | | options.detection.minLuma | number | Minimum mean luma (0–255) required to process a frame; skips too-dark frames | 30 | | options.detection.minSharpness | number | Minimum sharpness metric required; skips blurry frames | 12 | | options.detection.minConfidence | number | Minimum candidate confidence required before emitting | 0 | | options.detection.barcodeFallbackAfter | number | Frames without barcode hits before scanning all formats | 45 | | options.duplicateDebounceMs | number | Time in milliseconds to suppress duplicate VIN callbacks for the same value | 1500 | | options.showOverlay | boolean | Deprecated; overlay component removed | false | | options.overlayColors | OverlayColors | Deprecated; overlay component removed | { high: '#00FF00', medium: '#FFFF00', low: '#FF0000' } | | options.cameraSettings | CameraSettings | Camera configuration: { fps (clamped 24–30), lowLightBoost, videoStabilizationMode } | { fps: 24, lowLightBoost: true, videoStabilizationMode: 'cinematic' } | | options.onResult | (candidates, event) => void | Convenience callback when using useVinScanner; receives all candidates and the raw event | undefined | | options.onTextPending | (pending) => void | Invoked when text.requireConfirmation is true and text VINs are detected | undefined | | options.haptics | boolean | Enable built-in haptic cues (requires react-native-haptic-feedback installed) | true |

Behaviors & defaults

  • Barcode-first: barcodes emit immediately; text VINs can require confirmation.
  • Session dedupe: VINs are not re-emitted within a scan session (in addition to time-based debounce).
  • Quality gate: frames below minLuma or minSharpness are skipped.
  • Confidence gate: candidates below minConfidence are dropped, but the default is 0 because checksum validity is the primary source of truth.
  • Barcode formats: defaults to code-39, code-128, pdf-417 with automatic fallback to all formats after barcodeFallbackAfter empty frames.
  • Camera hints: FPS clamped to 24–30 and videoStabilizationMode defaults to cinematic to keep headroom and reduce jitter.

Text confirmation UI (barcode = instant, text = tap-to-confirm)

import { useState } from 'react';
import { Camera, useCameraDevice } from 'react-native-vision-camera';
import { useVinScanner, TextVinPrompt, VinCandidate } from '@mleonard9/vin-scanner';

export function ConfirmingScanner() {
  const device = useCameraDevice('back');
  const [pending, setPending] = useState<VinCandidate[]>([]);

  const { frameProcessor, pendingTextCandidates, confirmTextCandidate } = useVinScanner({
    text: { requireConfirmation: true },
    onTextPending: setPending,
    onResult: (candidates) => {
      // barcode VINs (or confirmed text VINs) arrive here
      console.log('confirmed VINs', candidates.map((c) => c.value));
    },
  });

  return (
    <>
      {device && (
        <Camera style={{ flex: 1 }} device={device} frameProcessor={frameProcessor} isActive />
      )}
      <TextVinPrompt
        visible={pendingTextCandidates.length > 0}
        candidates={pendingTextCandidates}
        buttonLabel=\"Book It\"
        buttonColor=\"#0A84FF\"
        onConfirm={(candidate) => confirmTextCandidate(candidate.value)}
        onDismiss={() => setPending([])}
      />
    </>
  );
}

Manual VIN keypad with checksum guard

import { ManualVinInput } from '@mleonard9/vin-scanner';

export function ManualEntry({ onSubmit }: { onSubmit: (vin: string) => void }) {
  return (
    <ManualVinInput
      buttonLabel=\"Book It\"
      buttonColor=\"#0A84FF\"
      onSubmit={onSubmit}
    />
  );
}

Pending banner (alternative to modal)

import { PendingVinBanner } from '@mleonard9/vin-scanner';

<PendingVinBanner
  visible={pendingTextCandidates.length > 0}
  candidates={pendingTextCandidates}
  buttonLabel=\"Book It\"
  buttonColor=\"#0A84FF\"
  onConfirm={(candidate) => confirmTextCandidate(candidate.value)}
  onDismiss={() => setPending([])}
/>;

OCRScanner-style chrome overlay

import { ScannerChromeOverlay } from '@mleonard9/vin-scanner';

<ScannerChromeOverlay
  onBackPress={() => navigation.goBack()}
  onManualEntryPress={() => setManualVisible(true)}
  onFlashPress={toggleTorch}
  isTorchOn={isTorchOn}
  manualEntryContent={<Ionicons name="keypad-outline" size={32} color="white" />}
  flashOffContent={<Ionicons name="flash-outline" size={32} color="white" />}
  flashOnContent={<Ionicons name="flash-off-outline" size={32} color="white" />}
  backContent={<Ionicons name="chevron-back-outline" size={32} color="white" />}
/>;

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 }) and a text scan interval of 3. 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 (set null to cover full frame / corner QR)
    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 }) 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.

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 public

Ensure the authenticated npm user has access to the @mleonard9 scope.