You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

iOS Web应用(React/TypeScript PWA)条码扫描精度低、不可靠的优化方案咨询

iOS Web应用(React/TypeScript PWA)条码扫描精度低、不可靠的优化方案咨询

我最近在开发一个React+TypeScript的PWA风格Web应用,里面包含了条码扫描功能。我的实现逻辑是:如果浏览器支持BarcodeDetector API就优先用它,不支持的话就 fallback 到@zxing/library

这套方案在安卓上表现完美——扫描又快又准,但到了iOS这边就彻底拉胯了:iPhone上经常扫不出条码,更糟的是还会识别错误的条码;而且新iPhone还经常出现对焦不上条码的问题,直接让扫描体验雪上加霜。

我已经尝试过以下几种优化手段,但效果都不理想:

  • getUserMedia里设置了分辨率的idealmin参数
  • 指定了facingMode: environment(调用后置摄像头)
  • setInterval节流扫描循环的执行频率
  • 同时测试了原生BarcodeDetector和zxing两种扫描逻辑

想请教各位大佬:

  1. 有没有适合iOS Safari的媒体约束配置(比如分辨率、帧率)或者技巧能提升扫描性能?
  2. 针对iOS的对焦问题、解码质量差的情况,有没有什么经过验证的最佳实践?

我知道html5-qrcode这个库,但更倾向于自己调优底层逻辑,除非这个库针对iOS有什么独门优化是我没注意到的。

以下是我当前的扫描器实现代码:

import { useState, useRef, useCallback, useEffect } from 'react';
import { BrowserMultiFormatReader, IScannerControls } from '@zxing/library';

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

export function useBarcodeScanner() {
  const [state, setState] = useState<any>({
    isScanning: false,
    error: null,
    lastResult: null
  });
  const videoRef = useRef<HTMLVideoElement>(null);
  const zxingControlsRef = useRef<IScannerControls | null>(null);
  const isInitializedRef = useRef(false);
  const codeReaderRef = useRef<BrowserMultiFormatReader | null>(null);

  const getCodeReader = useCallback(() => {
    if (!codeReaderRef.current) {
      codeReaderRef.current = new BrowserMultiFormatReader();
    }
    return codeReaderRef.current;
  }, []);

  const stopScanning = useCallback(() => {
    if (zxingControlsRef.current) {
      zxingControlsRef.current.stop();
      zxingControlsRef.current = null;
    } else if (videoRef.current && videoRef.current.srcObject) {
      const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
      tracks.forEach(track => {
        if (track.readyState === 'live') track.stop();
      });
    }
    isInitializedRef.current = false;
    setState(prev => ({ ...prev, isScanning: false }));
  }, []);

  const startScanning = useCallback(async (onResult: (result: BarcodeResult) => void) => {
    if (isInitializedRef.current) return;
    setState(prev => ({ ...prev, isScanning: true, error: null }));
    isInitializedRef.current = true;

    try {
      if ('BarcodeDetector' in window) {
        // --- Native BarcodeDetector Logic ---
        const stream = await navigator.mediaDevices.getUserMedia({
          video: {
            facingMode: 'environment',
            width: { ideal: 1280, min: 640 },
            height: { ideal: 720, min: 480 }
          }
        });

        if (videoRef.current) {
          videoRef.current.srcObject = stream;
          videoRef.current.setAttribute('playsinline', 'true');
          await videoRef.current.play();

          const barcodeDetector = new (window as any).BarcodeDetector({
            formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'upc_a', 'upc_e']
          });

          let scanLoopInterval: NodeJS.Timeout;
          const scanLoop = async () => {
            if (!videoRef.current || videoRef.current.paused || videoRef.current.ended || !isInitializedRef.current) {
              if (scanLoopInterval) clearInterval(scanLoopInterval);
              return;
            }
            try {
              const barcodes = await barcodeDetector.detect(videoRef.current);
              if (barcodes.length > 0) {
                const barcode = barcodes[0];
                const result: BarcodeResult = {
                  code: barcode.rawValue,
                  format: barcode.format
                };
                setState(prev => ({ ...prev, lastResult: result }));
                onResult(result);
                stopScanning(); // Stop after successful scan
              }
            } catch (err) {
              // Ignore scan errors
            }
          };
          scanLoopInterval = setInterval(scanLoop, 300);
        }
      } else {
        // --- ZXing Fallback Logic ---
        const codeReader = getCodeReader();
        const videoElement = videoRef.current;
        if (!videoElement) throw new Error('Video element not found for ZXing scanner.');

        codeReader.decodeFromVideoDevice(undefined, videoElement, (result, error, controls) => {
          if (result) {
            const barcodeData: BarcodeResult = {
              code: result.getText(),
              format: result.getBarcodeFormat().toString()
            };
            setState(prev => ({ ...prev, lastResult: barcodeData }));
            onResult(barcodeData);
            controls.stop();
            zxingControlsRef.current = null;
            isInitializedRef.current = false;
          }
          if (error && !error.message.includes('No MultiFormat Readers were able to decode')) {
            setState(prev => ({ ...prev, error: `Camera-Error: ${error.message}` }));
            stopScanning();
          }
        })
        .then(controls => {
          zxingControlsRef.current = controls;
        })
        .catch(err => {
          setState(prev => ({ ...prev, isScanning: false, error: err.message || 'Fehler beim Starten des Scanners mit ZXing.' }));
          stopScanning();
        });
      }
    } catch (err: any) {
      setState(prev => ({ ...prev, isScanning: false, error: err.message || 'Kamera-Zugriff oder Wiedergabe fehlgeschlagen' }));
      stopScanning();
    }
  }, [stopScanning, getCodeReader]);

  useEffect(() => {
    return () => stopScanning();
  }, [stopScanning]);

  return { ...state, videoRef, startScanning, stopScanning };
}

另外还有对应的BarcodeScanner.tsx组件,负责渲染<video>元素并关联上面的videoRef

内容来源于stack exchange

火山引擎 最新活动