id-scanner-lib 1.6.7 → 2.0.0
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.
- package/dist/id-scanner-lib.esm.js +994 -1139
- package/dist/id-scanner-lib.esm.js.map +1 -1
- package/dist/id-scanner-lib.js +995 -1144
- package/dist/id-scanner-lib.js.map +1 -1
- package/package.json +1 -1
- package/src/compat/index.ts +7 -0
- package/src/compat/v1-adapter.ts +84 -0
- package/src/core/camera-manager.ts +43 -76
- package/src/core/camera-stream-manager.ts +318 -0
- package/src/core/config.ts +113 -267
- package/src/core/errors.ts +68 -117
- package/src/core/logger.ts +158 -81
- package/src/core/resource-manager.ts +150 -0
- package/src/core/scanner.ts +109 -0
- package/src/core/utils/browser.ts +7 -0
- package/src/core/utils/canvas-pool.ts +171 -0
- package/src/core/utils/canvas.ts +7 -0
- package/src/core/utils/image.ts +7 -0
- package/src/core/utils/index.ts +9 -0
- package/src/core/utils/resource-manager.ts +155 -0
- package/src/core/utils/validate.ts +7 -0
- package/src/core/utils/worker.ts +130 -0
- package/src/modules/face/comparator/comparator.ts +45 -0
- package/src/modules/face/comparator/index.ts +1 -0
- package/src/modules/face/detector/detector.ts +83 -0
- package/src/modules/face/detector/index.ts +2 -0
- package/src/modules/face/detector/types.ts +80 -0
- package/src/modules/face/face-comparator.ts +150 -0
- package/src/modules/face/face-detector-options.ts +104 -0
- package/src/modules/face/face-detector.ts +121 -376
- package/src/modules/face/face-detector.ts.bak +991 -0
- package/src/modules/face/face-model-loader.ts +222 -0
- package/src/modules/face/face-result-converter.ts +225 -0
- package/src/modules/face/face-tracker.ts +207 -0
- package/src/modules/face/liveness/index.ts +7 -0
- package/src/modules/face/liveness-detector.ts +2 -2
- package/src/modules/face/tracker/index.ts +7 -0
- package/src/modules/id-card/anti-fake/index.ts +7 -0
- package/src/modules/id-card/detector/index.ts +7 -0
- package/src/modules/id-card/id-card-text-parser.ts +151 -0
- package/src/modules/id-card/ocr-processor.ts +20 -257
- package/src/modules/id-card/ocr-worker.ts +2 -183
- package/src/modules/id-card/parser/index.ts +7 -0
- package/src/modules/qr/scanner/index.ts +7 -0
- package/src/utils/canvas-pool.ts +273 -0
- package/src/utils/edge-detector.ts +232 -0
- package/src/utils/image-processing.ts +92 -419
- package/src/utils/index.ts +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file V1 Adapter (Compatibility Layer)
|
|
3
|
+
* @description Provides backward compatibility with v1 IDScanner API
|
|
4
|
+
* @module compat/v1-adapter
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Scanner, ScannerConfig } from '../core/scanner';
|
|
8
|
+
import { EventEmitter } from '../core/event-emitter';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* V1 IDScanner configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface IDScannerConfig {
|
|
14
|
+
/** Debug mode */
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
/** Callback when faces are detected */
|
|
17
|
+
onFaceDetected?: (faces: any[]) => void;
|
|
18
|
+
/** Callback on error */
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* IDScanner class - provides v1 API compatibility
|
|
24
|
+
* Wraps the new Scanner class with the old IDScanner interface
|
|
25
|
+
*/
|
|
26
|
+
export class IDScanner {
|
|
27
|
+
private _scanner: Scanner;
|
|
28
|
+
private _config: IDScannerConfig;
|
|
29
|
+
private _eventEmitter: EventEmitter;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a new IDScanner instance
|
|
33
|
+
* @param config Configuration
|
|
34
|
+
*/
|
|
35
|
+
constructor(config: IDScannerConfig = {}) {
|
|
36
|
+
this._config = config;
|
|
37
|
+
this._scanner = new Scanner({ debug: config.debug });
|
|
38
|
+
this._eventEmitter = new EventEmitter();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize the scanner
|
|
43
|
+
*/
|
|
44
|
+
async initialize(): Promise<void> {
|
|
45
|
+
await this._scanner.initialize();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Start face recognition on a video element
|
|
50
|
+
* @param video Video element to process
|
|
51
|
+
*/
|
|
52
|
+
async startFaceRecognition(video: HTMLVideoElement): Promise<void> {
|
|
53
|
+
const faces = await this._scanner.detectFace(video);
|
|
54
|
+
if (this._config.onFaceDetected) {
|
|
55
|
+
this._config.onFaceDetected(faces);
|
|
56
|
+
}
|
|
57
|
+
this._eventEmitter.emit('faceDetected', faces);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stop the scanner and release resources
|
|
62
|
+
*/
|
|
63
|
+
async stop(): Promise<void> {
|
|
64
|
+
await this._scanner.destroy();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register an event handler
|
|
69
|
+
* @param event Event name
|
|
70
|
+
* @param handler Event handler
|
|
71
|
+
*/
|
|
72
|
+
on(event: string, handler: (data?: any) => void): void {
|
|
73
|
+
this._eventEmitter.on(event, handler);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Unregister an event handler
|
|
78
|
+
* @param event Event name
|
|
79
|
+
* @param handler Event handler
|
|
80
|
+
*/
|
|
81
|
+
off(event: string, handler: (data?: any) => void): void {
|
|
82
|
+
this._eventEmitter.off(event, handler);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -10,6 +10,7 @@ import { ConfigManager } from './config';
|
|
|
10
10
|
import { Result } from './result';
|
|
11
11
|
import { CameraAccessError, DeviceError } from './errors';
|
|
12
12
|
import { getMediaConstraints } from '../utils';
|
|
13
|
+
import { CameraStreamManager, StreamState } from './camera-stream-manager';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 摄像头设备信息
|
|
@@ -108,8 +109,8 @@ export class CameraManager extends EventEmitter {
|
|
|
108
109
|
private readonly config: ConfigManager;
|
|
109
110
|
/** 视频元素 */
|
|
110
111
|
private videoElement: HTMLVideoElement | null = null;
|
|
111
|
-
/**
|
|
112
|
-
private
|
|
112
|
+
/** 摄像头流管理器 */
|
|
113
|
+
private streamManager: CameraStreamManager;
|
|
113
114
|
/** 摄像头状态 */
|
|
114
115
|
private status: CameraStatus = CameraStatus.NOT_INITIALIZED;
|
|
115
116
|
/** 可用的摄像头设备列表 */
|
|
@@ -138,6 +139,18 @@ export class CameraManager extends EventEmitter {
|
|
|
138
139
|
super();
|
|
139
140
|
this.logger = Logger.getInstance();
|
|
140
141
|
this.config = ConfigManager.getInstance();
|
|
142
|
+
this.streamManager = new CameraStreamManager();
|
|
143
|
+
|
|
144
|
+
// 转发流管理器的轨道结束事件
|
|
145
|
+
this.streamManager.on('stream:track:ended', () => {
|
|
146
|
+
this.emit(CameraEvent.TRACK_ENDED);
|
|
147
|
+
this.stop();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 转发流管理器的分辨率变化事件
|
|
151
|
+
this.streamManager.on('stream:resolution:change', (data: any) => {
|
|
152
|
+
this.emit(CameraEvent.RESOLUTION_CHANGE, data);
|
|
153
|
+
});
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
/**
|
|
@@ -272,33 +285,20 @@ export class CameraManager extends EventEmitter {
|
|
|
272
285
|
constraints = getMediaConstraints(width, height, facingMode, frameRate);
|
|
273
286
|
}
|
|
274
287
|
|
|
275
|
-
//
|
|
276
|
-
this.
|
|
277
|
-
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
278
|
-
|
|
279
|
-
// 停止旧的媒体流
|
|
280
|
-
this.stopMediaStream();
|
|
281
|
-
|
|
282
|
-
// 设置新的媒体流
|
|
283
|
-
this.mediaStream = stream;
|
|
288
|
+
// 使用流管理器启动摄像头
|
|
289
|
+
const stream = await this.streamManager.start(constraints, this.videoElement);
|
|
284
290
|
|
|
285
291
|
// 获取实际选择的设备ID
|
|
286
292
|
const videoTrack = stream.getVideoTracks()[0];
|
|
287
293
|
if (videoTrack) {
|
|
288
294
|
this.activeDeviceId = videoTrack.getSettings().deviceId || null;
|
|
289
|
-
|
|
290
|
-
// 监听轨道结束事件
|
|
291
|
-
videoTrack.onended = this.handleTrackEnded.bind(this);
|
|
292
295
|
}
|
|
293
296
|
|
|
294
|
-
//
|
|
297
|
+
// 创建视频准备就绪Promise
|
|
298
|
+
this.createVideoReadyPromise();
|
|
299
|
+
|
|
300
|
+
// 开始播放
|
|
295
301
|
if (this.videoElement) {
|
|
296
|
-
this.videoElement.srcObject = stream;
|
|
297
|
-
|
|
298
|
-
// 创建视频准备就绪Promise
|
|
299
|
-
this.createVideoReadyPromise();
|
|
300
|
-
|
|
301
|
-
// 开始播放
|
|
302
302
|
const playPromise = this.videoElement.play();
|
|
303
303
|
if (playPromise) {
|
|
304
304
|
await playPromise;
|
|
@@ -342,9 +342,7 @@ export class CameraManager extends EventEmitter {
|
|
|
342
342
|
return false;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
|
|
346
|
-
this.videoElement.pause();
|
|
347
|
-
}
|
|
345
|
+
this.streamManager.pause();
|
|
348
346
|
|
|
349
347
|
// 暂停帧处理
|
|
350
348
|
this.stopFrameProcessing();
|
|
@@ -363,29 +361,18 @@ export class CameraManager extends EventEmitter {
|
|
|
363
361
|
return false;
|
|
364
362
|
}
|
|
365
363
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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;
|
|
364
|
+
const resumed = await this.streamManager.resume();
|
|
365
|
+
|
|
366
|
+
if (resumed) {
|
|
367
|
+
// 恢复帧处理
|
|
368
|
+
if (this.frameProcessingEnabled) {
|
|
369
|
+
this.startFrameProcessing();
|
|
388
370
|
}
|
|
371
|
+
|
|
372
|
+
this.status = CameraStatus.ACTIVE;
|
|
373
|
+
this.emit(CameraEvent.RESUME);
|
|
374
|
+
|
|
375
|
+
return true;
|
|
389
376
|
}
|
|
390
377
|
|
|
391
378
|
return false;
|
|
@@ -408,8 +395,8 @@ export class CameraManager extends EventEmitter {
|
|
|
408
395
|
this.videoElement.srcObject = null;
|
|
409
396
|
}
|
|
410
397
|
|
|
411
|
-
//
|
|
412
|
-
this.
|
|
398
|
+
// 停止流
|
|
399
|
+
this.streamManager.stop();
|
|
413
400
|
|
|
414
401
|
this.status = CameraStatus.STOPPED;
|
|
415
402
|
this.emit(CameraEvent.STOP);
|
|
@@ -464,7 +451,7 @@ export class CameraManager extends EventEmitter {
|
|
|
464
451
|
async loadDevices(): Promise<CameraDevice[]> {
|
|
465
452
|
try {
|
|
466
453
|
// 请求媒体设备权限
|
|
467
|
-
if (!this.
|
|
454
|
+
if (!this.streamManager.getStream()) {
|
|
468
455
|
// 短暂获取摄像头权限以列出设备标签
|
|
469
456
|
const tempStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
|
470
457
|
|
|
@@ -545,7 +532,7 @@ export class CameraManager extends EventEmitter {
|
|
|
545
532
|
* 获取当前媒体流
|
|
546
533
|
*/
|
|
547
534
|
getMediaStream(): MediaStream | null {
|
|
548
|
-
return this.
|
|
535
|
+
return this.streamManager.getStream();
|
|
549
536
|
}
|
|
550
537
|
|
|
551
538
|
/**
|
|
@@ -567,9 +554,9 @@ export class CameraManager extends EventEmitter {
|
|
|
567
554
|
this.videoElement.playsInline = true; // iOS需要
|
|
568
555
|
this.videoElement.muted = true;
|
|
569
556
|
|
|
570
|
-
//
|
|
571
|
-
if (this.
|
|
572
|
-
this.videoElement.srcObject = this.
|
|
557
|
+
// 如果流已活动,附加到视频元素
|
|
558
|
+
if (this.streamManager.getStream() && this.status === CameraStatus.ACTIVE) {
|
|
559
|
+
this.videoElement.srcObject = this.streamManager.getStream();
|
|
573
560
|
this.videoElement.play().catch(error => {
|
|
574
561
|
this.logger.error('CameraManager', `Failed to play video: ${error.message}`, error);
|
|
575
562
|
});
|
|
@@ -671,8 +658,8 @@ export class CameraManager extends EventEmitter {
|
|
|
671
658
|
dispose(): void {
|
|
672
659
|
this.stop();
|
|
673
660
|
|
|
674
|
-
//
|
|
675
|
-
this.
|
|
661
|
+
// 释放流管理器资源
|
|
662
|
+
this.streamManager.dispose();
|
|
676
663
|
|
|
677
664
|
if (this.canvas) {
|
|
678
665
|
// 清空 canvas 内容
|
|
@@ -695,26 +682,6 @@ export class CameraManager extends EventEmitter {
|
|
|
695
682
|
this.logger.debug('CameraManager', 'Camera resources disposed');
|
|
696
683
|
}
|
|
697
684
|
|
|
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
685
|
/**
|
|
719
686
|
* 创建视频准备就绪的Promise
|
|
720
687
|
*/
|
|
@@ -810,4 +777,4 @@ export class CameraManager extends EventEmitter {
|
|
|
810
777
|
this.logger.error('CameraManager', `Frame processing error: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
|
|
811
778
|
}
|
|
812
779
|
}
|
|
813
|
-
}
|
|
780
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 摄像头流管理器
|
|
3
|
+
* @description 管理摄像头视频流的创建、销毁、暂停和恢复
|
|
4
|
+
* @module core/camera-stream-manager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from './event-emitter';
|
|
8
|
+
import { Logger } from './logger';
|
|
9
|
+
import { CameraAccessError } from './errors';
|
|
10
|
+
import { Disposable } from '../utils/resource-manager';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 摄像头流状态
|
|
14
|
+
*/
|
|
15
|
+
export enum StreamState {
|
|
16
|
+
/** 空闲 */
|
|
17
|
+
IDLE = 'idle',
|
|
18
|
+
/** 启动中 */
|
|
19
|
+
STARTING = 'starting',
|
|
20
|
+
/** 活动中 */
|
|
21
|
+
ACTIVE = 'active',
|
|
22
|
+
/** 已暂停 */
|
|
23
|
+
PAUSED = 'paused',
|
|
24
|
+
/** 已停止 */
|
|
25
|
+
STOPPED = 'stopped',
|
|
26
|
+
/** 错误 */
|
|
27
|
+
ERROR = 'error'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 摄像头流事件
|
|
32
|
+
*/
|
|
33
|
+
export enum CameraStreamEvent {
|
|
34
|
+
/** 流启动 */
|
|
35
|
+
START = 'stream:start',
|
|
36
|
+
/** 流停止 */
|
|
37
|
+
STOP = 'stream:stop',
|
|
38
|
+
/** 流暂停 */
|
|
39
|
+
PAUSE = 'stream:pause',
|
|
40
|
+
/** 流恢复 */
|
|
41
|
+
RESUME = 'stream:resume',
|
|
42
|
+
/** 流错误 */
|
|
43
|
+
ERROR = 'stream:error',
|
|
44
|
+
/** 轨道结束 */
|
|
45
|
+
TRACK_ENDED = 'stream:track:ended',
|
|
46
|
+
/** 分辨率变化 */
|
|
47
|
+
RESOLUTION_CHANGE = 'stream:resolution:change'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 摄像头流管理器接口
|
|
52
|
+
*/
|
|
53
|
+
export interface ICameraStreamManager {
|
|
54
|
+
/** 获取媒体流 */
|
|
55
|
+
getStream(): MediaStream | null;
|
|
56
|
+
/** 是否活动 */
|
|
57
|
+
isActive(): boolean;
|
|
58
|
+
/** 获取当前状态 */
|
|
59
|
+
getState(): StreamState;
|
|
60
|
+
/** 释放资源 */
|
|
61
|
+
dispose(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 摄像头流管理器
|
|
66
|
+
* 负责摄像头视频流的生命周期管理
|
|
67
|
+
*/
|
|
68
|
+
export class CameraStreamManager extends EventEmitter implements ICameraStreamManager, Disposable {
|
|
69
|
+
/** 日志记录器 */
|
|
70
|
+
private readonly logger: Logger;
|
|
71
|
+
/** 媒体流 */
|
|
72
|
+
private mediaStream: MediaStream | null = null;
|
|
73
|
+
/** 流状态 */
|
|
74
|
+
private state: StreamState = StreamState.IDLE;
|
|
75
|
+
/** 视频轨道 */
|
|
76
|
+
private videoTrack: MediaStreamTrack | null = null;
|
|
77
|
+
/** 音频轨道 */
|
|
78
|
+
private audioTrack: MediaStreamTrack | null = null;
|
|
79
|
+
/** 视频元素引用 */
|
|
80
|
+
private videoElement: HTMLVideoElement | null = null;
|
|
81
|
+
/** 是否已暂停(用于pause/resume) */
|
|
82
|
+
private isPaused: boolean = false;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 构造函数
|
|
86
|
+
*/
|
|
87
|
+
constructor() {
|
|
88
|
+
super();
|
|
89
|
+
this.logger = Logger.getInstance();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 获取媒体流
|
|
94
|
+
*/
|
|
95
|
+
getStream(): MediaStream | null {
|
|
96
|
+
return this.mediaStream;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 是否活动
|
|
101
|
+
*/
|
|
102
|
+
isActive(): boolean {
|
|
103
|
+
return this.state === StreamState.ACTIVE && !this.isPaused;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 获取当前状态
|
|
108
|
+
*/
|
|
109
|
+
getState(): StreamState {
|
|
110
|
+
return this.state;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 启动摄像头流
|
|
115
|
+
* @param constraints 媒体约束
|
|
116
|
+
* @param videoElement 可选的视频元素
|
|
117
|
+
*/
|
|
118
|
+
async start(
|
|
119
|
+
constraints: MediaStreamConstraints,
|
|
120
|
+
videoElement?: HTMLVideoElement | null
|
|
121
|
+
): Promise<MediaStream> {
|
|
122
|
+
if (this.state === StreamState.ACTIVE && this.mediaStream) {
|
|
123
|
+
this.logger.debug('CameraStreamManager', 'Stream already active');
|
|
124
|
+
return this.mediaStream;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.state = StreamState.STARTING;
|
|
128
|
+
this.logger.debug('CameraStreamManager', `Requesting camera access: ${JSON.stringify(constraints)}`);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// 停止旧流
|
|
132
|
+
this.stopStreamOnly();
|
|
133
|
+
|
|
134
|
+
// 获取新流
|
|
135
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
136
|
+
this.mediaStream = stream;
|
|
137
|
+
|
|
138
|
+
// 获取轨道
|
|
139
|
+
const tracks = stream.getTracks();
|
|
140
|
+
this.videoTrack = tracks.find(t => t.kind === 'video') || null;
|
|
141
|
+
this.audioTrack = tracks.find(t => t.kind === 'audio') || null;
|
|
142
|
+
|
|
143
|
+
// 监听轨道结束事件
|
|
144
|
+
if (this.videoTrack) {
|
|
145
|
+
this.videoTrack.onended = this.handleTrackEnded.bind(this);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 保存视频元素引用
|
|
149
|
+
this.videoElement = videoElement || null;
|
|
150
|
+
|
|
151
|
+
// 将流连接到视频元素
|
|
152
|
+
if (this.videoElement) {
|
|
153
|
+
this.videoElement.srcObject = stream;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.state = StreamState.ACTIVE;
|
|
157
|
+
this.isPaused = false;
|
|
158
|
+
|
|
159
|
+
this.emit(CameraStreamEvent.START, {
|
|
160
|
+
stream,
|
|
161
|
+
deviceId: this.videoTrack?.getSettings().deviceId,
|
|
162
|
+
settings: this.videoTrack?.getSettings()
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return stream;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.state = StreamState.ERROR;
|
|
168
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
169
|
+
this.logger.error('CameraStreamManager', `Failed to start stream: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
|
|
170
|
+
|
|
171
|
+
const cameraError = new CameraAccessError(errorMessage);
|
|
172
|
+
this.emit(CameraStreamEvent.ERROR, { error: cameraError });
|
|
173
|
+
throw cameraError;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 停止摄像头流
|
|
179
|
+
*/
|
|
180
|
+
stop(): void {
|
|
181
|
+
if (this.state !== StreamState.ACTIVE && this.state !== StreamState.PAUSED) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 停止所有轨道
|
|
186
|
+
this.stopStreamOnly();
|
|
187
|
+
|
|
188
|
+
// 断开视频元素连接
|
|
189
|
+
if (this.videoElement) {
|
|
190
|
+
this.videoElement.pause();
|
|
191
|
+
this.videoElement.srcObject = null;
|
|
192
|
+
this.videoElement = null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.state = StreamState.STOPPED;
|
|
196
|
+
this.isPaused = false;
|
|
197
|
+
|
|
198
|
+
this.emit(CameraStreamEvent.STOP);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 暂停摄像头流
|
|
203
|
+
*/
|
|
204
|
+
pause(): void {
|
|
205
|
+
if (this.state !== StreamState.ACTIVE || this.isPaused) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 暂停视频元素
|
|
210
|
+
if (this.videoElement) {
|
|
211
|
+
this.videoElement.pause();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.isPaused = true;
|
|
215
|
+
this.state = StreamState.PAUSED;
|
|
216
|
+
|
|
217
|
+
this.emit(CameraStreamEvent.PAUSE);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 恢复摄像头流
|
|
222
|
+
* @returns 是否成功恢复
|
|
223
|
+
*/
|
|
224
|
+
async resume(): Promise<boolean> {
|
|
225
|
+
if (this.state !== StreamState.PAUSED || !this.isPaused) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (this.videoElement && this.videoElement.paused && this.mediaStream) {
|
|
230
|
+
try {
|
|
231
|
+
await this.videoElement.play();
|
|
232
|
+
this.isPaused = false;
|
|
233
|
+
this.state = StreamState.ACTIVE;
|
|
234
|
+
|
|
235
|
+
this.emit(CameraStreamEvent.RESUME);
|
|
236
|
+
return true;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
239
|
+
this.logger.error('CameraStreamManager', `Failed to resume stream: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 连接到视频元素
|
|
249
|
+
* @param videoElement 视频元素
|
|
250
|
+
*/
|
|
251
|
+
attachToVideoElement(videoElement: HTMLVideoElement): void {
|
|
252
|
+
this.videoElement = videoElement;
|
|
253
|
+
|
|
254
|
+
if (this.mediaStream) {
|
|
255
|
+
videoElement.srcObject = this.mediaStream;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 断开视频元素连接
|
|
261
|
+
*/
|
|
262
|
+
detachVideoElement(): void {
|
|
263
|
+
if (this.videoElement) {
|
|
264
|
+
this.videoElement.pause();
|
|
265
|
+
this.videoElement.srcObject = null;
|
|
266
|
+
this.videoElement = null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 获取视频轨道设置
|
|
272
|
+
*/
|
|
273
|
+
getVideoSettings(): MediaTrackSettings | null {
|
|
274
|
+
return this.videoTrack?.getSettings() || null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 获取视频轨道
|
|
279
|
+
*/
|
|
280
|
+
getVideoTrack(): MediaStreamTrack | null {
|
|
281
|
+
return this.videoTrack;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 释放资源
|
|
286
|
+
*/
|
|
287
|
+
async dispose(): Promise<void> {
|
|
288
|
+
this.stop();
|
|
289
|
+
|
|
290
|
+
this.videoTrack = null;
|
|
291
|
+
this.audioTrack = null;
|
|
292
|
+
this.mediaStream = null;
|
|
293
|
+
this.state = StreamState.IDLE;
|
|
294
|
+
|
|
295
|
+
this.logger.debug('CameraStreamManager', 'CameraStreamManager resources disposed');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 停止媒体流(仅停止轨道不断开视频元素)
|
|
300
|
+
*/
|
|
301
|
+
private stopStreamOnly(): void {
|
|
302
|
+
if (this.mediaStream) {
|
|
303
|
+
this.mediaStream.getTracks().forEach(track => track.stop());
|
|
304
|
+
this.mediaStream = null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.videoTrack = null;
|
|
308
|
+
this.audioTrack = null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 处理轨道结束事件
|
|
313
|
+
*/
|
|
314
|
+
private handleTrackEnded(): void {
|
|
315
|
+
this.logger.debug('CameraStreamManager', 'Camera track ended');
|
|
316
|
+
this.emit(CameraStreamEvent.TRACK_ENDED);
|
|
317
|
+
}
|
|
318
|
+
}
|