jz-h5-scancode
Version:
H5环境下的二维码扫描插件,提供与 uni.scanCode 完全兼容的 API 接口,支持摄像头实时扫码和图片扫码。v1.1.0优化性能,修复卡死问题
244 lines (213 loc) • 8.25 kB
JavaScript
/**
* 摄像头扫描器模块
* 负责处理摄像头访问、视频流管理和图像数据获取
*/
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;