id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
181 lines (161 loc) • 5.14 kB
text/typescript
/* eslint-disable */
/**
* @file 相机工具类
* @description 提供访问和控制设备摄像头的功能
* @module Camera
*/
import { Logger } from '../core/logger';
/**
* 相机配置选项接口
*
* @interface CameraOptions
* @property {number} [width] - 视频宽度,默认为640
* @property {number} [height] - 视频高度,默认为480
* @property {string} [facingMode] - 摄像头朝向,'user'为前置摄像头,'environment'为后置摄像头,默认为'environment'
*/
export interface CameraOptions {
width?: number;
height?: number;
facingMode?: 'user' | 'environment';
}
/**
* 相机工具类
*
* 提供访问设备摄像头、获取视频流以及捕获图像帧的功能
*
* @example
* ```typescript
* // 创建相机实例
* const camera = new Camera({
* width: 1280,
* height: 720,
* facingMode: 'environment' // 使用后置摄像头
* });
*
* // 初始化相机
* const videoElement = document.getElementById('video') as HTMLVideoElement;
* await camera.start(videoElement);
*
* // 捕获当前视频帧
* const imageData = camera.captureFrame();
*
* // 使用结束后释放资源
* camera.stop();
* ```
*/
export class Camera {
private stream: MediaStream | null = null;
private videoElement: HTMLVideoElement | null = null;
/**
* 创建相机实例
* @param {CameraOptions} [options] - 相机配置选项
*/
constructor(private options: CameraOptions = {}) {
this.options = {
width: 640,
height: 480,
facingMode: 'environment',
...options
};
}
/**
* 启动摄像头并将视频流绑定到视频元素
* @param videoElement HTML视频元素
* @returns Promise<void>
*/
async start(videoElement: HTMLVideoElement): Promise<void> {
return this.initialize(videoElement);
}
/**
* 停止摄像头并释放资源
*/
stop(): void {
this.release();
}
/**
* 初始化相机,获取视频流并绑定到视频元素
*
* @param {HTMLVideoElement} videoElement - 用于显示视频流的视频元素
* @returns {Promise<void>} 初始化完成的Promise
* @throws 如果无法访问摄像头,将抛出错误
*/
async initialize(videoElement: HTMLVideoElement): Promise<void> {
this.videoElement = videoElement;
try {
// 构建媒体约束
const constraints: MediaStreamConstraints = {
video: {
width: { ideal: this.options.width },
height: { ideal: this.options.height },
facingMode: this.options.facingMode
}
};
// 获取视频流
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
// 绑定到视频元素
if (this.videoElement) {
this.videoElement.srcObject = this.stream;
// 添加超时保护,防止永久挂起
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('摄像头初始化超时')), 10000); // 10秒超时
});
const playPromise = new Promise<void>((resolve, reject) => {
if (this.videoElement) {
this.videoElement.onloadedmetadata = () => {
if (this.videoElement) {
this.videoElement.play()
.then(() => resolve())
.catch(err => reject(err));
}
};
this.videoElement.onerror = () => reject(new Error('视频加载失败'));
} else {
reject(new Error('视频元素未初始化'));
}
});
await Promise.race([playPromise, timeoutPromise]);
}
} catch (error) {
const logger = Logger.getInstance();
logger.error('Camera', '无法访问摄像头', error instanceof Error ? error : undefined);
throw new Error('无法访问摄像头。请确保已授予摄像头访问权限,并且摄像头未被其他应用程序占用。');
}
}
/**
* 捕获当前视频帧
*
* @returns {ImageData|null} 视频帧的ImageData对象,如果未初始化则返回null
*/
captureFrame(): ImageData | null {
if (!this.videoElement) {
return null;
}
// 创建Canvas元素用于捕获视频帧
const canvas = document.createElement('canvas');
canvas.width = this.videoElement.videoWidth;
canvas.height = this.videoElement.videoHeight;
const context = canvas.getContext('2d');
if (!context) {
return null;
}
// 将视频内容绘制到Canvas中
context.drawImage(this.videoElement, 0, 0, canvas.width, canvas.height);
// 获取ImageData对象
return context.getImageData(0, 0, canvas.width, canvas.height);
}
/**
* 释放摄像头资源
*/
release(): void {
// 停止视频流的所有轨道
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
// 清除视频元素绑定
if (this.videoElement) {
this.videoElement.srcObject = null;
this.videoElement = null;
}
}
}