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

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,18 @@ 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.94]
9
+
10
+ ### 🐛 Bugfixes
11
+
12
+ - **WebGPU Tab Switch Rendering Fix** — Fixed avatar rendering corruption (split/misaligned) when switching back to a tab. Root cause: WebGPU context was not reconfigured after canvas resize, causing surface size and screenSize mismatch in the shader.
13
+
14
+ ### ✨ Features
15
+
16
+ - **WebSocket Close Code Handling** — Server-defined custom close codes (4010 auth failure, 4001 insufficient balance, 4002 session timeout, 4003 concurrent limit) are now properly recognized and mapped to `ErrorCode`. Non-recoverable errors (4010/4001/4003) no longer trigger reconnection attempts.
17
+ - **Error Transparency** — All non-normal WebSocket close events are now propagated to `onError` callback and reported to PostHog telemetry. Previously, some close codes (e.g. 1006) were only logged internally without notifying the application.
18
+ - **New ErrorCode Values** — Added `insufficientBalance`, `sessionTimeout`, `concurrentLimitExceeded` to the `ErrorCode` enum.
19
+
8
20
  ## [1.0.0-beta.92]
9
21
 
10
22
  ### ✨ Features
package/README.md CHANGED
@@ -234,9 +234,10 @@ const configuration: Configuration = {
234
234
  // - LogLevel.error: Only error logs
235
235
  // - LogLevel.warning: Warning and error logs
236
236
  // - LogLevel.all: All logs (info, warning, error)
237
- audioFormat: { // Optional, default is { channelCount: 1, sampleRate: 16000 }
237
+ audioFormat: { // Default is { channelCount: 1, sampleRate: 16000 }
238
238
  channelCount: 1, // Fixed to 1 (mono)
239
239
  sampleRate: 16000 // Supported: 8000, 16000, 22050, 24000, 32000, 44100, 48000 Hz
240
+ // ⚠️ Must match your actual audio sample rate. Mismatched sample rate will cause playback issues.
240
241
  }
241
242
  // characterApiBaseUrl: 'https://custom-api.example.com' // Optional, internal debug config, can be ignored
242
243
  }
@@ -269,9 +270,18 @@ button.addEventListener('click', async () => {
269
270
  await avatarView.controller.initializeAudioContext()
270
271
 
271
272
  // 5. Start real-time communication (SDK mode only)
273
+ // Note: start() initiates the WebSocket connection asynchronously.
274
+ // Wait for onConnectionState === 'connected' before calling send().
272
275
  await avatarView.controller.start()
273
-
274
- // 6. Send audio data (SDK mode, must be mono PCM16 format matching configured sample rate)
276
+
277
+ // 6. Wait for connection to be ready
278
+ await new Promise<void>((resolve) => {
279
+ avatarView.controller.onConnectionState = (state) => {
280
+ if (state === ConnectionState.connected) resolve()
281
+ }
282
+ })
283
+
284
+ // 7. Send audio data (SDK mode, must be mono PCM16 format matching configured sample rate)
275
285
  // audioData: ArrayBuffer or Uint8Array containing PCM16 audio samples
276
286
  // - PCM files: Can be directly read as ArrayBuffer
277
287
  // - WAV files: Extract PCM data from WAV format (may require resampling)
@@ -592,21 +602,25 @@ avatarView.transform = { x, y, scale }
592
602
  avatarView.dispose()
593
603
  ```
594
604
 
595
- **Avatar Switching Example:**
605
+ **Switching Avatars:**
606
+
607
+ To switch avatars, dispose the old view and create a new one. Do NOT attempt to reuse or reset an existing AvatarView.
608
+ - `AvatarSDK.initialize()` and session token do not need to be called again.
609
+ - The old AvatarView's internal state is fully cleaned up by `dispose()`.
596
610
 
597
611
  ```typescript
598
- // To switch avatars, simply dispose the old view and create a new one
612
+ // 1. Dispose old avatar
599
613
  if (currentAvatarView) {
600
614
  currentAvatarView.dispose()
601
615
  }
602
616
 
603
- // Load new avatar
604
- const newAvatar = await avatarManager.load('new-character-id')
617
+ // 2. Load new avatar (SDK is already initialized, token is still valid)
618
+ const newAvatar = await AvatarManager.shared.load('new-character-id')
605
619
 
606
- // Create new AvatarView
620
+ // 3. Create new AvatarView
607
621
  currentAvatarView = new AvatarView(newAvatar, container)
608
622
 
609
- // SDK mode: start connection (will throw error if not in SDK mode)
623
+ // 4. Start connection if SDK mode
610
624
  await currentAvatarView.controller.start()
611
625
  ```
612
626
 
@@ -634,7 +648,7 @@ button.addEventListener('click', async () => {
634
648
  const conversationId = avatarView.controller.send(audioData: ArrayBuffer, end: boolean)
635
649
  // Returns: conversationId - Conversation ID for this conversation session
636
650
  // end: false (default) - Continue sending audio data for current conversation
637
- // end: true - Mark the end of current conversation round. After end=true, sending new audio data will interrupt any ongoing playback from the previous conversation round
651
+ // end: true - Mark the end of audio input for current conversation round. The avatar will continue playing remaining animation until finished, then automatically return to idle (notified via onConversationState). After end=true, sending new audio data will interrupt any ongoing playback from the previous conversation round
638
652
  })
639
653
 
640
654
  // Close service
@@ -706,7 +720,7 @@ const currentVolume = avatarView.controller.getVolume() // Get current volume (
706
720
  // Set event callbacks
707
721
  avatarView.controller.onConnectionState = (state: ConnectionState) => {} // SDK mode only
708
722
  avatarView.controller.onConversationState = (state: ConversationState) => {}
709
- avatarView.controller.onError = (error: Error) => {} // Usually AvatarError (includes code for SDK/server errors)
723
+ avatarView.controller.onError = (error: AvatarError) => {} // Includes error.code for specific error type
710
724
  ```
711
725
 
712
726
  #### Avatar Transform Methods
@@ -866,23 +880,41 @@ try {
866
880
  ```typescript
867
881
  import { AvatarError } from '@spatialwalk/avatarkit'
868
882
 
869
- avatarView.controller.onError = (error: Error) => {
870
- if (error instanceof AvatarError) {
871
- console.error('AvatarController error:', error.message, error.code)
872
- return
873
- }
874
-
875
- console.error('AvatarController unknown error:', error)
883
+ avatarView.controller.onError = (error: AvatarError) => {
884
+ console.error('Error:', error.code, error.message)
876
885
  }
877
886
  ```
878
887
 
879
- In SDK mode, server `MESSAGE_SERVER_ERROR` is forwarded to `onError` as `AvatarError`:
880
- - `error.message`: server-returned error message
881
- - `error.code` mapping:
882
- - `401` -> `sessionTokenExpired`
883
- - `400` -> `sessionTokenInvalid`
884
- - `404` -> `avatarIDUnrecognized`
885
- - other HTTP status -> original status code string (for example, `"500"`)
888
+ `error.code` values (from `ErrorCode` enum):
889
+
890
+ | Code | Description | Trigger |
891
+ |------|-------------|---------|
892
+ | **Authentication & Authorization** | | |
893
+ | `appIDUnrecognized` | App ID not recognized | Reserved |
894
+ | `sessionTokenInvalid` | Token invalid or appId mismatch | WebSocket close code 4010 |
895
+ | `sessionTokenExpired` | Token expired | WebSocket close code 4010 |
896
+ | `insufficientBalance` | Insufficient balance | WebSocket close code 4001 |
897
+ | `concurrentLimitExceeded` | Concurrent connection limit exceeded | WebSocket close code 4003 |
898
+ | **Resource Loading** | | |
899
+ | `avatarIDUnrecognized` | Avatar ID not found | Server error |
900
+ | `failedToFetchAvatarMetadata` | Metadata fetch failed | Network/server error |
901
+ | `failedToDownloadAvatarAssets` | Asset download failed | Network/server error |
902
+ | **Connection** | | |
903
+ | `websocketError` | WebSocket handshake or network error | Connection failure |
904
+ | `websocketClosedAbnormally` | Connection closed abnormally | Close code 1006 |
905
+ | `websocketClosedUnexpected` | Unexpected close code | Unknown close code |
906
+ | `sessionTimeout` | Session timeout | WebSocket close code 4002 |
907
+ | `connectionInProgress` | Connection already in progress | Duplicate `start()` call |
908
+ | **Playback** | | |
909
+ | `networkLayerNotAvailable` | Network layer not available | `send()` in host mode |
910
+ | `playbackStartFailed` | Failed to start playback | Internal error |
911
+ | `playbackInitFailed` | Playback initialization failed | Internal error |
912
+ | `audioOnlyInitFailed` | Audio-only playback init failed | Fallback mode error |
913
+ | `noAudio` | No audio data to play | Empty audio input |
914
+ | `audioContextNotInitialized` | Audio context not initialized | `send()` before `initializeAudioContext()` |
915
+ | `animationPlayerNotInitialized` | Animation player not initialized | Internal error |
916
+ | **Server** | | |
917
+ | `serverError` | Server-side error | Server MESSAGE_SERVER_ERROR |
886
918
 
887
919
  ## 🔄 Resource Management
888
920
 
@@ -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-C2higMlc.js";
4
+ import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-BDOaKeXW.js";
5
5
  class StreamingAudioPlayer {
6
6
  // Mark if AudioContext is being resumed, avoid concurrent resume requests
7
7
  constructor(options) {
@@ -1,5 +1,5 @@
1
1
  import { Avatar } from './Avatar';
2
- import { ConnectionState, DrivingServiceMode, ConversationState, PostProcessingConfig, KeyframeData } from '../types';
2
+ import { ConnectionState, AvatarError, DrivingServiceMode, ConversationState, PostProcessingConfig, KeyframeData } from '../types';
3
3
  import { FrameRateInfo } from '../performance/FrameRateMonitor';
4
4
  export declare class AvatarController {
5
5
  private networkLayer?;
@@ -9,7 +9,7 @@ export declare class AvatarController {
9
9
  private reqEnd;
10
10
  onConnectionState: ((state: ConnectionState) => void) | null;
11
11
  onConversationState: ((state: ConversationState) => void) | null;
12
- onError: ((error: Error) => void) | null;
12
+ onError: ((error: AvatarError) => void) | null;
13
13
  private eventListeners;
14
14
  private readonly frameRateMonitor;
15
15
  /** Frame rate monitoring callback. Fires with aggregated metrics from a 2-second sliding window. */
@@ -8588,11 +8588,26 @@ var AvatarState = /* @__PURE__ */ ((AvatarState2) => {
8588
8588
  })(AvatarState || {});
8589
8589
  var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
8590
8590
  ErrorCode2["appIDUnrecognized"] = "appIDUnrecognized";
8591
- ErrorCode2["avatarIDUnrecognized"] = "avatarIDUnrecognized";
8592
8591
  ErrorCode2["sessionTokenInvalid"] = "sessionTokenInvalid";
8593
8592
  ErrorCode2["sessionTokenExpired"] = "sessionTokenExpired";
8593
+ ErrorCode2["insufficientBalance"] = "insufficientBalance";
8594
+ ErrorCode2["concurrentLimitExceeded"] = "concurrentLimitExceeded";
8595
+ ErrorCode2["avatarIDUnrecognized"] = "avatarIDUnrecognized";
8594
8596
  ErrorCode2["failedToFetchAvatarMetadata"] = "failedToFetchAvatarMetadata";
8595
8597
  ErrorCode2["failedToDownloadAvatarAssets"] = "failedToDownloadAvatarAssets";
8598
+ ErrorCode2["websocketError"] = "websocketError";
8599
+ ErrorCode2["websocketClosedAbnormally"] = "websocketClosedAbnormally";
8600
+ ErrorCode2["websocketClosedUnexpected"] = "websocketClosedUnexpected";
8601
+ ErrorCode2["sessionTimeout"] = "sessionTimeout";
8602
+ ErrorCode2["connectionInProgress"] = "connectionInProgress";
8603
+ ErrorCode2["networkLayerNotAvailable"] = "networkLayerNotAvailable";
8604
+ ErrorCode2["playbackStartFailed"] = "playbackStartFailed";
8605
+ ErrorCode2["playbackInitFailed"] = "playbackInitFailed";
8606
+ ErrorCode2["audioOnlyInitFailed"] = "audioOnlyInitFailed";
8607
+ ErrorCode2["noAudio"] = "noAudio";
8608
+ ErrorCode2["audioContextNotInitialized"] = "audioContextNotInitialized";
8609
+ ErrorCode2["animationPlayerNotInitialized"] = "animationPlayerNotInitialized";
8610
+ ErrorCode2["serverError"] = "serverError";
8596
8611
  return ErrorCode2;
8597
8612
  })(ErrorCode || {});
8598
8613
  class AvatarError extends Error {
@@ -9495,7 +9510,7 @@ const _AnimationPlayer = class _AnimationPlayer {
9495
9510
  if (this.streamingPlayer) {
9496
9511
  return;
9497
9512
  }
9498
- const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-eQ2RRq5U.js");
9513
+ const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-BAjzNT1N.js");
9499
9514
  const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
9500
9515
  const audioFormat = AvatarSDK2.getAudioFormat();
9501
9516
  this.streamingPlayer = new StreamingAudioPlayer({
@@ -11473,7 +11488,7 @@ class AvatarSDK {
11473
11488
  __publicField(AvatarSDK, "_initializationState", "uninitialized");
11474
11489
  __publicField(AvatarSDK, "_initializingPromise", null);
11475
11490
  __publicField(AvatarSDK, "_configuration", null);
11476
- __publicField(AvatarSDK, "_version", "1.0.0-beta.92");
11491
+ __publicField(AvatarSDK, "_version", "1.0.0-beta.94");
11477
11492
  __publicField(AvatarSDK, "_avatarCore", null);
11478
11493
  __publicField(AvatarSDK, "_dynamicSdkConfig", null);
11479
11494
  __publicField(AvatarSDK, "_cachedDeviceScore", null);
@@ -11701,22 +11716,17 @@ class AnimationWebSocketClient extends EventEmitter {
11701
11716
  logger.warn("[AnimationWebSocketClient] Received non-binary data:", typeof event.data);
11702
11717
  }
11703
11718
  };
11704
- this.ws.onerror = (error) => {
11719
+ this.ws.onerror = () => {
11705
11720
  var _a;
11706
- const errorMessage = error instanceof Error ? error.message : String(error);
11707
11721
  const readyState = (_a = this.ws) == null ? void 0 : _a.readyState;
11708
11722
  const readyStateText = readyState === WebSocket.CONNECTING ? "CONNECTING" : readyState === WebSocket.OPEN ? "OPEN" : readyState === WebSocket.CLOSING ? "CLOSING" : readyState === WebSocket.CLOSED ? "CLOSED" : "UNKNOWN";
11709
- const errorDetails = `ReadyState: ${readyState} (${readyStateText}), URL: ${urlForLog}, Error: ${errorMessage}`;
11710
- logger.error("[AnimationWebSocketClient] WebSocket error:", errorDetails);
11711
- if (readyState === WebSocket.CLOSED) {
11712
- logger.warn("[AnimationWebSocketClient] Connection failed immediately. Possible causes:");
11713
- logger.warn(" 1. Server may not support v2 protocol yet");
11714
- logger.warn(" 2. Incorrect URL path or server configuration");
11715
- logger.warn(" 3. Authentication or authorization issue");
11716
- logger.warn(" 4. Network/firewall blocking the connection");
11717
- logger.warn(` Please check browser Network tab for detailed error information`);
11718
- }
11719
- this.emit("error", new Error(`WebSocket error (readyState: ${readyState})`));
11723
+ logger.error(`[AnimationWebSocketClient] WebSocket error: readyState=${readyStateText}, url=${urlForLog}`);
11724
+ logEvent("websocket_error", "error", {
11725
+ con_id: idManager.getConnectionId() || "",
11726
+ readyState: readyStateText,
11727
+ description: `WebSocket error (readyState: ${readyStateText})`
11728
+ });
11729
+ this.emit("error", new AvatarError(`WebSocket error (readyState: ${readyStateText})`, ErrorCode.websocketError));
11720
11730
  if (!this.isManuallyDisconnected && this.currentRetryCount < this.reconnectAttempts) {
11721
11731
  this.scheduleReconnect();
11722
11732
  }
@@ -11735,47 +11745,76 @@ class AnimationWebSocketClient extends EventEmitter {
11735
11745
  this.sessionConfigured = false;
11736
11746
  let sdkErrorCode = null;
11737
11747
  const reason = event.reason || "";
11738
- const reasonLower = reason.toLowerCase();
11739
- if (event.code === 1008 || reasonLower.includes("401") || reasonLower.includes("unauth") || reasonLower.includes("expired") || reasonLower.includes("token expired")) {
11740
- sdkErrorCode = ErrorCode.sessionTokenExpired;
11741
- logEvent("session_token_expired", "warning", {
11742
- con_id: idManager.getConnectionId() || "",
11743
- description: reason || "Session token expired",
11744
- session_duration_ms: sessionDurationMs
11745
- });
11746
- } else if (reasonLower.includes("400") || reasonLower.includes("invalid") || reasonLower.includes("bad request")) {
11747
- sdkErrorCode = ErrorCode.sessionTokenInvalid;
11748
- logEvent("session_token_invalid", "warning", {
11749
- con_id: idManager.getConnectionId() || "",
11750
- description: reason || "Session token invalid",
11751
- session_duration_ms: sessionDurationMs
11752
- });
11753
- } else if (reasonLower.includes("404") || reasonLower.includes("notfound") || reasonLower.includes("not found")) {
11754
- sdkErrorCode = ErrorCode.avatarIDUnrecognized;
11755
- logEvent("avatar_id_unrecognized", "error", {
11756
- avatar_id: this.currentCharacterId || "",
11757
- description: reason || "Avatar ID unrecognized",
11758
- session_duration_ms: sessionDurationMs
11759
- });
11748
+ switch (event.code) {
11749
+ case 4010:
11750
+ sdkErrorCode = ErrorCode.sessionTokenInvalid;
11751
+ logEvent("session_token_invalid", "warning", {
11752
+ sessionToken: this.jwtToken || "",
11753
+ userId: "",
11754
+ con_id: idManager.getConnectionId() || "",
11755
+ description: reason || "Authentication failed",
11756
+ session_duration_ms: sessionDurationMs
11757
+ });
11758
+ break;
11759
+ case 4001:
11760
+ sdkErrorCode = ErrorCode.insufficientBalance;
11761
+ logEvent("insufficient_balance", "warning", {
11762
+ con_id: idManager.getConnectionId() || "",
11763
+ description: reason || "Insufficient balance",
11764
+ session_duration_ms: sessionDurationMs
11765
+ });
11766
+ break;
11767
+ case 4002:
11768
+ sdkErrorCode = ErrorCode.sessionTimeout;
11769
+ logEvent("session_timeout", "warning", {
11770
+ con_id: idManager.getConnectionId() || "",
11771
+ description: reason || "Session timeout",
11772
+ session_duration_ms: sessionDurationMs
11773
+ });
11774
+ break;
11775
+ case 4003:
11776
+ sdkErrorCode = ErrorCode.concurrentLimitExceeded;
11777
+ logEvent("concurrent_limit_exceeded", "warning", {
11778
+ con_id: idManager.getConnectionId() || "",
11779
+ description: reason || "Concurrent connection limit exceeded",
11780
+ session_duration_ms: sessionDurationMs
11781
+ });
11782
+ break;
11783
+ case 1006:
11784
+ logEvent("websocket_closed_abnormally", "error", {
11785
+ con_id: idManager.getConnectionId() || "",
11786
+ code: event.code,
11787
+ description: reason || "Connection closed abnormally",
11788
+ session_duration_ms: sessionDurationMs
11789
+ });
11790
+ break;
11791
+ case 1012:
11792
+ logEvent("service_restarted", "warning", {
11793
+ con_id: idManager.getConnectionId() || "",
11794
+ description: reason || "Service restart",
11795
+ session_duration_ms: sessionDurationMs
11796
+ });
11797
+ break;
11760
11798
  }
11761
11799
  if (sdkErrorCode) {
11762
- const error = new AvatarError(
11800
+ this.emit("error", new AvatarError(
11763
11801
  reason || `WebSocket connection failed with code ${event.code}`,
11764
11802
  sdkErrorCode
11765
- );
11766
- this.emit("error", error);
11767
- }
11768
- if (event.code === 1006) {
11769
- logger.warn("[AnimationWebSocketClient] Connection closed abnormally (1006) - possible causes: network issue, server rejection, or protocol mismatch");
11770
- }
11771
- if (event.code === 1012) {
11772
- logEvent("service_restarted", "warning", {
11803
+ ));
11804
+ } else if (event.code !== 1e3) {
11805
+ logEvent("websocket_closed_unexpected", "error", {
11773
11806
  con_id: idManager.getConnectionId() || "",
11774
- description: event.reason || "Service restart",
11807
+ code: event.code,
11808
+ description: reason || `Unexpected close (code: ${event.code})`,
11775
11809
  session_duration_ms: sessionDurationMs
11776
11810
  });
11811
+ this.emit("error", new AvatarError(
11812
+ reason || `WebSocket connection closed (code: ${event.code})`,
11813
+ ErrorCode.websocketClosedUnexpected
11814
+ ));
11777
11815
  }
11778
- if (!this.isManuallyDisconnected && this.currentRetryCount < this.reconnectAttempts) {
11816
+ const isNonRecoverable = sdkErrorCode === ErrorCode.sessionTokenInvalid || sdkErrorCode === ErrorCode.insufficientBalance || sdkErrorCode === ErrorCode.concurrentLimitExceeded;
11817
+ if (!isNonRecoverable && !this.isManuallyDisconnected && this.currentRetryCount < this.reconnectAttempts) {
11779
11818
  this.scheduleReconnect();
11780
11819
  }
11781
11820
  };
@@ -11941,7 +11980,7 @@ class NetworkLayer {
11941
11980
  var _a, _b, _c, _d;
11942
11981
  if (this.isConnecting) {
11943
11982
  logger.warn("[NetworkLayer] Connection already in progress, waiting for current connection to complete");
11944
- throw new AvatarError("Connection already in progress", "CONNECTION_IN_PROGRESS");
11983
+ throw new AvatarError("Connection already in progress", ErrorCode.connectionInProgress);
11945
11984
  }
11946
11985
  (_b = (_a = this.dataController).onConnectionState) == null ? void 0 : _b.call(_a, ConnectionState.connecting);
11947
11986
  this.isFallbackMode = false;
@@ -12091,10 +12130,12 @@ class NetworkLayer {
12091
12130
  this.wsClient.on("reconnecting", () => {
12092
12131
  });
12093
12132
  this.wsClient.on("error", (error) => {
12094
- var _a, _b;
12133
+ var _a, _b, _c, _d;
12095
12134
  const message = error instanceof Error ? error.message : String(error);
12096
12135
  logger.error("[NetworkLayer] WebSocket error:", message);
12097
12136
  (_b = (_a = this.dataController).onConnectionState) == null ? void 0 : _b.call(_a, ConnectionState.failed);
12137
+ const avatarError = error instanceof AvatarError ? error : new AvatarError(message, ErrorCode.websocketError);
12138
+ (_d = (_c = this.dataController).onError) == null ? void 0 : _d.call(_c, avatarError);
12098
12139
  });
12099
12140
  this.wsClient.on("message", (message) => {
12100
12141
  this.handleMessage(message);
@@ -12187,7 +12228,6 @@ class NetworkLayer {
12187
12228
  description: message.serverError.message || `Server error: code=${message.serverError.code}`
12188
12229
  });
12189
12230
  const httpStatusCode = message.serverError.code;
12190
- const errorCodeStr = (httpStatusCode == null ? void 0 : httpStatusCode.toString()) ?? "";
12191
12231
  let sdkErrorCode;
12192
12232
  let errorMessage = message.serverError.message || "Server error occurred";
12193
12233
  if (httpStatusCode === 401) {
@@ -12206,7 +12246,7 @@ class NetworkLayer {
12206
12246
  sdkErrorCode = ErrorCode.avatarIDUnrecognized;
12207
12247
  errorMessage = errorMessage || "Avatar ID not recognized";
12208
12248
  } else {
12209
- sdkErrorCode = errorCodeStr || "SERVER_ERROR";
12249
+ sdkErrorCode = ErrorCode.serverError;
12210
12250
  }
12211
12251
  (_b = (_a = this.dataController).onError) == null ? void 0 : _b.call(_a, new AvatarError(
12212
12252
  errorMessage,
@@ -12745,7 +12785,7 @@ class AvatarController {
12745
12785
  logger.error("[AvatarController] Failed to initialize audio context:", message);
12746
12786
  throw new AvatarError(
12747
12787
  `Failed to initialize audio context: ${message}`,
12748
- "AUDIO_CONTEXT_INIT_FAILED"
12788
+ ErrorCode.audioContextNotInitialized
12749
12789
  );
12750
12790
  }
12751
12791
  }
@@ -12771,7 +12811,7 @@ class AvatarController {
12771
12811
  if (!((_a = this.animationPlayer) == null ? void 0 : _a.isStreamingReady())) {
12772
12812
  throw new AvatarError(
12773
12813
  "Audio context not initialized. Call initializeAudioContext() in a user gesture context first.",
12774
- "AUDIO_CONTEXT_NOT_INITIALIZED"
12814
+ ErrorCode.audioContextNotInitialized
12775
12815
  );
12776
12816
  }
12777
12817
  }
@@ -12783,7 +12823,7 @@ class AvatarController {
12783
12823
  if (!this.networkLayer) {
12784
12824
  throw new AvatarError(
12785
12825
  "Network layer not available. Use SDK mode.",
12786
- "NETWORK_LAYER_NOT_AVAILABLE"
12826
+ ErrorCode.networkLayerNotAvailable
12787
12827
  );
12788
12828
  }
12789
12829
  this.checkAudioContextInitialized();
@@ -12803,7 +12843,7 @@ class AvatarController {
12803
12843
  return null;
12804
12844
  }
12805
12845
  if (!this.networkLayer) {
12806
- (_b = this.onError) == null ? void 0 : _b.call(this, new AvatarError("Network layer not available", "NETWORK_LAYER_NOT_AVAILABLE"));
12846
+ (_b = this.onError) == null ? void 0 : _b.call(this, new AvatarError("Network layer not available", ErrorCode.networkLayerNotAvailable));
12807
12847
  return null;
12808
12848
  }
12809
12849
  const networkConversationId = this.networkLayer.getCurrentConversationId();
@@ -12867,7 +12907,7 @@ class AvatarController {
12867
12907
  this.enableFallbackMode("empty_animation");
12868
12908
  }
12869
12909
  if (this.pendingAudioChunks.length === 0) {
12870
- throw new AvatarError("No audio chunks to play", "NO_AUDIO");
12910
+ throw new AvatarError("No audio chunks to play", ErrorCode.noAudio);
12871
12911
  }
12872
12912
  if (this.isFallbackMode) {
12873
12913
  await this.startAudioOnlyPlayback();
@@ -13049,7 +13089,7 @@ class AvatarController {
13049
13089
  var _a2;
13050
13090
  this.isStartingPlayback = false;
13051
13091
  logger.error("[AvatarController] Failed to auto-start playback:", error);
13052
- (_a2 = this.onError) == null ? void 0 : _a2.call(this, new AvatarError("Failed to start playback", "PLAYBACK_START_FAILED"));
13092
+ (_a2 = this.onError) == null ? void 0 : _a2.call(this, new AvatarError("Failed to start playback", ErrorCode.playbackStartFailed));
13053
13093
  });
13054
13094
  }
13055
13095
  }
@@ -13409,7 +13449,7 @@ class AvatarController {
13409
13449
  }
13410
13450
  try {
13411
13451
  if (!this.animationPlayer) {
13412
- throw new AvatarError("Animation player not initialized", "ANIMATION_PLAYER_NOT_INITIALIZED");
13452
+ throw new AvatarError("Animation player not initialized", ErrorCode.animationPlayerNotInitialized);
13413
13453
  }
13414
13454
  await this.animationPlayer.prepareStreamingPlayer(() => {
13415
13455
  var _a2;
@@ -13453,7 +13493,7 @@ class AvatarController {
13453
13493
  } catch (error) {
13454
13494
  const message = error instanceof Error ? error.message : String(error);
13455
13495
  logger.error("[AvatarController] Failed to start streaming playback:", message);
13456
- (_b = this.onError) == null ? void 0 : _b.call(this, new AvatarError("Failed to start streaming playback", "INIT_FAILED"));
13496
+ (_b = this.onError) == null ? void 0 : _b.call(this, new AvatarError("Failed to start streaming playback", ErrorCode.playbackInitFailed));
13457
13497
  this.isPlaying = false;
13458
13498
  this.isStartingPlayback = false;
13459
13499
  }
@@ -13731,7 +13771,7 @@ class AvatarController {
13731
13771
  async startAudioOnlyPlayback() {
13732
13772
  var _a;
13733
13773
  if (!this.animationPlayer) {
13734
- throw new AvatarError("Animation player not initialized", "ANIMATION_PLAYER_NOT_INITIALIZED");
13774
+ throw new AvatarError("Animation player not initialized", ErrorCode.animationPlayerNotInitialized);
13735
13775
  }
13736
13776
  try {
13737
13777
  await this.animationPlayer.prepareStreamingPlayer(() => {
@@ -13767,7 +13807,7 @@ class AvatarController {
13767
13807
  } catch (error) {
13768
13808
  const message = error instanceof Error ? error.message : String(error);
13769
13809
  logger.error("[AvatarController] Failed to start audio-only playback:", message);
13770
- (_a = this.onError) == null ? void 0 : _a.call(this, new AvatarError("Failed to start audio-only playback", "AUDIO_ONLY_INIT_FAILED"));
13810
+ (_a = this.onError) == null ? void 0 : _a.call(this, new AvatarError("Failed to start audio-only playback", ErrorCode.audioOnlyInitFailed));
13771
13811
  this.isPlaying = false;
13772
13812
  this.isFallbackMode = false;
13773
13813
  throw error;
@@ -15678,6 +15718,9 @@ class WebGPURenderer {
15678
15718
  __publicField(this, "blitUniformBuffer", null);
15679
15719
  __publicField(this, "blitQuadBuffer", null);
15680
15720
  __publicField(this, "blitSampler", null);
15721
+ // 记录上次 configure 时的 canvas 尺寸,用于检测 resize
15722
+ __publicField(this, "configuredWidth", 0);
15723
+ __publicField(this, "configuredHeight", 0);
15681
15724
  this.canvas = canvas;
15682
15725
  this.backgroundColor = backgroundColor || [0, 0, 0, 0];
15683
15726
  this.alpha = alpha;
@@ -15698,15 +15741,34 @@ class WebGPURenderer {
15698
15741
  throw new Error("WebGPU: Failed to get canvas context");
15699
15742
  }
15700
15743
  this.presentationFormat = navigator.gpu.getPreferredCanvasFormat();
15744
+ this.configureContext();
15745
+ this.createUniformBuffer();
15746
+ this.createQuadVertexBuffer();
15747
+ await this.createRenderPipeline();
15748
+ await this.createBlitPipeline();
15749
+ }
15750
+ /**
15751
+ * 配置 WebGPU context 并记录尺寸
15752
+ */
15753
+ configureContext() {
15754
+ if (!this.context || !this.device)
15755
+ return;
15701
15756
  this.context.configure({
15702
15757
  device: this.device,
15703
15758
  format: this.presentationFormat,
15704
15759
  alphaMode: this.alpha ? "premultiplied" : "opaque"
15705
15760
  });
15706
- this.createUniformBuffer();
15707
- this.createQuadVertexBuffer();
15708
- await this.createRenderPipeline();
15709
- await this.createBlitPipeline();
15761
+ this.configuredWidth = this.canvas.width;
15762
+ this.configuredHeight = this.canvas.height;
15763
+ }
15764
+ /**
15765
+ * 检测 canvas 尺寸变化,必要时重新 configure context
15766
+ * 防止 tab 切换等场景下 surface 尺寸与 canvas 尺寸不一致导致渲染错位
15767
+ */
15768
+ ensureContextSize() {
15769
+ if (this.canvas.width !== this.configuredWidth || this.canvas.height !== this.configuredHeight) {
15770
+ this.configureContext();
15771
+ }
15710
15772
  }
15711
15773
  /**
15712
15774
  * 创建 Uniform Buffer
@@ -16126,6 +16188,7 @@ class WebGPURenderer {
16126
16188
  return;
16127
16189
  if (this.splatCount === 0 || !this.storageBindGroup)
16128
16190
  return;
16191
+ this.ensureContextSize();
16129
16192
  const [width, height] = screenSize;
16130
16193
  const needsTransform = transform && (transform.x !== 0 || transform.y !== 0 || transform.scale !== 1);
16131
16194
  this.updateUniforms(viewMatrix, projectionMatrix, screenSize);
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-C2higMlc.js";
1
+ import { b, c, m, f, d, j, g, C, i, D, E, k, h, L, R, n } from "./index-BDOaKeXW.js";
2
2
  export {
3
3
  b as Avatar,
4
4
  c as AvatarController,
@@ -65,20 +65,50 @@ export declare enum ConversationState {
65
65
  export declare enum ErrorCode {
66
66
  /** AppID not recognized (reserved, future appID validation logic) */
67
67
  appIDUnrecognized = "appIDUnrecognized",
68
- /** AvatarID not recognized */
69
- avatarIDUnrecognized = "avatarIDUnrecognized",
70
- /** Session Token invalid */
68
+ /** Session Token invalid (WebSocket close code 4010) */
71
69
  sessionTokenInvalid = "sessionTokenInvalid",
72
- /** Session Token expired */
70
+ /** Session Token expired (WebSocket close code 4010) */
73
71
  sessionTokenExpired = "sessionTokenExpired",
72
+ /** Insufficient balance (WebSocket close code 4001) */
73
+ insufficientBalance = "insufficientBalance",
74
+ /** Concurrent connection limit exceeded (WebSocket close code 4003) */
75
+ concurrentLimitExceeded = "concurrentLimitExceeded",
76
+ /** AvatarID not recognized */
77
+ avatarIDUnrecognized = "avatarIDUnrecognized",
74
78
  /** Failed to fetch avatar metadata */
75
79
  failedToFetchAvatarMetadata = "failedToFetchAvatarMetadata",
76
80
  /** Failed to download avatar assets */
77
- failedToDownloadAvatarAssets = "failedToDownloadAvatarAssets"
81
+ failedToDownloadAvatarAssets = "failedToDownloadAvatarAssets",
82
+ /** WebSocket connection error (handshake failure, network error) */
83
+ websocketError = "websocketError",
84
+ /** WebSocket connection closed abnormally (close code 1006) */
85
+ websocketClosedAbnormally = "websocketClosedAbnormally",
86
+ /** WebSocket closed with unexpected close code */
87
+ websocketClosedUnexpected = "websocketClosedUnexpected",
88
+ /** Session timeout (WebSocket close code 4002) */
89
+ sessionTimeout = "sessionTimeout",
90
+ /** Connection already in progress */
91
+ connectionInProgress = "connectionInProgress",
92
+ /** Network layer not available (SDK mode required) */
93
+ networkLayerNotAvailable = "networkLayerNotAvailable",
94
+ /** Failed to start playback */
95
+ playbackStartFailed = "playbackStartFailed",
96
+ /** Playback initialization failed */
97
+ playbackInitFailed = "playbackInitFailed",
98
+ /** Audio-only playback initialization failed */
99
+ audioOnlyInitFailed = "audioOnlyInitFailed",
100
+ /** No audio data to play */
101
+ noAudio = "noAudio",
102
+ /** Audio context not initialized */
103
+ audioContextNotInitialized = "audioContextNotInitialized",
104
+ /** Animation player not initialized */
105
+ animationPlayerNotInitialized = "animationPlayerNotInitialized",
106
+ /** Server-side error */
107
+ serverError = "serverError"
78
108
  }
79
109
  export declare class AvatarError extends Error {
80
- code?: (string | ErrorCode) | undefined;
81
- constructor(message: string, code?: (string | ErrorCode) | undefined);
110
+ code: ErrorCode;
111
+ constructor(message: string, code: ErrorCode);
82
112
  }
83
113
  export interface CameraConfig {
84
114
  position: [number, number, number];
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.92",
4
+ "version": "1.0.0-beta.94",
5
5
  "packageManager": "pnpm@10.18.2",
6
6
  "description": "AvatarKit SDK - 3D Gaussian Splatting Avatar Rendering SDK",
7
7
  "author": "AvatarKit Team",