id-scanner-lib 1.3.3 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +324 -410
  2. package/dist/id-scanner-lib.esm.js +4826 -0
  3. package/dist/id-scanner-lib.esm.js.map +1 -0
  4. package/dist/id-scanner-lib.js +4858 -0
  5. package/dist/id-scanner-lib.js.map +1 -0
  6. package/dist/types/browser-image-compression.d.ts +19 -0
  7. package/dist/types/tesseract.d.ts +280 -0
  8. package/package.json +89 -78
  9. package/src/core/base-module.ts +78 -0
  10. package/src/core/camera-manager.ts +813 -0
  11. package/src/core/config.ts +305 -0
  12. package/src/core/errors.ts +174 -0
  13. package/src/core/event-emitter.test.ts +42 -0
  14. package/src/core/event-emitter.ts +110 -0
  15. package/src/core/loading-state.test.ts +67 -0
  16. package/src/core/loading-state.ts +156 -0
  17. package/src/core/logger.test.ts +49 -0
  18. package/src/core/logger.ts +549 -0
  19. package/src/core/module-manager.ts +163 -0
  20. package/src/core/plugin-manager.ts +429 -0
  21. package/src/core/resource-manager.ts +762 -0
  22. package/src/core/result.ts +163 -0
  23. package/src/core/scanner-factory.ts +236 -0
  24. package/src/index.ts +117 -939
  25. package/src/interfaces/external-types.ts +200 -0
  26. package/src/interfaces/face-detection.ts +309 -0
  27. package/src/interfaces/scanner-module.ts +384 -0
  28. package/src/modules/face/face-detector.ts +988 -0
  29. package/src/modules/face/index.ts +208 -0
  30. package/src/modules/face/liveness-detector.ts +908 -0
  31. package/src/modules/face/types.ts +133 -0
  32. package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +274 -240
  33. package/src/modules/id-card/id-card-detector.ts +474 -0
  34. package/src/modules/id-card/index.ts +425 -0
  35. package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
  36. package/src/modules/id-card/ocr-worker.ts +259 -0
  37. package/src/modules/id-card/types.ts +178 -0
  38. package/src/modules/qrcode/index.ts +175 -0
  39. package/src/modules/qrcode/qr-code-scanner.ts +231 -0
  40. package/src/modules/qrcode/types.ts +169 -0
  41. package/src/types/common.test.ts +99 -0
  42. package/src/types/common.ts +166 -0
  43. package/src/types/tesseract.d.ts +265 -22
  44. package/src/utils/camera.test.ts +30 -0
  45. package/src/utils/camera.ts +4 -1
  46. package/src/utils/error-handler.test.ts +137 -0
  47. package/src/utils/error-handler.ts +110 -0
  48. package/src/utils/image-processing.ts +68 -49
  49. package/src/utils/index.test.ts +186 -0
  50. package/src/utils/index.ts +429 -0
  51. package/src/utils/performance.ts +168 -131
  52. package/src/utils/resource-manager.ts +65 -146
  53. package/src/utils/retry.test.ts +142 -0
  54. package/src/utils/retry.ts +282 -0
  55. package/src/utils/types.ts +90 -2
  56. package/src/utils/utils.test.ts +171 -0
  57. package/src/utils/worker.ts +123 -84
  58. package/src/version.ts +11 -0
  59. package/tools/scaffold.js +543 -0
  60. package/dist/id-scanner-core.esm.js +0 -11349
  61. package/dist/id-scanner-core.js +0 -11361
  62. package/dist/id-scanner-core.min.js +0 -1
  63. package/dist/id-scanner-ocr.esm.js +0 -2319
  64. package/dist/id-scanner-ocr.js +0 -2328
  65. package/dist/id-scanner-ocr.min.js +0 -1
  66. package/dist/id-scanner-qr.esm.js +0 -1296
  67. package/dist/id-scanner-qr.js +0 -1305
  68. package/dist/id-scanner-qr.min.js +0 -1
  69. package/dist/id-scanner.js +0 -4561
  70. package/dist/id-scanner.min.js +0 -1
  71. package/src/core.ts +0 -138
  72. package/src/demo/demo.ts +0 -204
  73. package/src/id-recognition/data-extractor.ts +0 -262
  74. package/src/id-recognition/id-detector.ts +0 -510
  75. package/src/id-recognition/ocr-worker.ts +0 -156
  76. package/src/index-umd.ts +0 -477
  77. package/src/ocr-module.ts +0 -187
  78. package/src/qr-module.ts +0 -179
  79. package/src/scanner/barcode-scanner.ts +0 -251
  80. package/src/scanner/qr-scanner.ts +0 -167
