UNPKG

jz-h5-scancode

Version:

H5环境下的二维码扫描插件,提供与 uni.scanCode 完全兼容的 API 接口,支持摄像头实时扫码和图片扫码。v1.1.0优化性能,修复卡死问题

244 lines (213 loc) 8.25 kB
/** * 摄像头扫描器模块 * 负责处理摄像头访问、视频流管理和图像数据获取 */ class CameraScanner { constructor() { this.video = null; this.canvas = null; this.ctx = null; this.stream = null; this.drawFrameId = null; // 添加绘制帧ID this.lastDrawTime = 0; // 上次绘制时间 this.drawInterval = 33; // 绘制间隔约30fps this.config = { width: 640, height: 480, facingMode: 'environment' // 后置摄像头 }; } /** * 初始化摄像头 * @param {HTMLCanvasElement} canvasElement - canvas元素 * @param {Object} config - 配置选项 */ async init(canvasElement, config = {}) { try { this.canvas = canvasElement; this.ctx = this.canvas.getContext('2d'); this.config = { ...this.config, ...config }; // 获取video元素(由UIManager创建) this.video = document.querySelector('.jz-scanner-video'); if (!this.video) { throw new Error('Video元素未找到'); } // 获取摄像头流 await this.startCamera(); console.log('摄像头初始化成功'); } catch (error) { console.error('摄像头初始化失败:', error); throw error; } } /** * 启动摄像头 */ async startCamera() { try { const constraints = { video: { width: { ideal: this.config.width }, height: { ideal: this.config.height }, facingMode: { ideal: this.config.facingMode } }, audio: false }; this.stream = await navigator.mediaDevices.getUserMedia(constraints); this.video.srcObject = this.stream; // 等待视频开始播放 await new Promise((resolve, reject) => { this.video.onloadedmetadata = () => { this.video.play().then(resolve).catch(reject); }; this.video.onerror = reject; }); // 设置canvas尺寸以匹配video this.canvas.width = this.video.videoWidth; this.canvas.height = this.video.videoHeight; // 开始绘制视频帧到canvas(用于二维码识别) this.drawVideoFrame(); console.log('摄像头启动成功', { videoWidth: this.video.videoWidth, videoHeight: this.video.videoHeight }); } catch (error) { console.error('启动摄像头失败:', error); throw new Error('无法访问摄像头: ' + error.message); } } /** * 绘制视频帧到canvas(用于二维码识别)- 优化版本 */ drawVideoFrame() { if (!this.video || !this.canvas || !this.ctx) { return; } const now = Date.now(); // 控制绘制频率,减少CPU占用 if (now - this.lastDrawTime >= this.drawInterval) { if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) { try { // 将video画面绘制到canvas(用于二维码识别) this.ctx.drawImage( this.video, 0, 0, this.canvas.width, this.canvas.height ); this.lastDrawTime = now; } catch (error) { console.warn('绘制视频帧失败:', error); } } } // 继续绘制下一帧 this.drawFrameId = requestAnimationFrame(() => { this.drawVideoFrame(); }); } /** * 获取当前图像数据 * @returns {ImageData|null} 图像数据 */ getImageData() { if (!this.canvas || !this.ctx) { return null; } try { return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); } catch (error) { console.error('获取图像数据失败:', error); return null; } } /** * 停止摄像头 */ async stop() { try { // 停止绘制循环 if (this.drawFrameId) { cancelAnimationFrame(this.drawFrameId); this.drawFrameId = null; } if (this.stream) { this.stream.getTracks().forEach(track => track.stop()); this.stream = null; } if (this.video) { this.video.srcObject = null; // 注意:video元素由UIManager管理,不在这里删除 this.video = null; } // 重置状态 this.lastDrawTime = 0; console.log('摄像头已停止'); } catch (error) { console.error('停止摄像头失败:', error); } } /** * 检查摄像头权限 */ async checkCameraPermission() { try { // 检查是否为HTTPS环境 if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { throw new Error('摄像头访问需要HTTPS环境,当前为HTTP协议。请使用HTTPS访问或在localhost环境下测试。'); } if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error('当前浏览器不支持摄像头访问功能'); } // 尝试获取权限状态 if (navigator.permissions) { const permission = await navigator.permissions.query({ name: 'camera' }); if (permission.state === 'denied') { throw new Error('摄像头权限被拒绝,请在浏览器设置中允许摄像头访问'); } } // 尝试访问摄像头验证权限 const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); stream.getTracks().forEach(track => track.stop()); return true; } catch (error) { console.error('检查摄像头权限失败:', error); // 根据错误类型提供更友好的提示 if (error.name === 'NotAllowedError') { throw new Error('摄像头权限被拒绝,请允许访问摄像头权限'); } else if (error.name === 'NotFoundError') { throw new Error('未检测到摄像头设备'); } else if (error.name === 'NotSupportedError' || error.name === 'OverconstrainedError') { throw new Error('摄像头不支持请求的配置'); } else if (error.message.includes('HTTPS')) { throw error; // 直接抛出HTTPS相关错误 } else { throw new Error('无法访问摄像头: ' + error.message); } } } /** * 检查是否支持摄像头 */ static async isSupported() { return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); } /** * 获取可用摄像头列表 */ static async getCameras() { try { if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { return []; } const devices = await navigator.mediaDevices.enumerateDevices(); return devices.filter(device => device.kind === 'videoinput'); } catch (error) { console.error('获取摄像头列表失败:', error); return []; } } } export default CameraScanner;