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
相关产品推荐
相关产品推荐

