@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 +12 -0
- package/README.md +57 -25
- package/dist/{StreamingAudioPlayer-eQ2RRq5U.js → StreamingAudioPlayer-BAjzNT1N.js} +1 -1
- package/dist/core/AvatarController.d.ts +2 -2
- package/dist/{index-C2higMlc.js → index-BDOaKeXW.js} +130 -67
- package/dist/index.js +1 -1
- package/dist/types/index.d.ts +37 -7
- package/package.json +1 -1
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: { //
|
|
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.
|
|
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
|
-
**
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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:
|
|
870
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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-
|
|
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:
|
|
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-
|
|
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.
|
|
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 = (
|
|
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
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
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
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
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
|
-
|
|
11800
|
+
this.emit("error", new AvatarError(
|
|
11763
11801
|
reason || `WebSocket connection failed with code ${event.code}`,
|
|
11764
11802
|
sdkErrorCode
|
|
11765
|
-
);
|
|
11766
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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.
|
|
15707
|
-
this.
|
|
15708
|
-
|
|
15709
|
-
|
|
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
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
81
|
-
constructor(message: string, code
|
|
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