UNPKG

id-scanner-lib

Version:

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

319 lines (273 loc) 7.68 kB
/** * @file 摄像头流管理器 * @description 管理摄像头视频流的创建、销毁、暂停和恢复 * @module core/camera-stream-manager */ import { EventEmitter } from './event-emitter'; import { Logger } from './logger'; import { CameraAccessError } from './errors'; import { Disposable } from '../utils/resource-manager'; /** * 摄像头流状态 */ export enum StreamState { /** 空闲 */ IDLE = 'idle', /** 启动中 */ STARTING = 'starting', /** 活动中 */ ACTIVE = 'active', /** 已暂停 */ PAUSED = 'paused', /** 已停止 */ STOPPED = 'stopped', /** 错误 */ ERROR = 'error' } /** * 摄像头流事件 */ export enum CameraStreamEvent { /** 流启动 */ START = 'stream:start', /** 流停止 */ STOP = 'stream:stop', /** 流暂停 */ PAUSE = 'stream:pause', /** 流恢复 */ RESUME = 'stream:resume', /** 流错误 */ ERROR = 'stream:error', /** 轨道结束 */ TRACK_ENDED = 'stream:track:ended', /** 分辨率变化 */ RESOLUTION_CHANGE = 'stream:resolution:change' } /** * 摄像头流管理器接口 */ export interface ICameraStreamManager { /** 获取媒体流 */ getStream(): MediaStream | null; /** 是否活动 */ isActive(): boolean; /** 获取当前状态 */ getState(): StreamState; /** 释放资源 */ dispose(): Promise<void>; } /** * 摄像头流管理器 * 负责摄像头视频流的生命周期管理 */ export class CameraStreamManager extends EventEmitter implements ICameraStreamManager, Disposable { /** 日志记录器 */ private readonly logger: Logger; /** 媒体流 */ private mediaStream: MediaStream | null = null; /** 流状态 */ private state: StreamState = StreamState.IDLE; /** 视频轨道 */ private videoTrack: MediaStreamTrack | null = null; /** 音频轨道 */ private audioTrack: MediaStreamTrack | null = null; /** 视频元素引用 */ private videoElement: HTMLVideoElement | null = null; /** 是否已暂停(用于pause/resume) */ private isPaused: boolean = false; /** * 构造函数 */ constructor() { super(); this.logger = Logger.getInstance(); } /** * 获取媒体流 */ getStream(): MediaStream | null { return this.mediaStream; } /** * 是否活动 */ isActive(): boolean { return this.state === StreamState.ACTIVE && !this.isPaused; } /** * 获取当前状态 */ getState(): StreamState { return this.state; } /** * 启动摄像头流 * @param constraints 媒体约束 * @param videoElement 可选的视频元素 */ async start( constraints: MediaStreamConstraints, videoElement?: HTMLVideoElement | null ): Promise<MediaStream> { if (this.state === StreamState.ACTIVE && this.mediaStream) { this.logger.debug('CameraStreamManager', 'Stream already active'); return this.mediaStream; } this.state = StreamState.STARTING; this.logger.debug('CameraStreamManager', `Requesting camera access: ${JSON.stringify(constraints)}`); try { // 停止旧流 this.stopStreamOnly(); // 获取新流 const stream = await navigator.mediaDevices.getUserMedia(constraints); this.mediaStream = stream; // 获取轨道 const tracks = stream.getTracks(); this.videoTrack = tracks.find(t => t.kind === 'video') || null; this.audioTrack = tracks.find(t => t.kind === 'audio') || null; // 监听轨道结束事件 if (this.videoTrack) { this.videoTrack.onended = this.handleTrackEnded.bind(this); } // 保存视频元素引用 this.videoElement = videoElement || null; // 将流连接到视频元素 if (this.videoElement) { this.videoElement.srcObject = stream; } this.state = StreamState.ACTIVE; this.isPaused = false; this.emit(CameraStreamEvent.START, { stream, deviceId: this.videoTrack?.getSettings().deviceId, settings: this.videoTrack?.getSettings() }); return stream; } catch (error) { this.state = StreamState.ERROR; const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('CameraStreamManager', `Failed to start stream: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); const cameraError = new CameraAccessError(errorMessage); this.emit(CameraStreamEvent.ERROR, { error: cameraError }); throw cameraError; } } /** * 停止摄像头流 */ stop(): void { if (this.state !== StreamState.ACTIVE && this.state !== StreamState.PAUSED) { return; } // 停止所有轨道 this.stopStreamOnly(); // 断开视频元素连接 if (this.videoElement) { this.videoElement.pause(); this.videoElement.srcObject = null; this.videoElement = null; } this.state = StreamState.STOPPED; this.isPaused = false; this.emit(CameraStreamEvent.STOP); } /** * 暂停摄像头流 */ pause(): void { if (this.state !== StreamState.ACTIVE || this.isPaused) { return; } // 暂停视频元素 if (this.videoElement) { this.videoElement.pause(); } this.isPaused = true; this.state = StreamState.PAUSED; this.emit(CameraStreamEvent.PAUSE); } /** * 恢复摄像头流 * @returns 是否成功恢复 */ async resume(): Promise<boolean> { if (this.state !== StreamState.PAUSED || !this.isPaused) { return false; } if (this.videoElement && this.videoElement.paused && this.mediaStream) { try { await this.videoElement.play(); this.isPaused = false; this.state = StreamState.ACTIVE; this.emit(CameraStreamEvent.RESUME); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('CameraStreamManager', `Failed to resume stream: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage)); return false; } } return false; } /** * 连接到视频元素 * @param videoElement 视频元素 */ attachToVideoElement(videoElement: HTMLVideoElement): void { this.videoElement = videoElement; if (this.mediaStream) { videoElement.srcObject = this.mediaStream; } } /** * 断开视频元素连接 */ detachVideoElement(): void { if (this.videoElement) { this.videoElement.pause(); this.videoElement.srcObject = null; this.videoElement = null; } } /** * 获取视频轨道设置 */ getVideoSettings(): MediaTrackSettings | null { return this.videoTrack?.getSettings() || null; } /** * 获取视频轨道 */ getVideoTrack(): MediaStreamTrack | null { return this.videoTrack; } /** * 释放资源 */ async dispose(): Promise<void> { this.stop(); this.videoTrack = null; this.audioTrack = null; this.mediaStream = null; this.state = StreamState.IDLE; this.logger.debug('CameraStreamManager', 'CameraStreamManager resources disposed'); } /** * 停止媒体流(仅停止轨道不断开视频元素) */ private stopStreamOnly(): void { if (this.mediaStream) { this.mediaStream.getTracks().forEach(track => track.stop()); this.mediaStream = null; } this.videoTrack = null; this.audioTrack = null; } /** * 处理轨道结束事件 */ private handleTrackEnded(): void { this.logger.debug('CameraStreamManager', 'Camera track ended'); this.emit(CameraStreamEvent.TRACK_ENDED); } }