UNPKG

id-scanner-lib

Version:

Browser-based ID card, QR code, and face recognition scanner with liveness detection

781 lines (660 loc) 21.4 kB
/** * @file 摄像头管理器 * @description 提供摄像头控制和视频流管理功能 * @module core/camera-manager */ import { EventEmitter } from './event-emitter'; import { Logger } from './logger'; import { ConfigManager } from './config'; import { Result } from './result'; import { CameraAccessError, DeviceError } from './errors'; import { getMediaConstraints } from '../utils'; import { CameraStreamManager, StreamState } from './camera-stream-manager'; /** * 摄像头设备信息 */ export interface CameraDevice { /** 设备ID */ deviceId: string; /** 设备标签(名称) */ label: string; /** 是否为前置摄像头 */ isFront: boolean; } /** * 摄像头状态 */ export enum CameraStatus { /** 未初始化 */ NOT_INITIALIZED = 'not_initialized', /** 初始化中 */ INITIALIZING = 'initializing', /** 就绪 */ READY = 'ready', /** 活动中 */ ACTIVE = 'active', /** 暂停 */ PAUSED = 'paused', /** 已停止 */ STOPPED = 'stopped', /** 错误状态 */ ERROR = 'error' } /** * 摄像头事件 */ export enum CameraEvent { /** 摄像头初始化开始 */ INITIALIZING = 'camera:initializing', /** 摄像头初始化完成 */ READY = 'camera:ready', /** 摄像头开始 */ START = 'camera:start', /** 摄像头暂停 */ PAUSE = 'camera:pause', /** 摄像头恢复 */ RESUME = 'camera:resume', /** 摄像头停止 */ STOP = 'camera:stop', /** 摄像头错误 */ ERROR = 'camera:error', /** 摄像头切换 */ SWITCH = 'camera:switch', /** 媒体流轨道结束 */ TRACK_ENDED = 'camera:track:ended', /** 摄像头分辨率变化 */ RESOLUTION_CHANGE = 'camera:resolution:change', /** 摄像头帧处理 */ FRAME = 'camera:frame' } /** * 摄像头初始化选项 */ export interface CameraOptions { /** 目标视频元素 */ videoElement?: HTMLVideoElement; /** 自动开始 */ autoStart?: boolean; /** 宽度 */ width?: number; /** 高度 */ height?: number; /** 帧率 */ frameRate?: number; /** 摄像头朝向 */ facingMode?: 'user' | 'environment'; /** 摄像头设备ID */ deviceId?: string; /** 启用帧处理 */ enableFrameProcessing?: boolean; /** 帧处理间隔(ms) */ frameProcessingInterval?: number; } /** * 摄像头管理类 * 提供摄像头控制和视频流管理功能 */ export class CameraManager extends EventEmitter { /** 单例实例 */ private static instance: CameraManager; /** 日志记录器 */ private readonly logger: Logger; /** 配置管理器 */ private readonly config: ConfigManager; /** 视频元素 */ private videoElement: HTMLVideoElement | null = null; /** 摄像头流管理器 */ private streamManager: CameraStreamManager; /** 摄像头状态 */ private status: CameraStatus = CameraStatus.NOT_INITIALIZED; /** 可用的摄像头设备列表 */ private devices: CameraDevice[] = []; /** 当前活动的摄像头设备 */ private activeDeviceId: string | null = null; /** 帧处理计时器ID */ private frameProcessingTimerId: number | null = null; /** 是否启用帧处理 */ private frameProcessingEnabled: boolean = false; /** 帧处理间隔(ms) */ private frameProcessingInterval: number = 100; /** 视频准备就绪的Promise */ private videoReadyPromise: Promise<void> | null = null; /** 视频准备就绪的Promise解析函数 */ private videoReadyResolver: (() => void) | null = null; /** Canvas元素,用于帧处理 */ private canvas: HTMLCanvasElement | null = null; /** Canvas 2D上下文 */ private canvasCtx: CanvasRenderingContext2D | null = null; /** * 私有构造函数 */ private constructor() { super(); this.logger = Logger.getInstance(); this.config = ConfigManager.getInstance(); this.streamManager = new CameraStreamManager(); // 转发流管理器的轨道结束事件 this.streamManager.on('stream:track:ended', () => { this.emit(CameraEvent.TRACK_ENDED); this.stop(); }); // 转发流管理器的分辨率变化事件 this.streamManager.on('stream:resolution:change', (data: any) => { this.emit(CameraEvent.RESOLUTION_CHANGE, data); }); } /** * 获取单例实例 */ public static getInstance(): CameraManager { if (!CameraManager.instance) { CameraManager.instance = new CameraManager(); } return CameraManager.instance; } /** * 初始化摄像头 * @param options 初始化选项 */ async init(options: CameraOptions = {}): Promise<Result<boolean>> { if (this.status !== CameraStatus.NOT_INITIALIZED && this.status !== CameraStatus.ERROR) { this.logger.warn('CameraManager', `Camera is already initialized with status: ${this.status}`); return Result.success(true); } this.status = CameraStatus.INITIALIZING; this.emit(CameraEvent.INITIALIZING); try { // 检查浏览器支持 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new CameraAccessError('Your browser does not support camera access'); } // 配置视频元素 if (options.videoElement) { this.setVideoElement(options.videoElement); } else { this.createVideoElement(); } // 启用帧处理 if (options.enableFrameProcessing !== undefined) { this.frameProcessingEnabled = options.enableFrameProcessing; if (options.frameProcessingInterval) { this.frameProcessingInterval = options.frameProcessingInterval; } if (this.frameProcessingEnabled) { this.initCanvas(); } } // 加载设备列表 await this.loadDevices(); this.status = CameraStatus.READY; this.emit(CameraEvent.READY); // 自动开始 if (options.autoStart) { const deviceId = options.deviceId || (options.facingMode === 'user' ? this.getFrontCamera()?.deviceId : this.getBackCamera()?.deviceId); await this.start({ deviceId, width: options.width, height: options.height, frameRate: options.frameRate, facingMode: options.facingMode }); } return Result.success(true); } catch (error) { this.status = CameraStatus.ERROR; const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('CameraManager', `Failed to initialize camera: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); const cameraError = error instanceof CameraAccessError ? error : new CameraAccessError(errorMessage); this.emit(CameraEvent.ERROR, { error: cameraError }); return Result.failure(cameraError); } } /** * 开始摄像头 * @param options 摄像头选项 */ async start(options: { deviceId?: string; width?: number; height?: number; frameRate?: number; facingMode?: 'user' | 'environment'; } = {}): Promise<Result<boolean>> { if (this.status === CameraStatus.ACTIVE) { this.logger.debug('CameraManager', 'Camera is already active'); return Result.success(true); } if (this.status !== CameraStatus.READY && this.status !== CameraStatus.STOPPED && this.status !== CameraStatus.PAUSED) { const error = new CameraAccessError(`Camera is not ready (status: ${this.status})`); return Result.failure(error); } try { // 构建媒体约束 const width = options.width || this.config.get('camera.resolution.width', 1280); const height = options.height || this.config.get('camera.resolution.height', 720); const frameRate = options.frameRate || this.config.get('camera.frameRate', 30); const facingMode = options.facingMode || this.config.get('camera.facingMode', 'environment'); let constraints: MediaStreamConstraints; if (options.deviceId) { // 使用指定的设备ID constraints = { video: { deviceId: { exact: options.deviceId }, width: { ideal: width }, height: { ideal: height }, frameRate: { ideal: frameRate } }, audio: false }; this.activeDeviceId = options.deviceId; } else { // 使用facingMode constraints = getMediaConstraints(width, height, facingMode, frameRate); } // 使用流管理器启动摄像头 const stream = await this.streamManager.start(constraints, this.videoElement); // 获取实际选择的设备ID const videoTrack = stream.getVideoTracks()[0]; if (videoTrack) { this.activeDeviceId = videoTrack.getSettings().deviceId || null; } // 创建视频准备就绪Promise this.createVideoReadyPromise(); // 开始播放 if (this.videoElement) { const playPromise = this.videoElement.play(); if (playPromise) { await playPromise; } // 等待视频准备就绪 await this.waitForVideoReady(); // 开始帧处理 if (this.frameProcessingEnabled) { this.startFrameProcessing(); } } this.status = CameraStatus.ACTIVE; this.emit(CameraEvent.START, { stream, deviceId: this.activeDeviceId, settings: videoTrack?.getSettings() }); return Result.success(true); } catch (error) { this.status = CameraStatus.ERROR; const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('CameraManager', `Failed to start camera: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); const cameraError = new CameraAccessError(errorMessage); this.emit(CameraEvent.ERROR, { error: cameraError }); return Result.failure(cameraError); } } /** * 暂停摄像头 */ pause(): boolean { if (this.status !== CameraStatus.ACTIVE) { return false; } this.streamManager.pause(); // 暂停帧处理 this.stopFrameProcessing(); this.status = CameraStatus.PAUSED; this.emit(CameraEvent.PAUSE); return true; } /** * 恢复摄像头 */ async resume(): Promise<boolean> { if (this.status !== CameraStatus.PAUSED) { return false; } const resumed = await this.streamManager.resume(); if (resumed) { // 恢复帧处理 if (this.frameProcessingEnabled) { this.startFrameProcessing(); } this.status = CameraStatus.ACTIVE; this.emit(CameraEvent.RESUME); return true; } return false; } /** * 停止摄像头 */ stop(): boolean { if (this.status !== CameraStatus.ACTIVE && this.status !== CameraStatus.PAUSED) { return false; } // 停止帧处理 this.stopFrameProcessing(); // 停止视频元素 if (this.videoElement) { this.videoElement.pause(); this.videoElement.srcObject = null; } // 停止流 this.streamManager.stop(); this.status = CameraStatus.STOPPED; this.emit(CameraEvent.STOP); return true; } /** * 切换摄像头 */ async switchCamera(): Promise<Result<boolean>> { // 确保有多个摄像头 if (this.devices.length <= 1) { return Result.failure(new DeviceError('No alternative camera found')); } // 查找当前活动摄像头的索引 const currentIndex = this.activeDeviceId ? this.devices.findIndex(dev => dev.deviceId === this.activeDeviceId) : -1; // 获取下一个摄像头的索引 const nextIndex = (currentIndex === -1 || currentIndex === this.devices.length - 1) ? 0 : currentIndex + 1; const nextDevice = this.devices[nextIndex]; try { // 停止当前摄像头 this.stop(); // 启动新摄像头 const result = await this.start({ deviceId: nextDevice.deviceId }); if (result.isSuccess()) { this.emit(CameraEvent.SWITCH, { previousDeviceId: this.activeDeviceId, currentDeviceId: nextDevice.deviceId, isFront: nextDevice.isFront }); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return Result.failure(new CameraAccessError(`Failed to switch camera: ${errorMessage}`)); } } /** * 加载可用的摄像头设备列表 */ async loadDevices(): Promise<CameraDevice[]> { try { // 请求媒体设备权限 if (!this.streamManager.getStream()) { // 短暂获取摄像头权限以列出设备标签 const tempStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); // 立即停止临时流 tempStream.getTracks().forEach(track => track.stop()); } // 获取设备列表 const devices = await navigator.mediaDevices.enumerateDevices(); // 过滤出视频输入设备 const videoDevices = devices.filter(device => device.kind === 'videoinput'); // 映射到摄像头设备 this.devices = videoDevices.map(device => { // 尝试判断是前置还是后置摄像头 let isFront = false; if (device.label.toLowerCase().includes('front') || device.label.toLowerCase().includes('facetime') || device.label.toLowerCase().includes('user')) { isFront = true; } return { deviceId: device.deviceId, label: device.label || `Camera ${this.devices.length + 1}`, isFront }; }); return this.devices; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('CameraManager', `Failed to load devices: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); throw new CameraAccessError(`Failed to load camera devices: ${errorMessage}`); } } /** * 获取前置摄像头 */ getFrontCamera(): CameraDevice | undefined { return this.devices.find(device => device.isFront); } /** * 获取后置摄像头 */ getBackCamera(): CameraDevice | undefined { return this.devices.find(device => !device.isFront); } /** * 获取所有摄像头设备 */ getDevices(): CameraDevice[] { return [...this.devices]; } /** * 获取当前活动的设备ID */ getActiveDeviceId(): string | null { return this.activeDeviceId; } /** * 获取当前活动的摄像头设备 */ getActiveDevice(): CameraDevice | undefined { if (!this.activeDeviceId) return undefined; return this.devices.find(device => device.deviceId === this.activeDeviceId); } /** * 获取当前媒体流 */ getMediaStream(): MediaStream | null { return this.streamManager.getStream(); } /** * 获取视频元素 */ getVideoElement(): HTMLVideoElement | null { return this.videoElement; } /** * 设置视频元素 * @param element 视频元素 */ setVideoElement(element: HTMLVideoElement): void { this.videoElement = element; // 设置视频元素属性 this.videoElement.autoplay = true; this.videoElement.playsInline = true; // iOS需要 this.videoElement.muted = true; // 如果流已活动,附加到视频元素 if (this.streamManager.getStream() && this.status === CameraStatus.ACTIVE) { this.videoElement.srcObject = this.streamManager.getStream(); this.videoElement.play().catch(error => { this.logger.error('CameraManager', `Failed to play video: ${error.message}`, error); }); } } /** * 创建视频元素 */ private createVideoElement(): HTMLVideoElement { if (!this.videoElement) { this.videoElement = document.createElement('video'); this.setVideoElement(this.videoElement); } return this.videoElement; } /** * 捕获当前画面 * @param format 图像格式 * @param quality 图像质量(0-1) */ captureFrame(format: 'image/png' | 'image/jpeg' = 'image/jpeg', quality: number = 0.95): string | null { if (this.status !== CameraStatus.ACTIVE || !this.videoElement) { return null; } // 确保画布已初始化 this.initCanvas(); const video = this.videoElement; const canvas = this.canvas!; const ctx = this.canvasCtx!; // 设置画布大小与视频一致 canvas.width = video.videoWidth; canvas.height = video.videoHeight; // 绘制视频帧 ctx.drawImage(video, 0, 0); // 返回图像数据 return canvas.toDataURL(format, quality); } /** * 捕获帧并返回ImageData */ captureFrameData(): ImageData | null { if (this.status !== CameraStatus.ACTIVE || !this.videoElement) { return null; } // 确保画布已初始化 this.initCanvas(); const video = this.videoElement; const canvas = this.canvas!; const ctx = this.canvasCtx!; // 设置画布大小与视频一致 canvas.width = video.videoWidth; canvas.height = video.videoHeight; // 绘制视频帧 ctx.drawImage(video, 0, 0); // 返回图像数据 return ctx.getImageData(0, 0, canvas.width, canvas.height); } /** * 获取当前状态 */ getStatus(): CameraStatus { return this.status; } /** * 检查摄像头是否活动 */ isActive(): boolean { return this.status === CameraStatus.ACTIVE; } /** * 初始化Canvas */ private initCanvas(): void { if (!this.canvas) { this.canvas = document.createElement('canvas'); this.canvasCtx = this.canvas.getContext('2d'); } } /** * 释放资源 */ dispose(): void { this.stop(); // 释放流管理器资源 this.streamManager.dispose(); if (this.canvas) { // 清空 canvas 内容 const ctx = this.canvas.getContext('2d'); if (ctx) ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.canvas.width = 0; this.canvas.height = 0; this.canvas = null; this.canvasCtx = null; } // 释放 video 元素 if (this.videoElement) { this.videoElement.srcObject = null; this.videoElement.load(); this.videoElement = null; } this.status = CameraStatus.NOT_INITIALIZED; this.logger.debug('CameraManager', 'Camera resources disposed'); } /** * 创建视频准备就绪的Promise */ private createVideoReadyPromise(): void { this.videoReadyPromise = new Promise((resolve) => { this.videoReadyResolver = resolve; if (!this.videoElement) { resolve(); return; } // 如果视频已经有足够的数据,直接解析 if (this.videoElement.readyState >= 2) { // HAVE_CURRENT_DATA resolve(); return; } // 否则等待loadeddata事件 const handleVideoReady = () => { if (this.videoElement) { this.videoElement.removeEventListener('loadeddata', handleVideoReady); // 发出分辨率变化事件 this.emit(CameraEvent.RESOLUTION_CHANGE, { width: this.videoElement.videoWidth, height: this.videoElement.videoHeight }); if (this.videoReadyResolver) { this.videoReadyResolver(); this.videoReadyResolver = null; } } }; this.videoElement.addEventListener('loadeddata', handleVideoReady); }); } /** * 等待视频准备就绪 */ private async waitForVideoReady(): Promise<void> { if (this.videoReadyPromise) { await this.videoReadyPromise; } } /** * 开始帧处理 */ private startFrameProcessing(): void { if (!this.frameProcessingEnabled || this.frameProcessingTimerId !== null) { return; } this.frameProcessingTimerId = window.setInterval(() => { this.processFrame(); }, this.frameProcessingInterval); } /** * 停止帧处理 */ private stopFrameProcessing(): void { if (this.frameProcessingTimerId !== null) { clearInterval(this.frameProcessingTimerId); this.frameProcessingTimerId = null; } } /** * 处理当前帧 */ private processFrame(): void { if (this.status !== CameraStatus.ACTIVE || !this.videoElement) { return; } try { const frameData = this.captureFrameData(); if (frameData) { this.emit(CameraEvent.FRAME, { frameData, timestamp: Date.now(), width: frameData.width, height: frameData.height }); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('CameraManager', `Frame processing error: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); } } }