facebetter 1.0.14 → 1.1.1

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.
@@ -53,7 +53,13 @@
53
53
  this.appId = config.appId || null;
54
54
  this.appKey = config.appKey || null;
55
55
  this.licenseJson = config.licenseJson || null;
56
- this.resourcePath = '/facebetter/resource.bundle';
56
+ this.resourcePath = '/resource.fbd';
57
+ /**
58
+ * Whether to use an external GL context (native platforms only).
59
+ * In Web/WASM environments, this field is kept for configuration structure alignment,
60
+ * but it does not take effect in the current implementation.
61
+ */
62
+ this.externalContext = !!config.externalContext;
57
63
  }
58
64
 
59
65
  /**
@@ -61,11 +67,11 @@
61
67
  * @returns {boolean} True if valid
62
68
  */
63
69
  isValid() {
64
- // 如果提供了 licenseJson,则使用授权数据验证
70
+ // If licenseJson is provided, use it for validation
65
71
  if (this.licenseJson) {
66
72
  return typeof this.licenseJson === 'string' && this.licenseJson.trim() !== '';
67
73
  }
68
- // 否则需要 appId appKey
74
+ // Otherwise appId and appKey are required
69
75
  return this.appId && typeof this.appId === 'string' && this.appId.trim() !== '' &&
70
76
  this.appKey && typeof this.appKey === 'string' && this.appKey.trim() !== '';
71
77
  }
@@ -88,61 +94,87 @@
88
94
  * Beauty type enumeration
89
95
  */
90
96
  const BeautyType$1 = {
91
- Basic: 0, // 基础美颜
92
- Reshape: 1, // 面部重塑
93
- Makeup: 2, // 美妆效果
94
- VirtualBackground: 3 // 虚拟背景
97
+ Basic: 0, // Basic beauty (smoothing, whitening, etc.)
98
+ Reshape: 1, // Face reshaping (face thinning, big eyes, etc.)
99
+ Makeup: 2, // Makeup effects (lipstick, blush, etc.)
100
+ VirtualBackground: 3, // Virtual background (blur, image replacement)
101
+ ChromaKey: 4, // Chroma key (green screen removal)
102
+ Filter: 5, // LUT based filter
103
+ Sticker: 6 // 2D/3D sticker
95
104
  };
96
105
 
97
106
  /**
98
107
  * Basic beauty parameter enumeration
108
+ * All values should be in range [0.0, 1.0]
99
109
  */
100
110
  const BasicParam$1 = {
101
- Smoothing: 0, // 磨皮
102
- Sharpening: 1, // 锐化
103
- Whitening: 2, // 美白
104
- Rosiness: 3 // 红润
111
+ Smoothing: 0, // Skin smoothing (0.0: none, 1.0: maximum)
112
+ Sharpening: 1, // Image sharpening (0.0: none, 1.0: maximum)
113
+ Whitening: 2, // Skin whitening (0.0: none, 1.0: maximum)
114
+ Rosiness: 3 // Skin rosiness (0.0: none, 1.0: maximum)
105
115
  };
106
116
 
107
117
  /**
108
118
  * Face reshape parameter enumeration
119
+ * All values should be in range [0.0, 1.0]
109
120
  */
110
121
  const ReshapeParam$1 = {
111
- FaceThin: 0, // 瘦脸
112
- FaceVShape: 1, // V
113
- FaceNarrow: 2, // 窄脸
114
- FaceShort: 3, // 短脸
115
- Cheekbone: 4, // 颧骨
116
- Jawbone: 5, // 下颌骨
117
- Chin: 6, // 下巴
118
- NoseSlim: 7, // 瘦鼻梁
119
- EyeSize: 8, // 大眼
120
- EyeDistance: 9 // 眼距
122
+ FaceThinning: 0, // Face thinning (0.0: none, 1.0: maximum)
123
+ FaceVShape: 1, // V-shape face (0.0: none, 1.0: maximum)
124
+ FaceNarrowing: 2, // Face narrowing (0.0: none, 1.0: maximum)
125
+ FaceShortening: 3,// Face shortening (0.0: none, 1.0: maximum)
126
+ Cheekbone: 4, // Cheekbone slimming (0.0: none, 1.0: maximum)
127
+ Jawbone: 5, // Jawbone slimming (0.0: none, 1.0: maximum)
128
+ Chin: 6, // Chin length adjustment (0.0: none, 1.0: maximum)
129
+ NoseSlimming: 7, // Nose slimming (0.0: none, 1.0: maximum)
130
+ EyeSize: 8, // Eye size (0.0: normal, 1.0: maximum)
131
+ EyeDistance: 9 // Eye distance (0.0: normal, 1.0: maximum)
121
132
  };
122
133
 
123
134
  /**
124
135
  * Makeup parameter enumeration
136
+ * All values should be in range [0.0, 1.0]
125
137
  */
126
138
  const MakeupParam$1 = {
127
- Lipstick: 0, // 口红
128
- Blush: 1 // 腮红
139
+ Lipstick: 0, // Lipstick intensity (0.0: none, 1.0: maximum)
140
+ Blush: 1 // Blush intensity (0.0: none, 1.0: maximum)
141
+ };
142
+
143
+ /**
144
+ * Chroma Key parameter enumeration
145
+ */
146
+ const ChromaKeyParam = {
147
+ KeyColor: 0, // Key color (0.0: Green, 1.0: Blue, 2.0: Red)
148
+ Similarity: 1, // Color similarity (0.0 - 1.0)
149
+ Smoothness: 2, // Edge smoothness (0.0 - 1.0)
150
+ Desaturation: 3 // Spill desaturation (0.0 - 1.0)
129
151
  };
