id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
781 lines (660 loc) • 21.4 kB
text/typescript
/**
* @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));
}
}
}