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