130
152
 
131
153
  /**
132
154
  * Background mode enumeration
133
155
  */
134
156
  const BackgroundMode$1 = {
135
- None: 0, // 无背景处理
136
- Blur: 1, // 模糊背景
137
- Image: 2 // 背景图片替换
157
+ None: 0, // No background processing
158
+ Blur: 1, // Blurred background
159
+ Image: 2 // Background image replacement
138
160
  };
139
161
 
140
162
  /**
141
- * Process mode enumeration
163
+ * Frame type enumeration
142
164
  */
143
- const ProcessMode$1 = {
144
- Image: 0, // 图片模式
145
- Video: 1 // 视频模式
165
+ const FrameType$1 = {
166
+ Image: 0, // Image mode
167
+ Video: 1 // Video mode
168
+ };
169
+
170
+ /**
171
+ * Mirror mode enumeration (apply to input before processing)
172
+ */
173
+ const MirrorMode$1 = {
174
+ None: 0, // No mirror
175
+ Horizontal: 1, // Mirror horizontally (e.g. front camera selfie)
176
+ Vertical: 2, // Mirror vertically
177
+ Both: 3 // Mirror both axes
146
178
  };
147
179
 
148
180
  /**
@@ -178,8 +210,10 @@
178
210
  BackgroundMode: BackgroundMode$1,
179
211
  BasicParam: BasicParam$1,
180
212
  BeautyType: BeautyType$1,
213
+ ChromaKeyParam: ChromaKeyParam,
214
+ FrameType: FrameType$1,
181
215
  MakeupParam: MakeupParam$1,
182
- ProcessMode: ProcessMode$1,
216
+ MirrorMode: MirrorMode$1,
183
217
  ReshapeParam: ReshapeParam$1,
184
218
  VirtualBackgroundOptions: VirtualBackgroundOptions$1
185
219
  });
@@ -217,7 +251,7 @@
217
251
  throw new FacebetterError('Platform API is required');
218
252
  }
219
253
 
220
- // 如果传入的是 EngineConfig 实例,直接使用;否则创建新的 EngineConfig
254
+ // If an EngineConfig instance is passed, use it directly; otherwise create a new EngineConfig
221
255
  let engineConfig;
222
256
  if (config instanceof EngineConfig) {
223
257
  engineConfig = config;
@@ -235,20 +269,23 @@
235
269
  this.appId = engineConfig.appId;
236
270
  this.appKey = engineConfig.appKey;
237
271
  this.licenseJson = engineConfig.licenseJson;
238
- this.resourcePath = '/facebetter/resource.bundle';
272
+ this.resourcePath = '/resource.fbd';
239
273
  this.enginePtr = null;
240
274
  this.initialized = false;
241
275
  this.srcBufferPtr = null;
242
276
  this.dstBufferPtr = null;
243
277
  this.bufferSize = 0;
244
278
 
279
+ // Callback related state
280
+ this._callbackSharedBufferPtr = null;
281
+ this._callbackSharedBufferSize = 0;
282
+
245
283
  // Store platform API
246
284
  this._platformAPI = platformAPI;
247
285
  this._loadWasmModule = platformAPI.loadWasmModule;
248
286
  this._getWasmModule = platformAPI.getWasmModule;
249
287
  this._getWasmBuffer = platformAPI.getWasmBuffer;
250
288
  this._toImageData = platformAPI.toImageData;
251
- this._verifyAppKeyOnline = platformAPI.verifyAppKeyOnline;
252
289
  this._ensureGPUPixelCanvas = platformAPI.ensureGPUPixelCanvas;
253
290
  this._cleanupGPUPixelCanvas = platformAPI.cleanupGPUPixelCanvas;
254
291
 
@@ -326,23 +363,22 @@
326
363
  * @returns {Promise<void>} Promise that resolves when initialization is complete
327
364
  */
328
365
  async init(options = {}) {
329
- // 并发控制:如果已经初始化,直接返回
366
+ // Concurrency control: if already initialized, return immediately
330
367
  if (this.initialized) {
331
368
  return;
332
369
  }
333
370
 
334
- // 并发控制:如果正在初始化,返回同一个 Promise
371
+ // Concurrency control: if initialization is in progress, return the same Promise
335
372
  if (this._initPromise) {
336
373
  return this._initPromise;
337
374
  }
338
375
 
339
- // 创建初始化 Promise
376
+ // Create initialization Promise
340
377
  this._initPromise = (async () => {
341
378
  try {
342
379
  const wasmTimeout = options.timeout || 30000;
343
- const authTimeout = options.authTimeout || 10000;
344
380
 
345
- // 等待 WASM 模块加载(带超时)
381
+ // Wait for WASM module loading (with timeout)
346
382
  try {
347
383
  await Promise.race([
348
384
  this._wasmLoadPromise,
@@ -354,48 +390,60 @@
354
390
 
355
391
  const Module = this._getWasmModule();
356
392
 
357
- let licenseJsonToUse = this.licenseJson;
358
-
359
- // 如果用户提供了 appId appKey(但没有提供 licenseJson),则在 JS 层发送 HTTP 请求
360
- if (this.appId && this.appKey && !this.licenseJson && this._verifyAppKeyOnline) {
361
- try {
362
- // 在线认证(带超时)
363
- const authResponse = await Promise.race([
364
- this._verifyAppKeyOnline(this.appId, this.appKey),
365
- this._createTimeout(authTimeout, 'Online authentication')
366
- ]);
367
-
368
- if (!authResponse) {
369
- throw new FacebetterError(
370
- 'Failed to get server response from online_auth API. The server returned an empty response.',
371
- 'AUTH_EMPTY_RESPONSE'
372
- );
393
+ // Setup usage report proxy for WASM
394
+ if (!Module.onReportUsage) {
395
+ Module.onReportUsage = async (payloadJson) => {
396
+ try {
397
+ const url = 'https://facebetter.pixpark.net/facebetter/v1/report';
398
+ const response = await fetch(url, {
399
+ method: 'POST',
400
+ headers: {
401
+ 'Content-Type': 'application/json'
402
+ },
403
+ body: payloadJson
404
+ });
405
+ return response.ok;
406
+ } catch (error) {
407
+ return false;
373
408
  }
409
+ };
410
+ }
374
411
 
375
- // 将服务器响应转换为 JSON 字符串,作为 licenseJson 传递给 WASM
376
- licenseJsonToUse = JSON.stringify(authResponse);
377
- console.log('Online auth response received, using as licenseJson');
378
- } catch (error) {
379
- if (error instanceof FacebetterError && error.code === 'TIMEOUT') {
380
- throw new FacebetterError(
381
- `Online authentication timed out after ${authTimeout}ms. Please check your network connection or provide licenseJson directly.`,
382
- 'AUTH_TIMEOUT'
383
- );
412
+ // Setup online auth proxy for WASM
413
+ if (!Module.onOnlineAuth) {
414
+ Module.onOnlineAuth = async (payloadJson) => {
415
+ try {
416
+ const url = 'https://facebetter.pixpark.net/facebetter/v1/auth';
417
+ const response = await fetch(url, {
418
+ method: 'POST',
419
+ headers: {
420
+ 'Content-Type': 'application/json'
421
+ },
422
+ body: payloadJson
423
+ });
424
+ if (!response.ok) {
425
+ console.warn('[JS Engine] Online auth fetch failed with status:', response.status);
426
+ return "";
427
+ }
428
+ return await response.text();
429
+ } catch (error) {
430
+ console.error('[JS Engine] Online auth fetch exception:', error);
431
+ return "";
384
432
  }
385
- throw this._enhanceError(error, 'Failed to verify app key online');
386
- }
433
+ };
387
434
  }
388
435
 
389
- // 如果用户直接提供了 licenseJson,则直接使用
390
- // 如果通过 appId/appKey 获取到了响应,则使用响应作为 licenseJson
391
- // WASM 层只需要验证 licenseJson,不需要发送 HTTP 请求
436
+ // Create engine instance, auth (online/offline) is handled by WASM layer via onOnlineAuth
392
437
  const enginePtr = Module.ccall(
393
- 'CreateBeautyEffectEngine',
438
+ 'CreateBeautyEffectEngineEx',
394
439
  'number',
395
- ['string', 'string'],
440
+ ['string', 'string', 'string', 'string', 'number'],
396
441
  [
397
442
  this.resourcePath,
398
- licenseJsonToUse || '' // 使用 licenseJson 验证
443
+ this.licenseJson || '',
444
+ this.appId || '',
445
+ this.appKey || '',
446
+ this.config.externalContext ? 1 : 0
399
447
  ]
400
448
  );
401
449
 
@@ -408,9 +456,9 @@
408
456
 
409
457
  this.enginePtr = enginePtr;
410
458
  this.initialized = true;
411
- this._initPromise = null; // 清除 Promise 缓存,允许重新初始化(如果需要)
459
+ this._initPromise = null; // Clear Promise cache, allow re-initialization (if needed)
412
460
  } catch (error) {
413
- this._initPromise = null; // 清除 Promise 缓存,允许重试
461
+ this._initPromise = null; // Clear Promise cache, allow retry
414
462
  throw error;
415
463
  }
416
464
  })();
@@ -438,11 +486,18 @@
438
486
  this.dstBufferPtr = null;
439
487
  }
440
488
 
489
+ // Clean up shared memory
490
+ if (this._callbackSharedBufferPtr) {
491
+ Module._free(this._callbackSharedBufferPtr);
492
+ this._callbackSharedBufferPtr = null;
493
+ this._callbackSharedBufferSize = 0;
494
+ }
495
+
441
496
  Module.ccall('DestroyBeautyEffectEngine', null, ['number'], [this.enginePtr]);
442
497
  this.enginePtr = null;
443
498
  this.initialized = false;
444
499
  this.bufferSize = 0;
445
- this._initPromise = null; // 清除初始化 Promise
500
+ this._initPromise = null; // Clear initialization Promise
446
501
 
447
502
  // Clean up offscreen canvas
448
503
  if (this._offscreenCanvas) {
@@ -547,7 +602,7 @@
547
602
 
548
603
  /**
549
604
  * Sets a basic beauty parameter
550
- * @param {number} param - Parameter (use BasicParam enum)
605
+ * @param {number} param - Parameter (use BasicParam enum, e.g., BasicParam.Smoothing)
551
606
  * @param {number} value - Parameter value (0.0 - 1.0)
552
607
  */
553
608
  setBasicParam(param, value) {
@@ -557,7 +612,7 @@
557
612
 
558
613
  /**
559
614
  * Sets a reshape parameter
560
- * @param {number} param - Parameter (use ReshapeParam enum)
615
+ * @param {number} param - Parameter (use ReshapeParam enum, e.g., ReshapeParam.FaceThinning)
561
616
  * @param {number} value - Parameter value (0.0 - 1.0)
562
617
  */
563
618
  setReshapeParam(param, value) {
@@ -567,7 +622,7 @@
567
622
 
568
623
  /**
569
624
  * Sets a makeup parameter
570
- * @param {number} param - Parameter (use MakeupParam enum)
625
+ * @param {number} param - Parameter (use MakeupParam enum, e.g., MakeupParam.Lipstick)
571
626
  * @param {number} value - Parameter value (0.0 - 1.0)
572
627
  */
573
628
  setMakeupParam(param, value) {
@@ -576,7 +631,253 @@
576
631
  }
577
632
 
578
633
  /**
579
- * Internal method to set beauty parameters
634
+ * Sets chroma key parameter
635
+ * @param {number} param - Parameter (use ChromaKeyParam enum, e.g., ChromaKeyParam.Similarity)
636
+ * @param {number} value - Parameter value (KeyColor: 0.0=Green, 1.0=Blue, 2.0=Red; others 0.0-1.0)
637
+ */
638
+ setChromaKeyParam(param, value) {
639
+ this._ensureInitialized();
640
+ // For KeyColor, values are 0, 1, 2, not 0.0-1.0
641
+ if (param === 0) { // ChromaKeyParam.KeyColor
642
+ const Module = this._getWasmModule();
643
+ const result = Module.ccall(
644
+ 'SetBeautyParamChromaKey',
645
+ 'number',
646
+ ['number', 'number', 'number'],
647
+ [this.enginePtr, param, value]
648
+ );
649
+ checkResult(result, `Failed to set chroma key parameter`);
650
+ return;
651
+ }
652
+ this._setBeautyParam('SetBeautyParamChromaKey', param, value);
653
+ }
654
+
655
+ /**
656
+ * Sets a LUT-based filter
657
+ * @param {string} filterId - Unique identifier of the filter (e.g., "chuxin"). Pass "" to clear.
658
+ */
659
+ setFilter(filterId) {
660
+ this._ensureInitialized();
661
+ const Module = this._getWasmModule();
662
+ const result = Module.ccall(
663
+ 'SetFilter',
664
+ 'number',
665
+ ['number', 'string'],
666
+ [this.enginePtr, filterId || '']
667
+ );
668
+ checkResult(result, 'Failed to set filter');
669
+ }
670
+
671
+ /**
672
+ * Sets the intensity of the current filter
673
+ * @param {number} intensity - Filter intensity (0.0 - 1.0)
674
+ */
675
+ setFilterIntensity(intensity) {
676
+ this._ensureInitialized();
677
+ const Module = this._getWasmModule();
678
+ const result = Module.ccall(
679
+ 'SetFilterIntensity',
680
+ 'number',
681
+ ['number', 'number'],
682
+ [this.enginePtr, intensity]
683
+ );
684
+ checkResult(result, 'Failed to set filter intensity');
685
+ }
686
+
687
+ /**
688
+ * Sets a 2D sticker
689
+ * @param {string} stickerId - Unique identifier of the sticker (e.g., "樱花"). Pass "" to clear.
690
+ */
691
+ setSticker(stickerId) {
692
+ this._ensureInitialized();
693
+ const Module = this._getWasmModule();
694
+ const result = Module.ccall(
695
+ 'SetSticker',
696
+ 'number',
697
+ ['number', 'string'],
698
+ [this.enginePtr, stickerId || '']
699
+ );
700
+ checkResult(result, 'Failed to set sticker');
701
+ }
702
+
703
+ /**
704
+ * Registers a filter from a file path or data
705
+ * @param {string} filterId - Unique identifier for the filter
706
+ * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data
707
+ */
708
+ registerFilter(filterId, resource) {
709
+ this._ensureInitialized();
710
+ const Module = this._getWasmModule();
711
+
712
+ if (typeof resource === 'string') {
713
+ const result = Module.ccall(
714
+ 'RegisterFilterPath',
715
+ 'number',
716
+ ['number', 'string', 'string'],
717
+ [this.enginePtr, filterId, resource]
718
+ );
719
+ checkResult(result, `Failed to register filter path: ${filterId}`);
720
+ } else if (resource instanceof Uint8Array) {
721
+ const dataPtr = Module._malloc(resource.length);
722
+ Module.HEAPU8.set(resource, dataPtr);
723
+ try {
724
+ const result = Module.ccall(
725
+ 'RegisterFilterData',
726
+ 'number',
727
+ ['number', 'string', 'number', 'number'],
728
+ [this.enginePtr, filterId, dataPtr, resource.length]
729
+ );
730
+ checkResult(result, `Failed to register filter data: ${filterId}`);
731
+ } finally {
732
+ Module._free(dataPtr);
733
+ }
734
+ } else {
735
+ throw new FacebetterError('Resource must be a string path or Uint8Array data');
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Registers a sticker from a file path or data
741
+ * @param {string} stickerId - Unique identifier for the sticker
742
+ * @param {string|Uint8Array} resource - Path to .fbd file or Uint8Array data
743
+ */
744
+ registerSticker(stickerId, resource) {
745
+ this._ensureInitialized();
746
+ const Module = this._getWasmModule();
747
+
748
+ if (typeof resource === 'string') {
749
+ const result = Module.ccall(
750
+ 'RegisterStickerPath',
751
+ 'number',
752
+ ['number', 'string', 'string'],
753
+ [this.enginePtr, stickerId, resource]
754
+ );
755
+ checkResult(result, `Failed to register sticker path: ${stickerId}`);
756
+ } else if (resource instanceof Uint8Array) {
757
+ const dataPtr = Module._malloc(resource.length);
758
+ Module.HEAPU8.set(resource, dataPtr);
759
+ try {
760
+ const result = Module.ccall(
761
+ 'RegisterStickerData',
762
+ 'number',
763
+ ['number', 'string', 'number', 'number'],
764
+ [this.enginePtr, stickerId, dataPtr, resource.length]
765
+ );
766
+ checkResult(result, `Failed to register sticker data: ${stickerId}`);
767
+ } finally {
768
+ Module._free(dataPtr);
769
+ }
770
+ } else {
771
+ throw new FacebetterError('Resource must be a string path or Uint8Array data');
772
+ }
773
+ }
774
+
775
+ /**
776
+ * Unregisters a specific filter
777
+ * @param {string} filterId - Filter ID to unregister
778
+ */
779
+ unregisterFilter(filterId) {
780
+ this._ensureInitialized();
781
+ const Module = this._getWasmModule();
782
+ const result = Module.ccall(
783
+ 'UnregisterFilter',
784
+ 'number',
785
+ ['number', 'string'],
786
+ [this.enginePtr, filterId]
787
+ );
788
+ checkResult(result, `Failed to unregister filter: ${filterId}`);
789
+ }
790
+
791
+ /**
792
+ * Unregisters all filters
793
+ */
794
+ unregisterAllFilters() {
795
+ this._ensureInitialized();
796
+ const Module = this._getWasmModule();
797
+ const result = Module.ccall(
798
+ 'UnregisterAllFilters',
799
+ 'number',
800
+ ['number'],
801
+ [this.enginePtr]
802
+ );
803
+ checkResult(result, 'Failed to unregister all filters');
804
+ }
805
+
806
+ /**
807
+ * Unregisters a specific sticker
808
+ * @param {string} stickerId - Sticker ID to unregister
809
+ */
810
+ unregisterSticker(stickerId) {
811
+ this._ensureInitialized();
812
+ const Module = this._getWasmModule();
813
+ const result = Module.ccall(
814
+ 'UnregisterSticker',
815
+ 'number',
816
+ ['number', 'string'],
817
+ [this.enginePtr, stickerId]
818
+ );
819
+ checkResult(result, `Failed to unregister sticker: ${stickerId}`);
820
+ }
821
+
822
+ /**
823
+ * Unregisters all stickers
824
+ */
825
+ unregisterAllStickers() {
826
+ this._ensureInitialized();
827
+ const Module = this._getWasmModule();
828
+ const result = Module.ccall(
829
+ 'UnregisterAllStickers',
830
+ 'number',
831
+ ['number'],
832
+ [this.enginePtr]
833
+ );
834
+ checkResult(result, 'Failed to unregister all stickers');
835
+ }
836
+
837
+ /**
838
+ * Gets the list of registered filter IDs
839
+ * @returns {string[]} Array of registered filter IDs
840
+ */
841
+ getRegisteredFilters() {
842
+ this._ensureInitialized();
843
+ const Module = this._getWasmModule();
844
+ const jsonStr = Module.ccall(
845
+ 'GetRegisteredFilters',
846
+ 'string',
847
+ ['number'],
848
+ [this.enginePtr]
849
+ );
850
+ try {
851
+ return JSON.parse(jsonStr || '[]');
852
+ } catch (e) {
853
+ console.error('Failed to parse registered filters JSON:', e);
854
+ return [];
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Gets the list of registered sticker IDs
860
+ * @returns {string[]} Array of registered sticker IDs
861
+ */
862
+ getRegisteredStickers() {
863
+ this._ensureInitialized();
864
+ const Module = this._getWasmModule();
865
+ const jsonStr = Module.ccall(
866
+ 'GetRegisteredStickers',
867
+ 'string',
868
+ ['number'],
869
+ [this.enginePtr]
870
+ );
871
+ try {
872
+ return JSON.parse(jsonStr || '[]');
873
+ } catch (e) {
874
+ console.error('Failed to parse registered stickers JSON:', e);
875
+ return [];
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Internal method to set beauty parameters
580
881
  * @private
581
882
  */
582
883
  _setBeautyParam(functionName, param, value) {
@@ -595,6 +896,139 @@
595
896
  checkResult(result, `Failed to set beauty parameter`);
596
897
  }
597
898
 
899
+ /**
900
+ * Sets engine callbacks (face landmarks detection)
901
+ * @param {Object} callbacks - Callback functions
902
+ * @param {Function} callbacks.onFaceLandmarks - Callback for face landmarks detection
903
+ * @param {number} [callbacks.maxFaces=10] - Maximum number of faces to support (affects shared buffer size)
904
+ */
905
+ setCallbacks(callbacks) {
906
+ this._ensureInitialized();
907
+ const Module = this._getWasmModule();
908
+
909
+ // Initialize callback storage (if it doesn't exist)
910
+ if (!Module._face_landmarks_callbacks) {
911
+ Module._face_landmarks_callbacks = [];
912
+ }
913
+
914
+ let callbackId = 0;
915
+ let sharedBufferPtr = null;
916
+ let sharedBufferSize = 0;
917
+
918
+ if (callbacks && callbacks.onFaceLandmarks) {
919
+ // Get max faces (default 10)
920
+ const maxFaces = callbacks.maxFaces || 10;
921
+
922
+ // Pre-allocate shared memory
923
+ // Metadata: 2 ints (frame_number, face_count) = 8 bytes
924
+ // Face data: maxFaces * 343 floats * 4 bytes = maxFaces * 1372 bytes
925
+ const metadataSize = 2 * 4; // 2 ints
926
+ const faceDataSize = maxFaces * 343 * 4; // 343 floats per face
927
+ sharedBufferSize = metadataSize + faceDataSize;
928
+ sharedBufferPtr = Module._malloc(sharedBufferSize);
929
+
930
+ if (!sharedBufferPtr) {
931
+ throw new FacebetterError('Failed to allocate shared buffer for callbacks');
932
+ }
933
+
934
+ // Register callback function (internal wrapper, reads data from shared memory)
935
+ // Push first, then get index as callbackId (allows callbackId to be 0)
936
+ Module._face_landmarks_callbacks.push(
937
+ (frameNumber, faceCount, dataPtr) => {
938
+ // Read data from shared memory (zero-copy)
939
+ const heap = Module.HEAPF32;
940
+ const dataOffset = dataPtr / 4; // float is 4 bytes
941
+
942
+ const results = [];
943
+ const FLOATS_PER_FACE = 343;
944
+
945
+ for (let i = 0; i < faceCount; i++) {
946
+ const faceOffset = dataOffset + i * FLOATS_PER_FACE;
947
+ let offset = faceOffset;
948
+
949
+ // Read rect
950
+ const rect = {
951
+ x: heap[offset++],
952
+ y: heap[offset++],
953
+ width: heap[offset++],
954
+ height: heap[offset++]
955
+ };
956
+
957
+ // Read basic fields
958
+ const faceId = Math.round(heap[offset++]);
959
+ const faceAction = Math.round(heap[offset++]);
960
+ const score = heap[offset++];
961
+ const pitch = heap[offset++];
962
+ const roll = heap[offset++];
963
+ const yaw = heap[offset++];
964
+
965
+ // Read key_points (111 points)
966
+ const keyPoints = [];
967
+ for (let j = 0; j < 111; j++) {
968
+ keyPoints.push({
969
+ x: heap[offset++],
970
+ y: heap[offset++]
971
+ });
972
+ }
973
+
974
+ // Read visibility (111 points)
975
+ const visibility = [];
976
+ for (let j = 0; j < 111; j++) {
977
+ visibility.push(heap[offset++]);
978
+ }
979
+
980
+ results.push({
981
+ rect,
982
+ key_points: keyPoints,
983
+ visibility,
984
+ face_id: faceId,
985
+ face_action: faceAction,
986
+ score,
987
+ pitch,
988
+ roll,
989
+ yaw
990
+ });
991
+ }
992
+
993
+ // Call user callback
994
+ callbacks.onFaceLandmarks(results);
995
+ }
996
+ );
997
+
998
+ // Get callback ID (index after push, can be 0)
999
+ callbackId = Module._face_landmarks_callbacks.length - 1;
1000
+ }
1001
+
1002
+ // Call C API
1003
+ const result = Module.ccall(
1004
+ 'SetCallbacks',
1005
+ 'number',
1006
+ ['number', 'number', 'number', 'number'],
1007
+ [
1008
+ this.enginePtr,
1009
+ callbackId,
1010
+ sharedBufferPtr || 0,
1011
+ sharedBufferSize
1012
+ ]
1013
+ );
1014
+
1015
+ checkResult(result, 'Failed to set callbacks');
1016
+
1017
+ // Clean up old shared memory (if it exists)
1018
+ if (this._callbackSharedBufferPtr) {
1019
+ Module._free(this._callbackSharedBufferPtr);
1020
+ }
1021
+
1022
+ // Store new shared memory pointer for cleanup
1023
+ if (sharedBufferPtr) {
1024
+ this._callbackSharedBufferPtr = sharedBufferPtr;
1025
+ this._callbackSharedBufferSize = sharedBufferSize;
1026
+ } else {
1027
+ this._callbackSharedBufferPtr = null;
1028
+ this._callbackSharedBufferSize = 0;
1029
+ }
1030
+ }
1031
+
598
1032
  /**
599
1033
  * Sets virtual background options (unified API, matches C++/Java/OC)
600
1034
  * @param {VirtualBackgroundOptions|Object} options - Virtual background options
@@ -612,7 +1046,7 @@
612
1046
  let imageBufferPtr = null;
613
1047
 
614
1048
  try {
615
- // 如果提供了背景图片,转换为 ImageData 并准备缓冲区
1049
+ // If a background image is provided, convert to ImageData and prepare buffer
616
1050
  if (opts.backgroundImage) {
617
1051
  imageData = this._toImageData(opts.backgroundImage);
618
1052
  const bufferSize = imageData.width * imageData.height * 4;
@@ -623,7 +1057,7 @@
623
1057
  view.set(imageData.data);
624
1058
  }
625
1059
 
626
- // 使用统一的 SetVirtualBackground C 接口(与其他平台一致)
1060
+ // Use unified SetVirtualBackground C interface (consistent with other platforms)
627
1061
  const result = Module.ccall(
628
1062
  'SetVirtualBackground',
629
1063
  'number',
@@ -631,7 +1065,7 @@
631
1065
  [
632
1066
  this.enginePtr,
633
1067
  opts.mode,
634
- imageBufferPtr || 0, // 如果为 null,传递 0
1068
+ imageBufferPtr || 0, // If null, pass 0
635
1069
  imageData ? imageData.width : 0,
636
1070
  imageData ? imageData.height : 0,
637
1071
  imageData ? imageData.width * 4 : 0
@@ -640,7 +1074,7 @@
640
1074
 
641
1075
  checkResult(result, 'Failed to set virtual background');
642
1076
  } finally {
643
- // 释放内存
1077
+ // Free memory
644
1078
  if (imageBufferPtr) {
645
1079
  Module._free(imageBufferPtr);
646
1080
  }
@@ -680,10 +1114,11 @@
680
1114
  * @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|Uint8ClampedArray} input - Input image
681
1115
  * @param {number} width - Image width (required if input is Uint8ClampedArray)
682
1116
  * @param {number} height - Image height (required if input is Uint8ClampedArray)
683
- * @param {number} processMode - Process mode (use ProcessMode enum, default: ProcessMode.Image)
1117
+ * @param {number} frameType - Frame type (use FrameType enum, default: FrameType.Video)
1118
+ * @param {number} mirrorMode - Mirror mode applied to input before processing (MirrorMode enum, default: MirrorMode.None)
684
1119
  * @returns {ImageData} Processed image data
685
1120
  */
686
- processImage(input, width, height, processMode = ProcessMode$1.Image) {
1121
+ processImage(input, width, height, frameType = FrameType$1.Video, mirrorMode = MirrorMode$1.None) {
687
1122
  this._ensureInitialized();
688
1123
  const imageData = this._platformAPI.toImageData(input, width, height);
689
1124
  const Module = this._getWasmModule();
@@ -701,7 +1136,7 @@
701
1136
  const result = Module.ccall(
702
1137
  'ProcessImageRGBA',
703
1138
  'number',
704
- ['number', 'number', 'number', 'number', 'number', 'number', 'number'],
1139
+ ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
705
1140
  [
706
1141
  this.enginePtr,
707
1142
  this.srcBufferPtr,
@@ -709,7 +1144,8 @@
709
1144
  imageData.height,
710
1145
  imageData.width * 4,
711
1146
  this.dstBufferPtr,
712
- processMode
1147
+ frameType,
1148
+ mirrorMode
713
1149
  ]
714
1150
  );
715
1151
 
@@ -728,6 +1164,65 @@
728
1164
  }
729
1165
  }
730
1166
 
1167
+ /**
1168
+ * Processes an external GPU texture.
1169
+ *
1170
+ * Note:
1171
+ * - This interface is mainly used for integration with internal WASM or advanced scenarios.
1172
+ * - textureHandle must be a texture handle (uint32) compatible with the OpenGL/WebGL context used by WASM.
1173
+ * - For common web scenarios, it is recommended to use processImage with ImageData/Canvas etc.
1174
+ *
1175
+ * @param {number} textureHandle - External texture handle (GL_TEXTURE_2D)
1176
+ * @param {number} width - Texture width
1177
+ * @param {number} height - Texture height
1178
+ * @param {number} stride - Row stride (usually width * 4)
1179
+ * @param {number} [frameType] - Frame type (FrameType.Image / FrameType.Video)
1180
+ * @param {number} [mirrorMode] - Mirror mode (MirrorMode enum; texture path does not support mirror, param reserved)
1181
+ * @returns {number} Processed texture handle (GL_TEXTURE_2D, uint32)
1182
+ */
1183
+ processTexture(textureHandle,
1184
+ width,
1185
+ height,
1186
+ stride,
1187
+ frameType = FrameType$1.Video,
1188
+ mirrorMode = MirrorMode$1.None) {
1189
+ this._ensureInitialized();
1190
+ if (!textureHandle || width <= 0 || height <= 0 || stride <= 0) {
1191
+ throw new FacebetterError('Invalid textureHandle or dimensions for processTexture');
1192
+ }
1193
+
1194
+ const Module = this._getWasmModule();
1195
+
1196
+ try {
1197
+ const result = Module.ccall(
1198
+ 'ProcessImageTexture',
1199
+ 'number',
1200
+ ['number', 'number', 'number', 'number', 'number', 'number', 'number'],
1201
+ [
1202
+ this.enginePtr,
1203
+ textureHandle,
1204
+ width,
1205
+ height,
1206
+ stride,
1207
+ frameType,
1208
+ mirrorMode
1209
+ ]
1210
+ );
1211
+
1212
+ if (!result || result === 0) {
1213
+ throw new FacebetterError('Failed to process external texture or got invalid output texture');
1214
+ }
1215
+
1216
+ // Return processed texture handle for caller to continue using in same GL context
1217
+ return result;
1218
+ } catch (error) {
1219
+ if (error instanceof FacebetterError) {
1220
+ throw error;
1221
+ }
1222
+ throw new FacebetterError(`Texture processing error: ${error.message}`);
1223
+ }
1224
+ }
1225
+
731
1226
  /**
732
1227
  * Ensures the engine is initialized
733
1228
  * @private
@@ -1055,77 +1550,6 @@
1055
1550
  throw new FacebetterError('Unsupported image source type');
1056
1551
  }
1057
1552
 
1058
- /**
1059
- * Gets User-Agent string for web platform
1060
- * @returns {string} User-Agent string
1061
- */
1062
- function getUserAgentString() {
1063
- return 'FB/1.0 (web; wasm32)';
1064
- }
1065
-
1066
- /**
1067
- * Generates HMAC-SHA256 signature (browser version)
1068
- * @param {string} key - HMAC key (app_key)
1069
- * @param {string} data - Data to sign (timestamp string)
1070
- * @returns {Promise<string>} Promise that resolves with hex-encoded signature
1071
- */
1072
- async function generateHMACSignature(key, data) {
1073
- const encoder = new TextEncoder();
1074
- const keyData = encoder.encode(key);
1075
- const messageData = encoder.encode(data);
1076
-
1077
- const cryptoKey = await crypto.subtle.importKey(
1078
- 'raw',
1079
- keyData,
1080
- { name: 'HMAC', hash: 'SHA-256' },
1081
- false,
1082
- ['sign']
1083
- );
1084
-
1085
- const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
1086
-
1087
- // Convert to hex string
1088
- const hashArray = Array.from(new Uint8Array(signature));
1089
- const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
1090
- return hashHex;
1091
- }
1092
-
1093
- /**
1094
- * Calls online_auth API to verify app_key (browser version)
1095
- * @param {string} appId - Application ID
1096
- * @param {string} appKey - Application key
1097
- * @returns {Promise<Object>} Promise that resolves with response data
1098
- */
1099
- async function verifyAppKeyOnline(appId, appKey) {
1100
- try {
1101
- const timestamp = Math.floor(Date.now() / 1000);
1102
- const timestampStr = timestamp.toString();
1103
- const hmacSignature = await generateHMACSignature(appKey, timestampStr);
1104
-
1105
- const requestBody = {
1106
- p_app_id: appId,
1107
- p_hmac_signature: hmacSignature,
1108
- p_timestamp: timestamp,
1109
- p_user_agent: getUserAgentString()
1110
- };
1111
-
1112
- const response = await fetch('https://facebetter.pixpark.net/rest/v1/rpc/online_auth', {
1113
- method: 'POST',
1114
- headers: {
1115
- 'Content-Type': 'application/json',
1116
- 'User-Agent': getUserAgentString()
1117
- },
1118
- body: JSON.stringify(requestBody)
1119
- });
1120
-
1121
- const responseData = await response.json();
1122
- return responseData;
1123
- } catch (error) {
1124
- console.error('Online auth request failed:', error);
1125
- return null;
1126
- }
1127
- }
1128
-
1129
1553
  /**
1130
1554
  * Browser Entry Point
1131
1555
  * For <script> tag usage (UMD format)
@@ -1162,7 +1586,6 @@
1162
1586
  getWasmModule: getWasmModule,
1163
1587
  getWasmBuffer: getWasmBuffer,
1164
1588
  toImageData: toImageData,
1165
- verifyAppKeyOnline: verifyAppKeyOnline,
1166
1589
  ensureGPUPixelCanvas,
1167
1590
  cleanupGPUPixelCanvas
1168
1591
  };
@@ -1187,7 +1610,8 @@
1187
1610
  const ReshapeParam = ReshapeParam$1;
1188
1611
  const MakeupParam = MakeupParam$1;
1189
1612
  const BackgroundMode = BackgroundMode$1;
1190
- const ProcessMode = ProcessMode$1;
1613
+ const FrameType = FrameType$1;
1614
+ const MirrorMode = MirrorMode$1;
1191
1615
  const VirtualBackgroundOptions = VirtualBackgroundOptions$1;
1192
1616
 
1193
1617
  // Default export
@@ -1205,8 +1629,9 @@
1205
1629
  exports.BeautyType = BeautyType;
1206
1630
  exports.EngineConfig = EngineConfig;
1207
1631
  exports.FacebetterError = FacebetterError;
1632
+ exports.FrameType = FrameType;
1208
1633
  exports.MakeupParam = MakeupParam;
1209
- exports.ProcessMode = ProcessMode;
1634
+ exports.MirrorMode = MirrorMode;
1210
1635
  exports.ReshapeParam = ReshapeParam;
1211
1636
  exports.VirtualBackgroundOptions = VirtualBackgroundOptions;
1212
1637
  exports.createBeautyEffectEngine = createBeautyEffectEngine;