camera-serial-utils
Version:
A utility package for camera capture and serial communication using Web Serial API
263 lines (232 loc) • 8.64 kB
JavaScript
/**
* 打开指定相机拍照并返回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;
}