@spatialwalk/avatarkit 1.0.0-beta.76 → 1.0.0-beta.78

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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0-beta.78] - 2026-02-04
9
+
10
+ ### 🐛 Bugfixes
11
+ - **Telemetry Accuracy** - Fixed environment field reporting issue in analytics events
12
+
13
+ ### ✨ New Features
14
+ - **Performance Monitoring** - Added loading performance metrics for avatar resources
15
+ - `fetch_avatar_latency` - Total loading time
16
+ - `fetch_avatar_metadata_latency` - Metadata fetch time
17
+ - `download_avatar_assets_latency` - Asset download time
18
+
19
+ ### 🔧 Improvements
20
+ - **Internal Architecture** - Unified configuration constants across platforms for better maintainability
21
+
8
22
  ## [1.0.0-beta.74] - 2026-01-22
9
23
 
10
24
  ### 🔧 Improvements
@@ -1,7 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-ZN-iK3b8.js";
4
+ import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-DEJMvfST.js";
5
5
  class StreamingAudioPlayer {
6
6
  // Mark if AudioContext is being resumed, avoid concurrent resume requests
7
7
  constructor(options) {
@@ -4,9 +4,12 @@ export declare class AvatarManager {
4
4
  private static _instance;
5
5
  private avatarDownloader;
6
6
  private avatarCache;
7
- private loadingPromises;
7
+ /** 下载队列:FIFO 顺序 */
8
8
  private downloadQueue;
9
- private isDownloading;
9
+ /** 当前正在执行的任务 */
10
+ private currentTask;
11
+ /** 任务索引:快速查找 id 对应的任务 */
12
+ private taskIndex;
10
13
  /**
11
14
  * Access via global singleton
12
15
  */
@@ -18,6 +21,12 @@ export declare class AvatarManager {
18
21
  * @returns Promise<Avatar>
19
22
  */
20
23
  load(id: string, onProgress?: (progress: LoadProgressInfo) => void): Promise<Avatar>;
24
+ /**
25
+ * Cancel a pending or running download task
26
+ * @param id Avatar ID to cancel
27
+ * @returns true if task was found and cancelled
28
+ */
29
+ cancelLoad(id: string): boolean;
21
30
  /**
22
31
  * Get cached avatar
23
32
  * @param id Avatar ID
@@ -30,7 +39,7 @@ export declare class AvatarManager {
30
39
  */
31
40
  clear(id: string): void;
32
41
  /**
33
- * Clear all avatar cache and resource loader cache
42
+ * Clear all avatar cache and cancel all tasks
34
43
  */
35
44
  clearAll(): void;
36
45
  }
@@ -30,6 +30,7 @@ export declare class AvatarView {
30
30
  private characterHandle;
31
31
  private characterId;
32
32
  private isPureRenderingMode;
33
+ private _renderingEnabled;
33
34
  private avatarActiveTimer;
34
35
  private readonly AVATAR_ACTIVE_INTERVAL;
35
36
  /**
@@ -61,6 +62,38 @@ export declare class AvatarView {
61
62
  frameCount: number;
62
63
  useLinear?: boolean;
63
64
  }): Promise<KeyframeData[]>;
65
+ /**
66
+ * Pause rendering loop
67
+ *
68
+ * When called:
69
+ * - Rendering loop stops (no GPU/canvas updates)
70
+ * - Audio playback continues normally
71
+ * - Animation state machine continues running
72
+ *
73
+ * Use `resumeRendering()` to resume rendering.
74
+ *
75
+ * @example
76
+ * // Stop rendering to save GPU resources (audio continues)
77
+ * avatarView.pauseRendering()
78
+ */
79
+ pauseRendering(): void;
80
+ /**
81
+ * Resume rendering loop
82
+ *
83
+ * When called:
84
+ * - Rendering loop resumes from current state
85
+ * - If in Idle state, immediately renders current frame to restore display
86
+ *
87
+ * @example
88
+ * // Resume rendering
89
+ * avatarView.resumeRendering()
90
+ */
91
+ resumeRendering(): void;
92
+ /**
93
+ * Check if rendering is currently enabled
94
+ * @returns true if rendering is enabled, false if paused
95
+ */
96
+ isRenderingEnabled(): boolean;
64
97
  /**
65
98
  * Get or set avatar transform in canvas
66
99
  *
@@ -1400,6 +1400,32 @@ function base64FromBytes$1(arr) {
1400
1400
  function isSet$1(value) {
1401
1401
  return value !== null && value !== void 0;
1402
1402
  }
1403
+ function convertProtoFlameToWasmParams(protoFlame) {
1404
+ var _a;
1405
+ return {
1406
+ translation: protoFlame.translation || [0, 0, 0],
1407
+ rotation: protoFlame.rotation || [0, 0, 0],
1408
+ neck_pose: protoFlame.neckPose || [0, 0, 0],
1409
+ jaw_pose: protoFlame.jawPose || [0, 0, 0],
1410
+ eyes_pose: protoFlame.eyePose || [0, 0, 0, 0, 0, 0],
1411
+ eyelid: protoFlame.eyeLid || [0, 0],
1412
+ expr_params: protoFlame.expression || [],
1413
+ shape_params: [],
1414
+ // Realtime doesn't provide shape params, use default
1415
+ has_eyelid: (((_a = protoFlame.eyeLid) == null ? void 0 : _a.length) || 0) > 0
1416
+ };
1417
+ }
1418
+ function convertWasmParamsToProtoFlame(wasmParams) {
1419
+ return {
1420
+ translation: wasmParams.translation || [0, 0, 0],
1421
+ rotation: wasmParams.rotation || [0, 0, 0],
1422
+ neckPose: wasmParams.neck_pose || [0, 0, 0],
1423
+ jawPose: wasmParams.jaw_pose || [0, 0, 0],
1424
+ eyePose: wasmParams.eyes_pose || [0, 0, 0, 0, 0, 0],
1425
+ eyeLid: wasmParams.eyelid || [0, 0],
1426
+ expression: wasmParams.expr_params || []
1427
+ };
1428
+ }
1403
1429
  const POSTHOG_HOST_INTL = "https://i.spatialwalk.ai";
1404
1430
  const POSTHOG_API_KEY_INTL = "phc_IFTLa6Z6VhTaNvsxB7klvG2JeNwcSpnnwz8YvZRC96Q";
1405
1431
  function getPostHogConfig(_environment) {
@@ -1452,32 +1478,6 @@ const APP_CONFIG = {
1452
1478
  }
1453
1479
  }
1454
1480
  };
1455
- function convertProtoFlameToWasmParams(protoFlame) {
1456
- var _a;
1457
- return {
1458
- translation: protoFlame.translation || [0, 0, 0],
1459
- rotation: protoFlame.rotation || [0, 0, 0],
1460
- neck_pose: protoFlame.neckPose || [0, 0, 0],
1461
- jaw_pose: protoFlame.jawPose || [0, 0, 0],
1462
- eyes_pose: protoFlame.eyePose || [0, 0, 0, 0, 0, 0],
1463
- eyelid: protoFlame.eyeLid || [0, 0],
1464
- expr_params: protoFlame.expression || [],
1465
- shape_params: [],
1466
- // Realtime doesn't provide shape params, use default
1467
- has_eyelid: (((_a = protoFlame.eyeLid) == null ? void 0 : _a.length) || 0) > 0
1468
- };
1469
- }
1470
- function convertWasmParamsToProtoFlame(wasmParams) {
1471
- return {
1472
- translation: wasmParams.translation || [0, 0, 0],
1473
- rotation: wasmParams.rotation || [0, 0, 0],
1474
- neckPose: wasmParams.neck_pose || [0, 0, 0],
1475
- jawPose: wasmParams.jaw_pose || [0, 0, 0],
1476
- eyePose: wasmParams.eyes_pose || [0, 0, 0, 0, 0, 0],
1477
- eyeLid: wasmParams.eyelid || [0, 0],
1478
- expression: wasmParams.expr_params || []
1479
- };
1480
- }
1481
1481
  var t = "undefined" != typeof window ? window : void 0, i = "undefined" != typeof globalThis ? globalThis : t;
1482
1482
  "undefined" == typeof self && (i.self = i), "undefined" == typeof File && (i.File = function() {
1483
1483
  });
@@ -7294,7 +7294,6 @@ function extractResourceUrls(meta) {
7294
7294
  var Environment = /* @__PURE__ */ ((Environment2) => {
7295
7295
  Environment2["cn"] = "cn";
7296
7296
  Environment2["intl"] = "intl";
7297
- Environment2["test"] = "test";
7298
7297
  return Environment2;
7299
7298
  })(Environment || {});
7300
7299
  var DrivingServiceMode = /* @__PURE__ */ ((DrivingServiceMode2) => {
@@ -7944,7 +7943,8 @@ function getCommonFields() {
7944
7943
  return {
7945
7944
  platform: "Web",
7946
7945
  sdk_version: sdkVersion,
7947
- environment: currentEnvironment || "unknown",
7946
+ env: currentEnvironment || "unknown",
7947
+ // environment 缩写为 env
7948
7948
  app_id: logContext.app_id || "",
7949
7949
  user_id: logContext.user_id || "",
7950
7950
  // 没有就传空字符串
@@ -8139,7 +8139,6 @@ function cleanupPostHog() {
8139
8139
  }
8140
8140
  function logEvent(event, level = "info", contents = {}) {
8141
8141
  const context = {
8142
- environment: currentEnvironment || "unknown",
8143
8142
  sessionToken: idManager.getSessionToken() ?? "",
8144
8143
  userId: idManager.getUserId() ?? "",
8145
8144
  ...contents
@@ -8244,7 +8243,7 @@ const _AnimationPlayer = class _AnimationPlayer {
8244
8243
  if (this.streamingPlayer) {
8245
8244
  return;
8246
8245
  }
8247
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-CuNtk6eL.js");
8246
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BNj8LpDw.js");
8248
8247
  const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
8249
8248
  const audioFormat = AvatarSDK2.getAudioFormat();
8250
8249
  this.streamingPlayer = new StreamingAudioPlayer({
@@ -8443,10 +8442,42 @@ const _AnimationPlayer = class _AnimationPlayer {
8443
8442
  };
8444
8443
  __publicField(_AnimationPlayer, "audioUnlocked", false);
8445
8444
  let AnimationPlayer = _AnimationPlayer;
8445
+ const FLAME_FRAME_RATE = 25;
8446
+ const START_TRANSITION_DURATION_S = 0.2;
8447
+ const END_TRANSITION_DURATION_S = 1.6;
8448
+ const START_TRANSITION_DURATION_MS = START_TRANSITION_DURATION_S * 1e3;
8449
+ const END_TRANSITION_DURATION_MS = END_TRANSITION_DURATION_S * 1e3;
8450
+ const AUDIO_SAMPLE_RATE = 16e3;
8451
+ const AUDIO_CHANNELS = 1;
8452
+ const AUDIO_BYTES_PER_SAMPLE = 2;
8453
+ const AUDIO_BYTES_PER_SECOND = AUDIO_SAMPLE_RATE * AUDIO_CHANNELS * AUDIO_BYTES_PER_SAMPLE;
8454
+ const BEZIER_CURVES$1 = {
8455
+ /** 下颌: 快速启动,平滑停止 */
8456
+ jaw: [0.2, 0.8, 0.3, 1],
8457
+ /** 表情: 平滑 S 曲线 */
8458
+ expression: [0.4, 0, 0.2, 1],
8459
+ /** 眼部: 柔和 S 曲线 */
8460
+ eye: [0.3, 0, 0.1, 1],
8461
+ /** 颈部: 慢启动,惯性停止 */
8462
+ neck: [0.1, 0.2, 0.2, 1],
8463
+ /** 全局: 标准 ease-in-out */
8464
+ global: [0.42, 0, 0.58, 1]
8465
+ };
8466
+ const TIME_SCALE = {
8467
+ /** 下颌: 40% 时间完成 (1/2.5) */
8468
+ jaw: 2.5,
8469
+ /** 表情: 62.5% 时间完成 (1/1.6) */
8470
+ expression: 1.6,
8471
+ /** 眼部: 77% 时间完成 (1/1.3) */
8472
+ eye: 1.3,
8473
+ /** 颈部: 100% 时间完成 */
8474
+ neck: 1,
8475
+ /** 全局: 100% 时间完成 */
8476
+ global: 1
8477
+ };
8446
8478
  const DEFAULT_SDK_CONFIG = {
8447
8479
  [Environment.cn]: "https://api.open.spatialwalk.top",
8448
- [Environment.intl]: "https://api.intl.spatialwalk.cloud",
8449
- [Environment.test]: "https://api-test.spatialwalk.top"
8480
+ [Environment.intl]: "https://api.intl.spatialwalk.cloud"
8450
8481
  };
8451
8482
  const configCache = {
8452
8483
  config: null,
@@ -8484,9 +8515,6 @@ async function fetchSdkConfig(version) {
8484
8515
  } else if (data.endpoints.intl) {
8485
8516
  config[Environment.intl] = `https://${data.endpoints.intl}`;
8486
8517
  }
8487
- if (data.endpoints.test) {
8488
- config[Environment.test] = `https://${data.endpoints.test}`;
8489
- }
8490
8518
  configCache.config = config;
8491
8519
  logger.log(`[SdkConfigLoader] SDK config fetched successfully:`, config);
8492
8520
  } catch (error) {
@@ -9941,7 +9969,7 @@ class AvatarSDK {
9941
9969
  }
9942
9970
  __publicField(AvatarSDK, "_isInitialized", false);
9943
9971
  __publicField(AvatarSDK, "_configuration", null);
9944
- __publicField(AvatarSDK, "_version", "1.0.0-beta.76");
9972
+ __publicField(AvatarSDK, "_version", "1.0.0-beta.78");
9945
9973
  __publicField(AvatarSDK, "_avatarCore", null);
9946
9974
  __publicField(AvatarSDK, "_dynamicSdkConfig", null);
9947
9975
  const AvatarSDK$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -11819,7 +11847,7 @@ class AvatarController {
11819
11847
  recvFirstFlameTimestamp: 0,
11820
11848
  didRecvFirstFlame: false
11821
11849
  });
11822
- __publicField(this, "audioBytesPerSecond", 16e3 * 2);
11850
+ __publicField(this, "audioBytesPerSecond", AUDIO_BYTES_PER_SECOND);
11823
11851
  this.avatar = avatar;
11824
11852
  this.playbackMode = (options == null ? void 0 : options.playbackMode) ?? DrivingServiceMode.sdk;
11825
11853
  if (this.playbackMode === DrivingServiceMode.sdk) {
@@ -12726,7 +12754,7 @@ class AvatarController {
12726
12754
  if (this.playbackLoopId) {
12727
12755
  return;
12728
12756
  }
12729
- const fps = APP_CONFIG.animation.fps;
12757
+ const fps = FLAME_FRAME_RATE;
12730
12758
  const playLoop = async () => {
12731
12759
  if (!this.isPlaying || this.currentState === AvatarState.paused || !this.animationPlayer) {
12732
12760
  this.playbackLoopId = null;
@@ -13296,47 +13324,71 @@ function getCacheInfo(url, response) {
13296
13324
  };
13297
13325
  }
13298
13326
  async function downloadResource(url, options) {
13327
+ const { signal, characterId, resourceType, maxRetries = 3 } = options || {};
13328
+ if (signal == null ? void 0 : signal.aborted) {
13329
+ throw new Error("Download cancelled");
13330
+ }
13299
13331
  try {
13300
13332
  let cached = null;
13301
13333
  let pwaCacheSubtype = void 0;
13302
- if (options == null ? void 0 : options.characterId) {
13303
- cached = await PwaCacheManager.getCharacterResource(options.characterId, url);
13334
+ if (characterId) {
13335
+ cached = await PwaCacheManager.getCharacterResource(characterId, url);
13304
13336
  if (cached) {
13305
13337
  pwaCacheSubtype = "character";
13306
13338
  }
13307
- } else if ((options == null ? void 0 : options.resourceType) === "template") {
13339
+ } else if (resourceType === "template") {
13308
13340
  cached = await PwaCacheManager.getTemplateResource(url);
13309
13341
  if (cached) {
13310
13342
  pwaCacheSubtype = "template";
13311
13343
  }
13312
13344
  }
13313
13345
  if (cached) {
13314
- const response2 = new Response(cached);
13346
+ const response = new Response(cached);
13315
13347
  await new Promise((resolve2) => setTimeout(resolve2, 0));
13316
- const cacheInfo2 = getCacheInfo(url, response2);
13317
- cacheInfo2.cacheHit = true;
13318
- cacheInfo2.cacheType = "pwa";
13319
- cacheInfo2.pwaCacheSubtype = pwaCacheSubtype;
13320
- return { data: cached, cacheInfo: cacheInfo2 };
13321
- }
13322
- const response = await fetch(url);
13323
- if (!response.ok) {
13324
- throw new Error(`HTTP ${response.status} ${response.statusText}`);
13325
- }
13326
- const arrayBuffer = await response.arrayBuffer();
13327
- if (options == null ? void 0 : options.characterId) {
13328
- PwaCacheManager.putCharacterResource(options.characterId, url, arrayBuffer).catch((err) => {
13329
- logger.warn(`[downloadResource] Failed to cache character resource:`, err);
13330
- });
13331
- } else if ((options == null ? void 0 : options.resourceType) === "template") {
13332
- PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
13333
- logger.warn(`[downloadResource] Failed to cache template resource:`, err);
13334
- });
13348
+ const cacheInfo = getCacheInfo(url, response);
13349
+ cacheInfo.cacheHit = true;
13350
+ cacheInfo.cacheType = "pwa";
13351
+ cacheInfo.pwaCacheSubtype = pwaCacheSubtype;
13352
+ return { data: cached, cacheInfo };
13353
+ }
13354
+ let lastError = null;
13355
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
13356
+ if (signal == null ? void 0 : signal.aborted) {
13357
+ throw new Error("Download cancelled");
13358
+ }
13359
+ try {
13360
+ const response = await fetch(url, { signal });
13361
+ if (!response.ok) {
13362
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
13363
+ }
13364
+ const arrayBuffer = await response.arrayBuffer();
13365
+ if (characterId) {
13366
+ PwaCacheManager.putCharacterResource(characterId, url, arrayBuffer).catch((err) => {
13367
+ logger.warn(`[downloadResource] Failed to cache character resource:`, err);
13368
+ });
13369
+ } else if (resourceType === "template") {
13370
+ PwaCacheManager.putTemplateResource(url, arrayBuffer).catch((err) => {
13371
+ logger.warn(`[downloadResource] Failed to cache template resource:`, err);
13372
+ });
13373
+ }
13374
+ await new Promise((resolve2) => setTimeout(resolve2, 0));
13375
+ const cacheInfo = getCacheInfo(url, response);
13376
+ return { data: arrayBuffer, cacheInfo };
13377
+ } catch (err) {
13378
+ if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) {
13379
+ throw err;
13380
+ }
13381
+ lastError = err instanceof Error ? err : new Error(String(err));
13382
+ if (attempt < maxRetries) {
13383
+ logger.warn(`[downloadResource] Attempt ${attempt}/${maxRetries} failed for ${url}, retrying immediately...`);
13384
+ }
13385
+ }
13335
13386
  }
13336
- await new Promise((resolve2) => setTimeout(resolve2, 0));
13337
- const cacheInfo = getCacheInfo(url, response);
13338
- return { data: arrayBuffer, cacheInfo };
13387
+ throw lastError || new Error(`Failed to download ${url} after ${maxRetries} attempts`);
13339
13388
  } catch (err) {
13389
+ if (err instanceof Error && (err.name === "AbortError" || err.message === "Download cancelled")) {
13390
+ throw err;
13391
+ }
13340
13392
  const msg = errorToMessage(err);
13341
13393
  throw new Error(`[downloadResource] ${url} → ${msg}`);
13342
13394
  }
@@ -13482,16 +13534,21 @@ class AvatarDownloader {
13482
13534
  * Load camera settings from CharacterMeta (optional)
13483
13535
  * @internal
13484
13536
  */
13485
- async loadCameraSettings(characterMeta) {
13537
+ async loadCameraSettings(characterMeta, options) {
13486
13538
  var _a, _b;
13539
+ const { signal } = options || {};
13487
13540
  const cameraUrl = (_b = (_a = characterMeta.camera) == null ? void 0 : _a.resource) == null ? void 0 : _b.remote;
13488
13541
  if (!cameraUrl) {
13489
13542
  logger.log("ℹ️ No camera resource URL provided");
13490
13543
  return void 0;
13491
13544
  }
13545
+ if (signal == null ? void 0 : signal.aborted) {
13546
+ throw new Error("Load cancelled");
13547
+ }
13492
13548
  try {
13493
13549
  logger.log(`📥 Loading camera info from: ${cameraUrl}`);
13494
13550
  const { data: arrayBuffer } = await downloadResource(cameraUrl, {
13551
+ signal,
13495
13552
  characterId: characterMeta.characterId ?? void 0,
13496
13553
  resourceType: "character"
13497
13554
  });
@@ -13510,7 +13567,10 @@ class AvatarDownloader {
13510
13567
  */
13511
13568
  async loadCharacterData(characterMeta, options) {
13512
13569
  var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j;
13513
- const { progressCallback = null } = options || {};
13570
+ const { progressCallback = null, signal } = options || {};
13571
+ if (signal == null ? void 0 : signal.aborted) {
13572
+ throw new Error("Download cancelled");
13573
+ }
13514
13574
  const totalStartTime = Date.now();
13515
13575
  const shapeUrl = (_c = (_b = (_a = characterMeta.models) == null ? void 0 : _a.shape) == null ? void 0 : _b.resource) == null ? void 0 : _c.remote;
13516
13576
  const pointCloudUrl = (_f = (_e2 = (_d = characterMeta.models) == null ? void 0 : _d.gsStandard) == null ? void 0 : _e2.resource) == null ? void 0 : _f.remote;
@@ -13553,6 +13613,7 @@ class AvatarDownloader {
13553
13613
  updateProgress(filename, false);
13554
13614
  try {
13555
13615
  const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
13616
+ signal,
13556
13617
  characterId: characterMeta.characterId ?? void 0,
13557
13618
  resourceType: "character"
13558
13619
  });
@@ -13567,6 +13628,9 @@ class AvatarDownloader {
13567
13628
  updateProgress(filename, true);
13568
13629
  return { key, success: true, size: arrayBuffer.byteLength };
13569
13630
  } catch (error) {
13631
+ if (error instanceof Error && (error.name === "AbortError" || error.message === "Download cancelled")) {
13632
+ throw error;
13633
+ }
13570
13634
  if (!optional) {
13571
13635
  const errorMessage = error instanceof Error ? error.message : String(error);
13572
13636
  logEvent("download_avatar_assets_failed", "error", {
@@ -13599,9 +13663,12 @@ class AvatarDownloader {
13599
13663
  const totalSize = Object.values(characterData).reduce((sum, buffer) => {
13600
13664
  return sum + (buffer ? buffer.byteLength : 0);
13601
13665
  }, 0);
13602
- logEvent("download_avatar_assets_total_measure", "info", {
13666
+ logEvent("download_avatar_assets_latency", "info", {
13667
+ resolution: "default",
13668
+ // 目前写死 default
13603
13669
  avatar_id: characterMeta.characterId ?? "unknown",
13604
- total_duration: totalDuration,
13670
+ duration: totalDuration,
13671
+ // 保留其他有用的字段
13605
13672
  parallel_duration: parallelDuration,
13606
13673
  total_size: totalSize,
13607
13674
  file_count: filesToLoad.length,
@@ -13615,9 +13682,13 @@ class AvatarDownloader {
13615
13682
  * @internal
13616
13683
  */
13617
13684
  async preloadResources(characterMeta, options) {
13618
- const { progressCallback = null } = options || {};
13685
+ const { progressCallback = null, signal } = options || {};
13686
+ if (signal == null ? void 0 : signal.aborted) {
13687
+ throw new Error("Preload cancelled");
13688
+ }
13619
13689
  const [characterData, preloadCameraSettings] = await Promise.all([
13620
13690
  this.loadCharacterData(characterMeta, {
13691
+ signal,
13621
13692
  progressCallback: (info) => {
13622
13693
  if (progressCallback) {
13623
13694
  progressCallback({
@@ -13627,7 +13698,7 @@ class AvatarDownloader {
13627
13698
  }
13628
13699
  }
13629
13700
  }),
13630
- this.loadCameraSettings(characterMeta)
13701
+ this.loadCameraSettings(characterMeta, { signal })
13631
13702
  ]);
13632
13703
  return {
13633
13704
  characterData,
@@ -13661,7 +13732,8 @@ class AvatarDownloader {
13661
13732
  ...headers,
13662
13733
  ...options.headers
13663
13734
  },
13664
- body: options.body ? JSON.stringify(options.body) : void 0
13735
+ body: options.body ? JSON.stringify(options.body) : void 0,
13736
+ signal: options.signal
13665
13737
  });
13666
13738
  if (!response.ok) {
13667
13739
  let error;
@@ -13694,16 +13766,21 @@ class AvatarDownloader {
13694
13766
  * Returns CharacterMeta with nested resource structure
13695
13767
  * @internal
13696
13768
  */
13697
- async getCharacterById(characterId) {
13769
+ async getCharacterById(characterId, options) {
13698
13770
  var _a;
13771
+ const { signal } = options || {};
13699
13772
  const startTime = Date.now();
13700
13773
  try {
13774
+ if (signal == null ? void 0 : signal.aborted) {
13775
+ throw new Error("Request cancelled");
13776
+ }
13701
13777
  const client = this.getSdkApiClient();
13702
13778
  const response = await client.request(`/v2/character/${characterId}`, {
13703
- method: "GET"
13779
+ method: "GET",
13780
+ signal
13704
13781
  });
13705
13782
  const duration = Date.now() - startTime;
13706
- logEvent("fetch_avatar_metadata_measure", "info", {
13783
+ logEvent("fetch_avatar_metadata_latency", "info", {
13707
13784
  avatar_id: characterId,
13708
13785
  duration
13709
13786
  });
@@ -13735,10 +13812,12 @@ const _AvatarManager = class _AvatarManager {
13735
13812
  constructor() {
13736
13813
  __publicField(this, "avatarDownloader", null);
13737
13814
  __publicField(this, "avatarCache", /* @__PURE__ */ new Map());
13738
- __publicField(this, "loadingPromises", /* @__PURE__ */ new Map());
13739
- // 下载队列:确保资源下载串行执行
13815
+ /** 下载队列:FIFO 顺序 */
13740
13816
  __publicField(this, "downloadQueue", []);
13741
- __publicField(this, "isDownloading", false);
13817
+ /** 当前正在执行的任务 */
13818
+ __publicField(this, "currentTask", null);
13819
+ /** 任务索引:快速查找 id 对应的任务 */
13820
+ __publicField(this, "taskIndex", /* @__PURE__ */ new Map());
13742
13821
  }
13743
13822
  /**
13744
13823
  * Access via global singleton
@@ -13756,28 +13835,139 @@ const _AvatarManager = class _AvatarManager {
13756
13835
  * @returns Promise<Avatar>
13757
13836
  */
13758
13837
  async load(id, onProgress) {
13759
- const loadingPromise = this.loadingPromises.get(id);
13760
- if (loadingPromise) {
13761
- logger.log(`[AvatarManager] Avatar ${id} is already loading, reusing promise`);
13762
- return loadingPromise;
13763
- }
13764
13838
  if (!AvatarSDK.isInitialized) {
13765
13839
  throw new Error("AvatarSDK not initialized. Please call AvatarSDK.initialize() first.");
13766
13840
  }
13767
13841
  if (!this.avatarDownloader) {
13768
13842
  this.avatarDownloader = new AvatarDownloader();
13769
13843
  }
13770
- logger.log(`[AvatarManager] Fetching latest metadata for avatar ${id}...`);
13771
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 5 });
13772
- const newCharacterMeta = await this.avatarDownloader.getCharacterById(id);
13844
+ const existingTask = this.taskIndex.get(id);
13845
+ if (existingTask && existingTask.status !== "completed" && existingTask.status !== "failed" && existingTask.status !== "cancelled") {
13846
+ logger.log(`[AvatarManager] Avatar ${id} already in queue/loading, adding callback to existing task`);
13847
+ onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 0 });
13848
+ return new Promise((resolve2, reject) => {
13849
+ existingTask.requests.push({ onProgress, resolve: resolve2, reject });
13850
+ });
13851
+ }
13852
+ logger.log(`[AvatarManager] Queuing new avatar download for id: ${id}`);
13853
+ let taskResolve;
13854
+ let taskReject;
13855
+ const taskPromise = new Promise((resolve2, reject) => {
13856
+ taskResolve = resolve2;
13857
+ taskReject = reject;
13858
+ });
13859
+ const task = {
13860
+ id,
13861
+ status: "queued",
13862
+ requests: [{ onProgress, resolve: taskResolve, reject: taskReject }],
13863
+ abortController: new AbortController(),
13864
+ promise: taskPromise,
13865
+ _resolve: taskResolve,
13866
+ _reject: taskReject
13867
+ };
13868
+ onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 0 });
13869
+ this.downloadQueue.push(task);
13870
+ this.taskIndex.set(id, task);
13871
+ this.processQueue();
13872
+ return taskPromise;
13873
+ }
13874
+ /**
13875
+ * Cancel a pending or running download task
13876
+ * @param id Avatar ID to cancel
13877
+ * @returns true if task was found and cancelled
13878
+ */
13879
+ cancelLoad(id) {
13880
+ const task = this.taskIndex.get(id);
13881
+ if (!task) {
13882
+ logger.log(`[AvatarManager] No task found for id: ${id}`);
13883
+ return false;
13884
+ }
13885
+ if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
13886
+ logger.log(`[AvatarManager] Task ${id} already finished with status: ${task.status}`);
13887
+ return false;
13888
+ }
13889
+ logger.log(`[AvatarManager] Cancelling task for id: ${id}`);
13890
+ task.abortController.abort();
13891
+ task.status = "cancelled";
13892
+ const cancelError = new Error(`Download cancelled for avatar: ${id}`);
13893
+ this.notifyAllRequests(task, { type: LoadProgress.failed, error: cancelError });
13894
+ task.requests.forEach((req) => req.reject(cancelError));
13895
+ this.taskIndex.delete(id);
13896
+ const queueIndex = this.downloadQueue.indexOf(task);
13897
+ if (queueIndex !== -1) {
13898
+ this.downloadQueue.splice(queueIndex, 1);
13899
+ }
13900
+ if (this.currentTask === task) {
13901
+ this.currentTask = null;
13902
+ this.processQueue();
13903
+ }
13904
+ return true;
13905
+ }
13906
+ /**
13907
+ * Process download queue (FIFO)
13908
+ * @internal
13909
+ */
13910
+ async processQueue() {
13911
+ var _a, _b;
13912
+ if (this.currentTask || this.downloadQueue.length === 0) {
13913
+ return;
13914
+ }
13915
+ const task = this.downloadQueue.shift();
13916
+ this.currentTask = task;
13917
+ logger.log(`[AvatarManager] Processing task: ${task.id} (${this.downloadQueue.length} items remaining)`);
13918
+ try {
13919
+ const avatar = await this.executeTask(task);
13920
+ if (task.status === "cancelled") {
13921
+ return;
13922
+ }
13923
+ task.status = "completed";
13924
+ this.notifyAllRequests(task, { type: LoadProgress.completed });
13925
+ task.requests.forEach((req) => req.resolve(avatar));
13926
+ (_a = task._resolve) == null ? void 0 : _a.call(task, avatar);
13927
+ } catch (error) {
13928
+ if (task.status === "cancelled") {
13929
+ return;
13930
+ }
13931
+ task.status = "failed";
13932
+ const err = error instanceof Error ? error : new Error(String(error));
13933
+ this.notifyAllRequests(task, { type: LoadProgress.failed, error: err });
13934
+ task.requests.forEach((req) => req.reject(err));
13935
+ (_b = task._reject) == null ? void 0 : _b.call(task, err);
13936
+ } finally {
13937
+ if (task.status !== "cancelled") {
13938
+ this.taskIndex.delete(task.id);
13939
+ }
13940
+ this.currentTask = null;
13941
+ this.processQueue();
13942
+ }
13943
+ }
13944
+ /**
13945
+ * Execute a single download task
13946
+ * @internal
13947
+ */
13948
+ async executeTask(task) {
13949
+ const { id, abortController } = task;
13950
+ const signal = abortController.signal;
13951
+ const startTime = Date.now();
13952
+ if (signal.aborted) {
13953
+ throw new Error("Task cancelled");
13954
+ }
13955
+ task.status = "fetching-meta";
13956
+ logger.log(`[AvatarManager] Fetching metadata for avatar ${id}...`);
13957
+ const characterMeta = await this.avatarDownloader.getCharacterById(id, { signal });
13958
+ task.characterMeta = characterMeta;
13959
+ if (signal.aborted) {
13960
+ throw new Error("Task cancelled");
13961
+ }
13773
13962
  const cached = this.avatarCache.get(id);
13774
13963
  if (cached) {
13775
13964
  const cachedMeta = cached.getCharacterMeta();
13776
13965
  const cachedVersion = cachedMeta.version;
13777
- const newVersion = newCharacterMeta.version;
13966
+ const newVersion = characterMeta.version;
13778
13967
  if (cachedVersion === newVersion) {
13779
- logger.log(`[AvatarManager] Avatar ${id} found in cache with same version (${cachedVersion}), updating meta and returning cached`);
13780
- cached.updateCharacterMeta(newCharacterMeta);
13968
+ logger.log(`[AvatarManager] Avatar ${id} found in cache with same version (${cachedVersion}), returning cached`);
13969
+ cached.updateCharacterMeta(characterMeta);
13970
+ this.notifyAllRequests(task, { type: LoadProgress.downloading, progress: 100 });
13781
13971
  return cached;
13782
13972
  } else {
13783
13973
  logger.log(`[AvatarManager] Avatar ${id} version mismatch: cached=${cachedVersion}, new=${newVersion}, reloading...`);
@@ -13786,77 +13976,71 @@ const _AvatarManager = class _AvatarManager {
13786
13976
  await PwaCacheManager2.clearCharacterCache(id);
13787
13977
  }
13788
13978
  }
13789
- logger.log(`[AvatarManager] Queuing avatar download for id: ${id}`);
13790
- const loadPromise = new Promise((resolve2, reject) => {
13791
- this.downloadQueue.push({
13792
- id,
13793
- characterMeta: newCharacterMeta,
13794
- onProgress,
13795
- resolve: resolve2,
13796
- reject
13797
- });
13979
+ const totalAssets = this.countTotalAssets(characterMeta);
13980
+ const metaProgress = Math.round(1 / (1 + totalAssets) * 100);
13981
+ this.notifyAllRequests(task, { type: LoadProgress.downloading, progress: metaProgress });
13982
+ if (signal.aborted) {
13983
+ throw new Error("Task cancelled");
13984
+ }
13985
+ task.status = "downloading";
13986
+ logger.log("[AvatarManager] Downloading resources...");
13987
+ let downloadedCount = 0;
13988
+ const resources = await this.avatarDownloader.preloadResources(characterMeta, {
13989
+ signal,
13990
+ progressCallback: (info) => {
13991
+ if (info.loaded > downloadedCount) {
13992
+ downloadedCount = info.loaded;
13993
+ const progress = Math.round((1 + downloadedCount) / (1 + totalAssets) * 100);
13994
+ this.notifyAllRequests(task, { type: LoadProgress.downloading, progress });
13995
+ }
13996
+ }
13798
13997
  });
13799
- this.loadingPromises.set(id, loadPromise);
13800
- loadPromise.finally(() => {
13801
- this.loadingPromises.delete(id);
13998
+ if (signal.aborted) {
13999
+ throw new Error("Task cancelled");
14000
+ }
14001
+ logger.log("[AvatarManager] Creating Avatar instance...");
14002
+ const avatar = new Avatar(id, characterMeta, resources);
14003
+ this.avatarCache.set(id, avatar);
14004
+ logger.log("[AvatarManager] Avatar loaded successfully");
14005
+ const totalDuration = Date.now() - startTime;
14006
+ logEvent("fetch_avatar_latency", "info", {
14007
+ resolution: "default",
14008
+ // 目前写死 default
14009
+ avatar_id: id,
14010
+ duration: totalDuration
13802
14011
  });
13803
- this.processDownloadQueue();
13804
- return loadPromise;
14012
+ logEvent("character_load", "info", {
14013
+ avatar_id: id,
14014
+ event: "load_success"
14015
+ });
14016
+ return avatar;
13805
14017
  }
13806
14018
  /**
13807
- * Process download queue (ensure serial execution)
14019
+ * 计算需要下载的资源总数
13808
14020
  * @internal
13809
14021
  */
13810
- async processDownloadQueue() {
13811
- if (this.isDownloading || this.downloadQueue.length === 0) {
13812
- return;
13813
- }
13814
- this.isDownloading = true;
13815
- const item = this.downloadQueue.shift();
13816
- logger.log(`[AvatarManager] Processing download queue item: ${item.id} (${this.downloadQueue.length} items remaining)`);
13817
- try {
13818
- const avatar = await this.doLoad(item.id, item.characterMeta, item.onProgress);
13819
- item.resolve(avatar);
13820
- } catch (error) {
13821
- item.reject(error instanceof Error ? error : new Error(String(error)));
13822
- } finally {
13823
- this.isDownloading = false;
13824
- this.processDownloadQueue();
13825
- }
14022
+ countTotalAssets(meta) {
14023
+ var _a, _b, _c, _d, _e2, _f, _g, _h, _i2, _j, _k;
14024
+ let count = 0;
14025
+ if ((_c = (_b = (_a = meta.models) == null ? void 0 : _a.shape) == null ? void 0 : _b.resource) == null ? void 0 : _c.remote) count++;
14026
+ if ((_f = (_e2 = (_d = meta.models) == null ? void 0 : _d.gsStandard) == null ? void 0 : _e2.resource) == null ? void 0 : _f.remote) count++;
14027
+ if ((_i2 = (_h = (_g = meta.animations) == null ? void 0 : _g.frameIdle) == null ? void 0 : _h.resource) == null ? void 0 : _i2.remote) count++;
14028
+ if ((_k = (_j = meta.camera) == null ? void 0 : _j.resource) == null ? void 0 : _k.remote) count++;
14029
+ return count;
13826
14030
  }
13827
14031
  /**
13828
- * Execute actual loading logic (private method)
14032
+ * 通知所有请求者进度更新
13829
14033
  * @internal
13830
14034
  */
13831
- async doLoad(id, characterMeta, onProgress) {
13832
- try {
13833
- logger.log("[AvatarManager] Step 1: Downloading resources...");
13834
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.downloading, progress: 30 });
13835
- const resources = await this.avatarDownloader.preloadResources(characterMeta, {
13836
- progressCallback: (info) => {
13837
- const externalProgress = 30 + info.progress / 100 * 70;
13838
- onProgress == null ? void 0 : onProgress({
13839
- type: LoadProgress.downloading,
13840
- progress: Math.round(externalProgress)
13841
- });
13842
- }
13843
- });
13844
- logger.log("[AvatarManager] Step 2: Creating Avatar instance...");
13845
- const avatar = new Avatar(id, characterMeta, resources);
13846
- this.avatarCache.set(id, avatar);
13847
- logger.log("[AvatarManager] Avatar loaded successfully");
13848
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.completed });
13849
- logEvent("character_load", "info", {
13850
- avatar_id: id,
13851
- event: "load_success"
13852
- });
13853
- return avatar;
13854
- } catch (error) {
13855
- const message = error instanceof Error ? error.message : String(error);
13856
- logger.error("Failed to load avatar:", message);
13857
- onProgress == null ? void 0 : onProgress({ type: LoadProgress.failed, error });
13858
- throw error;
13859
- }
14035
+ notifyAllRequests(task, progress) {
14036
+ task.requests.forEach((req) => {
14037
+ var _a;
14038
+ try {
14039
+ (_a = req.onProgress) == null ? void 0 : _a.call(req, progress);
14040
+ } catch (err) {
14041
+ logger.warn(`[AvatarManager] Error in progress callback:`, err);
14042
+ }
14043
+ });
13860
14044
  }
13861
14045
  /**
13862
14046
  * Get cached avatar
@@ -13871,17 +14055,53 @@ const _AvatarManager = class _AvatarManager {
13871
14055
  * @param id Avatar ID
13872
14056
  */
13873
14057
  clear(id) {
14058
+ this.cancelLoad(id);
13874
14059
  const removed = this.avatarCache.delete(id);
13875
14060
  if (removed) {
13876
14061
  logger.log(`[AvatarManager] Cleared avatar cache for id: ${id}`);
13877
14062
  }
13878
14063
  }
13879
14064
  /**
13880
- * Clear all avatar cache and resource loader cache
14065
+ * Clear all avatar cache and cancel all tasks
13881
14066
  */
13882
14067
  clearAll() {
14068
+ for (const task of this.taskIndex.values()) {
14069
+ if (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
14070
+ this.cancelLoad(task.id);
14071
+ }
14072
+ }
13883
14073
  this.avatarCache.clear();
13884
- logger.log("[AvatarManager] Cleared all avatar cache");
14074
+ this.downloadQueue = [];
14075
+ this.taskIndex.clear();
14076
+ this.currentTask = null;
14077
+ logger.log("[AvatarManager] Cleared all avatar cache and cancelled all tasks");
14078
+ }
14079
+ /**
14080
+ * Get the number of pending tasks in the queue
14081
+ * @internal For testing purposes
14082
+ */
14083
+ get pendingTaskCount() {
14084
+ return this.downloadQueue.length + (this.currentTask ? 1 : 0);
14085
+ }
14086
+ /**
14087
+ * Check if a task is in progress for the given ID
14088
+ * @internal For testing purposes
14089
+ */
14090
+ isLoading(id) {
14091
+ const task = this.taskIndex.get(id);
14092
+ return task !== void 0 && task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled";
14093
+ }
14094
+ /**
14095
+ * @internal For testing - get loadingPromises map (compatibility)
14096
+ */
14097
+ get loadingPromises() {
14098
+ const map = /* @__PURE__ */ new Map();
14099
+ for (const [id, task] of this.taskIndex) {
14100
+ if (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
14101
+ map.set(id, task.promise);
14102
+ }
14103
+ }
14104
+ return map;
13885
14105
  }
13886
14106
  };
13887
14107
  __publicField(_AvatarManager, "_instance", null);
@@ -15556,7 +15776,7 @@ function linearLerp(from, to2, progress) {
15556
15776
  expression: lerpArrays(from.expression || [], to2.expression || [], progress)
15557
15777
  };
15558
15778
  }
15559
- function generateTransitionFramesLinear(from, to2, durationMs, fps = 25) {
15779
+ function generateTransitionFramesLinear(from, to2, durationMs, fps = FLAME_FRAME_RATE) {
15560
15780
  const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
15561
15781
  const frames = Array.from({ length: steps });
15562
15782
  if (steps === 1) {
@@ -15599,27 +15819,11 @@ function createBezierEasing(x1, y1, x2, y2) {
15599
15819
  };
15600
15820
  }
15601
15821
  const BEZIER_CURVES = {
15602
- // jaw: fast start, smooth stop
15603
- jaw: createBezierEasing(0.2, 0.8, 0.3, 1),
15604
- // expression: smooth S curve
15605
- expression: createBezierEasing(0.4, 0, 0.2, 1),
15606
- // eye: softer S curve
15607
- eye: createBezierEasing(0.3, 0, 0.1, 1),
15608
- // neck: slow start, inertial stop
15609
- neck: createBezierEasing(0.1, 0.2, 0.2, 1),
15610
- // global: standard ease-in-out
15611
- global: createBezierEasing(0.42, 0, 0.58, 1)
15612
- };
15613
- const TIME_SCALE = {
15614
- jaw: 2.5,
15615
- // 40% time to complete
15616
- expression: 1.6,
15617
- // 62.5% time to complete
15618
- eye: 1.3,
15619
- // 77% time to complete
15620
- neck: 1,
15621
- // 100% time to complete
15622
- global: 1
15822
+ jaw: createBezierEasing(...BEZIER_CURVES$1.jaw),
15823
+ expression: createBezierEasing(...BEZIER_CURVES$1.expression),
15824
+ eye: createBezierEasing(...BEZIER_CURVES$1.eye),
15825
+ neck: createBezierEasing(...BEZIER_CURVES$1.neck),
15826
+ global: createBezierEasing(...BEZIER_CURVES$1.global)
15623
15827
  };
15624
15828
  function bezierLerp(from, to2, progress) {
15625
15829
  const getT = (key) => {
@@ -15642,7 +15846,7 @@ function bezierLerp(from, to2, progress) {
15642
15846
  expression: lerpArrays(from.expression || [], to2.expression || [], getT("expression"))
15643
15847
  };
15644
15848
  }
15645
- function generateTransitionFrames(from, to2, durationMs, fps = 25) {
15849
+ function generateTransitionFrames(from, to2, durationMs, fps = FLAME_FRAME_RATE) {
15646
15850
  const steps = Math.max(1, Math.floor(durationMs / 1e3 * fps));
15647
15851
  const frames = Array.from({ length: steps });
15648
15852
  if (steps === 1) {
@@ -15692,9 +15896,9 @@ class AvatarView {
15692
15896
  // Transition animation data
15693
15897
  __publicField(this, "transitionKeyframes", []);
15694
15898
  __publicField(this, "transitionStartTime", 0);
15695
- __publicField(this, "startTransitionDurationMs", 200);
15899
+ __publicField(this, "startTransitionDurationMs", START_TRANSITION_DURATION_MS);
15696
15900
  // Idle -> Speaking 过渡时长
15697
- __publicField(this, "endTransitionDurationMs", 1600);
15901
+ __publicField(this, "endTransitionDurationMs", END_TRANSITION_DURATION_MS);
15698
15902
  // Speaking -> Idle 过渡时长
15699
15903
  __publicField(this, "cachedIdleFirstFrame", null);
15700
15904
  __publicField(this, "idleCurrentFrameIndex", 0);
@@ -15706,6 +15910,8 @@ class AvatarView {
15706
15910
  // Unique ID for this character instance
15707
15911
  // 纯渲染模式标志(阻止 idle 循环渲染)
15708
15912
  __publicField(this, "isPureRenderingMode", false);
15913
+ // 渲染开关标志(控制是否进行渲染循环)
15914
+ __publicField(this, "_renderingEnabled", true);
15709
15915
  // avatar_active 埋点相关
15710
15916
  __publicField(this, "avatarActiveTimer", null);
15711
15917
  __publicField(this, "AVATAR_ACTIVE_INTERVAL", 6e5);
@@ -15777,12 +15983,12 @@ class AvatarView {
15777
15983
  aligned.from,
15778
15984
  aligned.to,
15779
15985
  durationMs,
15780
- APP_CONFIG.animation.fps
15986
+ FLAME_FRAME_RATE
15781
15987
  ) : generateTransitionFrames(
15782
15988
  aligned.from,
15783
15989
  aligned.to,
15784
15990
  durationMs,
15785
- APP_CONFIG.animation.fps
15991
+ FLAME_FRAME_RATE
15786
15992
  );
15787
15993
  if (keyframes.length < 2) {
15788
15994
  keyframes = [aligned.from, aligned.to];
@@ -16111,7 +16317,7 @@ class AvatarView {
16111
16317
  }
16112
16318
  this.idleCurrentFrameIndex = 0;
16113
16319
  let lastTime = 0;
16114
- const targetFPS = APP_CONFIG.animation.fps;
16320
+ const targetFPS = FLAME_FRAME_RATE;
16115
16321
  const frameInterval = 1e3 / targetFPS;
16116
16322
  this.initFPS();
16117
16323
  const renderFrame = async (currentTime) => {
@@ -16133,6 +16339,10 @@ class AvatarView {
16133
16339
  this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
16134
16340
  return;
16135
16341
  }
16342
+ if (!this._renderingEnabled) {
16343
+ this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
16344
+ return;
16345
+ }
16136
16346
  const avatarCore = AvatarSDK.getAvatarCore();
16137
16347
  if (!avatarCore) {
16138
16348
  return;
@@ -16146,8 +16356,7 @@ class AvatarView {
16146
16356
  if (this.isPureRenderingMode) {
16147
16357
  return;
16148
16358
  }
16149
- this.renderSystem.loadSplatsFromPackedData(splatData);
16150
- this.renderSystem.renderFrame();
16359
+ this.doRender(splatData);
16151
16360
  }
16152
16361
  this.idleAnimationLoopId = requestAnimationFrame(renderFrame);
16153
16362
  } catch (error) {
@@ -16168,7 +16377,7 @@ class AvatarView {
16168
16377
  this.stopRealtimeAnimationLoop();
16169
16378
  }
16170
16379
  let lastTime = 0;
16171
- const targetFPS = APP_CONFIG.animation.fps;
16380
+ const targetFPS = FLAME_FRAME_RATE;
16172
16381
  const frameInterval = 1e3 / targetFPS;
16173
16382
  this.initFPS();
16174
16383
  const renderFrame = async (currentTime) => {
@@ -16216,8 +16425,7 @@ class AvatarView {
16216
16425
  if (avatarCore) {
16217
16426
  const sd = await avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0);
16218
16427
  if (sd) {
16219
- this.renderSystem.loadSplatsFromPackedData(sd);
16220
- this.renderSystem.renderFrame();
16428
+ this.doRender(sd);
16221
16429
  }
16222
16430
  }
16223
16431
  if (progress >= 1) {
@@ -16295,6 +16503,18 @@ class AvatarView {
16295
16503
  this.stopIdleAnimationLoop();
16296
16504
  this.stopRealtimeAnimationLoop();
16297
16505
  }
16506
+ /**
16507
+ * Unified render method - all rendering goes through here
16508
+ * This is the single point of control for renderingEnabled flag
16509
+ * @internal
16510
+ */
16511
+ doRender(splatData) {
16512
+ if (!this.renderSystem || !this._renderingEnabled) {
16513
+ return;
16514
+ }
16515
+ this.renderSystem.loadSplatsFromPackedData(splatData);
16516
+ this.renderSystem.renderFrame();
16517
+ }
16298
16518
  /**
16299
16519
  * Render realtime frame (called by playback layer callback)
16300
16520
  * @internal
@@ -16303,13 +16523,12 @@ class AvatarView {
16303
16523
  if (!this.renderSystem || this.renderingState !== "speaking") {
16304
16524
  return;
16305
16525
  }
16306
- this.renderSystem.loadSplatsFromPackedData(splatData);
16307
- this.renderSystem.renderFrame();
16308
16526
  this.lastRenderedFrameIndex = frameIndex;
16309
16527
  if (frameIndex >= 0 && frameIndex < this.currentKeyframes.length) {
16310
16528
  this.lastRealtimeProtoFrame = this.currentKeyframes[frameIndex];
16311
16529
  this.currentPlayingFrame = this.lastRealtimeProtoFrame;
16312
16530
  }
16531
+ this.doRender(splatData);
16313
16532
  }
16314
16533
  /**
16315
16534
  * State transition method
@@ -16676,8 +16895,7 @@ class AvatarView {
16676
16895
  this.characterHandle ?? void 0
16677
16896
  );
16678
16897
  if (splatData) {
16679
- this.renderSystem.loadSplatsFromPackedData(splatData);
16680
- this.renderSystem.renderFrame();
16898
+ this.doRender(splatData);
16681
16899
  }
16682
16900
  } catch (error) {
16683
16901
  logger.error("[AvatarView] Failed to render flame:", error instanceof Error ? error.message : String(error));
@@ -16714,7 +16932,7 @@ class AvatarView {
16714
16932
  const aligned = this.alignFlamePair(fromFrame, toFrame);
16715
16933
  const alignedFrom = aligned.from;
16716
16934
  const alignedTo = aligned.to;
16717
- const fps = APP_CONFIG.animation.fps;
16935
+ const fps = FLAME_FRAME_RATE;
16718
16936
  const durationMs = frameCount / fps * 1e3;
16719
16937
  const transitionFrames = useLinear ? generateTransitionFramesLinear(alignedFrom, alignedTo, durationMs, fps) : generateTransitionFrames(alignedFrom, alignedTo, durationMs, fps);
16720
16938
  transitionFrames[0] = alignedFrom;
@@ -16746,6 +16964,76 @@ class AvatarView {
16746
16964
  this.renderSystem.handleResize();
16747
16965
  }
16748
16966
  }
16967
+ /**
16968
+ * Pause rendering loop
16969
+ *
16970
+ * When called:
16971
+ * - Rendering loop stops (no GPU/canvas updates)
16972
+ * - Audio playback continues normally
16973
+ * - Animation state machine continues running
16974
+ *
16975
+ * Use `resumeRendering()` to resume rendering.
16976
+ *
16977
+ * @example
16978
+ * // Stop rendering to save GPU resources (audio continues)
16979
+ * avatarView.pauseRendering()
16980
+ */
16981
+ pauseRendering() {
16982
+ if (!this._renderingEnabled) {
16983
+ return;
16984
+ }
16985
+ this._renderingEnabled = false;
16986
+ logger.log("[AvatarView] Rendering paused");
16987
+ }
16988
+ /**
16989
+ * Resume rendering loop
16990
+ *
16991
+ * When called:
16992
+ * - Rendering loop resumes from current state
16993
+ * - If in Idle state, immediately renders current frame to restore display
16994
+ *
16995
+ * @example
16996
+ * // Resume rendering
16997
+ * avatarView.resumeRendering()
16998
+ */
16999
+ resumeRendering() {
17000
+ if (this._renderingEnabled) {
17001
+ return;
17002
+ }
17003
+ this._renderingEnabled = true;
17004
+ logger.log("[AvatarView] Rendering resumed");
17005
+ if (this.isInitialized && this.renderSystem && this.renderingState === "idle") {
17006
+ this.renderCurrentIdleFrame();
17007
+ }
17008
+ }
17009
+ /**
17010
+ * Check if rendering is currently enabled
17011
+ * @returns true if rendering is enabled, false if paused
17012
+ */
17013
+ isRenderingEnabled() {
17014
+ return this._renderingEnabled;
17015
+ }
17016
+ /**
17017
+ * Render current idle frame immediately
17018
+ * @internal
17019
+ */
17020
+ async renderCurrentIdleFrame() {
17021
+ const avatarCore = AvatarSDK.getAvatarCore();
17022
+ if (!avatarCore || !this.renderSystem) {
17023
+ return;
17024
+ }
17025
+ try {
17026
+ const splatData = await avatarCore.computeCompleteFrameFlat(
17027
+ { frameIndex: this.idleCurrentFrameIndex },
17028
+ this.characterHandle ?? void 0
17029
+ );
17030
+ if (splatData) {
17031
+ this.doRender(splatData);
17032
+ }
17033
+ } catch (error) {
17034
+ logger.warn("[AvatarView] Failed to render current idle frame:", error);
17035
+ }
17036
+ }
16749
17037
  /**
16750
17038
  * 获取渲染性能统计
16751
17039
  * @returns 渲染性能统计数据,如果渲染系统未初始化则返回 null
@@ -16786,7 +17074,7 @@ class AvatarView {
16786
17074
  const { x: x2, y: y2, scale } = value;
16787
17075
  logger.log(`[AvatarView] Setting transform: x=${x2}, y=${y2}, scale=${scale}`);
16788
17076
  this.renderSystem.setTransform(x2, y2, scale);
16789
- if (this.isInitialized && this.renderSystem) {
17077
+ if (this.isInitialized && this.renderSystem && this._renderingEnabled) {
16790
17078
  this.renderSystem.renderFrame();
16791
17079
  }
16792
17080
  }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { b, c, m, f, d, j, g, C, i, D, E, k, h, L, R, n } from "./index-ZN-iK3b8.js";
1
+ import { b, c, m, f, d, j, g, C, i, D, E, k, h, L, R, n } from "./index-DEJMvfST.js";
2
2
  export {
3
3
  b as Avatar,
4
4
  c as AvatarController,
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Avatar SDK 内部共享常量
3
+ * 跨平台统一配置,确保行为一致
4
+ *
5
+ * 对应:
6
+ * - Android: AvatarPlayer.kt 中的 companion object
7
+ * - iOS: (待实现)
8
+ *
9
+ * @internal
10
+ */
11
+ /** 帧间隔 (毫秒) */
12
+ export declare const FLAME_FRAME_INTERVAL_MS: number;
13
+ /** 开始过渡时长 (秒): idle -> speaking */
14
+ export declare const START_TRANSITION_DURATION_S = 0.2;
15
+ /** 结束过渡时长 (秒): speaking -> idle */
16
+ export declare const END_TRANSITION_DURATION_S = 1.6;
17
+ /** 开始过渡时长 (毫秒) */
18
+ export declare const START_TRANSITION_DURATION_MS: number;
19
+ /** 结束过渡时长 (毫秒) */
20
+ export declare const END_TRANSITION_DURATION_MS: number;
21
+ /** 默认音频采样率 (Hz) */
22
+ export declare const AUDIO_SAMPLE_RATE = 16000;
23
+ /** 音频声道数 (mono) */
24
+ export declare const AUDIO_CHANNELS = 1;
25
+ /** 每样本字节数 (16bit PCM) */
26
+ export declare const AUDIO_BYTES_PER_SAMPLE = 2;
27
+ /** 每秒音频字节数 */
28
+ export declare const AUDIO_BYTES_PER_SECOND: number;
29
+ /**
30
+ * 各部位插值曲线控制点
31
+ * 格式: [x1, y1, x2, y2] 对应 cubic-bezier(x1, y1, x2, y2)
32
+ *
33
+ * 与 Android FlameFrameData.kt 中的 BEZIER_CURVES 完全一致
34
+ */
35
+ export declare const BEZIER_CURVES: {
36
+ /** 下颌: 快速启动,平滑停止 */
37
+ readonly jaw: readonly [0.2, 0.8, 0.3, 1];
38
+ /** 表情: 平滑 S 曲线 */
39
+ readonly expression: readonly [0.4, 0, 0.2, 1];
40
+ /** 眼部: 柔和 S 曲线 */
41
+ readonly eye: readonly [0.3, 0, 0.1, 1];
42
+ /** 颈部: 慢启动,惯性停止 */
43
+ readonly neck: readonly [0.1, 0.2, 0.2, 1];
44
+ /** 全局: 标准 ease-in-out */
45
+ readonly global: readonly [0.42, 0, 0.58, 1];
46
+ };
47
+ /**
48
+ * 各部位时间缩放因子
49
+ * 控制各部位完成过渡所需的相对时间
50
+ *
51
+ * 与 Android FlameFrameData.kt 中的 TIME_SCALE 完全一致
52
+ */
53
+ export declare const TIME_SCALE: {
54
+ /** 下颌: 40% 时间完成 (1/2.5) */
55
+ readonly jaw: 2.5;
56
+ /** 表情: 62.5% 时间完成 (1/1.6) */
57
+ readonly expression: 1.6;
58
+ /** 眼部: 77% 时间完成 (1/1.3) */
59
+ readonly eye: 1.3;
60
+ /** 颈部: 100% 时间完成 */
61
+ readonly neck: 1;
62
+ /** 全局: 100% 时间完成 */
63
+ readonly global: 1;
64
+ };
65
+ export type BezierCurveKey = keyof typeof BEZIER_CURVES;
66
+ export type TimeScaleKey = keyof typeof TIME_SCALE;
67
+ export declare const AvatarConstants: {
68
+ readonly FLAME_FRAME_RATE: 25;
69
+ readonly FLAME_FRAME_INTERVAL_MS: number;
70
+ readonly START_TRANSITION_DURATION_S: 0.2;
71
+ readonly END_TRANSITION_DURATION_S: 1.6;
72
+ readonly START_TRANSITION_DURATION_MS: number;
73
+ readonly END_TRANSITION_DURATION_MS: number;
74
+ readonly AUDIO_SAMPLE_RATE: 16000;
75
+ readonly AUDIO_CHANNELS: 1;
76
+ readonly AUDIO_BYTES_PER_SAMPLE: 2;
77
+ readonly AUDIO_BYTES_PER_SECOND: number;
78
+ readonly BEZIER_CURVES: {
79
+ /** 下颌: 快速启动,平滑停止 */
80
+ readonly jaw: readonly [0.2, 0.8, 0.3, 1];
81
+ /** 表情: 平滑 S 曲线 */
82
+ readonly expression: readonly [0.4, 0, 0.2, 1];
83
+ /** 眼部: 柔和 S 曲线 */
84
+ readonly eye: readonly [0.3, 0, 0.1, 1];
85
+ /** 颈部: 慢启动,惯性停止 */
86
+ readonly neck: readonly [0.1, 0.2, 0.2, 1];
87
+ /** 全局: 标准 ease-in-out */
88
+ readonly global: readonly [0.42, 0, 0.58, 1];
89
+ };
90
+ readonly TIME_SCALE: {
91
+ /** 下颌: 40% 时间完成 (1/2.5) */
92
+ readonly jaw: 2.5;
93
+ /** 表情: 62.5% 时间完成 (1/1.6) */
94
+ readonly expression: 1.6;
95
+ /** 眼部: 77% 时间完成 (1/1.3) */
96
+ readonly eye: 1.3;
97
+ /** 颈部: 100% 时间完成 */
98
+ readonly neck: 1;
99
+ /** 全局: 100% 时间完成 */
100
+ readonly global: 1;
101
+ };
102
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Internal Module - 统一常量和配置
3
+ *
4
+ * 与 Android SDK 保持一致的配置,方便跨平台维护
5
+ * @internal
6
+ */
7
+ export {};
@@ -3,8 +3,7 @@
3
3
  */
4
4
  export declare enum Environment {
5
5
  cn = "cn",
6
- intl = "intl",
7
- test = "test"
6
+ intl = "intl"
8
7
  }
9
8
  export declare enum DrivingServiceMode {
10
9
  /** Driven by SDK directly */
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Animation Interpolation Utilities (SDK)
3
3
  * Layered interpolation with different timing for each facial component
4
- * Aligned with app implementation and iOS behavior
4
+ * Aligned with app implementation and iOS/Android behavior
5
+ *
6
+ * 贝塞尔曲线和时间缩放配置与 Android FlameFrameData.kt 保持一致
5
7
  */
6
8
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spatialwalk/avatarkit",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.76",
4
+ "version": "1.0.0-beta.78",
5
5
  "description": "AvatarKit SDK - 3D Gaussian Splatting Avatar Rendering SDK",
6
6
  "author": "AvatarKit Team",
7
7
  "license": "MIT",