@sssxyd/face-liveness-detector 0.2.30 → 0.2.32

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/README.md CHANGED
@@ -31,53 +31,6 @@ pnpm add @sssxyd/face-liveness-detector @vladmandic/human @techstark/opencv-js
31
31
 
32
32
  > **Note**: `@vladmandic/human` and `@techstark/opencv-js` are peer dependencies and must be installed separately to avoid bundling large libraries. This keeps your final bundle size smaller if you're already using these libraries elsewhere in your project.
33
33
 
34
- ## Quick Start - Using Local Model Files (Recommended)
35
-
36
- To improve performance and reduce external dependencies, you can download and use local copies of model files:
37
-
38
- ### Step 1: Download Model Files
39
-
40
- ```bash
41
- # Copy Human.js models locally
42
- node copy-human-models.js
43
-
44
- # Download TensorFlow.js WASM files
45
- node download-tensorflow-wasm.js
46
- ```
47
-
48
- This will create:
49
- - `public/models/` - Human.js face detection models
50
- - `public/wasm/` - TensorFlow.js WASM backend files
51
-
52
- ### Step 2: Initialize Engine with Local Files
53
-
54
- ```typescript
55
- import FaceDetectionEngine from '@sssxyd/face-liveness-detector'
56
-
57
- // Configure to use local model files
58
- const engine = new FaceDetectionEngine({
59
- human_model_path: '/models', // Path to downloaded models
60
- tensorflow_wasm_path: '/wasm', // Path to WASM files
61
- min_face_ratio: 0.5,
62
- max_face_ratio: 0.9,
63
- liveness_action_count: 1,
64
- liveness_action_list: ['blink']
65
- })
66
-
67
- // Initialize and start detection
68
- await engine.initialize()
69
- const videoElement = document.getElementById('video') as HTMLVideoElement
70
- await engine.startDetection(videoElement)
71
- ```
72
-
73
- ### Step 3: Serve Static Files
74
-
75
- Make sure your web server serves the `public/` directory:
76
-
77
- ```typescript
78
- // Express.js example
79
- app.use(express.static('public'))
80
- ```
81
34
 
82
35
  ## Quick Start - Using Default CDN Files
83
36
 
