Skip to content

Quagga initialization failed #559

Closed
Closed
@Qodestackr

Description

@Qodestackr

What causes this issue?
My implementation:

"use client"

import React, { useEffect, useRef, useState } from "react"
import Quagga from '@ericblade/quagga2'
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { ScanLine } from "./scan-line"
import { QrCode, Camera, XCircle, CheckCircle2, Loader2, Volume2, VolumeX } from "lucide-react"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { toast } from "sonner"

interface Camera {
    id: string;
    label: string;
}

interface ScanResult {
    code: string;
    format: string;
}

interface BarcodeFormat {
    value: string;
    label: string;
}

interface BarcodeConstraints {
    width?: number;
    height?: number;
    facingMode?: string;
    deviceId?: string | { exact: string };
}

interface BarcodeScannerProps {
    onDetected?: (result: any) => void;
    onError?: (error: any) => void;
    onScannerClose?: () => void;
    readers?: string[];
    fullScreen?: boolean;
    constraints?: BarcodeConstraints;
}

// Supported barcode formats
const BARCODE_FORMATS: BarcodeFormat[] = [
    { value: "code_128_reader", label: "Code 128" },
    { value: "ean_reader", label: "EAN-13" },
    { value: "ean_8_reader", label: "EAN-8" },
    { value: "code_39_reader", label: "Code 39" },
    { value: "code_39_vin_reader", label: "Code 39 VIN" },
    { value: "codabar_reader", label: "Codabar" },
    { value: "upc_reader", label: "UPC" },
    { value: "upc_e_reader", label: "UPC-E" },
    { value: "i2of5_reader", label: "I2of5" },
    { value: "2of5_reader", label: "2of5" },
    { value: "code_93_reader", label: "Code 93" },
    { value: "code_32_reader", label: "Code 32" }
]

/**
 * Barcode Scanner Component
 * 
 * @param props Component props
 * @returns JSX.Element
 */
