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

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.
@@ -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-DWVeTI7D.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", {
@@ -15666,6 +15678,9 @@ class WebGPURenderer {
15666
15678
  __publicField(this, "blitUniformBuffer", null);
15667
15679
  __publicField(this, "blitQuadBuffer", null);
15668
15680
  __publicField(this, "blitSampler", null);
15681
+ // 记录上次 configure 时的 canvas 尺寸,用于检测 resize
15682
+ __publicField(this, "configuredWidth", 0);
15683
+ __publicField(this, "configuredHeight", 0);
15669
15684
  this.canvas = canvas;
15670
15685
  this.backgroundColor = backgroundColor || [0, 0, 0, 0];
15671
15686
  this.alpha = alpha;
@@ -15686,15 +15701,34 @@ class WebGPURenderer {
15686
15701
  throw new Error("WebGPU: Failed to get canvas context");
15687
15702
  }
15688
15703
  this.presentationFormat = navigator.gpu.getPreferredCanvasFormat();
15704
+ this.configureContext();
15705
+ this.createUniformBuffer();
15706
+ this.createQuadVertexBuffer();
15707
+ await this.createRenderPipeline();
15708
+ await this.createBlitPipeline();
15709
+ }
15710
+ /**
15711
+ * 配置 WebGPU context 并记录尺寸
15712
+ */
15713
+ configureContext() {
15714
+ if (!this.context || !this.device)
15715
+ return;
15689
15716
  this.context.configure({
15690
15717
  device: this.device,
15691
15718
  format: this.presentationFormat,
15692
15719
  alphaMode: this.alpha ? "premultiplied" : "opaque"
15693
15720
  });
15694
- this.createUniformBuffer();
15695
- this.createQuadVertexBuffer();
15696
- await this.createRenderPipeline();
15697
- await this.createBlitPipeline();
15721
+ this.configuredWidth = this.canvas.width;
15722
+ this.configuredHeight = this.canvas.height;
15723
+ }
15724
+ /**
15725
+ * 检测 canvas 尺寸变化,必要时重新 configure context
15726
+ * 防止 tab 切换等场景下 surface 尺寸与 canvas 尺寸不一致导致渲染错位
15727
+ */
15728
+ ensureContextSize() {
15729
+ if (this.canvas.width !== this.configuredWidth || this.canvas.height !== this.configuredHeight) {
15730
+ this.configureContext();
15731
+ }
15698
15732
  }
15699
15733
  /**
15700
15734
  * 创建 Uniform Buffer
@@ -16114,6 +16148,7 @@ class WebGPURenderer {
16114
16148
  return;
16115
16149
  if (this.splatCount === 0 || !this.storageBindGroup)
16116
16150
  return;
16151
+ this.ensureContextSize();
16117
16152
  const [width, height] = screenSize;
16118
16153
  const needsTransform = transform && (transform.x !== 0 || transform.y !== 0 || transform.scale !== 1);
16119
16154
  this.updateUniforms(viewMatrix, projectionMatrix, screenSize);
@@ -16346,6 +16381,14 @@ class RenderSystem {
16346
16381
  __publicField(this, "offsetX", 0);
16347
16382
  __publicField(this, "offsetY", 0);
16348
16383
  __publicField(this, "scale", 1);
16384
+ /**
16385
+ * Load packed Splat data (zero-copy, GPU format)
16386
+ * Directly receives WASM packed data
16387
+ *
16388
+ * @param packedData Float32Array [pos3, color4, cov6] x N points
16389
+ */
16390
+ // @debug: dump first frame flag
16391
+ __publicField(this, "_dumpedFirstFrame", false);
16349
16392
  this.options = options;
16350
16393
  this.canvas = options.canvas;
16351
16394
  this.backgroundColor = options.backgroundColor || [0, 0, 0, 0];
@@ -16389,15 +16432,28 @@ class RenderSystem {
16389
16432
  logger.log("✅ Using WebGL renderer");
16390
16433
  this.updateCameraAspect();
16391
16434
  }
16392
- /**
16393
- * Load packed Splat data (zero-copy, GPU format)
16394
- * Directly receives WASM packed data
16395
- *
16396
- * @param packedData Float32Array [pos3, color4, cov6] x N points
16397
- */
16398
16435
  loadSplatsFromPackedData(packedData) {
16399
16436
  if (!this.renderer)
16400
16437
  throw new Error("Renderer not initialized");
16438
+ if (!this._dumpedFirstFrame) {
16439
+ this._dumpedFirstFrame = true;
16440
+ const pointCount = Math.floor(packedData.length / 13);
16441
+ console.log(`[DEBUG] First frame packed data: ${pointCount} points, ${packedData.length} floats`);
16442
+ for (let i2 = 0; i2 < Math.min(5, pointCount); i2++) {
16443
+ const o2 = i2 * 13;
16444
+ console.log(`[DEBUG] Point ${i2}: pos(${packedData[o2]}, ${packedData[o2 + 1]}, ${packedData[o2 + 2]}) color(${packedData[o2 + 3]}, ${packedData[o2 + 4]}, ${packedData[o2 + 5]}, ${packedData[o2 + 6]}) covA(${packedData[o2 + 7]}, ${packedData[o2 + 8]}, ${packedData[o2 + 9]}) covB(${packedData[o2 + 10]}, ${packedData[o2 + 11]}, ${packedData[o2 + 12]})`);
16445
+ }
16446
+ const copy = new ArrayBuffer(packedData.byteLength);
16447
+ new Uint8Array(copy).set(new Uint8Array(packedData.buffer, packedData.byteOffset, packedData.byteLength));
16448
+ const blob = new Blob([copy], { type: "application/octet-stream" });
16449
+ const url = URL.createObjectURL(blob);
16450
+ const a2 = document.createElement("a");
16451
+ a2.href = url;
16452
+ a2.download = "core_packed_first_frame.bin";
16453
+ a2.click();
16454
+ URL.revokeObjectURL(url);
16455
+ console.log("[DEBUG] Packed data downloaded as core_packed_first_frame.bin");
16456
+ }
16401
16457
  this.originalPackedData = packedData;
16402
16458
  }
16403
16459
  /**
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-CDywZ8iv.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.93",
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",
Binary file
@@ -1 +0,0 @@
1
- export {};