You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中html5qrcode相机2倍缩放的Android WebView适配问题

解决Android WebView中Html5Qrcode相机缩放失效问题

问题根源

桌面浏览器与Android WebView对MediaStream Track的zoom约束取值逻辑存在差异:桌面端通常支持百分比(如200代表2倍),而Android设备的摄像头API大多使用倍数数值(如2.0代表2倍);同时WebView默认配置可能未完全启用MediaDevices相关特性,导致约束无法生效。

解决方案

1. 调整WebView配置(Android原生代码)

确保WebView开启必要权限与特性,在Android项目的WebView初始化代码中添加:

WebSettings webSettings = webView.getSettings();
// 启用JavaScript
webSettings.setJavaScriptEnabled(true);
// 允许媒体播放无需用户手势(避免摄像头启动被拦截)
webSettings.setMediaPlaybackRequiresUserGesture(false);
// 启用DOM存储
webSettings.setDomStorageEnabled(true);
// 开启硬件加速
webSettings.setHardwareAccelerationEnabled(true);
// 允许文件访问
webSettings.setAllowFileAccess(true);

同时确保已动态申请CAMERA权限,在AndroidManifest.xml中声明:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

2. 修改React端缩放逻辑

将zoom取值从百分比改为倍数,并直接在getUserMedia的约束中指定,避免先获取流再修改约束的兼容性问题:

修改后的Scanner组件代码:

// hooks
import { useEffect, useState } from "react";
import { Html5Qrcode } from "html5-qrcode";

// icons
import { PlayCircleFilledWhite, StopCircle } from "@mui/icons-material";

// components
import { Button } from "@mui/material";

const brConfig = { fps: 20, qrbox: { width: 400, height: 150 } };

let html5QrCode;

export const Scanner = (props) => {
  const [cameraStart, setCameraStart] = useState(false);

  useEffect(() => {
    const startCamera = async () => {
      html5QrCode = new Html5Qrcode("reader");
      const qrCodeSuccessCallback = (decodedText, decodedResult) => {
        props.onResult(props.localAvoidMulti, decodedText, decodedResult);
      };

      try {
        // 先获取摄像头设备信息,锁定后置摄像头
        const devices = await navigator.mediaDevices.enumerateDevices();
        const cameraDevice = devices.find(device => device.kind === "videoinput" && device.label.includes("back"));
        
        // 直接在getUserMedia约束中配置zoom(使用倍数而非百分比)
        const videoConstraints = {
          facingMode: "environment",
          deviceId: cameraDevice?.deviceId,
          advanced: [{ zoom: 2.0 }]
        };

        const stream = await navigator.mediaDevices.getUserMedia({
          video: videoConstraints,
        });

        const videoTrack = stream.getVideoTracks()[0];
        const capabilities = videoTrack.getCapabilities();
        const settings = videoTrack.getSettings();

        console.log("Camera Capabilities: ", capabilities);
        console.log("Applied Zoom: ", settings.zoom);

        // 验证zoom是否生效,未生效则动态调整到支持范围内
        if (capabilities.zoom && settings.zoom !== 2.0) {
          const appliedZoom = Math.min(Math.max(capabilities.zoom.min, 2.0), capabilities.zoom.max);
          await videoTrack.applyConstraints({
            advanced: [{ zoom: appliedZoom }]
          });
          console.log("Post-adjust Zoom: ", videoTrack.getSettings().zoom);
        } else if (!capabilities.zoom) {
          console.warn("Zoom not supported by this camera.");
          alert("Zoom not supported by this camera.");
        }

        await html5QrCode.start(
          { deviceId: settings.deviceId },
          brConfig,
          qrCodeSuccessCallback
        );

        setCameraStart(true);
      } catch (error) {
        console.error("Error starting the camera:", error);
        alert(`Error starting the camera: ${error.message}`);
        setCameraStart(false);
      }
    };

    startCamera();

    return () => {
      handleStop();
    };
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [props.localAvoidMulti]);

  const handleClickAdvanced = async () => {
    const qrCodeSuccessCallback = (decodedText, decodedResult) => {
      props.onResult(decodedText);
      handleStop();
    };

    try {
      const devices = await navigator.mediaDevices.enumerateDevices();
      const cameraDevice = devices.find(device => device.kind === "videoinput" && device.label.includes("back"));

      const videoConstraints = {
        facingMode: "environment",
        deviceId: cameraDevice?.deviceId,
        advanced: [{ zoom: 1.5 }]
      };

      const stream = await navigator.mediaDevices.getUserMedia({
        video: videoConstraints,
      });

      const videoTrack = stream.getVideoTracks()[0];
      const capabilities = videoTrack.getCapabilities();
      const settings = videoTrack.getSettings();

      console.log("Camera Capabilities: ", capabilities);
      console.log("Applied Zoom: ", settings.zoom);

      if (capabilities.zoom && settings.zoom !== 1.5) {
        const appliedZoom = Math.min(Math.max(capabilities.zoom.min, 1.5), capabilities.zoom.max);
        await videoTrack.applyConstraints({
          advanced: [{ zoom: appliedZoom }]
        });
        console.log("Post-adjust Zoom: ", videoTrack.getSettings().zoom);
      } else if (!capabilities.zoom) {
        console.warn("Zoom not supported by this camera.");
        alert("Zoom not supported by this camera.");
      }

      await html5QrCode.start(
        { deviceId: settings.deviceId },
        brConfig,
        qrCodeSuccessCallback
      );

      const oldRegion = document.getElementById("qr-shaded-region");
      if (oldRegion) oldRegion.innerHTML = "";

      setCameraStart(true);
    } catch (error) {
      console.error("Error starting the camera:", error);
      alert(`Error starting the camera: ${error.message}`);
      setCameraStart(false);
    }
  };

  const handleStop = () => {
    try {
      if (html5QrCode) {
        html5QrCode
          .stop()
          .then(() => {
            if (html5QrCode) {
              html5QrCode.clear();
            }
            setCameraStart(false);
          })
          .catch(() => {
            setCameraStart(false);
          });
      }
    } catch (err) {
      setCameraStart(false);
    }
  };

  return (
    <div style={{ width: "100%" }}>
      <div id="reader" width="100%"></div>
      <div
        style={{
          display: "flex",
          justifyContent: "space-evenly",
          marginTop: "10px",
        }}
      >
        {!cameraStart ? (
          <Button
            variant="contained"
            className="button save-btn"
            onClick={handleClickAdvanced}
            color="success"
          >
            <PlayCircleFilledWhite
              sx={{ fontSize: "50px" }}
              className="save-icon"
            />
            Iniciar
          </Button>
        ) : (
          <StopCircle
            onClick={handleStop}
            sx={{ fontSize: "50px", color: "red" }}
          />
        )}
      </div>
    </div>
  );
};

3. 额外优化点

  • 添加zoom生效验证:在applyConstraints后再次获取settings,确认zoom是否正确应用
  • 优先指定后置摄像头设备ID,避免设备识别偏差
  • 捕获applyConstraints的异常,避免因zoom取值超出范围导致摄像头启动失败

内容的提问来源于stack exchange,提问作者Joao Carvalho

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 13:39:54