@spatialwalk/avatarkit 1.0.0-beta.91 → 1.0.0-beta.92

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,12 @@ 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.92]
9
+
10
+ ### ✨ Features
11
+
12
+ - **Host Mode Animation End Signal** — `yieldFramesData()` now returns a `boolean` indicating whether the server has sent all animation data for the current conversation (`end` flag from protobuf). Non-breaking: callers that ignore the return value are unaffected.
13
+
8
14
  ## [1.0.0-beta.91] - 2026-03-19
9
15
 
10
16
  ### 🐛 Bugfixes
@@ -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-BYr_FIpm.js";
4
+ import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-C2higMlc.js";
5
5
  class StreamingAudioPlayer {
6
6
  // Mark if AudioContext is being resumed, avoid concurrent resume requests
7
7
  constructor(options) {
@@ -91,8 +91,9 @@ export declare class AvatarController {
91
91
  * @param keyframesDataArray - Animation keyframes binary data array (each element is a protobuf encoded Message) or empty array to trigger audio-only mode
92
92
  * @param conversationId - Conversation ID (required). If conversationId doesn't match current conversationId, keyframes will be discarded.
93
93
  * Use getCurrentConversationId() to get the current conversationId.
94
+ * @returns `true` if the server has sent all animation data for this conversation (end signal received), `false` otherwise.
94
95
  */
95
- yieldFramesData(keyframesDataArray: (Uint8Array | ArrayBuffer)[], conversationId: string): void;
96
+ yieldFramesData(keyframesDataArray: (Uint8Array | ArrayBuffer)[], conversationId: string): boolean;
96
97
  /**
97
98
  * Send animation keyframes (host mode)
98
99
  * Stream animation keyframes data after yieldAudioData()
@@ -9495,7 +9495,7 @@ const _AnimationPlayer = class _AnimationPlayer {
9495
9495
  if (this.streamingPlayer) {
9496
9496
  return;
9497
9497
  }
9498
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-CGUA8-w0.js");
9498
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-eQ2RRq5U.js");
9499
9499
  const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
9500
9500
  const audioFormat = AvatarSDK2.getAudioFormat();
9501
9501
  this.streamingPlayer = new StreamingAudioPlayer({
@@ -11473,7 +11473,7 @@ class AvatarSDK {
11473
11473
  __publicField(AvatarSDK, "_initializationState", "uninitialized");
11474
11474
  __publicField(AvatarSDK, "_initializingPromise", null);
11475
11475
  __publicField(AvatarSDK, "_configuration", null);
11476
- __publicField(AvatarSDK, "_version", "1.0.0-beta.91");
11476
+ __publicField(AvatarSDK, "_version", "1.0.0-beta.92");
11477
11477
  __publicField(AvatarSDK, "_avatarCore", null);
11478
11478
  __publicField(AvatarSDK, "_dynamicSdkConfig", null);
11479
11479
  __publicField(AvatarSDK, "_cachedDeviceScore", null);
@@ -11904,6 +11904,8 @@ class NetworkLayer {
11904
11904
  __publicField(this, "audioMetrics", this.createAudioMetrics());
11905
11905
  __publicField(this, "isFallbackMode", false);
11906
11906
  // 连接超时降级模式标记
11907
+ /** @internal 降级原因,供 AvatarController 读取 */
11908
+ __publicField(this, "_fallbackReason", "");
11907
11909
  __publicField(this, "isConnecting", false);
11908
11910
  // 避免并发连接
11909
11911
  // 驱动服务心跳检测相关
@@ -11943,6 +11945,7 @@ class NetworkLayer {
11943
11945
  }
11944
11946
  (_b = (_a = this.dataController).onConnectionState) == null ? void 0 : _b.call(_a, ConnectionState.connecting);
11945
11947
  this.isFallbackMode = false;
11948
+ this._fallbackReason = "";
11946
11949
  this.setupWebSocketListeners();
11947
11950
  this.isConnecting = true;
11948
11951
  const CONNECTION_TIMEOUT_MS = 15e3;
@@ -11953,6 +11956,7 @@ class NetworkLayer {
11953
11956
  var _a2, _b2;
11954
11957
  if (!this.dataController.connected) {
11955
11958
  this.isFallbackMode = true;
11959
+ this._fallbackReason = "connection_timeout";
11956
11960
  logger.warn(`[NetworkLayer] WebSocket connection timeout (${CONNECTION_TIMEOUT_MS}ms) - entering fallback mode`);
11957
11961
  (_b2 = (_a2 = this.dataController).onConnectionState) == null ? void 0 : _b2.call(_a2, ConnectionState.failed);
11958
11962
  }
@@ -11970,6 +11974,7 @@ class NetworkLayer {
11970
11974
  }
11971
11975
  if (!this.isFallbackMode) {
11972
11976
  this.isFallbackMode = true;
11977
+ this._fallbackReason = "connection_failed";
11973
11978
  logger.warn("[NetworkLayer] WebSocket connection failed - entering fallback mode");
11974
11979
  }
11975
11980
  (_d = (_c = this.dataController).onConnectionState) == null ? void 0 : _d.call(_c, ConnectionState.failed);
@@ -12034,6 +12039,7 @@ class NetworkLayer {
12034
12039
  */
12035
12040
  disconnect() {
12036
12041
  this.isFallbackMode = false;
12042
+ this._fallbackReason = "";
12037
12043
  this.isConnecting = false;
12038
12044
  this.wsClient.removeAllListeners();
12039
12045
  this.wsClient.disconnect();
@@ -12069,6 +12075,7 @@ class NetworkLayer {
12069
12075
  }
12070
12076
  if (!this.dataController.connected) {
12071
12077
  this.isFallbackMode = false;
12078
+ this._fallbackReason = "";
12072
12079
  this.dataController.setConnected(true);
12073
12080
  (_b = (_a = this.dataController).onConnectionState) == null ? void 0 : _b.call(_a, ConnectionState.connected);
12074
12081
  }
@@ -12665,7 +12672,7 @@ class AvatarController {
12665
12672
  case AvatarState.idle:
12666
12673
  if (this.shouldReportPlaybackStats) {
12667
12674
  const stats = this.frameRateMonitor.getPlaybackStats();
12668
- if (stats.durationMs >= 2e3) {
12675
+ if (stats.durationMs >= 2e3 && stats.frameCount > 0 && Math.random() < 0.3) {
12669
12676
  const props = {
12670
12677
  avg_fps: Number(stats.avgFps.toFixed(1)),
12671
12678
  frame_count: stats.frameCount,
@@ -12788,7 +12795,7 @@ class AvatarController {
12788
12795
  * @returns conversationId - Conversation ID for this audio session
12789
12796
  */
12790
12797
  send(audioData, end = false) {
12791
- var _a, _b, _c;
12798
+ var _a, _b;
12792
12799
  try {
12793
12800
  this.checkAudioContextInitialized();
12794
12801
  } catch (error) {
@@ -12799,14 +12806,6 @@ class AvatarController {
12799
12806
  (_b = this.onError) == null ? void 0 : _b.call(this, new AvatarError("Network layer not available", "NETWORK_LAYER_NOT_AVAILABLE"));
12800
12807
  return null;
12801
12808
  }
12802
- if (!this.networkLayer.canSend()) {
12803
- (_c = this.onError) == null ? void 0 : _c.call(this, new AvatarError("Service not connected", "NOT_CONNECTED"));
12804
- logEvent("character_manager", "warning", {
12805
- avatar_id: this.avatar.id,
12806
- event: "send_not_connected"
12807
- });
12808
- return null;
12809
- }
12810
12809
  const networkConversationId = this.networkLayer.getCurrentConversationId();
12811
12810
  if (this.reqEnd && this.isPlaying && networkConversationId) {
12812
12811
  this.interrupt();
@@ -12865,12 +12864,7 @@ class AvatarController {
12865
12864
  this.emit("keyframesUpdate", this.currentKeyframes);
12866
12865
  } else {
12867
12866
  logger.warn("[AvatarController] Empty animation data in playback - enabling fallback mode");
12868
- logEvent("character_manager", "info", {
12869
- avatar_id: this.avatar.id,
12870
- event: "empty_animation_data_audio_only_fallback",
12871
- conversationId: this.currentConversationId
12872
- });
12873
- this.isFallbackMode = true;
12867
+ this.enableFallbackMode("empty_animation");
12874
12868
  }
12875
12869
  if (this.pendingAudioChunks.length === 0) {
12876
12870
  throw new AvatarError("No audio chunks to play", "NO_AUDIO");
@@ -12943,14 +12937,16 @@ class AvatarController {
12943
12937
  * @param keyframesDataArray - Animation keyframes binary data array (each element is a protobuf encoded Message) or empty array to trigger audio-only mode
12944
12938
  * @param conversationId - Conversation ID (required). If conversationId doesn't match current conversationId, keyframes will be discarded.
12945
12939
  * Use getCurrentConversationId() to get the current conversationId.
12940
+ * @returns `true` if the server has sent all animation data for this conversation (end signal received), `false` otherwise.
12946
12941
  */
12947
12942
  yieldFramesData(keyframesDataArray, conversationId) {
12948
- var _a, _b;
12943
+ var _a, _b, _c;
12949
12944
  if (!keyframesDataArray || keyframesDataArray.length === 0) {
12950
12945
  this.yieldKeyframes([], conversationId);
12951
- return;
12946
+ return false;
12952
12947
  }
12953
12948
  let allKeyframes = [];
12949
+ let isEnd = false;
12954
12950
  for (let i2 = 0; i2 < keyframesDataArray.length; i2++) {
12955
12951
  const item = keyframesDataArray[i2];
12956
12952
  const binaryData = item instanceof ArrayBuffer ? new Uint8Array(item) : item;
@@ -12966,6 +12962,9 @@ class AvatarController {
12966
12962
  } else {
12967
12963
  logger.warn(`[AvatarController] Message chunk ${i2 + 1} does not contain animation keyframes`);
12968
12964
  }
12965
+ if ((_c = message.serverResponseAnimation) == null ? void 0 : _c.end) {
12966
+ isEnd = true;
12967
+ }
12969
12968
  } catch (error) {
12970
12969
  const errorMessage = error instanceof Error ? error.message : String(error);
12971
12970
  logger.error(`[AvatarController] Failed to decode animation data chunk ${i2 + 1}: ${errorMessage}`);
@@ -12983,6 +12982,7 @@ class AvatarController {
12983
12982
  logger.warn(`[AvatarController] No keyframes decoded from ${keyframesDataArray.length} message chunks`);
12984
12983
  }
12985
12984
  this.yieldKeyframes(allKeyframes, conversationId);
12985
+ return isEnd;
12986
12986
  }
12987
12987
  /**
12988
12988
  * Send animation keyframes (host mode)
@@ -12997,6 +12997,7 @@ class AvatarController {
12997
12997
  * Use getCurrentConversationId() to get the current conversationId.
12998
12998
  */
12999
12999
  yieldKeyframes(keyframes, conversationId) {
13000
+ var _a;
13000
13001
  if (!conversationId || typeof conversationId !== "string") {
13001
13002
  logger.error(`[AvatarController] yieldKeyframes requires a valid conversationId. Use getCurrentConversationId() to get the current conversationId.`);
13002
13003
  logEvent("character_manager", "error", {
@@ -13028,12 +13029,8 @@ class AvatarController {
13028
13029
  }
13029
13030
  if (!keyframes || keyframes.length === 0) {
13030
13031
  logger.warn("[AvatarController] Empty animation data received - enabling fallback mode");
13031
- logEvent("character_manager", "info", {
13032
- avatar_id: this.avatar.id,
13033
- event: "empty_animation_data_audio_only_fallback",
13034
- conversationId
13035
- });
13036
- this.enableFallbackMode();
13032
+ const reason = ((_a = this.networkLayer) == null ? void 0 : _a._fallbackReason) || "empty_animation";
13033
+ this.enableFallbackMode(reason);
13037
13034
  return;
13038
13035
  }
13039
13036
  const flameKeyframes = keyframes;
@@ -13049,10 +13046,10 @@ class AvatarController {
13049
13046
  this.emit("keyframesUpdate", this.currentKeyframes);
13050
13047
  if (!this.isPlaying && !this.isStartingPlayback && this.pendingAudioChunks.length > 0 && this.currentKeyframes.length > 0) {
13051
13048
  this.startStreamingPlayback().catch((error) => {
13052
- var _a;
13049
+ var _a2;
13053
13050
  this.isStartingPlayback = false;
13054
13051
  logger.error("[AvatarController] Failed to auto-start playback:", error);
13055
- (_a = this.onError) == null ? void 0 : _a.call(this, new AvatarError("Failed to start playback", "PLAYBACK_START_FAILED"));
13052
+ (_a2 = this.onError) == null ? void 0 : _a2.call(this, new AvatarError("Failed to start playback", "PLAYBACK_START_FAILED"));
13056
13053
  });
13057
13054
  }
13058
13055
  }
@@ -13704,12 +13701,18 @@ class AvatarController {
13704
13701
  * Once enabled, subsequent animation data in this session will be ignored
13705
13702
  * @internal
13706
13703
  */
13707
- enableFallbackMode() {
13704
+ enableFallbackMode(reason = "empty_animation") {
13708
13705
  if (this.isFallbackMode) {
13709
13706
  return;
13710
13707
  }
13711
13708
  logger.warn("[AvatarController] Enabling fallback mode");
13712
13709
  this.isFallbackMode = true;
13710
+ logEvent("fallback_mode_entered", "warning", {
13711
+ avatar_id: this.avatar.id,
13712
+ reason,
13713
+ con_id: idManager.getConnectionId() || "",
13714
+ req_id: this.currentConversationId || ""
13715
+ });
13713
13716
  if (this.isPlaying) {
13714
13717
  this.stopPlaybackLoop();
13715
13718
  this.emit("stopRendering");
@@ -14479,6 +14482,9 @@ class AvatarDownloader {
14479
14482
  return { key, success: true, size: arrayBuffer.byteLength };
14480
14483
  } catch (error) {
14481
14484
  if (error instanceof Error && (error.name === "AbortError" || error.message === "Download cancelled")) {
14485
+ logEvent("download_avatar_assets_cancelled", "info", {
14486
+ avatar_id: characterMeta.characterId ?? "unknown"
14487
+ });
14482
14488
  throw error;
14483
14489
  }
14484
14490
  if (!optional) {
@@ -14659,6 +14665,12 @@ class AvatarDownloader {
14659
14665
  });
14660
14666
  return response;
14661
14667
  } catch (error) {
14668
+ if (error instanceof Error && (error.name === "AbortError" || error.message === "Request cancelled")) {
14669
+ logEvent("fetch_avatar_metadata_cancelled", "info", {
14670
+ avatar_id: characterId ?? "unknown"
14671
+ });
14672
+ throw error;
14673
+ }
14662
14674
  logger.errorWithError("Failed to fetch character:", error);
14663
14675
  if (error instanceof AvatarError) {
14664
14676
  logEvent("fetch_avatar_metadata_failed", "error", {
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-BYr_FIpm.js";
1
+ import { b, c, m, f, d, j, g, C, i, D, E, k, h, L, R, n } from "./index-C2higMlc.js";
2
2
  export {
3
3
  b as Avatar,
4
4
  c as AvatarController,
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.91",
4
+ "version": "1.0.0-beta.92",
5
5
  "packageManager": "pnpm@10.18.2",
6
6
  "description": "AvatarKit SDK - 3D Gaussian Splatting Avatar Rendering SDK",
7
7
  "author": "AvatarKit Team",
@@ -51,6 +51,7 @@
51
51
  "dev": "vite build --mode library --watch",
52
52
  "demo": "vite --config demo/vite.config.mjs",
53
53
  "demo:benchmark": "vite --config benchmark-demo/vite.config.mjs",
54
+ "test:integration": "vite --config tests/integration-runner/vite.config.mjs",
54
55
  "clean": "rm -rf dist",
55
56
  "typecheck": "tsc --noEmit",
56
57
  "test": "cd tests && pnpm test",
@@ -1 +0,0 @@
1
- export {};