UNPKG

camera-serial-utils

Version:

A utility package for camera capture and serial communication using Web Serial API

263 lines (232 loc) 8.64 kB
/** * 打开指定相机拍照并返回base64格式图片(逆时针旋转90度) * @param {Object} options 配置选项 * @param {string} [options.targetCamera="QHD Webcam (0bda:0567)"] 目标相机名称 * @param {number} [options.width=480] 图片宽度(旋转后实际高度) * @param {number} [options.height=640] 图片高度(旋转后实际宽度) * @param {string} [options.mimeType='image/jpeg'] 图片类型 * @param {number} [options.quality=0.92] 图片质量(0-1) * @param {number} [options.maxResolution=1920] 最大分辨率限制(提升性能) * @returns {Promise<string>} 返回base64格式的图片数据 */ export async function captureCamera(options = {}) { const { targetCamera = "QHD Webcam", width = 480, height = 640, mimeType = 'image/jpeg', quality = 0.92, maxResolution = 1920 } = options; if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !navigator.mediaDevices.enumerateDevices) { throw new Error('Camera API is not supported in this browser'); } // 检查是否存在指定相机 let arr = []; try { // 获取用户媒体权限(临时) const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true }); // 枚举所有媒体设备 const devices = await navigator.mediaDevices.enumerateDevices(); arr = devices.filter(device => device.kind === 'videoinput'); // 立即停止临时流 mediaStream.getTracks().forEach(track => track.stop()); } catch (error) { console.error('Error accessing media devices:', error); throw new Error('无法访问媒体设备'); } const targetDevice = arr.find(device => device.label.includes(targetCamera)); if (!targetDevice) { throw new Error(`未找到指定的相机设备: ${targetCamera}`); } // 创建拍照弹层 const modal = createCameraModal(); document.body.appendChild(modal); // 等待用户拍照 return new Promise((resolve, reject) => { let stream; let video; // 获取弹层元素 const captureBtn = modal.querySelector('.capture-btn'); const cancelBtn = modal.querySelector('.cancel-btn'); const previewVideo = modal.querySelector('.camera-preview'); // 初始化相机(带性能优化) const initCamera = async () => { try { // 计算实际分辨率(不超过maxResolution) const actualWidth = Math.min(width, maxResolution); const actualHeight = Math.min(height, maxResolution); // 应用约束(降低分辨率以提高性能) const constraints = { video: { deviceId: targetDevice.deviceId, width: { ideal: actualWidth }, height: { ideal: actualHeight }, frameRate: { ideal: 30 } }, audio: false }; stream = await navigator.mediaDevices.getUserMedia(constraints); // 应用性能优化到视频轨道 const videoTrack = stream.getVideoTracks()[0]; if (videoTrack && videoTrack.applyConstraints) { try { await videoTrack.applyConstraints({ width: { ideal: actualWidth }, height: { ideal: actualHeight }, frameRate: { ideal: 30 } }); } catch (constraintError) { console.warn('无法应用视频约束:', constraintError); } } video = previewVideo; video.srcObject = stream; // 旋转视频预览90度 video.style.transform = 'rotate(-90deg)'; video.style.width = `${actualHeight}px`; video.style.height = `${actualWidth}px`; await new Promise((resolve) => { video.onloadedmetadata = () => { video.play(); resolve(); }; }); } catch (error) { closeModal(); reject(new Error(`无法启动相机: ${error.message}`)); } }; // 拍照处理(逆时针旋转90度) const capturePhoto = async () => { try { const canvas = document.createElement('canvas'); // 交换宽高以适应旋转 canvas.width = height; canvas.height = width; const context = canvas.getContext('2d'); // 旋转画布并绘制图像 context.translate(0, width); context.rotate(-Math.PI / 2); context.drawImage(video, 0, 0, width, height); const base64Image = canvas.toDataURL(mimeType, quality); closeModal(); resolve(base64Image); } catch (error) { closeModal(); reject(new Error(`拍照失败: ${error.message}`)); } }; // 关闭弹层和释放资源 const closeModal = () => { if (stream) { stream.getTracks().forEach(track => { track.stop(); track.enabled = false; }); stream = null; } if (modal.parentNode) { document.body.removeChild(modal); } }; // 事件绑定 captureBtn.addEventListener('click', capturePhoto); cancelBtn.addEventListener('click', () => { closeModal(); reject(new Error('用户取消了拍照')); }); // 错误处理 const handleError = (error) => { closeModal(); reject(error); }; // 初始化相机 initCamera().catch(handleError); }); } // 创建相机弹层(优化样式) function createCameraModal() { const modal = document.createElement('div'); modal.className = 'camera-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10000; `; const videoContainer = document.createElement('div'); videoContainer.style.cssText = ` position: relative; width: 80%; max-width: 100vh; height: 60vh; display: flex; justify-content: center; align-items: center; overflow: hidden; background-color: #000; border-radius: 8px; `; const video = document.createElement('video'); video.className = 'camera-preview'; video.autoplay = true; video.playsInline = true; video.style.cssText = ` position: absolute; transform-origin: center center; object-fit: contain; `; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` margin-top: 20px; display: flex; gap: 20px; `; const captureBtn = document.createElement('button'); captureBtn.className = 'capture-btn'; captureBtn.textContent = '拍照'; captureBtn.style.cssText = ` padding: 12px 24px; background-color: #4CAF50; color: white; border: none; border-radius: 50px; cursor: pointer; font-size: 16px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); transition: all 0.3s; `; const cancelBtn = document.createElement('button'); cancelBtn.className = 'cancel-btn'; cancelBtn.textContent = '取消'; cancelBtn.style.cssText = ` padding: 12px 24px; background-color: #f44336; color: white; border: none; border-radius: 50px; cursor: pointer; font-size: 16px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); transition: all 0.3s; `; // 悬停效果 captureBtn.onmouseenter = () => captureBtn.style.transform = 'scale(1.05)'; captureBtn.onmouseleave = () => captureBtn.style.transform = 'scale(1)'; cancelBtn.onmouseenter = () => cancelBtn.style.transform = 'scale(1.05)'; cancelBtn.onmouseleave = () => cancelBtn.style.transform = 'scale(1)'; videoContainer.appendChild(video); buttonContainer.appendChild(captureBtn); buttonContainer.appendChild(cancelBtn); modal.appendChild(videoContainer); modal.appendChild(buttonContainer); return modal; }