@@ -0,0 +1,813 @@
1
+ /**
2
+ * @file 摄像头管理器
3
+ * @description 提供摄像头控制和视频流管理功能
4
+ * @module core/camera-manager
5
+ */
6
+
7
+ import { EventEmitter } from './event-emitter';
8
+ import { Logger } from './logger';
9
+ import { ConfigManager } from './config';
10
+ import { Result } from './result';
11
+ import { CameraAccessError, DeviceError } from './errors';
12
+ import { getMediaConstraints } from '../utils';
13
+
14
+ /**
15
+ * 摄像头设备信息
16
+ */
17
+ export interface CameraDevice {
18
+ /** 设备ID */
19
+ deviceId: string;
20
+ /** 设备标签(名称) */
21
+ label: string;
22
+ /** 是否为前置摄像头 */
23
+ isFront: boolean;
24
+ }
25
+
26
+ /**
27
+ * 摄像头状态
28
+ */
29
+ export enum CameraStatus {
30
+ /** 未初始化 */
31
+ NOT_INITIALIZED = 'not_initialized',
32
+ /** 初始化中 */
33
+ INITIALIZING = 'initializing',
34
+ /** 就绪 */
35
+ READY = 'ready',
36
+ /** 活动中 */
37
+ ACTIVE = 'active',
38
+ /** 暂停 */
39
+ PAUSED = 'paused',
40
+ /** 已停止 */
41
+ STOPPED = 'stopped',
42
+ /** 错误状态 */
43
+ ERROR = 'error'
44
+ }
45
+
46
+ /**
47
+ * 摄像头事件
48
+ */
49
+ export enum CameraEvent {
50
+ /** 摄像头初始化开始 */
51
+ INITIALIZING = 'camera:initializing',
52
+ /** 摄像头初始化完成 */
53
+ READY = 'camera:ready',
54
+ /** 摄像头开始 */
55
+ START = 'camera:start',
56
+ /** 摄像头暂停 */
57
+ PAUSE = 'camera:pause',
58
+ /** 摄像头恢复 */
59
+ RESUME = 'camera:resume',
60
+ /** 摄像头停止 */
61
+ STOP = 'camera:stop',
62
+ /** 摄像头错误 */
63
+ ERROR = 'camera:error',
64
+ /** 摄像头切换 */
65
+ SWITCH = 'camera:switch',
66
+ /** 媒体流轨道结束 */
67
+ TRACK_ENDED = 'camera:track:ended',
68
+ /** 摄像头分辨率变化 */
69
+ RESOLUTION_CHANGE = 'camera:resolution:change',
70
+ /** 摄像头帧处理 */
71
+ FRAME = 'camera:frame'
72
+ }
73
+
74
+ /**
75
+ * 摄像头初始化选项
76
+ */
77
+ export interface CameraOptions {
78
+ /** 目标视频元素 */
79
+ videoElement?: HTMLVideoElement;
80
+ /** 自动开始 */
81
+ autoStart?: boolean;
82
+ /** 宽度 */
83
+ width?: number;
84
+ /** 高度 */
85
+ height?: number;
86
+ /** 帧率 */
87
+ frameRate?: number;
88
+ /** 摄像头朝向 */
89
+ facingMode?: 'user' | 'environment';
90
+ /** 摄像头设备ID */
91
+ deviceId?: string;
92
+ /** 启用帧处理 */
93
+ enableFrameProcessing?: boolean;
94
+ /** 帧处理间隔(ms) */
95
+ frameProcessingInterval?: number;
96
+ }
97
+
98
+ /**
99
+ * 摄像头管理类
100
+ * 提供摄像头控制和视频流管理功能
101
+ */
102
+ export class CameraManager extends EventEmitter {
103
+ /** 单例实例 */
104
+ private static instance: CameraManager;
105
+ /** 日志记录器 */
106
+ private readonly logger: Logger;
107
+ /** 配置管理器 */
108
+ private readonly config: ConfigManager;
109
+ /** 视频元素 */
110
+ private videoElement: HTMLVideoElement | null = null;
111
+ /** 媒体流 */
112
+ private mediaStream: MediaStream | null = null;
113
+ /** 摄像头状态 */
114
+ private status: CameraStatus = CameraStatus.NOT_INITIALIZED;
115
+ /** 可用的摄像头设备列表 */
116
+ private devices: CameraDevice[] = [];
117
+ /** 当前活动的摄像头设备 */
118
+ private activeDeviceId: string | null = null;
119
+ /** 帧处理计时器ID */
120
+ private frameProcessingTimerId: number | null = null;
121
+ /** 是否启用帧处理 */
122
+ private frameProcessingEnabled: boolean = false;
123
+ /** 帧处理间隔(ms) */
124
+ private frameProcessingInterval: number = 100;
125
+ /** 视频准备就绪的Promise */
126
+ private videoReadyPromise: Promise<void> | null = null;
127
+ /** 视频准备就绪的Promise解析函数 */
128
+ private videoReadyResolver: (() => void) | null = null;
129
+ /** Canvas元素,用于帧处理 */
130
+ private canvas: HTMLCanvasElement | null = null;
131
+ /** Canvas 2D上下文 */
132
+ private canvasCtx: CanvasRenderingContext2D | null = null;
133
+
134
+ /**
135
+ * 私有构造函数
136
+ */
137
+ private constructor() {
138
+ super();
139
+ this.logger = Logger.getInstance();
140
+ this.config = ConfigManager.getInstance();
141
+ }
142
+
143
+ /**
144
+ * 获取单例实例
145
+ */
146
+ public static getInstance(): CameraManager {
147
+ if (!CameraManager.instance) {
148
+ CameraManager.instance = new CameraManager();
149
+ }
150
+ return CameraManager.instance;
151
+ }
152
+
153
+ /**
154
+ * 初始化摄像头
155
+ * @param options 初始化选项
156
+ */
157
+ async init(options: CameraOptions = {}): Promise<Result<boolean>> {
158
+ if (this.status !== CameraStatus.NOT_INITIALIZED && this.status !== CameraStatus.ERROR) {
159
+ this.logger.warn('CameraManager', `Camera is already initialized with status: ${this.status}`);
160
+ return Result.success(true);
161
+ }
162
+
163
+ this.status = CameraStatus.INITIALIZING;
164
+ this.emit(CameraEvent.INITIALIZING);
165
+
166
+ try {
167
+ // 检查浏览器支持
168
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
169
+ throw new CameraAccessError('Your browser does not support camera access');
170
+ }
171
+
172
+ // 配置视频元素
173
+ if (options.videoElement) {
174
+ this.setVideoElement(options.videoElement);
175
+ } else {
176
+ this.createVideoElement();
177
+ }
178
+
179
+ // 启用帧处理
180
+ if (options.enableFrameProcessing !== undefined) {
181
+ this.frameProcessingEnabled = options.enableFrameProcessing;
182
+
183
+ if (options.frameProcessingInterval) {
184
+ this.frameProcessingInterval = options.frameProcessingInterval;
185
+ }
186
+
187
+ if (this.frameProcessingEnabled) {
188
+ this.initCanvas();
189
+ }
190
+ }
191
+
192
+ // 加载设备列表
193
+ await this.loadDevices();
194
+
195
+ this.status = CameraStatus.READY;
196
+ this.emit(CameraEvent.READY);
197
+
198
+ // 自动开始
199
+ if (options.autoStart) {
200
+ const deviceId = options.deviceId || (options.facingMode === 'user' ?
201
+ this.getFrontCamera()?.deviceId : this.getBackCamera()?.deviceId);
202
+
203
+ await this.start({
204
+ deviceId,
205
+ width: options.width,
206
+ height: options.height,
207
+ frameRate: options.frameRate,
208
+ facingMode: options.facingMode
209
+ });
210
+ }
211
+
212
+ return Result.success(true);
213
+ } catch (error) {
214
+ this.status = CameraStatus.ERROR;
215
+
216
+ const errorMessage = error instanceof Error ? error.message : String(error);
217
+ this.logger.error('CameraManager', `Failed to initialize camera: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
218
+
219
+ const cameraError = error instanceof CameraAccessError ?
220
+ error : new CameraAccessError(errorMessage);
221
+
222
+ this.emit(CameraEvent.ERROR, { error: cameraError });
223
+
224
+ return Result.failure(cameraError);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * 开始摄像头
230
+ * @param options 摄像头选项
231
+ */
232
+ async start(options: {
233
+ deviceId?: string;
234
+ width?: number;
235
+ height?: number;
236
+ frameRate?: number;
237
+ facingMode?: 'user' | 'environment';
238
+ } = {}): Promise<Result<boolean>> {
239
+ if (this.status === CameraStatus.ACTIVE) {
240
+ this.logger.debug('CameraManager', 'Camera is already active');
241
+ return Result.success(true);
242
+ }
243
+
244
+ if (this.status !== CameraStatus.READY && this.status !== CameraStatus.STOPPED && this.status !== CameraStatus.PAUSED) {
245
+ const error = new CameraAccessError(`Camera is not ready (status: ${this.status})`);
246
+ return Result.failure(error);
247
+ }
248
+
249
+ try {
250
+ // 构建媒体约束
251
+ const width = options.width || this.config.get('camera.resolution.width', 1280);
252
+ const height = options.height || this.config.get('camera.resolution.height', 720);
253
+ const frameRate = options.frameRate || this.config.get('camera.frameRate', 30);
254
+ const facingMode = options.facingMode || this.config.get('camera.facingMode', 'environment');
255
+
256
+ let constraints: MediaStreamConstraints;
257
+
258
+ if (options.deviceId) {
259
+ // 使用指定的设备ID
260
+ constraints = {
261
+ video: {
262
+ deviceId: { exact: options.deviceId },
263
+ width: { ideal: width },
264
+ height: { ideal: height },
265
+ frameRate: { ideal: frameRate }
266
+ },
267
+ audio: false
268
+ };
269
+ this.activeDeviceId = options.deviceId;
270
+ } else {
271
+ // 使用facingMode
272
+ constraints = getMediaConstraints(width, height, facingMode, frameRate);
273
+ }
274
+
275
+ // 获取媒体流
276
+ this.logger.debug('CameraManager', `Requesting camera access: ${JSON.stringify(constraints)}`);
277
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
278
+
279
+ // 停止旧的媒体流
280
+ this.stopMediaStream();
281
+
282
+ // 设置新的媒体流
283
+ this.mediaStream = stream;
284
+
285
+ // 获取实际选择的设备ID
286
+ const videoTrack = stream.getVideoTracks()[0];
287
+ if (videoTrack) {
288
+ this.activeDeviceId = videoTrack.getSettings().deviceId || null;
289
+
290
+ // 监听轨道结束事件
291
+ videoTrack.onended = this.handleTrackEnded.bind(this);
292
+ }
293
+
294
+ // 将流连接到视频元素
295
+ if (this.videoElement) {
296
+ this.videoElement.srcObject = stream;
297
+
298
+ // 创建视频准备就绪Promise
299
+ this.createVideoReadyPromise();
300
+
301
+ // 开始播放
302
+ const playPromise = this.videoElement.play();
303
+ if (playPromise) {
304
+ await playPromise;
305
+ }
306
+
307
+ // 等待视频准备就绪
308
+ await this.waitForVideoReady();
309
+
310
+ // 开始帧处理
311
+ if (this.frameProcessingEnabled) {
312
+ this.startFrameProcessing();
313
+ }
314
+ }
315
+
316
+ this.status = CameraStatus.ACTIVE;
317
+ this.emit(CameraEvent.START, {
318
+ stream,
319
+ deviceId: this.activeDeviceId,
320
+ settings: videoTrack?.getSettings()
321
+ });
322
+
323
+ return Result.success(true);
324
+ } catch (error) {
325
+ this.status = CameraStatus.ERROR;
326
+
327
+ const errorMessage = error instanceof Error ? error.message : String(error);
328
+ this.logger.error('CameraManager', `Failed to start camera: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
329
+
330
+ const cameraError = new CameraAccessError(errorMessage);
331
+ this.emit(CameraEvent.ERROR, { error: cameraError });
332
+
333
+ return Result.failure(cameraError);
334
+ }
335
+ }
336
+
337
+ /**
338
+ * 暂停摄像头
339
+ */
340
+ pause(): boolean {
341
+ if (this.status !== CameraStatus.ACTIVE) {
342
+ return false;
343
+ }
344
+
345
+ if (this.videoElement) {
346
+ this.videoElement.pause();
347
+ }
348
+
349
+ // 暂停帧处理
350
+ this.stopFrameProcessing();
351
+
352
+ this.status = CameraStatus.PAUSED;
353
+ this.emit(CameraEvent.PAUSE);
354
+
355
+ return true;
356
+ }
357
+
358
+ /**
359
+ * 恢复摄像头
360
+ */
361
+ async resume(): Promise<boolean> {
362
+ if (this.status !== CameraStatus.PAUSED) {
363
+ return false;
364
+ }
365
+
366
+ if (this.videoElement && this.videoElement.paused && this.mediaStream) {
367
+ try {
368
+ await this.videoElement.play();
369
+
370
+ // 恢复帧处理
371
+ if (this.frameProcessingEnabled) {
372
+ this.startFrameProcessing();
373
+ }
374
+
375
+ this.status = CameraStatus.ACTIVE;
376
+ this.emit(CameraEvent.RESUME);
377
+
378
+ return true;
379
+ } catch (error) {
380
+ const errorMessage = error instanceof Error ? error.message : String(error);
381
+ this.logger.error('CameraManager', `Failed to resume camera: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
382
+
383
+ this.emit(CameraEvent.ERROR, {
384
+ error: new CameraAccessError(`Resume failed: ${errorMessage}`)
385
+ });
386
+
387
+ return false;
388
+ }
389
+ }
390
+
391
+ return false;
392
+ }
393
+
394
+ /**
395
+ * 停止摄像头
396
+ */
397
+ stop(): boolean {
398
+ if (this.status !== CameraStatus.ACTIVE && this.status !== CameraStatus.PAUSED) {
399
+ return false;
400
+ }
401
+
402
+ // 停止帧处理
403
+ this.stopFrameProcessing();
404
+
405
+ // 停止视频元素
406
+ if (this.videoElement) {
407
+ this.videoElement.pause();
408
+ this.videoElement.srcObject = null;
409
+ }
410
+
411
+ // 停止媒体流
412
+ this.stopMediaStream();
413
+
414
+ this.status = CameraStatus.STOPPED;
415
+ this.emit(CameraEvent.STOP);
416
+
417
+ return true;
418
+ }
419
+
420
+ /**
421
+ * 切换摄像头
422
+ */
423
+ async switchCamera(): Promise<Result<boolean>> {
424
+ // 确保有多个摄像头
425
+ if (this.devices.length <= 1) {
426
+ return Result.failure(new DeviceError('No alternative camera found'));
427
+ }
428
+
429
+ // 查找当前活动摄像头的索引
430
+ const currentIndex = this.activeDeviceId ?
431
+ this.devices.findIndex(dev => dev.deviceId === this.activeDeviceId) : -1;
432
+
433
+ // 获取下一个摄像头的索引
434
+ const nextIndex = (currentIndex === -1 || currentIndex === this.devices.length - 1) ?
435
+ 0 : currentIndex + 1;
436
+
437
+ const nextDevice = this.devices[nextIndex];
438
+
439
+ try {
440
+ // 停止当前摄像头
441
+ this.stop();
442
+
443
+ // 启动新摄像头
444
+ const result = await this.start({ deviceId: nextDevice.deviceId });
445
+
446
+ if (result.isSuccess()) {
447
+ this.emit(CameraEvent.SWITCH, {
448
+ previousDeviceId: this.activeDeviceId,
449
+ currentDeviceId: nextDevice.deviceId,
450
+ isFront: nextDevice.isFront
451
+ });
452
+ }
453
+
454
+ return result;
455
+ } catch (error) {
456
+ const errorMessage = error instanceof Error ? error.message : String(error);
457
+ return Result.failure(new CameraAccessError(`Failed to switch camera: ${errorMessage}`));
458
+ }
459
+ }
460
+
461
+ /**
462
+ * 加载可用的摄像头设备列表
463
+ */
464
+ async loadDevices(): Promise<CameraDevice[]> {
465
+ try {
466
+ // 请求媒体设备权限
467
+ if (!this.mediaStream) {
468
+ // 短暂获取摄像头权限以列出设备标签
469
+ const tempStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
470
+
471
+ // 立即停止临时流
472
+ tempStream.getTracks().forEach(track => track.stop());
473
+ }
474
+
475
+ // 获取设备列表
476
+ const devices = await navigator.mediaDevices.enumerateDevices();
477
+
478
+ // 过滤出视频输入设备
479
+ const videoDevices = devices.filter(device => device.kind === 'videoinput');
480
+
481
+ // 映射到摄像头设备
482
+ this.devices = videoDevices.map(device => {
483
+ // 尝试判断是前置还是后置摄像头
484
+ let isFront = false;
485
+
486
+ if (device.label.toLowerCase().includes('front') ||
487
+ device.label.toLowerCase().includes('facetime') ||
488
+ device.label.toLowerCase().includes('user')) {
489
+ isFront = true;
490
+ }
491
+
492
+ return {
493
+ deviceId: device.deviceId,
494
+ label: device.label || `Camera ${this.devices.length + 1}`,
495
+ isFront
496
+ };
497
+ });
498
+
499
+ return this.devices;
500
+ } catch (error) {
501
+ const errorMessage = error instanceof Error ? error.message : String(error);
502
+ this.logger.error('CameraManager', `Failed to load devices: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
503
+
504
+ throw new CameraAccessError(`Failed to load camera devices: ${errorMessage}`);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * 获取前置摄像头
510
+ */
511
+ getFrontCamera(): CameraDevice | undefined {
512
+ return this.devices.find(device => device.isFront);
513
+ }
514
+
515
+ /**
516
+ * 获取后置摄像头
517
+ */
518
+ getBackCamera(): CameraDevice | undefined {
519
+ return this.devices.find(device => !device.isFront);
520
+ }
521
+
522
+ /**
523
+ * 获取所有摄像头设备
524
+ */
525
+ getDevices(): CameraDevice[] {
526
+ return [...this.devices];
527
+ }
528
+
529
+ /**
530
+ * 获取当前活动的设备ID
531
+ */
532
+ getActiveDeviceId(): string | null {
533
+ return this.activeDeviceId;
534
+ }
535
+
536
+ /**
537
+ * 获取当前活动的摄像头设备
538
+ */
539
+ getActiveDevice(): CameraDevice | undefined {
540
+ if (!this.activeDeviceId) return undefined;
541
+ return this.devices.find(device => device.deviceId === this.activeDeviceId);
542
+ }
543
+
544
+ /**
545
+ * 获取当前媒体流
546
+ */
547
+ getMediaStream(): MediaStream | null {
548
+ return this.mediaStream;
549
+ }
550
+
551
+ /**
552
+ * 获取视频元素
553
+ */
554
+ getVideoElement(): HTMLVideoElement | null {
555
+ return this.videoElement;
556
+ }
557
+
558
+ /**
559
+ * 设置视频元素
560
+ * @param element 视频元素
561
+ */
562
+ setVideoElement(element: HTMLVideoElement): void {
563
+ this.videoElement = element;
564
+
565
+ // 设置视频元素属性
566
+ this.videoElement.autoplay = true;
567
+ this.videoElement.playsInline = true; // iOS需要
568
+ this.videoElement.muted = true;
569
+
570
+ // 如果已有流,附加到视频元素
571
+ if (this.mediaStream && this.status === CameraStatus.ACTIVE) {
572
+ this.videoElement.srcObject = this.mediaStream;
573
+ this.videoElement.play().catch(error => {
574
+ this.logger.error('CameraManager', `Failed to play video: ${error.message}`, error);
575
+ });
576
+ }
577
+ }
578
+
579
+ /**
580
+ * 创建视频元素
581
+ */
582
+ private createVideoElement(): HTMLVideoElement {
583
+ if (!this.videoElement) {
584
+ this.videoElement = document.createElement('video');
585
+ this.setVideoElement(this.videoElement);
586
+ }
587
+ return this.videoElement;
588
+ }
589
+
590
+ /**
591
+ * 捕获当前画面
592
+ * @param format 图像格式
593
+ * @param quality 图像质量(0-1)
594
+ */
595
+ captureFrame(format: 'image/png' | 'image/jpeg' = 'image/jpeg', quality: number = 0.95): string | null {
596
+ if (this.status !== CameraStatus.ACTIVE || !this.videoElement) {
597
+ return null;
598
+ }
599
+
600
+ // 确保画布已初始化
601
+ this.initCanvas();
602
+
603
+ const video = this.videoElement;
604
+ const canvas = this.canvas!;
605
+ const ctx = this.canvasCtx!;
606
+
607
+ // 设置画布大小与视频一致
608
+ canvas.width = video.videoWidth;
609
+ canvas.height = video.videoHeight;
610
+
611
+ // 绘制视频帧
612
+ ctx.drawImage(video, 0, 0);
613
+
614
+ // 返回图像数据
615
+ return canvas.toDataURL(format, quality);
616
+ }
617
+
618
+ /**
619
+ * 捕获帧并返回ImageData
620
+ */
621
+ captureFrameData(): ImageData | null {
622
+ if (this.status !== CameraStatus.ACTIVE || !this.videoElement) {
623
+ return null;
624
+ }
625
+
626
+ // 确保画布已初始化
627
+ this.initCanvas();
628
+
629
+ const video = this.videoElement;
630
+ const canvas = this.canvas!;
631
+ const ctx = this.canvasCtx!;
632
+
633
+ // 设置画布大小与视频一致
634
+ canvas.width = video.videoWidth;
635
+ canvas.height = video.videoHeight;
636
+
637
+ // 绘制视频帧
638
+ ctx.drawImage(video, 0, 0);
639
+
640
+ // 返回图像数据
641
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
642
+ }
643
+
644
+ /**
645
+ * 获取当前状态
646
+ */
647
+ getStatus(): CameraStatus {
648
+ return this.status;
649
+ }
650
+
651
+ /**
652
+ * 检查摄像头是否活动
653
+ */
654
+ isActive(): boolean {
655
+ return this.status === CameraStatus.ACTIVE;
656
+ }
657
+
658
+ /**
659
+ * 初始化Canvas
660
+ */
661
+ private initCanvas(): void {
662
+ if (!this.canvas) {
663
+ this.canvas = document.createElement('canvas');
664
+ this.canvasCtx = this.canvas.getContext('2d');
665
+ }
666
+ }
667
+
668
+ /**
669
+ * 释放资源
670
+ */
671
+ dispose(): void {
672
+ this.stop();
673
+
674
+ // 停止并释放媒体流
675
+ this.stopMediaStream();
676
+
677
+ if (this.canvas) {
678
+ // 清空 canvas 内容
679
+ const ctx = this.canvas.getContext('2d');
680
+ if (ctx) ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
681
+ this.canvas.width = 0;
682
+ this.canvas.height = 0;
683
+ this.canvas = null;
684
+ this.canvasCtx = null;
685
+ }
686
+
687
+ // 释放 video 元素
688
+ if (this.videoElement) {
689
+ this.videoElement.srcObject = null;
690
+ this.videoElement.load();
691
+ this.videoElement = null;
692
+ }
693
+
694
+ this.status = CameraStatus.NOT_INITIALIZED;
695
+ this.logger.debug('CameraManager', 'Camera resources disposed');
696
+ }
697
+
698
+ /**
699
+ * 停止媒体流并释放轨道
700
+ */
701
+ private stopMediaStream(): void {
702
+ if (this.mediaStream) {
703
+ this.mediaStream.getTracks().forEach(track => track.stop());
704
+ this.mediaStream = null;
705
+ }
706
+ }
707
+
708
+ /**
709
+ * 处理媒体流轨道结束事件
710
+ */
711
+ private handleTrackEnded(): void {
712
+ this.logger.debug('CameraManager', 'Camera track ended');
713
+
714
+ this.emit(CameraEvent.TRACK_ENDED);
715
+ this.stop();
716
+ }
717
+
718
+ /**
719
+ * 创建视频准备就绪的Promise
720
+ */
721
+ private createVideoReadyPromise(): void {
722
+ this.videoReadyPromise = new Promise((resolve) => {
723
+ this.videoReadyResolver = resolve;
724
+
725
+ if (!this.videoElement) {
726
+ resolve();
727
+ return;
728
+ }
729
+
730
+ // 如果视频已经有足够的数据,直接解析
731
+ if (this.videoElement.readyState >= 2) { // HAVE_CURRENT_DATA
732
+ resolve();
733
+ return;
734
+ }
735
+
736
+ // 否则等待loadeddata事件
737
+ const handleVideoReady = () => {
738
+ if (this.videoElement) {
739
+ this.videoElement.removeEventListener('loadeddata', handleVideoReady);
740
+
741
+ // 发出分辨率变化事件
742
+ this.emit(CameraEvent.RESOLUTION_CHANGE, {
743
+ width: this.videoElement.videoWidth,
744
+ height: this.videoElement.videoHeight
745
+ });
746
+
747
+ if (this.videoReadyResolver) {
748
+ this.videoReadyResolver();
749
+ this.videoReadyResolver = null;
750
+ }
751
+ }
752
+ };
753
+
754
+ this.videoElement.addEventListener('loadeddata', handleVideoReady);
755
+ });
756
+ }
757
+
758
+ /**
759
+ * 等待视频准备就绪
760
+ */
761
+ private async waitForVideoReady(): Promise<void> {
762
+ if (this.videoReadyPromise) {
763
+ await this.videoReadyPromise;
764
+ }
765
+ }
766
+
767
+ /**
768
+ * 开始帧处理
769
+ */
770
+ private startFrameProcessing(): void {
771
+ if (!this.frameProcessingEnabled || this.frameProcessingTimerId !== null) {
772
+ return;
773
+ }
774
+
775
+ this.frameProcessingTimerId = window.setInterval(() => {
776
+ this.processFrame();
777
+ }, this.frameProcessingInterval);
778
+ }
779
+
780
+ /**
781
+ * 停止帧处理
782
+ */
783
+ private stopFrameProcessing(): void {
784
+ if (this.frameProcessingTimerId !== null) {
785
+ clearInterval(this.frameProcessingTimerId);
786
+ this.frameProcessingTimerId = null;
787
+ }
788
+ }
789
+
790
+ /**
791
+ * 处理当前帧
792
+ */
793
+ private processFrame(): void {
794
+ if (this.status !== CameraStatus.ACTIVE || !this.videoElement) {
795
+ return;
796
+ }
797
+
798
+ try {
799
+ const frameData = this.captureFrameData();
800
+ if (frameData) {
801
+ this.emit(CameraEvent.FRAME, {
802
+ frameData,
803
+ timestamp: Date.now(),
804
+ width: frameData.width,
805
+ height: frameData.height
806
+ });
807
+ }
808
+ } catch (error) {
809
+ const errorMessage = error instanceof Error ? error.message : String(error);
810
+ this.logger.error('CameraManager', `Frame processing error: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
811
+ }
812
+ }
813
+ }