package/dist/index.esm.js CHANGED
@@ -66,6 +66,7 @@ const DEFAULT_CONFIG = Object.freeze({
66
66
  // resource paths
67
67
  human_model_path: undefined,
68
68
  tensorflow_wasm_path: undefined,
69
+ tensorflow_backend: 'auto', // 'auto' | 'webgl' | 'wasm'
69
70
  // DetectionSettings defaults
70
71
  video_width: 640,
71
72
  video_height: 640,
@@ -118,6 +119,9 @@ function mergeConfig(userConfig) {
118
119
  if (userConfig.tensorflow_wasm_path !== undefined) {
119
120
  merged.tensorflow_wasm_path = userConfig.tensorflow_wasm_path;
120
121
  }
122
+ if (userConfig.tensorflow_backend !== undefined) {
123
+ merged.tensorflow_backend = userConfig.tensorflow_backend;
124
+ }
121
125
  if (userConfig.video_width !== undefined) {
122
126
  merged.video_width = userConfig.video_width;
123
127
  }
@@ -304,18 +308,72 @@ function _isWebGLAvailable() {
304
308
  return false;
305
309
  }
306
310
  }
307
- function _detectOptimalBackend() {
311
+ function _detectBrowserEngine(userAgent) {
312
+ const ua = userAgent.toLowerCase();
313
+ // 1. 检测 Gecko (Firefox)
314
+ if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) {
315
+ return 'gecko';
316
+ }
317
+ // 2. 检测 WebKit
318
+ // 包括:Safari、iOS 浏览器、以及那些虽然包含 Chrome 标识但实际是 WebKit 的浏览器(Quark、支付宝、微信等)
319
+ if (/webkit/i.test(ua)) {
320
+ // WebKit 特征明显,包括以下几种情况:
321
+ // - 真正的 Safari(有 Safari 标识)
322
+ // - iOS 浏览器(有 Mobile Safari 标识)
323
+ // - Quark、支付宝、微信等虽然包含 Chrome 标识但是基于 WebKit 的浏览器
324
+ return 'webkit';
325
+ }
326
+ // 3. 检测 Chromium/Blink
327
+ // Chrome-based browsers: Chrome, Chromium, Edge, Brave, Opera, Vivaldi, Whale, Arc, etc.
328
+ if (/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
329
+ return 'chromium';
330
+ }
331
+ // 4. 其他浏览器 - 保守方案,使用 WASM
332
+ return 'other';
333
+ }
334
+ function _getOptimalBackendForEngine(engine) {
335
+ // 针对不同内核的优化策略
336
+ const backendConfig = {
337
+ chromium: 'webgl', // Chromium 内核:优先 WebGL
338
+ webkit: 'wasm', // WebKit(Safari、iOS):使用 WASM
339
+ gecko: 'webgl', // Firefox:优先 WebGL
340
+ other: 'wasm' // 未知浏览器:保守使用 WASM
341
+ };
342
+ return backendConfig[engine];
343
+ }
344
+ function _detectOptimalBackend(preferredBackend) {
345
+ // If user explicitly specified a backend, honor it (unless it's 'auto')
346
+ if (preferredBackend && preferredBackend !== 'auto') {
347
+ console.log('[Backend Detection] Using user-specified backend:', {
348
+ backend: preferredBackend,
349
+ userAgent: navigator.userAgent
350
+ });
351
+ return preferredBackend;
352
+ }
308
353
  const userAgent = navigator.userAgent.toLowerCase();
309
- // Special browsers: prefer WASM
310
- if ((/safari/.test(userAgent) && !/chrome/.test(userAgent)) ||
311
- /micromessenger/i.test(userAgent) ||
312
- /alipay/.test(userAgent) ||
313
- /qq/.test(userAgent) ||
314
- /(wechat|alipay|qq)webview/i.test(userAgent)) {
315
- return 'wasm';
316
- }
317
- // Desktop: prefer WebGL
318
- return _isWebGLAvailable() ? 'webgl' : 'wasm';
354
+ const engine = _detectBrowserEngine(userAgent);
355
+ console.log('[Backend Detection] Detected browser engine:', {
356
+ engine,
357
+ userAgent: navigator.userAgent
358
+ });
359
+ // 获取该内核的推荐后端
360
+ let preferredBackendForEngine = _getOptimalBackendForEngine(engine);
361
+ // 对于 Chromium 和 Gecko,检查 WebGL 是否可用
362
+ if (preferredBackendForEngine === 'webgl') {
363
+ const hasWebGL = _isWebGLAvailable();
364
+ console.log('[Backend Detection] WebGL availability check:', {
365
+ engine,
366
+ hasWebGL,
367
+ selectedBackend: hasWebGL ? 'webgl' : 'wasm'
368
+ });
369
+ return hasWebGL ? 'webgl' : 'wasm';
370
+ }
371
+ // 对于 WebKit 和 other,直接使用 WASM
372
+ console.log('[Backend Detection] Using backend for engine:', {
373
+ engine,
374
+ backend: preferredBackendForEngine
375
+ });
376
+ return preferredBackendForEngine;
319
377
  }
320
378
  /**
321
379
  * 预加载 OpenCV.js 以确保全局 cv 对象可用
@@ -512,11 +570,13 @@ function getCvSync() {
512
570
  * Load Human.js
513
571
  * @param modelPath - Path to model files (optional)
514
572
  * @param wasmPath - Path to WASM files (optional)
573
+ * @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
515
574
  * @returns Promise that resolves with Human instance
516
575
  */
517
- async function loadHuman(modelPath, wasmPath) {
576
+ async function loadHuman(modelPath, wasmPath, preferredBackend) {
577
+ const selectedBackend = _detectOptimalBackend(preferredBackend);
518
578
  const config = {
519
- backend: _detectOptimalBackend(),
579
+ backend: selectedBackend,
520
580
  face: {
521
581
  enabled: true,
522
582
  detector: { rotation: false, return: true },
@@ -540,30 +600,176 @@ async function loadHuman(modelPath, wasmPath) {
540
600
  console.log('[FaceDetectionEngine] Human.js config:', {
541
601
  backend: config.backend,
542
602
  modelBasePath: config.modelBasePath || '(using default)',
543
- wasmPath: config.wasmPath || '(using default)'
603
+ wasmPath: config.wasmPath || '(using default)',
604
+ userAgent: navigator.userAgent,
605
+ platform: navigator.platform
544
606
  });
545
607
  const initStartTime = performance.now();
546
608
  console.log('[FaceDetectionEngine] Creating Human instance...');
547
- const human = new Human(config);
609
+ let human;
610
+ try {
611
+ human = new Human(config);
612
+ }
613
+ catch (error) {
614
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error during Human instantiation';
615
+ const stack = error instanceof Error ? error.stack : 'N/A';
616
+ console.error('[FaceDetectionEngine] Failed to create Human instance:', {
617
+ errorMsg,
618
+ stack,
619
+ backend: config.backend,
620
+ userAgent: navigator.userAgent
621
+ });
622
+ throw new Error(`Human instantiation failed: ${errorMsg}`);
623
+ }
548
624
  const instanceCreateTime = performance.now() - initStartTime;
549
625
  console.log(`[FaceDetectionEngine] Human instance created, took ${instanceCreateTime.toFixed(2)}ms`);
626
+ // 验证 Human 实例
627
+ if (!human) {
628
+ throw new Error('Human instance is null after creation');
629
+ }
630
+ // 验证 Human 实例结构(早期检测 WASM 问题)
631
+ if (!human.config) {
632
+ console.warn('[FaceDetectionEngine] Warning: human.config is missing');
633
+ }
550
634
  console.log('[FaceDetectionEngine] Loading Human.js models...');
551
635
  const modelLoadStartTime = performance.now();
552
636
  try {
553
- await human.load();
637
+ // 添加超时机制防止无限等待
638
+ const loadTimeout = new Promise((_, reject) => {
639
+ const timeoutId = setTimeout(() => {
640
+ reject(new Error('Human.js load() timeout after 60 seconds - possible issue with model loading on mobile'));
641
+ }, 60000);
642
+ });
643
+ // 竞速:哪个先完成就用哪个
644
+ try {
645
+ await Promise.race([
646
+ human.load(),
647
+ loadTimeout
648
+ ]);
649
+ }
650
+ catch (raceError) {
651
+ // 如果是超时错误,直接抛出
652
+ if (raceError instanceof Error && raceError.message.includes('timeout')) {
653
+ throw raceError;
654
+ }
655
+ // 其他错误,提供更多上下文
656
+ const raceErrorMsg = raceError instanceof Error ? raceError.message : 'Unknown error';
657
+ const raceErrorStack = raceError instanceof Error ? raceError.stack : 'N/A';
658
+ console.error('[FaceDetectionEngine] Error during human.load():', {
659
+ errorMsg: raceErrorMsg,
660
+ stack: raceErrorStack,
661
+ backend: config.backend,
662
+ hasModels: !!human.models,
663
+ modelsKeys: human.models ? Object.keys(human.models).length : 0
664
+ });
665
+ throw new Error(`Model loading error: ${raceErrorMsg}`);
666
+ }
554
667
  const loadTime = performance.now() - modelLoadStartTime;
555
668
  const totalTime = performance.now() - initStartTime;
556
669
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
557
670
  modelLoadTime: `${loadTime.toFixed(2)}ms`,
558
671
  totalInitTime: `${totalTime.toFixed(2)}ms`,
559
- version: human.version
672
+ version: human.version,
673
+ config: human.config
560
674
  });
675
+ // 验证加载后的 Human 实例有必要的方法和属性
676
+ if (typeof human.detect !== 'function') {
677
+ throw new Error('Human.detect method not available after loading');
678
+ }
679
+ if (!human.version) {
680
+ console.warn('[FaceDetectionEngine] Human.js loaded but version is missing');
681
+ }
682
+ // 关键验证:检查模型是否真的加载了
683
+ if (!human.models || Object.keys(human.models).length === 0) {
684
+ console.error('[FaceDetectionEngine] CRITICAL: human.models is empty after loading!');
685
+ throw new Error('No models were loaded - human.models is empty');
686
+ }
687
+ // 详细检查每个关键模型及其结构
688
+ const criticalModels = ['face', 'antispoof', 'liveness'];
689
+ const missingModels = [];
690
+ for (const modelName of criticalModels) {
691
+ const model = human.models[modelName];
692
+ if (!model) {
693
+ missingModels.push(modelName);
694
+ console.error(`[FaceDetectionEngine] CRITICAL: Model '${modelName}' is missing!`);
695
+ }
696
+ else {
697
+ const isLoaded = model.loaded || model.state === 'loaded' || !!model.model;
698
+ // 检查模型是否有必要的内部结构(防止 "Cannot read properties of undefined (reading 'inputs')" 错误)
699
+ const hasExecutor = !!model['executor'];
700
+ const hasInputs = !!model.inputs && Array.isArray(model.inputs) && model.inputs.length > 0;
701
+ const hasModelUrl = !!model['modelUrl'];
702
+ console.log(`[FaceDetectionEngine] Model '${modelName}':`, {
703
+ loaded: isLoaded,
704
+ state: model.state,
705
+ hasModel: !!model.model,
706
+ hasExecutor,
707
+ hasInputs,
708
+ hasModelUrl,
709
+ inputsType: typeof model.inputs,
710
+ inputsLength: Array.isArray(model.inputs) ? model.inputs.length : 'N/A'
711
+ });
712
+ // 严格检查:模型必须有以下结构才能正常工作
713
+ if (!isLoaded || !hasExecutor || !hasModelUrl) {
714
+ missingModels.push(`${modelName} (incomplete)`);
715
+ console.error(`[FaceDetectionEngine] WARNING: Model '${modelName}' may not be fully loaded - missing structure`);
716
+ }
717
+ // 如果 inputs 未定义会导致 "Cannot read properties of undefined (reading 'inputs')" 错误
718
+ if (!hasInputs && modelName !== 'antispoof') {
719
+ console.warn(`[FaceDetectionEngine] WARNING: Model '${modelName}' has no inputs - may cause errors during detection`);
720
+ missingModels.push(`${modelName} (no inputs)`);
721
+ }
722
+ }
723
+ }
724
+ if (missingModels.length > 0) {
725
+ console.error('[FaceDetectionEngine] Some critical models failed to load:', missingModels);
726
+ throw new Error(`Critical models not loaded: ${missingModels.join(', ')}`);
727
+ }
728
+ // 打印加载的模型信息
729
+ if (human.models) {
730
+ const loadedModels = Object.entries(human.models).map(([name, model]) => ({
731
+ name,
732
+ loaded: model?.loaded || model?.state === 'loaded',
733
+ type: typeof model,
734
+ hasModel: !!model?.model
735
+ }));
736
+ console.log('[FaceDetectionEngine] All loaded models:', {
737
+ totalModels: Object.keys(human.models).length,
738
+ models: loadedModels,
739
+ allModelNames: Object.keys(human.models)
740
+ });
741
+ }
561
742
  return human;
562
743
  }
563
744
  catch (error) {
564
745
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
565
- console.error('[FaceDetectionEngine] Human.js load failed:', errorMsg);
566
- throw error;
746
+ const stack = error instanceof Error ? error.stack : 'N/A';
747
+ console.error('[FaceDetectionEngine] Human.js load failed:', {
748
+ errorMsg,
749
+ stack,
750
+ userAgent: navigator.userAgent,
751
+ platform: navigator.platform,
752
+ humanVersion: human?.version,
753
+ humanConfig: human?.config,
754
+ backend: config.backend
755
+ });
756
+ // 如果是 WASM 后端失败,尝试降级到 WebGL
757
+ if (selectedBackend === 'wasm' && !errorMsg.includes('timeout') && _isWebGLAvailable()) {
758
+ console.warn('[FaceDetectionEngine] WASM backend failed, attempting fallback to WebGL...');
759
+ config.backend = 'webgl';
760
+ try {
761
+ console.log('[FaceDetectionEngine] Retrying with WebGL backend');
762
+ human = new Human(config);
763
+ await human.load();
764
+ console.log('[FaceDetectionEngine] Successfully loaded with WebGL fallback');
765
+ return human;
766
+ }
767
+ catch (fallbackError) {
768
+ const fallbackMsg = fallbackError instanceof Error ? fallbackError.message : 'Unknown error';
769
+ console.error('[FaceDetectionEngine] WebGL fallback also failed:', fallbackMsg);
770
+ }
771
+ }
772
+ throw new Error(`Human.js loading failed: ${errorMsg}`);
567
773
  }
568
774
  }
569
775
  /**
@@ -1599,11 +1805,64 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1599
1805
  console.log('[FaceDetectionEngine] Loading Human.js models...');
1600
1806
  this.emitDebug('initialization', 'Loading Human.js...');
1601
1807
  const humanStartTime = performance.now();
1602
- this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path);
1808
+ try {
1809
+ this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path, this.config.tensorflow_backend);
1810
+ }
1811
+ catch (humanError) {
1812
+ const errorMsg = humanError instanceof Error ? humanError.message : 'Unknown error';
1813
+ const stack = humanError instanceof Error ? humanError.stack : 'N/A';
1814
+ // 分析错误类型,提供针对性的建议
1815
+ let errorContext = {
1816
+ error: errorMsg,
1817
+ stack,
1818
+ userAgent: navigator.userAgent,
1819
+ platform: navigator.platform,
1820
+ browser: this.detectBrowserInfo(),
1821
+ backend: this.config.tensorflow_backend,
1822
+ source: 'human.js'
1823
+ };
1824
+ // 特定错误类型的诊断
1825
+ if (errorMsg.includes('inputs')) {
1826
+ errorContext.diagnosis = 'Human.js internal error: Model structure incomplete';
1827
+ errorContext.rootCause = 'Human.js library issue - models not fully loaded or WASM backend initialization incomplete';
1828
+ errorContext.suggestion = 'This is a Human.js library issue. Models may not have proper executor or inputs structure. Check WASM initialization and model integrity.';
1829
+ }
1830
+ else if (errorMsg.includes('timeout')) {
1831
+ errorContext.diagnosis = 'Model loading timeout';
1832
+ errorContext.suggestion = 'Network issue or model file too large - check network conditions';
1833
+ }
1834
+ else if (errorMsg.includes('Critical models not loaded')) {
1835
+ errorContext.diagnosis = 'Human.js failed to load required models';
1836
+ errorContext.rootCause = 'Models (face, antispoof, liveness) are missing or incomplete';
1837
+ errorContext.suggestion = 'Check model files and ensure WASM backend is properly initialized';
1838
+ }
1839
+ else if (errorMsg.includes('empty')) {
1840
+ errorContext.diagnosis = 'Models object is empty after loading';
1841
+ errorContext.suggestion = 'Model path may be incorrect or HTTP response failed';
1842
+ }
1843
+ else if (errorMsg.includes('incomplete')) {
1844
+ errorContext.diagnosis = 'Models loaded but structure is incomplete';
1845
+ errorContext.rootCause = 'Human.js internal issue - missing executor, inputs, or modelUrl';
1846
+ errorContext.suggestion = 'Ensure all model resources are fully loaded and accessible';
1847
+ }
1848
+ console.error('[FaceDetectionEngine] Human.js loading failed with detailed error:', errorContext);
1849
+ this.emitDebug('initialization', 'Human.js loading failed with exception', errorContext, 'error');
1850
+ this.emit('detector-loaded', {
1851
+ success: false,
1852
+ error: `Failed to load Human.js: ${errorMsg}`,
1853
+ details: errorContext
1854
+ });
1855
+ this.emit('detector-error', {
1856
+ code: ErrorCode.DETECTOR_NOT_INITIALIZED,
1857
+ message: `Human.js loading error: ${errorMsg}`,
1858
+ details: errorContext
1859
+ });
1860
+ return;
1861
+ }
1603
1862
  const humanLoadTime = performance.now() - humanStartTime;
1604
1863
  if (!this.human) {
1605
1864
  const errorMsg = 'Failed to load Human.js: instance is null';
1606
- console.log('[FaceDetectionEngine] ' + errorMsg);
1865
+ console.error('[FaceDetectionEngine] ' + errorMsg);
1607
1866
  this.emitDebug('initialization', errorMsg, { loadTime: humanLoadTime }, 'error');
1608
1867
  this.emit('detector-loaded', {
1609
1868
  success: false,
@@ -1615,13 +1874,35 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1615
1874
  });
1616
1875
  return;
1617
1876
  }
1877
+ // Verify Human.js instance has required properties
1878
+ if (!this.human.version || typeof this.human.detect !== 'function') {
1879
+ const errorMsg = 'Human.js instance is incomplete: missing version or detect method';
1880
+ console.error('[FaceDetectionEngine] ' + errorMsg);
1881
+ this.emitDebug('initialization', errorMsg, {
1882
+ hasVersion: !!this.human.version,
1883
+ hasDetect: typeof this.human.detect === 'function',
1884
+ instanceKeys: Object.keys(this.human || {})
1885
+ }, 'error');
1886
+ this.emit('detector-loaded', {
1887
+ success: false,
1888
+ error: errorMsg
1889
+ });
1890
+ this.emit('detector-error', {
1891
+ code: ErrorCode.DETECTOR_NOT_INITIALIZED,
1892
+ message: errorMsg
1893
+ });
1894
+ return;
1895
+ }
1618
1896
  this.emitDebug('initialization', 'Human.js loaded successfully', {
1619
1897
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1620
- version: this.human.version
1898
+ version: this.human.version,
1899
+ backend: this.human.config?.backend || 'unknown',
1900
+ config: this.human.config
1621
1901
  });
1622
1902
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
1623
1903
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1624
- version: this.human.version
1904
+ version: this.human.version,
1905
+ backend: this.human.config?.backend || 'unknown'
1625
1906
  });
1626
1907
  this.isReady = true;
1627
1908
  const loadedData = {
@@ -1898,13 +2179,31 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1898
2179
  try {
1899
2180
  // Check video is ready
1900
2181
  if (this.videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1901
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2182
+ this.scheduleNextDetection(this.config.error_retry_delay);
1902
2183
  return;
1903
2184
  }
1904
2185
  // Perform face detection
1905
- const result = await this.human.detect(this.videoElement);
2186
+ let result;
2187
+ try {
2188
+ result = await this.human.detect(this.videoElement);
2189
+ }
2190
+ catch (detectError) {
2191
+ const errorMsg = detectError instanceof Error ? detectError.message : 'Unknown error';
2192
+ this.emitDebug('detection', 'Human.detect() call failed', {
2193
+ error: errorMsg,
2194
+ stack: detectError instanceof Error ? detectError.stack : 'N/A',
2195
+ hasHuman: !!this.human,
2196
+ humanVersion: this.human?.version,
2197
+ videoReadyState: this.videoElement?.readyState,
2198
+ videoWidth: this.videoElement?.videoWidth,
2199
+ videoHeight: this.videoElement?.videoHeight
2200
+ }, 'error');
2201
+ this.scheduleNextDetection(this.config.error_retry_delay);
2202
+ return;
2203
+ }
1906
2204
  if (!result) {
1907
- this.scheduleNextDetection(this.config.error_retry_delay); // DETECTION_FRAME_DELAY
2205
+ this.emitDebug('detection', 'Face detection returned null result', {}, 'warn');
2206
+ this.scheduleNextDetection(this.config.error_retry_delay);
1908
2207
  return;
1909
2208
  }
1910
2209
  const faces = result.face || [];
@@ -1917,11 +2216,12 @@ class FaceDetectionEngine extends SimpleEventEmitter {
1917
2216
  }
1918
2217
  }
1919
2218
  catch (error) {
1920
- this.emitDebug('detection', 'Detection error', {
1921
- error: error.message,
1922
- stack: error.stack
2219
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
2220
+ this.emitDebug('detection', 'Unexpected error in detection loop', {
2221
+ error: errorMsg,
2222
+ stack: error instanceof Error ? error.stack : 'N/A'
1923
2223
  }, 'error');
1924
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2224
+ this.scheduleNextDetection(this.config.error_retry_delay);
1925
2225
  }
1926
2226
  }
1927
2227
  getPerformActionCount() {
@@ -2285,6 +2585,29 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2285
2585
  };
2286
2586
  this.emit('status-prompt', promptData);
2287
2587
  }
2588
+ /**
2589
+ * Detect browser information for debugging
2590
+ */
2591
+ detectBrowserInfo() {
2592
+ const ua = navigator.userAgent.toLowerCase();
2593
+ if (/quark/i.test(ua))
2594
+ return 'Quark Browser';
2595
+ if (/micromessenger/i.test(ua))
2596
+ return 'WeChat';
2597
+ if (/alipay/i.test(ua))
2598
+ return 'Alipay';
2599
+ if (/qq/i.test(ua))
2600
+ return 'QQ';
2601
+ if (/safari/i.test(ua) && !/chrome/i.test(ua))
2602
+ return 'Safari';
2603
+ if (/chrome/i.test(ua))
2604
+ return 'Chrome';
2605
+ if (/firefox/i.test(ua))
2606
+ return 'Firefox';
2607
+ if (/edge/i.test(ua))
2608
+ return 'Edge';
2609
+ return 'Unknown';
2610
+ }
2288
2611
  /**
2289
2612
  * Emit debug event
2290
2613
  */