const BarcodeScanner: React.FC<BarcodeScannerProps> = ({
    onDetected,
    onError,
    onScannerClose,
    readers = ["code_128_reader", "ean_reader"],
    fullScreen = false,
    constraints = {
        width: 640,
        height: 480,
        facingMode: "environment"
    }
}) => {
    const scannerRef = useRef<HTMLDivElement>(null)
    const [scanning, setScanning] = useState<boolean>(false)
    const [initialized, setInitialized] = useState<boolean>(false)
    const [error, setError] = useState<any>(null)
    const [cameras, setCameras] = useState<Camera[]>([])
    const [activeCamera, setActiveCamera] = useState<string | null>(null)
    const [torchEnabled, setTorchEnabled] = useState<boolean>(false)
    const [soundEnabled, setSoundEnabled] = useState<boolean>(true)
    const [selectedReaders, setSelectedReaders] = useState<string[]>(readers)
    const [lastResult, setLastResult] = useState<ScanResult | null>(null)
    const [scanCount, setScanCount] = useState<number>(0)
    const beepRef = useRef<HTMLAudioElement>(null)

    const [initAttempts, setInitAttempts] = useState<number>(0)
    const [errorHandled, setErrorHandled] = useState<boolean>(false)


    useEffect(() => {
        if (!scanning) return
        if (!scanning || initAttempts > 3) return // max retry limit

        const initScanner = async () => {
            try {
                // Get available cameras
                const devices = await Quagga.CameraAccess.enumerateVideoDevices()
                const formattedCameras = devices.map((device: MediaDeviceInfo) => ({
                    id: device.deviceId,
                    label: device.label || `Camera ${devices.indexOf(device) + 1}`
                }))
                setCameras(formattedCameras)

                // If no active camera is set, use the first one
                if (!activeCamera && formattedCameras.length > 0) {
                    setActiveCamera(formattedCameras[0].id)
                }

                const cameraConstraints: BarcodeConstraints = {
                    ...constraints,
                    deviceId: activeCamera ? { exact: activeCamera } : undefined
                }

                setError(null)
                setErrorHandled(false)

                // Initialize Quagga
                await Quagga.init({
                    inputStream: {
                        name: "Live",
                        type: "LiveStream",
                        target: scannerRef.current as HTMLDivElement,
                        constraints: cameraConstraints,
                        area: { // Define a smaller area for detection to improve accuracy
                            top: "15%",
                            right: "10%",
                            left: "10%",
                            bottom: "15%"
                        }
                    },
                    locate: true,
                    locator: {
                        patchSize: "medium",
                        halfSample: true
                    },
                    numOfWorkers: navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 4,
                    decoder: {
                        readers: selectedReaders,
                        debug: {
                            drawBoundingBox: true,
                            showFrequency: false,
                            drawScanline: true,
                            showPattern: false
                        }
                    },
                    frequency: 10
                }, (err: any) => {
                    if (err) {
                        console.error("Quagga initialization failed:", err)
                        //!! IMPORTANT: Track that we've handled this error already to avoid duplicates
                        if (!errorHandled) {
                            setError(err)
                            setErrorHandled(true)
                            setInitAttempts(prev => prev + 1)

                            // Call onError only once per error
                            if (onError) onError(err)
                        }
                        return
                    }

                    setInitAttempts(0)
                    console.log("Quagga initialized successfully")
                    setInitialized(true)
                    Quagga.start()
                })

                Quagga.onDetected((result: any) => {
                    if (result && result.codeResult) {
                        const code = result.codeResult.code
                        const format = result.codeResult.format

                        // Play beep sound if enabled
                        if (soundEnabled && beepRef.current) {
                            beepRef.current.play().catch((err) => console.error("Error playing sound:", err))
                        }

                        // Show success animation
                        setLastResult({ code, format })
                        setScanCount(prevCount => prevCount + 1)

                        // Call onDetected callback with result
                        if (onDetected) onDetected(result)
                    }
                })

                // Set up processed handler for debugging
                Quagga.onProcessed((result: any) => {
                    // TODO: visual feedback here...
                })

            } catch (err) {
                console.error("Error initializing scanner:", err)
                setError(err)
                if (onError) onError(err)
            }
        }

        initScanner()

        return () => {
            if (initialized) {
                Quagga.offDetected()
                Quagga.offProcessed()
                Quagga.stop()
                setInitialized(false)
            }
        }
    }, [scanning, activeCamera, selectedReaders, onError, onDetected, constraints, initialized])

    const toggleTorch = async () => {
        try {
            if (torchEnabled) {
                await Quagga.CameraAccess.disableTorch()
                setTorchEnabled(false)
                toast.success("Torch disabled")
            } else {
                await Quagga.CameraAccess.enableTorch()
                setTorchEnabled(true)
                toast.success("Torch enabled")
            }
        } catch (err) {
            console.error("Error toggling torch:", err)
            toast.error("Torch functionality not available on this device")
        }
    }

    // Change camera
    const changeCamera = (deviceId: string) => {
        if (initialized) {
            Quagga.stop()
            setInitialized(false)
        }

        setActiveCamera(deviceId)

        // Restart scanner with new camera
        if (!scanning) {
            setScanning(true)
        }
    }

    // Add/remove barcode readers
    const toggleReader = (reader: string) => {
        if (selectedReaders.includes(reader)) {
            setSelectedReaders(selectedReaders.filter(r => r !== reader))
        } else {
            setSelectedReaders([...selectedReaders, reader])
        }

        // Restart scanner with new readers
        if (initialized) {
            Quagga.stop()
            setInitialized(false)
        }
    }

    const closeScanner = () => {
        if (initialized) {
            Quagga.stop()
            setInitialized(false)
        }

        setScanning(false)
        setError(null)
        setErrorHandled(false)
        setInitAttempts(0)

        if (onScannerClose) onScannerClose()
    }

    const startScanning = () => {
        setScanning(true)
        setError(null)
        setErrorHandled(false)
        setInitAttempts(0)
    }

    return (
        <div className={`barcode-scanner ${fullScreen ? "fixed inset-0 z-50 bg-black" : "relative"}`}>
            {!scanning ? (
                <Card className="w-full max-w-md mx-auto">
                    <CardHeader>
                        <CardTitle className="flex items-center gap-2">
                            <QrCode className="h-5 w-5" />
                            Barcode Scanner
                        </CardTitle>
                    </CardHeader>
                    <CardContent>
                        <p className="text-muted-foreground mb-4">
                            Scan barcodes to quickly find products, check inventory, or add items to your cart.
                        </p>

                        <div className="space-y-4">
                            <div className="flex items-center justify-between">
                                <Label htmlFor="soundEnabled">Sound notification</Label>
                                <Switch
                                    id="soundEnabled"
                                    checked={soundEnabled}
                                    onCheckedChange={setSoundEnabled}
                                />
                            </div>

                            <Select
                                value={selectedReaders[0]}
                                onValueChange={(value) => setSelectedReaders([value])}
                            >
                                <SelectTrigger>
                                    <SelectValue placeholder="Select barcode format" />
                                </SelectTrigger>
                                <SelectContent>
                                    {BARCODE_FORMATS.map(format => (
                                        <SelectItem key={format.value} value={format.value}>
                                            {format.label}
                                        </SelectItem>
                                    ))}
                                </SelectContent>
                            </Select>
                        </div>
                    </CardContent>
                    <CardFooter>
                        <Button onClick={startScanning} className="w-full">
                            <Camera className="mr-2 h-4 w-4" />
                            Start Scanning
                        </Button>
                    </CardFooter>
                </Card>
            ) : (
                <div className="relative">
                    {/* Scanner viewport */}
                    <div
                        ref={scannerRef}
                        className={`overflow-hidden relative ${fullScreen ? "w-full h-screen" : "w-full max-w-md mx-auto aspect-[4/3] rounded-md"}`}
                    >
                        {/* Loading state */}
                        {!initialized && !error && (
                            <div className="absolute inset-0 bg-black/80 flex items-center justify-center z-20">
                                <div className="text-center">
                                    <Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
                                    <p className="text-white">Initializing camera...</p>
                                </div>
                            </div>
                        )}

                        {/* Error state */}
                        {error && (
                            <div className="absolute inset-0 bg-black/80 flex items-center justify-center z-20">
                                <div className="text-center p-4">
                                    <XCircle className="h-8 w-8 text-destructive mx-auto mb-2" />
                                    <p className="text-white mb-2">Failed to initialize camera</p>
                                    <p className="text-sm text-white/70 mb-4">
                                        {error.message || "Please ensure you've granted camera permissions"}
                                    </p>
                                    <Button variant="destructive" onClick={closeScanner}>
                                        Close Scanner
                                    </Button>
                                </div>
                            </div>
                        )}

                        {/* Success overlay */}
                        {lastResult && (
                            <div className="absolute inset-0 bg-black/30 flex items-center justify-center z-10 animate-fade-out">
                                <div className="bg-green-500/20 rounded-full p-8">
                                    <CheckCircle2 className="h-12 w-12 text-green-500" />
                                </div>
                            </div>
                        )}

                        {/* Scan guides */}
                        <div className="absolute top-0 left-0 w-full h-full pointer-events-none z-10">
                            <div className="absolute top-[15%] right-[10%] bottom-[15%] left-[10%] border-2 border-white/40 rounded-lg">
                                <ScanLine />
                            </div>
                        </div>
                    </div>

                    <div className={`mt-4 ${fullScreen ? "absolute bottom-0 left-0 right-0 p-4 bg-black/80" : ""}`}>
                        <div className="flex items-center justify-between gap-2 mb-2">
                            <Button variant="outline" size="sm" onClick={closeScanner}>
                                <XCircle className="mr-2 h-4 w-4" />
                                Cancel
                            </Button>

                            <div className="flex gap-2">
                                {/* Camera selection */}
                                {cameras.length > 1 && (
                                    <Popover>
                                        <PopoverTrigger asChild>
                                            <Button variant="outline" size="sm">
                                                <Camera className="h-4 w-4" />
                                            </Button>
                                        </PopoverTrigger>
                                        <PopoverContent className="w-60">
                                            <p className="font-medium text-sm mb-2">Select camera</p>
                                            <div className="space-y-2">
                                                {cameras.map(camera => (
                                                    <Button
                                                        key={camera.id}
                                                        variant={activeCamera === camera.id ? "default" : "outline"}
                                                        size="sm"
                                                        className="w-full justify-start"
                                                        onClick={() => changeCamera(camera.id)}
                                                    >
                                                        {camera.label}
                                                    </Button>
                                                ))}
                                            </div>
                                        </PopoverContent>
                                    </Popover>
                                )}

                                <Button
                                    variant={torchEnabled ? "default" : "outline"}
                                    size="sm"
                                    onClick={toggleTorch}
                                >
                                    {torchEnabled ? "Torch On" : "Torch Off"}
                                </Button>

                                <Button
                                    variant="outline"
                                    size="sm"
                                    onClick={() => setSoundEnabled(!soundEnabled)}
                                >
                                    {soundEnabled ? (
                                        <Volume2 className="h-4 w-4" />
                                    ) : (
                                        <VolumeX className="h-4 w-4" />
                                    )}
                                </Button>
                            </div>
                        </div>

                        {initialized && (
                            <Alert>
                                <AlertTitle>Scanner active</AlertTitle>
                                <AlertDescription>
                                    {scanCount > 0 ? (
                                        <>Last scan: <span className="font-mono">{lastResult?.code}</span> ({lastResult?.format})</>
                                    ) : (
                                        "Position barcode within the scanning area"
                                    )}
                                </AlertDescription>
                            </Alert>
                        )}
                    </div>

                    <audio ref={beepRef} src="/beep.mp3" preload="auto" />
                </div>
            )}
        </div>
    )
}

export default BarcodeScanner

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions