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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0-beta.78] - 2026-02-04
9
+
10
+ ### 🐛 Bugfixes
11
+ - **Telemetry Accuracy** - Fixed environment field reporting issue in analytics events
12
+
13
+ ### ✨ New Features
14
+ - **Performance Monitoring** - Added loading performance metrics for avatar resources
15
+ - `fetch_avatar_latency` - Total loading time
16
+ - `fetch_avatar_metadata_latency` - Metadata fetch time
17
+ - `download_avatar_assets_latency` - Asset download time
18
+
19
+ ### 🔧 Improvements
20
+ - **Internal Architecture** - Unified configuration constants across platforms for better maintainability
21
+
8
22
  ## [1.0.0-beta.74] - 2026-01-22
9
23
 
10
24
  ### 🔧 Improvements
@@ -1,7 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-CTh2onXJ.js";
4
+ import { A as APP_CONFIG, l as logger, e as errorToMessage, a as logEvent } from "./index-DEJMvfST.js";
5
5
  class StreamingAudioPlayer {
6
6
  // Mark if AudioContext is being resumed, avoid concurrent resume requests
7
7
  constructor(options) {
@@ -40,18 +40,6 @@ class StreamingAudioPlayer {
40
40
  // Timestamp of last getCurrentTime log (for throttling)
41
41
  // Track start time (absolute time) and duration of each scheduled chunk for accurate current playback time calculation
42
42
  __publicField(this, "scheduledChunkInfo", []);
43
- // 🚀 Chunk 合并优化:减少 AudioNode 创建数量
44
- __publicField(this, "pendingChunks", []);
45
- // 待合并的小 chunk
46
- __publicField(this, "pendingBytesTotal", 0);
47
- // 待合并的总字节数
48
- __publicField(this, "isFirstMerge", true);
49
- // 是否是首次合并(用于首包缓冲)
50
- // 合并阈值:动态计算
51
- __publicField(this, "MERGE_THRESHOLD_MS", 500);
52
- // 后续合并阈值(500ms)
53
- __publicField(this, "FIRST_BUFFER_THRESHOLD_MS", 250);
54
- // 首包缓冲阈值(250ms)- 牺牲一点延迟换取流畅播放
55
43
  // Volume control
56
44
  __publicField(this, "gainNode", null);
57
45
  __publicField(this, "volume", 1);
@@ -165,37 +153,6 @@ class StreamingAudioPlayer {
165
153
  }
166
154
  }
167
155
  }
168
- /**
169
- * 🚀 合并待处理的小 chunks 成一个大 chunk
170
- */
171
- mergePendingChunks() {
172
- if (this.pendingChunks.length === 0) {
173
- return new Uint8Array(0);
174
- }
175
- if (this.pendingChunks.length === 1) {
176
- const result = this.pendingChunks[0];
177
- this.pendingChunks = [];
178
- this.pendingBytesTotal = 0;
179
- return result;
180
- }
181
- const merged = new Uint8Array(this.pendingBytesTotal);
182
- let offset = 0;
183
- for (const chunk of this.pendingChunks) {
184
- merged.set(chunk, offset);
185
- offset += chunk.length;
186
- }
187
- this.pendingChunks = [];
188
- this.pendingBytesTotal = 0;
189
- return merged;
190
- }
191
- /**
192
- * 计算合并阈值(基于实际采样率)
193
- * @param isFirst 是否是首次合并(使用较小的首包缓冲阈值)
194
- */
195
- getMergeThresholdBytes(isFirst = false) {
196
- const thresholdMs = isFirst ? this.FIRST_BUFFER_THRESHOLD_MS : this.MERGE_THRESHOLD_MS;
197
- return this.sampleRate * this.channelCount * 2 * (thresholdMs / 1e3);
198
- }
199
156
  /**
200
157
  * Add audio chunk (16-bit PCM)
201
158
  */
@@ -209,29 +166,14 @@ class StreamingAudioPlayer {
209
166
  logger.errorWithError("[StreamingAudioPlayer] Failed to ensure AudioContext running in addChunk:", err);
210
167
  });
211
168
  }
212
- if (pcmData.length > 0) {
213
- this.pendingChunks.push(pcmData);
214
- this.pendingBytesTotal += pcmData.length;
215
- }
216
- const mergeThreshold = this.getMergeThresholdBytes(this.isFirstMerge);
217
- const shouldMerge = isLast || this.pendingBytesTotal >= mergeThreshold;
218
- if (shouldMerge && this.pendingBytesTotal > 0) {
219
- const mergedData = this.mergePendingChunks();
220
- this.audioChunks.push({ data: mergedData, isLast });
221
- this.isFirstMerge = false;
222
- this.log(`Added merged chunk ${this.audioChunks.length}`, {
223
- size: mergedData.length,
224
- totalChunks: this.audioChunks.length,
225
- isLast,
226
- isPlaying: this.isPlaying,
227
- scheduledChunks: this.scheduledChunks
228
- });
229
- } else if (isLast && this.pendingBytesTotal === 0) {
230
- this.audioChunks.push({ data: new Uint8Array(0), isLast: true });
231
- } else {
232
- this.log(`Accumulating chunk, pending: ${this.pendingBytesTotal} bytes (threshold: ${mergeThreshold}, isFirstMerge: ${this.isFirstMerge})`);
233
- return;
234
- }
169
+ this.audioChunks.push({ data: pcmData, isLast });
170
+ this.log(`Added chunk ${this.audioChunks.length}`, {
171
+ size: pcmData.length,
172
+ totalChunks: this.audioChunks.length,
173
+ isLast,
174
+ isPlaying: this.isPlaying,
175
+ scheduledChunks: this.scheduledChunks
176
+ });
235
177
  if (this.autoContinue && this.isPaused) {
236
178
  this.log("[StreamingAudioPlayer] autoContinue=true, auto-resuming playback");
237
179
  this.autoContinue = false;
@@ -259,9 +201,6 @@ class StreamingAudioPlayer {
259
201
  this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
260
202
  this.audioChunks = [];
261
203
  this.scheduledChunks = 0;
262
- this.pendingChunks = [];
263
- this.pendingBytesTotal = 0;
264
- this.isFirstMerge = true;
265
204
  this.pausedTimeOffset = 0;
266
205
  this.pausedAt = 0;
267
206
  this.pausedAudioContextTime = 0;
@@ -571,9 +510,6 @@ class StreamingAudioPlayer {
571
510
  this.activeSources.clear();
572
511
  this.audioChunks = [];
573
512
  this.scheduledChunks = 0;
574
- this.pendingChunks = [];
575
- this.pendingBytesTotal = 0;
576
- this.isFirstMerge = true;
577
513
  this.autoContinue = false;
578
514
  this.log("[StreamingAudioPlayer] Playback stopped, state reset");
579
515
  }
@@ -634,9 +570,6 @@ class StreamingAudioPlayer {
634
570
  }
635
571
  this.audioChunks = [];
636
572
  this.scheduledChunks = 0;
637
- this.pendingChunks = [];
638
- this.pendingBytesTotal = 0;
639
- this.isFirstMerge = true;
640
573
  this.sessionStartTime = 0;
641
574
  this.pausedTimeOffset = 0;
642
575
  this.pausedAt = 0;
@@ -656,9 +589,6 @@ class StreamingAudioPlayer {
656
589
  this.stop();
657
590
  this.audioChunks = [];
658
591
  this.scheduledChunks = 0;
659
- this.pendingChunks = [];
660
- this.pendingBytesTotal = 0;
661
- this.isFirstMerge = true;
662
592
  this.sessionStartTime = 0;
663
593
  this.pausedAt = 0;
664
594
  this.scheduledTime = 0;
File without changes
@@ -25,7 +25,6 @@ export declare class AvatarController {
25
25
  private renderCallback?;
26
26
  private characterHandle;
27
27
  private characterId;
28
- private useGPUPath;
29
28
  private postProcessingConfig;
30
29
  private playbackLoopId;
31
30
  private lastRenderedFrameIndex;
@@ -4,9 +4,12 @@ export declare class AvatarManager {
4
4
  private static _instance;
5
5
  private avatarDownloader;
6
6
  private avatarCache;
7
- private loadingPromises;
7
+ /** 下载队列:FIFO 顺序 */
8
8
  private downloadQueue;
9
- private isDownloading;
9
+ /** 当前正在执行的任务 */
10
+ private currentTask;
11
+ /** 任务索引:快速查找 id 对应的任务 */
12
+ private taskIndex;
10
13
  /**
11
14
  * Access via global singleton
12
15
  */
@@ -18,6 +21,12 @@ export declare class AvatarManager {
18
21
  * @returns Promise<Avatar>
19
22
  */
20
23
  load(id: string, onProgress?: (progress: LoadProgressInfo) => void): Promise<Avatar>;
24
+ /**
25
+ * Cancel a pending or running download task
26
+ * @param id Avatar ID to cancel
27
+ * @returns true if task was found and cancelled
28
+ */
29
+ cancelLoad(id: string): boolean;
21
30
  /**
22
31
  * Get cached avatar
23
32
  * @param id Avatar ID
@@ -30,7 +39,7 @@ export declare class AvatarManager {
30
39
  */
31
40
  clear(id: string): void;
32
41
  /**
33
- * Clear all avatar cache and resource loader cache
42
+ * Clear all avatar cache and cancel all tasks
34
43
  */
35
44
  clearAll(): void;
36
45
  }
@@ -30,6 +30,7 @@ export declare class AvatarView {
30
30
  private characterHandle;
31
31
  private characterId;
32
32
  private isPureRenderingMode;
33
+ private _renderingEnabled;
33
34
  private avatarActiveTimer;
34
35
  private readonly AVATAR_ACTIVE_INTERVAL;
35
36
  /**
@@ -61,6 +62,38 @@ export declare class AvatarView {
61
62
  frameCount: number;
62
63
  useLinear?: boolean;
63
64
  }): Promise<KeyframeData[]>;
65
+ /**
66
+ * Pause rendering loop
67
+ *
68
+ * When called:
69
+ * - Rendering loop stops (no GPU/canvas updates)
70
+ * - Audio playback continues normally
71
+ * - Animation state machine continues running
72
+ *
73
+ * Use `resumeRendering()` to resume rendering.
74
+ *
75
+ * @example
76
+ * // Stop rendering to save GPU resources (audio continues)
77
+ * avatarView.pauseRendering()
78
+ */
79
+ pauseRendering(): void;
80
+ /**
81
+ * Resume rendering loop
82
+ *
83
+ * When called:
84
+ * - Rendering loop resumes from current state
85
+ * - If in Idle state, immediately renders current frame to restore display
86
+ *
87
+ * @example
88
+ * // Resume rendering
89
+ * avatarView.resumeRendering()
90
+ */
91
+ resumeRendering(): void;
92
+ /**
93
+ * Check if rendering is currently enabled
94
+ * @returns true if rendering is enabled, false if paused
95
+ */
96
+ isRenderingEnabled(): boolean;
64
97
  /**
65
98
  * Get or set avatar transform in canvas
66
99
  *