@sssxyd/face-liveness-detector 0.2.29 → 0.2.31

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/index.js CHANGED
@@ -88,6 +88,7 @@
88
88
  // resource paths
89
89
  human_model_path: undefined,
90
90
  tensorflow_wasm_path: undefined,
91
+ tensorflow_backend: 'auto', // 'auto' | 'webgl' | 'wasm'
91
92
  // DetectionSettings defaults
92
93
  video_width: 640,
93
94
  video_height: 640,
@@ -140,6 +141,9 @@
140
141
  if (userConfig.tensorflow_wasm_path !== undefined) {
141
142
  merged.tensorflow_wasm_path = userConfig.tensorflow_wasm_path;
142
143
  }
144
+ if (userConfig.tensorflow_backend !== undefined) {
145
+ merged.tensorflow_backend = userConfig.tensorflow_backend;
146
+ }
143
147
  if (userConfig.video_width !== undefined) {
144
148
  merged.video_width = userConfig.video_width;
145
149
  }
@@ -326,18 +330,68 @@
326
330
  return false;
327
331
  }
328
332
  }
329
- function _detectOptimalBackend() {
333
+ function _detectBrowserEngine(userAgent) {
334
+ const ua = userAgent.toLowerCase();
335
+ // 检测 Gecko (Firefox)
336
+ if (/firefox/i.test(ua) && !/seamonkey/i.test(ua)) {
337
+ return 'gecko';
338
+ }
339
+ // 检测 WebKit (Safari, iOS browsers)
340
+ // Safari 的特征:有 Safari 但没有 Chrome
341
+ if (/safari/i.test(ua) && !/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
342
+ return 'webkit';
343
+ }
344
+ // 检测 Chromium/Blink
345
+ // Chrome-based browsers: Chrome, Chromium, Edge, Brave, Opera, Vivaldi, Whale, Arc, etc.
346
+ if (/chrome|chromium|crios|edge|edgios|edg|brave|opera|vivaldi|whale|arc|yabrowser|samsung|kiwi|ghostery/i.test(ua)) {
347
+ return 'chromium';
348
+ }
349
+ // 默认为 other
350
+ return 'other';
351
+ }
352
+ function _getOptimalBackendForEngine(engine) {
353
+ // 针对不同内核的优化策略
354
+ const backendConfig = {
355
+ chromium: 'webgl', // Chromium 内核:优先 WebGL
356
+ webkit: 'wasm', // WebKit(Safari、iOS):使用 WASM
357
+ gecko: 'webgl', // Firefox:优先 WebGL
358
+ other: 'wasm' // 未知浏览器:保守使用 WASM
359
+ };
360
+ return backendConfig[engine];
361
+ }
362
+ function _detectOptimalBackend(preferredBackend) {
363
+ // If user explicitly specified a backend, honor it (unless it's 'auto')
364
+ if (preferredBackend && preferredBackend !== 'auto') {
365
+ console.log('[Backend Detection] Using user-specified backend:', {
366
+ backend: preferredBackend,
367
+ userAgent: navigator.userAgent
368
+ });
369
+ return preferredBackend;
370
+ }
330
371
  const userAgent = navigator.userAgent.toLowerCase();
331
- // Special browsers: prefer WASM
332
- if ((/safari/.test(userAgent) && !/chrome/.test(userAgent)) ||
333
- /micromessenger/i.test(userAgent) ||
334
- /alipay/.test(userAgent) ||
335
- /qq/.test(userAgent) ||
336
- /(wechat|alipay|qq)webview/i.test(userAgent)) {
337
- return 'wasm';
338
- }
339
- // Desktop: prefer WebGL
340
- return _isWebGLAvailable() ? 'webgl' : 'wasm';
372
+ const engine = _detectBrowserEngine(userAgent);
373
+ console.log('[Backend Detection] Detected browser engine:', {
374
+ engine,
375
+ userAgent: navigator.userAgent
376
+ });
377
+ // 获取该内核的推荐后端
378
+ let preferredBackendForEngine = _getOptimalBackendForEngine(engine);
379
+ // 对于 Chromium 和 Gecko,检查 WebGL 是否可用
380
+ if (preferredBackendForEngine === 'webgl') {
381
+ const hasWebGL = _isWebGLAvailable();
382
+ console.log('[Backend Detection] WebGL availability check:', {
383
+ engine,
384
+ hasWebGL,
385
+ selectedBackend: hasWebGL ? 'webgl' : 'wasm'
386
+ });
387
+ return hasWebGL ? 'webgl' : 'wasm';
388
+ }
389
+ // 对于 WebKit 和 other,直接使用 WASM
390
+ console.log('[Backend Detection] Using backend for engine:', {
391
+ engine,
392
+ backend: preferredBackendForEngine
393
+ });
394
+ return preferredBackendForEngine;
341
395
  }
342
396
  /**
343
397
  * 预加载 OpenCV.js 以确保全局 cv 对象可用
@@ -472,10 +526,6 @@
472
526
  try {
473
527
  await opencvInitPromise;
474
528
  cv = getCvSync();
475
- if (cv && cv.Mat) {
476
- console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
477
- return { cv };
478
- }
479
529
  }
480
530
  catch (error) {
481
531
  // 失败后清除 Promise,允许重试
@@ -495,10 +545,6 @@
495
545
  try {
496
546
  await opencvInitPromise;
497
547
  cv = getCvSync();
498
- if (cv && cv.Mat) {
499
- console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
500
- return { cv };
501
- }
502
548
  }
503
549
  catch (error) {
504
550
  // 失败后清除 Promise,允许重试
@@ -506,13 +552,17 @@
506
552
  throw error;
507
553
  }
508
554
  }
509
- // 如果到这里还没有返回,说明加载失败
510
- console.error('[FaceDetectionEngine] OpenCV module is invalid:', {
511
- hasMat: cv && cv.Mat,
512
- type: typeof cv,
513
- keys: cv ? Object.keys(cv).slice(0, 10) : 'N/A'
514
- });
515
- throw new Error('OpenCV.js loaded but module is invalid (no Mat class found)');
555
+ // 最终验证
556
+ if (!cv || !cv.Mat) {
557
+ console.error('[FaceDetectionEngine] OpenCV module is invalid:', {
558
+ hasMat: cv && cv.Mat,
559
+ type: typeof cv,
560
+ keys: cv ? Object.keys(cv).slice(0, 10) : 'N/A'
561
+ });
562
+ throw new Error('OpenCV.js loaded but module is invalid (no Mat class found)');
563
+ }
564
+ console.log('[FaceDetectionEngine] OpenCV.js loaded successfully');
565
+ return { cv };
516
566
  }
517
567
  catch (error) {
518
568
  console.error('[FaceDetectionEngine] Failed to load OpenCV.js:', error);
@@ -538,11 +588,12 @@
538
588
  * Load Human.js
539
589
  * @param modelPath - Path to model files (optional)
540
590
  * @param wasmPath - Path to WASM files (optional)
591
+ * @param preferredBackend - Preferred TensorFlow backend: 'auto' | 'webgl' | 'wasm' (default: 'auto')
541
592
  * @returns Promise that resolves with Human instance
542
593
  */
543
- async function loadHuman(modelPath, wasmPath) {
594
+ async function loadHuman(modelPath, wasmPath, preferredBackend) {
544
595
  const config = {
545
- backend: _detectOptimalBackend(),
596
+ backend: _detectOptimalBackend(preferredBackend),
546
597
  face: {
547
598
  enabled: true,
548
599
  detector: { rotation: false, return: true },
@@ -566,30 +617,90 @@
566
617
  console.log('[FaceDetectionEngine] Human.js config:', {
567
618
  backend: config.backend,
568
619
  modelBasePath: config.modelBasePath || '(using default)',
569
- wasmPath: config.wasmPath || '(using default)'
620
+ wasmPath: config.wasmPath || '(using default)',
621
+ userAgent: navigator.userAgent,
622
+ platform: navigator.platform
570
623
  });
571
624
  const initStartTime = performance.now();
572
625
  console.log('[FaceDetectionEngine] Creating Human instance...');
573
- const human = new Human(config);
626
+ let human;
627
+ try {
628
+ human = new Human(config);
629
+ }
630
+ catch (error) {
631
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error during Human instantiation';
632
+ console.error('[FaceDetectionEngine] Failed to create Human instance:', errorMsg);
633
+ throw new Error(`Human instantiation failed: ${errorMsg}`);
634
+ }
574
635
  const instanceCreateTime = performance.now() - initStartTime;
575
636
  console.log(`[FaceDetectionEngine] Human instance created, took ${instanceCreateTime.toFixed(2)}ms`);
637
+ // 验证 Human 实例
638
+ if (!human) {
639
+ throw new Error('Human instance is null after creation');
640
+ }
576
641
  console.log('[FaceDetectionEngine] Loading Human.js models...');
577
642
  const modelLoadStartTime = performance.now();
578
643
  try {
579
- await human.load();
644
+ // 添加超时机制防止无限等待
645
+ const loadTimeout = new Promise((_, reject) => {
646
+ const timeoutId = setTimeout(() => {
647
+ reject(new Error('Human.js load() timeout after 60 seconds - possible issue with model loading on mobile'));
648
+ }, 60000);
649
+ });
650
+ // 竞速:哪个先完成就用哪个
651
+ await Promise.race([
652
+ human.load(),
653
+ loadTimeout
654
+ ]);
580
655
  const loadTime = performance.now() - modelLoadStartTime;
581
656
  const totalTime = performance.now() - initStartTime;
582
657
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
583
658
  modelLoadTime: `${loadTime.toFixed(2)}ms`,
584
659
  totalInitTime: `${totalTime.toFixed(2)}ms`,
585
- version: human.version
660
+ version: human.version,
661
+ config: human.config
586
662
  });
663
+ // 验证加载后的 Human 实例有必要的方法和属性
664
+ if (typeof human.detect !== 'function') {
665
+ throw new Error('Human.detect method not available after loading');
666
+ }
667
+ if (!human.version) {
668
+ console.warn('[FaceDetectionEngine] Human.js loaded but version is missing');
669
+ }
670
+ // 打印加载的模型信息
671
+ if (human.models) {
672
+ const loadedModels = Object.entries(human.models).map(([name, model]) => ({
673
+ name,
674
+ loaded: model?.loaded || model?.state === 'loaded',
675
+ type: typeof model
676
+ }));
677
+ console.log('[FaceDetectionEngine] Loaded models:', {
678
+ totalModels: Object.keys(human.models).length,
679
+ models: loadedModels,
680
+ allModels: Object.keys(human.models)
681
+ });
682
+ }
683
+ else {
684
+ console.warn('[FaceDetectionEngine] human.models is not available');
685
+ }
686
+ // 额外验证:检查模型是否加载成功
687
+ if (!human.models || Object.keys(human.models).length === 0) {
688
+ console.warn('[FaceDetectionEngine] Warning: human.models appears to be empty after loading');
689
+ }
587
690
  return human;
588
691
  }
589
692
  catch (error) {
590
693
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
591
- console.error('[FaceDetectionEngine] Human.js load failed:', errorMsg);
592
- throw error;
694
+ const stack = error instanceof Error ? error.stack : 'N/A';
695
+ console.error('[FaceDetectionEngine] Human.js load failed:', {
696
+ errorMsg,
697
+ stack,
698
+ userAgent: navigator.userAgent,
699
+ platform: navigator.platform,
700
+ humanVersion: human?.version,
701
+ humanConfig: human?.config
702
+ });
703
+ throw new Error(`Human.js loading failed: ${errorMsg}`);
593
704
  }
594
705
  }
595
706
  /**
@@ -1625,7 +1736,29 @@
1625
1736
  console.log('[FaceDetectionEngine] Loading Human.js models...');
1626
1737
  this.emitDebug('initialization', 'Loading Human.js...');
1627
1738
  const humanStartTime = performance.now();
1628
- this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path);
1739
+ try {
1740
+ this.human = await loadHuman(this.config.human_model_path, this.config.tensorflow_wasm_path, this.config.tensorflow_backend);
1741
+ }
1742
+ catch (humanError) {
1743
+ const errorMsg = humanError instanceof Error ? humanError.message : 'Unknown error';
1744
+ console.error('[FaceDetectionEngine] Human.js loading failed with error:', errorMsg);
1745
+ this.emitDebug('initialization', 'Human.js loading failed with exception', {
1746
+ error: errorMsg,
1747
+ stack: humanError instanceof Error ? humanError.stack : 'N/A',
1748
+ userAgent: navigator.userAgent,
1749
+ platform: navigator.platform,
1750
+ browser: this.detectBrowserInfo()
1751
+ }, 'error');
1752
+ this.emit('detector-loaded', {
1753
+ success: false,
1754
+ error: `Failed to load Human.js: ${errorMsg}`
1755
+ });
1756
+ this.emit('detector-error', {
1757
+ code: exports.ErrorCode.DETECTOR_NOT_INITIALIZED,
1758
+ message: `Human.js loading error: ${errorMsg}`
1759
+ });
1760
+ return;
1761
+ }
1629
1762
  const humanLoadTime = performance.now() - humanStartTime;
1630
1763
  if (!this.human) {
1631
1764
  const errorMsg = 'Failed to load Human.js: instance is null';
@@ -1641,13 +1774,35 @@
1641
1774
  });
1642
1775
  return;
1643
1776
  }
1777
+ // Verify Human.js instance has required properties
1778
+ if (!this.human.version || typeof this.human.detect !== 'function') {
1779
+ const errorMsg = 'Human.js instance is incomplete: missing version or detect method';
1780
+ console.error('[FaceDetectionEngine] ' + errorMsg);
1781
+ this.emitDebug('initialization', errorMsg, {
1782
+ hasVersion: !!this.human.version,
1783
+ hasDetect: typeof this.human.detect === 'function',
1784
+ instanceKeys: Object.keys(this.human || {})
1785
+ }, 'error');
1786
+ this.emit('detector-loaded', {
1787
+ success: false,
1788
+ error: errorMsg
1789
+ });
1790
+ this.emit('detector-error', {
1791
+ code: exports.ErrorCode.DETECTOR_NOT_INITIALIZED,
1792
+ message: errorMsg
1793
+ });
1794
+ return;
1795
+ }
1644
1796
  this.emitDebug('initialization', 'Human.js loaded successfully', {
1645
1797
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1646
- version: this.human.version
1798
+ version: this.human.version,
1799
+ backend: this.human.config?.backend || 'unknown',
1800
+ config: this.human.config
1647
1801
  });
1648
1802
  console.log('[FaceDetectionEngine] Human.js loaded successfully', {
1649
1803
  loadTime: `${humanLoadTime.toFixed(2)}ms`,
1650
- version: this.human.version
1804
+ version: this.human.version,
1805
+ backend: this.human.config?.backend || 'unknown'
1651
1806
  });
1652
1807
  this.isReady = true;
1653
1808
  const loadedData = {
@@ -1924,13 +2079,31 @@
1924
2079
  try {
1925
2080
  // Check video is ready
1926
2081
  if (this.videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1927
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2082
+ this.scheduleNextDetection(this.config.error_retry_delay);
1928
2083
  return;
1929
2084
  }
1930
2085
  // Perform face detection
1931
- const result = await this.human.detect(this.videoElement);
2086
+ let result;
2087
+ try {
2088
+ result = await this.human.detect(this.videoElement);
2089
+ }
2090
+ catch (detectError) {
2091
+ const errorMsg = detectError instanceof Error ? detectError.message : 'Unknown error';
2092
+ this.emitDebug('detection', 'Human.detect() call failed', {
2093
+ error: errorMsg,
2094
+ stack: detectError instanceof Error ? detectError.stack : 'N/A',
2095
+ hasHuman: !!this.human,
2096
+ humanVersion: this.human?.version,
2097
+ videoReadyState: this.videoElement?.readyState,
2098
+ videoWidth: this.videoElement?.videoWidth,
2099
+ videoHeight: this.videoElement?.videoHeight
2100
+ }, 'error');
2101
+ this.scheduleNextDetection(this.config.error_retry_delay);
2102
+ return;
2103
+ }
1932
2104
  if (!result) {
1933
- this.scheduleNextDetection(this.config.error_retry_delay); // DETECTION_FRAME_DELAY
2105
+ this.emitDebug('detection', 'Face detection returned null result', {}, 'warn');
2106
+ this.scheduleNextDetection(this.config.error_retry_delay);
1934
2107
  return;
1935
2108
  }
1936
2109
  const faces = result.face || [];
@@ -1943,18 +2116,24 @@
1943
2116
  }
1944
2117
  }
1945
2118
  catch (error) {
1946
- this.emitDebug('detection', 'Detection error', {
1947
- error: error.message,
1948
- stack: error.stack
2119
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
2120
+ this.emitDebug('detection', 'Unexpected error in detection loop', {
2121
+ error: errorMsg,
2122
+ stack: error instanceof Error ? error.stack : 'N/A'
1949
2123
  }, 'error');
1950
- this.scheduleNextDetection(this.config.error_retry_delay); // ERROR_RETRY_DELAY
2124
+ this.scheduleNextDetection(this.config.error_retry_delay);
1951
2125
  }
1952
2126
  }
1953
2127
  getPerformActionCount() {
1954
2128
  if (this.config.liveness_action_count <= 0) {
2129
+ this.emitDebug('config', 'liveness_action_count is 0 or negative', { count: this.config.liveness_action_count }, 'warn');
1955
2130
  return 0;
1956
2131
  }
1957
- return Math.min(this.config.liveness_action_count, this.config.liveness_action_list.length);
2132
+ const actionListLength = this.config.liveness_action_list?.length ?? 0;
2133
+ if (actionListLength === 0) {
2134
+ this.emitDebug('config', 'liveness_action_list is empty', { actionListLength }, 'warn');
2135
+ }
2136
+ return Math.min(this.config.liveness_action_count, actionListLength);
1958
2137
  }
1959
2138
  /**
1960
2139
  * Handle single face detection
@@ -2006,6 +2185,7 @@
2006
2185
  validateFaceBox(faceBox) {
2007
2186
  if (!faceBox) {
2008
2187
  console.warn('[FaceDetector] Face detected but no box/boxRaw property');
2188
+ this.emitDebug('detection', 'Face box is missing - face detected but no box/boxRaw property', {}, 'warn');
2009
2189
  this.scheduleNextDetection(this.config.error_retry_delay);
2010
2190
  return false;
2011
2191
  }
@@ -2179,20 +2359,25 @@
2179
2359
  this.detectionState.collectCount++;
2180
2360
  return;
2181
2361
  }
2182
- const frameImageData = this.captureFrame();
2183
- if (!frameImageData) {
2184
- this.emitDebug('detection', 'Failed to capture current frame image', {}, 'warn');
2185
- return;
2362
+ try {
2363
+ const frameImageData = this.captureFrame();
2364
+ if (!frameImageData) {
2365
+ this.emitDebug('detection', 'Failed to capture current frame image', { frameQuality, bestQualityScore: this.detectionState.bestQualityScore }, 'warn');
2366
+ return;
2367
+ }
2368
+ const faceImageData = this.captureFrame(faceBox);
2369
+ if (!faceImageData) {
2370
+ this.emitDebug('detection', 'Failed to capture face image', { faceBox }, 'warn');
2371
+ return;
2372
+ }
2373
+ this.detectionState.collectCount++;
2374
+ this.detectionState.bestQualityScore = frameQuality;
2375
+ this.detectionState.bestFrameImage = frameImageData;
2376
+ this.detectionState.bestFaceImage = faceImageData;
2186
2377
  }
2187
- const faceImageData = this.captureFrame(faceBox);
2188
- if (!faceImageData) {
2189
- this.emitDebug('detection', 'Failed to capture face image', {}, 'warn');
2190
- return;
2378
+ catch (error) {
2379
+ this.emitDebug('detection', 'Error during image collection', { error: error.message }, 'error');
2191
2380
  }
2192
- this.detectionState.collectCount++;
2193
- this.detectionState.bestQualityScore = frameQuality;
2194
- this.detectionState.bestFrameImage = frameImageData;
2195
- this.detectionState.bestFaceImage = faceImageData;
2196
2381
  }
2197
2382
  emitLivenessDetected(passed, size, frontal = 0, quality = 0, real = 0, live = 0) {
2198
2383
  this.emit('face-detected', { passed, size, frontal, quality, real, live });
@@ -2203,6 +2388,7 @@
2203
2388
  selectNextAction() {
2204
2389
  const availableActions = (this.config.liveness_action_list ?? []).filter(action => !this.detectionState.completedActions.has(action));
2205
2390
  if (availableActions.length === 0) {
2391
+ this.emitDebug('liveness', 'No available actions to perform', { completedActions: Array.from(this.detectionState.completedActions), totalActions: this.config.liveness_action_list?.length ?? 0 }, 'warn');
2206
2392
  return;
2207
2393
  }
2208
2394
  let nextAction = availableActions[0];
@@ -2248,36 +2434,45 @@
2248
2434
  * Detect specific action
2249
2435
  */
2250
2436
  detectAction(action, gestures) {
2251
- if (!gestures || gestures.length === 0)
2437
+ if (!gestures || gestures.length === 0) {
2438
+ this.emitDebug('liveness', 'No gestures detected for action verification', { action, gestureCount: gestures?.length ?? 0 }, 'info');
2439
+ return false;
2440
+ }
2441
+ try {
2442
+ switch (action) {
2443
+ case exports.LivenessAction.BLINK:
2444
+ return gestures.some(g => {
2445
+ if (!g.gesture)
2446
+ return false;
2447
+ return g.gesture.includes('blink');
2448
+ });
2449
+ case exports.LivenessAction.MOUTH_OPEN:
2450
+ return gestures.some(g => {
2451
+ const gestureStr = g.gesture;
2452
+ if (!gestureStr || !gestureStr.includes('mouth'))
2453
+ return false;
2454
+ const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
2455
+ if (!percentMatch || !percentMatch[1])
2456
+ return false;
2457
+ const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
2458
+ return percent > (this.config.min_mouth_open_percent ?? 0.2);
2459
+ });
2460
+ case exports.LivenessAction.NOD:
2461
+ return gestures.some(g => {
2462
+ if (!g.gesture)
2463
+ return false;
2464
+ // Check for continuous head movement (up -> down or down -> up)
2465
+ const headPattern = g.gesture.match(/head\s+(up|down)/i);
2466
+ return !!headPattern && !!headPattern[1];
2467
+ });
2468
+ default:
2469
+ this.emitDebug('liveness', 'Unknown action type in detection', { action }, 'warn');
2470
+ return false;
2471
+ }
2472
+ }
2473
+ catch (error) {
2474
+ this.emitDebug('liveness', 'Error during action detection', { action, error: error.message }, 'error');
2252
2475
  return false;
2253
- switch (action) {
2254
- case exports.LivenessAction.BLINK:
2255
- return gestures.some(g => {
2256
- if (!g.gesture)
2257
- return false;
2258
- return g.gesture.includes('blink');
2259
- });
2260
- case exports.LivenessAction.MOUTH_OPEN:
2261
- return gestures.some(g => {
2262
- const gestureStr = g.gesture;
2263
- if (!gestureStr || !gestureStr.includes('mouth'))
2264
- return false;
2265
- const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
2266
- if (!percentMatch || !percentMatch[1])
2267
- return false;
2268
- const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
2269
- return percent > (this.config.min_mouth_open_percent ?? 0.2);
2270
- });
2271
- case exports.LivenessAction.NOD:
2272
- return gestures.some(g => {
2273
- if (!g.gesture)
2274
- return false;
2275
- // Check for continuous head movement (up -> down or down -> up)
2276
- const headPattern = g.gesture.match(/head\s+(up|down)/i);
2277
- return !!headPattern && !!headPattern[1];
2278
- });
2279
- default:
2280
- return false;
2281
2476
  }
2282
2477
  }
2283
2478
  /**
@@ -2290,6 +2485,29 @@
2290
2485
  };
2291
2486
  this.emit('status-prompt', promptData);
2292
2487
  }
2488
+ /**
2489
+ * Detect browser information for debugging
2490
+ */
2491
+ detectBrowserInfo() {
2492
+ const ua = navigator.userAgent.toLowerCase();
2493
+ if (/quark/i.test(ua))
2494
+ return 'Quark Browser';
2495
+ if (/micromessenger/i.test(ua))
2496
+ return 'WeChat';
2497
+ if (/alipay/i.test(ua))
2498
+ return 'Alipay';
2499
+ if (/qq/i.test(ua))
2500
+ return 'QQ';
2501
+ if (/safari/i.test(ua) && !/chrome/i.test(ua))
2502
+ return 'Safari';
2503
+ if (/chrome/i.test(ua))
2504
+ return 'Chrome';
2505
+ if (/firefox/i.test(ua))
2506
+ return 'Firefox';
2507
+ if (/edge/i.test(ua))
2508
+ return 'Edge';
2509
+ return 'Unknown';
2510
+ }
2293
2511
  /**
2294
2512
  * Emit debug event
2295
2513
  */
@@ -2402,26 +2620,35 @@
2402
2620
  */
2403
2621
  captureFrame(box) {
2404
2622
  if (!this.frameCanvasElement) {
2623
+ this.emitDebug('capture', 'Frame canvas element is null, cannot capture frame', {}, 'error');
2405
2624
  return null;
2406
2625
  }
2407
2626
  if (!box) {
2408
2627
  return this.canvasToBase64(this.frameCanvasElement);
2409
2628
  }
2410
- const x = box[0], y = box[1], width = box[2], height = box[3];
2411
- // If cached canvas size does not match, recreate it
2412
- if (!this.faceCanvasElement || this.faceCanvasElement.width !== width || this.faceCanvasElement.height !== height) {
2413
- this.clearFaceCanvas();
2414
- this.faceCanvasElement = document.createElement('canvas');
2629
+ try {
2630
+ const x = box[0], y = box[1], width = box[2], height = box[3];
2631
+ // If cached canvas size does not match, recreate it
2632
+ if (!this.faceCanvasElement || this.faceCanvasElement.width !== width || this.faceCanvasElement.height !== height) {
2633
+ this.clearFaceCanvas();
2634
+ this.faceCanvasElement = document.createElement('canvas');
2635
+ this.faceCanvasElement.width = width;
2636
+ this.faceCanvasElement.height = height;
2637
+ this.faceCanvasContext = this.faceCanvasElement.getContext('2d');
2638
+ }
2639
+ if (!this.faceCanvasContext) {
2640
+ this.emitDebug('capture', 'Failed to get face canvas 2D context', { width, height }, 'error');
2641
+ return null;
2642
+ }
2415
2643
  this.faceCanvasElement.width = width;
2416
2644
  this.faceCanvasElement.height = height;
2417
- this.faceCanvasContext = this.faceCanvasElement.getContext('2d');
2645
+ this.faceCanvasContext.drawImage(this.frameCanvasElement, x, y, width, height, 0, 0, width, height);
2646
+ return this.canvasToBase64(this.faceCanvasElement);
2418
2647
  }
2419
- if (!this.faceCanvasContext)
2648
+ catch (error) {
2649
+ this.emitDebug('capture', 'Error during face frame capture', { box, error: error.message }, 'error');
2420
2650
  return null;
2421
- this.faceCanvasElement.width = width;
2422
- this.faceCanvasElement.height = height;
2423
- this.faceCanvasContext.drawImage(this.frameCanvasElement, x, y, width, height, 0, 0, width, height);
2424
- return this.canvasToBase64(this.faceCanvasElement);
2651
+ }
2425
2652
  }
2426
2653
  }
2427
2654