@spatius/avatarkit 1.3.0 → 1.3.1-beta.2
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,11 @@ 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.3.1-beta.1] - 2026-07-16
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- The `region` configuration now defaults to automatic selection: when left unset, the SDK picks the closest serving region at initialization. Passing an explicit `region` continues to force that region, unchanged. If automatic selection can't be reached, the SDK falls back to a default region and continues initializing.
|
|
12
|
+
|
|
8
13
|
## [1.3.0] - 2026-07-04
|
|
9
14
|
|
|
10
15
|
### Added
|
|
@@ -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-CPolDcOo.js";
|
|
5
5
|
class StreamingAudioPlayer {
|
|
6
6
|
// Mark if AudioContext is being resumed, avoid concurrent resume requests
|
|
7
7
|
constructor(options) {
|
|
@@ -46,6 +46,8 @@ class StreamingAudioPlayer {
|
|
|
46
46
|
// Default volume 1.0 (0.0 - 1.0)
|
|
47
47
|
// Event callbacks
|
|
48
48
|
__publicField(this, "onEndedCallback");
|
|
49
|
+
// 音频缓冲卡顿(缓冲耗尽自动暂停 / 有数据自动恢复)通知:true=进入卡顿,false=恢复。
|
|
50
|
+
__publicField(this, "onAudioStallCallback");
|
|
49
51
|
// AudioContext state management
|
|
50
52
|
__publicField(this, "stateChangeHandler");
|
|
51
53
|
__publicField(this, "isResuming", false);
|
|
@@ -156,6 +158,7 @@ class StreamingAudioPlayer {
|
|
|
156
158
|
* Add audio chunk (16-bit PCM)
|
|
157
159
|
*/
|
|
158
160
|
addChunk(pcmData, isLast = false) {
|
|
161
|
+
var _a;
|
|
159
162
|
if (!this.audioContext) {
|
|
160
163
|
logger.error("AudioContext not initialized");
|
|
161
164
|
return;
|
|
@@ -176,6 +179,7 @@ class StreamingAudioPlayer {
|
|
|
176
179
|
if (this.autoContinue && this.isPaused) {
|
|
177
180
|
this.log("[StreamingAudioPlayer] autoContinue=true, auto-resuming playback");
|
|
178
181
|
this.autoContinue = false;
|
|
182
|
+
(_a = this.onAudioStallCallback) == null ? void 0 : _a.call(this, false);
|
|
179
183
|
this.resume().catch((err) => {
|
|
180
184
|
logger.error("Failed to auto-resume playback:", err);
|
|
181
185
|
});
|
|
@@ -229,13 +233,13 @@ class StreamingAudioPlayer {
|
|
|
229
233
|
this.log("[StreamingAudioPlayer] Cannot start playback: Already playing");
|
|
230
234
|
return;
|
|
231
235
|
}
|
|
232
|
-
await this.ensureAudioContextRunning();
|
|
233
236
|
this.isPlaying = true;
|
|
234
237
|
this.sessionStartTime = this.audioContext.currentTime;
|
|
235
238
|
this.scheduledTime = this.sessionStartTime;
|
|
236
239
|
this.lastScheduledChunkEndTime = 0;
|
|
237
240
|
this.scheduledChunkInfo = [];
|
|
238
241
|
this.autoContinue = false;
|
|
242
|
+
await this.ensureAudioContextRunning();
|
|
239
243
|
this.log("[StreamingAudioPlayer] Starting playback", {
|
|
240
244
|
sessionStartTime: this.sessionStartTime,
|
|
241
245
|
bufferedChunks: this.audioChunks.length,
|
|
@@ -304,18 +308,17 @@ class StreamingAudioPlayer {
|
|
|
304
308
|
});
|
|
305
309
|
this.activeSources.add(source);
|
|
306
310
|
source.onended = () => {
|
|
311
|
+
var _a;
|
|
307
312
|
this.activeSources.delete(source);
|
|
308
313
|
if (this.activeSources.size === 0) {
|
|
309
314
|
const lastChunk = this.audioChunks[this.scheduledChunks - 1];
|
|
310
|
-
|
|
315
|
+
const sessionEnded = !!(lastChunk == null ? void 0 : lastChunk.isLast);
|
|
316
|
+
if (!sessionEnded) {
|
|
311
317
|
this.log("All audio chunks ended but end=false, pausing and setting autoContinue");
|
|
312
|
-
|
|
313
|
-
scheduled_chunks: this.scheduledChunks,
|
|
314
|
-
audio_time: this.getCurrentTime()
|
|
315
|
-
});
|
|
318
|
+
(_a = this.onAudioStallCallback) == null ? void 0 : _a.call(this, true);
|
|
316
319
|
this.autoContinue = true;
|
|
317
320
|
this.pause();
|
|
318
|
-
} else
|
|
321
|
+
} else {
|
|
319
322
|
this.log("Last audio chunk ended, marking playback as ended");
|
|
320
323
|
this.markEnded();
|
|
321
324
|
}
|
|
@@ -557,6 +560,12 @@ class StreamingAudioPlayer {
|
|
|
557
560
|
onEnded(callback) {
|
|
558
561
|
this.onEndedCallback = callback;
|
|
559
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* 设置音频缓冲卡顿回调:缓冲耗尽自动暂停时 stalled=true,有数据自动恢复时 stalled=false。
|
|
565
|
+
*/
|
|
566
|
+
onAudioStall(callback) {
|
|
567
|
+
this.onAudioStallCallback = callback;
|
|
568
|
+
}
|
|
560
569
|
/**
|
|
561
570
|
* Check if playing
|
|
562
571
|
*/
|
|
@@ -46,6 +46,11 @@ export declare class AvatarController {
|
|
|
46
46
|
private isFallbackMode;
|
|
47
47
|
private frameStarvationEvents;
|
|
48
48
|
private isFrameStarved;
|
|
49
|
+
private audioStallEvents;
|
|
50
|
+
private isAudioBufferStalled;
|
|
51
|
+
private playbackEndReason;
|
|
52
|
+
private receivedAudioBytes;
|
|
53
|
+
private receivedAnimationFrames;
|
|
49
54
|
/**
|
|
50
55
|
* Whether this round's final animation batch (ServerResponseAnimation.end) has arrived.
|
|
51
56
|
* Frame starvation is only possible BEFORE this — once all frames are in, any remaining
|
|
@@ -4189,6 +4189,7 @@ const RENDER_QUALITY_PARAMS = {
|
|
|
4189
4189
|
]: { renderScale: 1, splatRadius: 3 }
|
|
4190
4190
|
};
|
|
4191
4191
|
const DEFAULT_REGION = "us-west";
|
|
4192
|
+
const DEFAULT_REGION_REQUEST = "auto";
|
|
4192
4193
|
var LoadProgress = /* @__PURE__ */ ((LoadProgress2) => {
|
|
4193
4194
|
LoadProgress2["downloading"] = "downloading";
|
|
4194
4195
|
LoadProgress2["completed"] = "completed";
|
|
@@ -11586,11 +11587,287 @@ class EventStore {
|
|
|
11586
11587
|
}
|
|
11587
11588
|
}
|
|
11588
11589
|
const eventStore = new EventStore();
|
|
11590
|
+
const BOOTSTRAP_URL = "https://global.spatialwalk.top/bootstrap";
|
|
11591
|
+
async function fetchBootstrap(opts, signal, onResponse) {
|
|
11592
|
+
const res = await fetch(BOOTSTRAP_URL, {
|
|
11593
|
+
method: "POST",
|
|
11594
|
+
body: JSON.stringify({
|
|
11595
|
+
app_id: opts.appId,
|
|
11596
|
+
sdk_version: opts.sdkVersion,
|
|
11597
|
+
region: opts.region ?? "auto",
|
|
11598
|
+
platform: opts.platform ?? "web"
|
|
11599
|
+
}),
|
|
11600
|
+
cache: "no-store",
|
|
11601
|
+
signal
|
|
11602
|
+
});
|
|
11603
|
+
onResponse == null ? void 0 : onResponse();
|
|
11604
|
+
if (!res.ok) {
|
|
11605
|
+
throw new Error(`bootstrap HTTP ${res.status}`);
|
|
11606
|
+
}
|
|
11607
|
+
return await res.json();
|
|
11608
|
+
}
|
|
11609
|
+
const PROBE_TIMEOUT_MS = 5e3;
|
|
11610
|
+
const SAMPLE_COUNT = 3;
|
|
11611
|
+
const RECALIBRATE_INTERVAL_MS = 20 * 60 * 1e3;
|
|
11612
|
+
class ClockSync {
|
|
11613
|
+
constructor() {
|
|
11614
|
+
__publicField(this, "localBase", Date.now());
|
|
11615
|
+
__publicField(this, "serverBase", null);
|
|
11616
|
+
__publicField(this, "monoAtCalibrate", performance.now());
|
|
11617
|
+
/** 首次校准是否已落定(落定前埋点应挂队列) */
|
|
11618
|
+
__publicField(this, "calibrated", false);
|
|
11619
|
+
__publicField(this, "appId", "");
|
|
11620
|
+
__publicField(this, "sdkVersion", "");
|
|
11621
|
+
__publicField(this, "platform", "web");
|
|
11622
|
+
__publicField(this, "timer", null);
|
|
11623
|
+
__publicField(this, "visibilityHandler", null);
|
|
11624
|
+
__publicField(this, "syncing", false);
|
|
11625
|
+
/** 首次校准落定后触发的回调(供上报层 flush 挂起队列) */
|
|
11626
|
+
__publicField(this, "readyCallbacks", []);
|
|
11627
|
+
}
|
|
11628
|
+
/** 首次校准是否已完成(供上报闸门判断) */
|
|
11629
|
+
isReady() {
|
|
11630
|
+
return this.calibrated;
|
|
11631
|
+
}
|
|
11632
|
+
/**
|
|
11633
|
+
* 注册"首次校准落定"回调(成功或失败都算落定)。
|
|
11634
|
+
* 若已落定则立即同步触发一次。
|
|
11635
|
+
*/
|
|
11636
|
+
onReady(cb) {
|
|
11637
|
+
if (this.calibrated) {
|
|
11638
|
+
cb();
|
|
11639
|
+
return;
|
|
11640
|
+
}
|
|
11641
|
+
this.readyCallbacks.push(cb);
|
|
11642
|
+
}
|
|
11643
|
+
fireReady() {
|
|
11644
|
+
const cbs = this.readyCallbacks;
|
|
11645
|
+
this.readyCallbacks = [];
|
|
11646
|
+
for (const cb of cbs) {
|
|
11647
|
+
try {
|
|
11648
|
+
cb();
|
|
11649
|
+
} catch {
|
|
11650
|
+
}
|
|
11651
|
+
}
|
|
11652
|
+
}
|
|
11653
|
+
/** 本地时间戳:本地基准 + 单调流逝。一定有值。 */
|
|
11654
|
+
localNow() {
|
|
11655
|
+
return this.localAt(performance.now());
|
|
11656
|
+
}
|
|
11657
|
+
/** 后台时间戳:后台基准 + 单调流逝。未校准到后台时返回 null(上报留空)。 */
|
|
11658
|
+
serverNow() {
|
|
11659
|
+
return this.serverAt(performance.now());
|
|
11660
|
+
}
|
|
11661
|
+
/**
|
|
11662
|
+
* 仅供单测:直接设定基准,绕过真实网络校准。
|
|
11663
|
+
* @internal
|
|
11664
|
+
*/
|
|
11665
|
+
__setBaselineForTest(opts) {
|
|
11666
|
+
this.localBase = opts.localBase;
|
|
11667
|
+
this.serverBase = opts.serverBase;
|
|
11668
|
+
this.monoAtCalibrate = opts.monoAtCalibrate;
|
|
11669
|
+
this.calibrated = opts.calibrated ?? true;
|
|
11670
|
+
}
|
|
11671
|
+
/** 用"指定单调读数"算本地时间戳(供入队事件按入队时刻还原)。取整到毫秒。 */
|
|
11672
|
+
localAt(mono) {
|
|
11673
|
+
return Math.round(this.localBase + (mono - this.monoAtCalibrate));
|
|
11674
|
+
}
|
|
11675
|
+
/** 用"指定单调读数"算后台时间戳;未校准到后台时返回 null。取整到毫秒。 */
|
|
11676
|
+
serverAt(mono) {
|
|
11677
|
+
if (this.serverBase === null) return null;
|
|
11678
|
+
return Math.round(this.serverBase + (mono - this.monoAtCalibrate));
|
|
11679
|
+
}
|
|
11680
|
+
/**
|
|
11681
|
+
* 用时间戳做逻辑(端到端延迟 tap_N/anim_N 等)时用:优先后台时间戳,
|
|
11682
|
+
* 拿不到后台才退本地,保证与后台时刻同一时间线对齐。
|
|
11683
|
+
*/
|
|
11684
|
+
timelineNow() {
|
|
11685
|
+
return this.serverNow() ?? this.localNow();
|
|
11686
|
+
}
|
|
11687
|
+
/**
|
|
11688
|
+
* 把"采集时刻的单调读数"换算成时间线时刻(优先后台、退本地)。
|
|
11689
|
+
* 供标记解析用——采集时若校准未完成,先记单调读数,上报时再用最新基准换算。
|
|
11690
|
+
* @param mono 采集那一刻的 performance.now()
|
|
11691
|
+
*/
|
|
11692
|
+
resolveMono(mono) {
|
|
11693
|
+
const drift = mono - this.monoAtCalibrate;
|
|
11694
|
+
const base = this.serverBase ?? this.localBase;
|
|
11695
|
+
return Math.round(base + drift);
|
|
11696
|
+
}
|
|
11697
|
+
/**
|
|
11698
|
+
* 启动:立即校准一次,并注册定时 + 页面可见性重校准。
|
|
11699
|
+
* 幂等;fire-and-forget,不阻塞初始化。
|
|
11700
|
+
* @internal
|
|
11701
|
+
*/
|
|
11702
|
+
start(opts) {
|
|
11703
|
+
this.appId = opts.appId;
|
|
11704
|
+
this.sdkVersion = opts.sdkVersion;
|
|
11705
|
+
void this.calibrate();
|
|
11706
|
+
if (this.timer === null) {
|
|
11707
|
+
this.timer = setInterval(() => void this.calibrate(), RECALIBRATE_INTERVAL_MS);
|
|
11708
|
+
}
|
|
11709
|
+
if (this.visibilityHandler === null && typeof document !== "undefined") {
|
|
11710
|
+
this.visibilityHandler = () => {
|
|
11711
|
+
if (document.visibilityState === "visible") {
|
|
11712
|
+
void this.calibrate();
|
|
11713
|
+
}
|
|
11714
|
+
};
|
|
11715
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
11716
|
+
}
|
|
11717
|
+
}
|
|
11718
|
+
/**
|
|
11719
|
+
* 校准一次:多采样取最小 RTT 的样本,同时刷新本地/后台基准与单调零点。
|
|
11720
|
+
* - 拿到后台:serverBase 更新为估算值。
|
|
11721
|
+
* - 拿不到后台:serverBase 置空(后台时间戳留空),localBase 照常更新为当前墙钟。
|
|
11722
|
+
* 已校准过后若本次全失败:保留旧基准、跳过(不把好基准换成更差的)。
|
|
11723
|
+
* @internal
|
|
11724
|
+
*/
|
|
11725
|
+
async calibrate() {
|
|
11726
|
+
if (this.syncing) return;
|
|
11727
|
+
this.syncing = true;
|
|
11728
|
+
try {
|
|
11729
|
+
const samples = [];
|
|
11730
|
+
for (let i2 = 0; i2 < SAMPLE_COUNT; i2++) {
|
|
11731
|
+
const s2 = await this.probe();
|
|
11732
|
+
if (s2) samples.push(s2);
|
|
11733
|
+
}
|
|
11734
|
+
if (samples.length > 0) {
|
|
11735
|
+
const best = samples.reduce((a2, b2) => b2.rttNet < a2.rttNet ? b2 : a2);
|
|
11736
|
+
const firstCalibration = !this.calibrated;
|
|
11737
|
+
this.serverBase = best.serverBase;
|
|
11738
|
+
this.localBase = best.localBase;
|
|
11739
|
+
this.monoAtCalibrate = best.monoAtCalibrate;
|
|
11740
|
+
this.calibrated = true;
|
|
11741
|
+
if (firstCalibration) this.fireReady();
|
|
11742
|
+
logMetric("time_calibrated", "info", {
|
|
11743
|
+
has_server: true,
|
|
11744
|
+
rtt_ms: Math.round(best.rttNet)
|
|
11745
|
+
});
|
|
11746
|
+
return;
|
|
11747
|
+
}
|
|
11748
|
+
if (this.calibrated) {
|
|
11749
|
+
logger.log("[ClockSync] recalibrate got no server time, keeping previous baseline");
|
|
11750
|
+
return;
|
|
11751
|
+
}
|
|
11752
|
+
this.localBase = Date.now();
|
|
11753
|
+
this.monoAtCalibrate = performance.now();
|
|
11754
|
+
this.serverBase = null;
|
|
11755
|
+
this.calibrated = true;
|
|
11756
|
+
this.fireReady();
|
|
11757
|
+
logMetric("time_calibrated", "info", { has_server: false });
|
|
11758
|
+
} finally {
|
|
11759
|
+
this.syncing = false;
|
|
11760
|
+
}
|
|
11761
|
+
}
|
|
11762
|
+
/** 单次采样:请求 bootstrap,返回一组 (rttNet, serverBase, monoAtCalibrate, localBase);失败返回 null。 */
|
|
11763
|
+
async probe() {
|
|
11764
|
+
const controller = new AbortController();
|
|
11765
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
11766
|
+
try {
|
|
11767
|
+
let t1 = 0;
|
|
11768
|
+
let localAtT1 = 0;
|
|
11769
|
+
const t0 = performance.now();
|
|
11770
|
+
const body = await fetchBootstrap(
|
|
11771
|
+
{
|
|
11772
|
+
appId: this.appId,
|
|
11773
|
+
sdkVersion: this.sdkVersion,
|
|
11774
|
+
region: "auto",
|
|
11775
|
+
platform: this.platform
|
|
11776
|
+
},
|
|
11777
|
+
controller.signal,
|
|
11778
|
+
() => {
|
|
11779
|
+
t1 = performance.now();
|
|
11780
|
+
localAtT1 = Date.now();
|
|
11781
|
+
}
|
|
11782
|
+
);
|
|
11783
|
+
const ts2 = body == null ? void 0 : body.time_sync;
|
|
11784
|
+
if (!ts2 || typeof ts2.server_receive_ms !== "number" || typeof ts2.server_send_ms !== "number") {
|
|
11785
|
+
return null;
|
|
11786
|
+
}
|
|
11787
|
+
const rttNet = t1 - t0 - (ts2.server_send_ms - ts2.server_receive_ms);
|
|
11788
|
+
const serverBase = ts2.server_send_ms + rttNet / 2;
|
|
11789
|
+
return {
|
|
11790
|
+
rttNet: Math.max(0, rttNet),
|
|
11791
|
+
serverBase,
|
|
11792
|
+
monoAtCalibrate: t1,
|
|
11793
|
+
localBase: localAtT1
|
|
11794
|
+
};
|
|
11795
|
+
} catch {
|
|
11796
|
+
return null;
|
|
11797
|
+
} finally {
|
|
11798
|
+
clearTimeout(timer);
|
|
11799
|
+
}
|
|
11800
|
+
}
|
|
11801
|
+
/** 清理定时器与监听器。 */
|
|
11802
|
+
cleanup() {
|
|
11803
|
+
if (this.timer !== null) {
|
|
11804
|
+
clearInterval(this.timer);
|
|
11805
|
+
this.timer = null;
|
|
11806
|
+
}
|
|
11807
|
+
if (this.visibilityHandler !== null && typeof document !== "undefined") {
|
|
11808
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
11809
|
+
this.visibilityHandler = null;
|
|
11810
|
+
}
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
const clockSync = new ClockSync();
|
|
11814
|
+
const FILTERED_HOSTNAMES = ["localhost", "127.0.0.1", "0.0.0.0"];
|
|
11815
|
+
function shouldFilterHostname() {
|
|
11816
|
+
if (typeof window === "undefined") {
|
|
11817
|
+
return false;
|
|
11818
|
+
}
|
|
11819
|
+
const hostname = window.location.hostname;
|
|
11820
|
+
return FILTERED_HOSTNAMES.includes(hostname);
|
|
11821
|
+
}
|
|
11822
|
+
const MARK_KEY = "__mark";
|
|
11823
|
+
function monoTimestamp() {
|
|
11824
|
+
return { [MARK_KEY]: "mono", value: performance.now() };
|
|
11825
|
+
}
|
|
11826
|
+
function isMark(v2) {
|
|
11827
|
+
return typeof v2 === "object" && v2 !== null && MARK_KEY in v2;
|
|
11828
|
+
}
|
|
11829
|
+
function resolveMark(mark) {
|
|
11830
|
+
switch (mark[MARK_KEY]) {
|
|
11831
|
+
case "mono":
|
|
11832
|
+
return clockSync.resolveMono(mark.value);
|
|
11833
|
+
}
|
|
11834
|
+
}
|
|
11835
|
+
function resolveMarks(contents) {
|
|
11836
|
+
return resolveValue(contents);
|
|
11837
|
+
}
|
|
11838
|
+
function resolveValue(value) {
|
|
11839
|
+
if (isMark(value)) return resolveMark(value);
|
|
11840
|
+
if (Array.isArray(value)) {
|
|
11841
|
+
let out = null;
|
|
11842
|
+
for (let i2 = 0; i2 < value.length; i2++) {
|
|
11843
|
+
const resolved = resolveValue(value[i2]);
|
|
11844
|
+
if (resolved !== value[i2]) {
|
|
11845
|
+
if (!out) out = [...value];
|
|
11846
|
+
out[i2] = resolved;
|
|
11847
|
+
}
|
|
11848
|
+
}
|
|
11849
|
+
return out ?? value;
|
|
11850
|
+
}
|
|
11851
|
+
if (typeof value === "object" && value !== null) {
|
|
11852
|
+
const obj = value;
|
|
11853
|
+
let out = null;
|
|
11854
|
+
for (const [k2, v2] of Object.entries(obj)) {
|
|
11855
|
+
const resolved = resolveValue(v2);
|
|
11856
|
+
if (resolved !== v2) {
|
|
11857
|
+
if (!out) out = { ...obj };
|
|
11858
|
+
out[k2] = resolved;
|
|
11859
|
+
}
|
|
11860
|
+
}
|
|
11861
|
+
return out ?? value;
|
|
11862
|
+
}
|
|
11863
|
+
return value;
|
|
11864
|
+
}
|
|
11589
11865
|
const OTEL_LOGGER_NAME = "spatius-avatarkit";
|
|
11590
11866
|
let sdkVersion$1 = "1.0.0";
|
|
11591
11867
|
let isInitialized$1 = false;
|
|
11592
11868
|
let loggerProvider = null;
|
|
11593
11869
|
const eventQueue$1 = [];
|
|
11870
|
+
const preCalibrationQueue = [];
|
|
11594
11871
|
function buildBasicAuthHeader() {
|
|
11595
11872
|
const credentials = `${OTEL_USERNAME}:${OTEL_PASSWORD}`;
|
|
11596
11873
|
return `Basic ${btoa(credentials)}`;
|
|
@@ -11644,6 +11921,7 @@ function initializeOtel(version, resourceAttrs) {
|
|
|
11644
11921
|
isInitialized$1 = true;
|
|
11645
11922
|
logger.log(`[OTel] Initialized successfully - endpoint: ${OTEL_LOGS_ENDPOINT}, stream: ${OTEL_STREAM_NAME}`);
|
|
11646
11923
|
flushEventQueue$1();
|
|
11924
|
+
clockSync.onReady(() => flushPreCalibrationQueue());
|
|
11647
11925
|
replayPersistedEvents().catch(() => {
|
|
11648
11926
|
});
|
|
11649
11927
|
} catch (error) {
|
|
@@ -11704,7 +11982,27 @@ function flushEventQueue$1() {
|
|
|
11704
11982
|
eventQueue$1.length = 0;
|
|
11705
11983
|
}
|
|
11706
11984
|
function trackEventOtel(event, level = "info", contents = {}) {
|
|
11707
|
-
|
|
11985
|
+
if (shouldFilterHostname()) return;
|
|
11986
|
+
if (!clockSync.isReady()) {
|
|
11987
|
+
preCalibrationQueue.push({ event, level, contents, enqueueMono: performance.now() });
|
|
11988
|
+
return;
|
|
11989
|
+
}
|
|
11990
|
+
emitOtelEvent(event, level, contents, performance.now());
|
|
11991
|
+
}
|
|
11992
|
+
function flushPreCalibrationQueue() {
|
|
11993
|
+
if (preCalibrationQueue.length === 0) return;
|
|
11994
|
+
const pending = preCalibrationQueue.splice(0, preCalibrationQueue.length);
|
|
11995
|
+
for (const e2 of pending) {
|
|
11996
|
+
emitOtelEvent(e2.event, e2.level, e2.contents, e2.enqueueMono);
|
|
11997
|
+
}
|
|
11998
|
+
}
|
|
11999
|
+
function emitOtelEvent(event, level, contents, atMono) {
|
|
12000
|
+
contents = resolveMarks(contents);
|
|
12001
|
+
const timestamp = clockSync.localAt(atMono);
|
|
12002
|
+
const serverTs = clockSync.serverAt(atMono);
|
|
12003
|
+
if (serverTs !== null) {
|
|
12004
|
+
contents = { ...contents, server_timestamp: serverTs };
|
|
12005
|
+
}
|
|
11708
12006
|
eventStore.add({ event, level, contents, timestamp }).then((id) => {
|
|
11709
12007
|
const contentsWithIndex = { ...contents, _index: id };
|
|
11710
12008
|
if (!loggerProvider) return;
|
|
@@ -11807,6 +12105,15 @@ function cleanupOtel() {
|
|
|
11807
12105
|
loggerProvider = null;
|
|
11808
12106
|
}
|
|
11809
12107
|
}
|
|
12108
|
+
function timestampFields(enqueueMono) {
|
|
12109
|
+
const local = enqueueMono === void 0 ? clockSync.localNow() : clockSync.localAt(enqueueMono);
|
|
12110
|
+
const server = enqueueMono === void 0 ? clockSync.serverNow() : clockSync.serverAt(enqueueMono);
|
|
12111
|
+
const fields = { timestamp: local };
|
|
12112
|
+
if (server !== null) {
|
|
12113
|
+
fields.server_timestamp = server;
|
|
12114
|
+
}
|
|
12115
|
+
return fields;
|
|
12116
|
+
}
|
|
11810
12117
|
let sdkVersion = "1.0.0";
|
|
11811
12118
|
let isInitialized = false;
|
|
11812
12119
|
let commonRegion = "";
|
|
@@ -11817,14 +12124,6 @@ function isPostHogEnabled() {
|
|
|
11817
12124
|
const SDK_POSTHOG_INSTANCE_NAME = "spatius-posthog";
|
|
11818
12125
|
let sdkPosthogInstance = null;
|
|
11819
12126
|
const eventQueue = [];
|
|
11820
|
-
const FILTERED_HOSTNAMES = ["localhost", "127.0.0.1", "0.0.0.0"];
|
|
11821
|
-
function shouldFilterHostname() {
|
|
11822
|
-
if (typeof window === "undefined") {
|
|
11823
|
-
return false;
|
|
11824
|
-
}
|
|
11825
|
-
const hostname = window.location.hostname;
|
|
11826
|
-
return FILTERED_HOSTNAMES.includes(hostname);
|
|
11827
|
-
}
|
|
11828
12127
|
function getCommonFields() {
|
|
11829
12128
|
const logContext = idManager.getLogContext();
|
|
11830
12129
|
return {
|
|
@@ -11885,6 +12184,7 @@ function initializePostHog(version, commonFields) {
|
|
|
11885
12184
|
}
|
|
11886
12185
|
posthogInstance.setPersonPropertiesForFlags(userProperties);
|
|
11887
12186
|
flushEventQueue();
|
|
12187
|
+
clockSync.onReady(() => flushEventQueue());
|
|
11888
12188
|
}
|
|
11889
12189
|
}, SDK_POSTHOG_INSTANCE_NAME);
|
|
11890
12190
|
} catch (error) {
|
|
@@ -11948,20 +12248,20 @@ function updatePostHogPersonPropertiesForFlags() {
|
|
|
11948
12248
|
instance.setPersonPropertiesForFlags(commonFields);
|
|
11949
12249
|
}
|
|
11950
12250
|
function flushEventQueue() {
|
|
11951
|
-
if (!sdkPosthogInstance || eventQueue.length === 0) {
|
|
12251
|
+
if (!sdkPosthogInstance || !clockSync.isReady() || eventQueue.length === 0) {
|
|
11952
12252
|
return;
|
|
11953
12253
|
}
|
|
11954
12254
|
logger.log(`[PostHog] Flushing ${eventQueue.length} queued events`);
|
|
11955
12255
|
setTimeout(() => {
|
|
11956
|
-
for (const { event, level, contents } of eventQueue) {
|
|
12256
|
+
for (const { event, level, contents, enqueueMono } of eventQueue) {
|
|
11957
12257
|
try {
|
|
11958
12258
|
const commonFields = getCommonFields();
|
|
11959
12259
|
const properties = {
|
|
11960
12260
|
...commonFields,
|
|
11961
12261
|
level,
|
|
11962
12262
|
service_module: "sdk",
|
|
11963
|
-
|
|
11964
|
-
...contents
|
|
12263
|
+
...timestampFields(enqueueMono),
|
|
12264
|
+
...resolveMarks(contents)
|
|
11965
12265
|
};
|
|
11966
12266
|
sdkPosthogInstance.capture(event, properties);
|
|
11967
12267
|
} catch (error) {
|
|
@@ -11977,8 +12277,8 @@ function trackEvent(event, level = "info", contents = {}) {
|
|
|
11977
12277
|
return;
|
|
11978
12278
|
}
|
|
11979
12279
|
const instance = getSdkPosthogInstance();
|
|
11980
|
-
if (!instance) {
|
|
11981
|
-
eventQueue.push({ event, level, contents });
|
|
12280
|
+
if (!instance || !clockSync.isReady()) {
|
|
12281
|
+
eventQueue.push({ event, level, contents, enqueueMono: performance.now() });
|
|
11982
12282
|
return;
|
|
11983
12283
|
}
|
|
11984
12284
|
sdkPosthogInstance = instance;
|
|
@@ -11988,8 +12288,8 @@ function trackEvent(event, level = "info", contents = {}) {
|
|
|
11988
12288
|
...commonFields,
|
|
11989
12289
|
level,
|
|
11990
12290
|
service_module: "sdk",
|
|
11991
|
-
|
|
11992
|
-
...contents
|
|
12291
|
+
...timestampFields(),
|
|
12292
|
+
...resolveMarks(contents)
|
|
11993
12293
|
};
|
|
11994
12294
|
sdkPosthogInstance.capture(event, properties);
|
|
11995
12295
|
} catch (error) {
|
|
@@ -12021,28 +12321,39 @@ function cleanupPostHog() {
|
|
|
12021
12321
|
sdkPosthogInstance = null;
|
|
12022
12322
|
}
|
|
12023
12323
|
}
|
|
12024
|
-
|
|
12324
|
+
const TELEMETRY_LOG_METHOD = {
|
|
12325
|
+
debug: "log",
|
|
12326
|
+
info: "log",
|
|
12327
|
+
warning: "warn",
|
|
12328
|
+
error: "error"
|
|
12329
|
+
};
|
|
12330
|
+
function buildTelemetryContext(event, level, contents) {
|
|
12025
12331
|
const sessionToken = idManager.getSessionToken() ?? "";
|
|
12026
12332
|
const context2 = {
|
|
12027
12333
|
session_token_suffix: sessionToken ? sessionToken.slice(-8) : "",
|
|
12028
12334
|
...contents
|
|
12029
12335
|
};
|
|
12030
|
-
const
|
|
12031
|
-
debug: "log",
|
|
12032
|
-
info: "log",
|
|
12033
|
-
warning: "warn",
|
|
12034
|
-
error: "error"
|
|
12035
|
-
};
|
|
12036
|
-
const logMethod = logLevels[level] ?? "log";
|
|
12336
|
+
const logMethod = TELEMETRY_LOG_METHOD[level] ?? "log";
|
|
12037
12337
|
const propsDescription = Object.entries(contents).map(([k2, v2]) => `${k2}=${v2}`).join(", ");
|
|
12038
12338
|
logger[logMethod](`[Telemetry] ${event} [${propsDescription}]`);
|
|
12039
|
-
|
|
12339
|
+
return context2;
|
|
12340
|
+
}
|
|
12341
|
+
function emitToOtel(event, level, context2) {
|
|
12040
12342
|
try {
|
|
12041
12343
|
trackEventOtel(event, level, context2);
|
|
12042
12344
|
} catch (error) {
|
|
12043
12345
|
logger.warn("[OTel] Dual-send failed:", error instanceof Error ? error.message : String(error));
|
|
12044
12346
|
}
|
|
12045
12347
|
}
|
|
12348
|
+
function logEvent(event, level = "info", contents = {}) {
|
|
12349
|
+
const context2 = buildTelemetryContext(event, level, contents);
|
|
12350
|
+
trackEvent(event, level, context2);
|
|
12351
|
+
emitToOtel(event, level, context2);
|
|
12352
|
+
}
|
|
12353
|
+
function logMetric(event, level = "info", contents = {}) {
|
|
12354
|
+
const context2 = buildTelemetryContext(event, level, contents);
|
|
12355
|
+
emitToOtel(event, level, context2);
|
|
12356
|
+
}
|
|
12046
12357
|
const _LogSink = class _LogSink {
|
|
12047
12358
|
constructor() {
|
|
12048
12359
|
__publicField(this, "db", null);
|
|
@@ -12544,6 +12855,8 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
12544
12855
|
__publicField(this, "_isPlaying", false);
|
|
12545
12856
|
__publicField(this, "fps", APP_CONFIG.animation.fps);
|
|
12546
12857
|
__publicField(this, "onEndedCallback");
|
|
12858
|
+
// 音频缓冲卡顿回调(透传给 streamingPlayer;控制器注册,用于并入 playback_quality)
|
|
12859
|
+
__publicField(this, "onAudioStallCallback");
|
|
12547
12860
|
__publicField(this, "useStreaming", false);
|
|
12548
12861
|
}
|
|
12549
12862
|
/**
|
|
@@ -12627,7 +12940,7 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
12627
12940
|
if (this.streamingPlayer) {
|
|
12628
12941
|
return;
|
|
12629
12942
|
}
|
|
12630
|
-
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-
|
|
12943
|
+
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-2bLnaLam.js");
|
|
12631
12944
|
const { AvatarSDK: AvatarSDK2 } = await Promise.resolve().then(() => AvatarSDK$1);
|
|
12632
12945
|
const audioFormat = AvatarSDK2.getAudioFormat();
|
|
12633
12946
|
this.streamingPlayer = new StreamingAudioPlayer({
|
|
@@ -12666,6 +12979,22 @@ const _AnimationPlayer = class _AnimationPlayer {
|
|
|
12666
12979
|
this._isPlaying = false;
|
|
12667
12980
|
(_a = this.onEndedCallback) == null ? void 0 : _a.call(this);
|
|
12668
12981
|
});
|
|
12982
|
+
this.streamingPlayer.onAudioStall((stalled) => {
|
|
12983
|
+
var _a;
|
|
12984
|
+
(_a = this.onAudioStallCallback) == null ? void 0 : _a.call(this, stalled);
|
|
12985
|
+
});
|
|
12986
|
+
}
|
|
12987
|
+
/**
|
|
12988
|
+
* 注册音频缓冲卡顿回调,透传给底层 streamingPlayer(若已就绪则立即接上)。
|
|
12989
|
+
* @internal
|
|
12990
|
+
*/
|
|
12991
|
+
onAudioStall(callback) {
|
|
12992
|
+
var _a;
|
|
12993
|
+
this.onAudioStallCallback = callback;
|
|
12994
|
+
(_a = this.streamingPlayer) == null ? void 0 : _a.onAudioStall((stalled) => {
|
|
12995
|
+
var _a2;
|
|
12996
|
+
(_a2 = this.onAudioStallCallback) == null ? void 0 : _a2.call(this, stalled);
|
|
12997
|
+
});
|
|
12669
12998
|
}
|
|
12670
12999
|
setupEventListeners() {
|
|
12671
13000
|
if (!this.audio)
|
|
@@ -12885,6 +13214,38 @@ class AvatarViewRegistry {
|
|
|
12885
13214
|
}
|
|
12886
13215
|
}
|
|
12887
13216
|
const avatarViewRegistry = new AvatarViewRegistry();
|
|
13217
|
+
const RESOLVE_TIMEOUT_MS = 5e3;
|
|
13218
|
+
async function resolveRegion(opts) {
|
|
13219
|
+
var _a;
|
|
13220
|
+
const { appId, sdkVersion: sdkVersion2, requestedRegion } = opts;
|
|
13221
|
+
if (requestedRegion !== DEFAULT_REGION_REQUEST) {
|
|
13222
|
+
return requestedRegion;
|
|
13223
|
+
}
|
|
13224
|
+
const controller = new AbortController();
|
|
13225
|
+
const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
|
|
13226
|
+
try {
|
|
13227
|
+
const res = await fetchBootstrap(
|
|
13228
|
+
{ appId, sdkVersion: sdkVersion2, region: "auto" },
|
|
13229
|
+
controller.signal
|
|
13230
|
+
);
|
|
13231
|
+
const current = (_a = res.region) == null ? void 0 : _a.current;
|
|
13232
|
+
if (typeof current === "string" && current.length > 0) {
|
|
13233
|
+
logger.log(`[RegionResolver] auto → ${current}`);
|
|
13234
|
+
return current;
|
|
13235
|
+
}
|
|
13236
|
+
throw new Error("bootstrap response missing region.current");
|
|
13237
|
+
} catch (error) {
|
|
13238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13239
|
+
logger.warn(`[RegionResolver] auto resolve failed, falling back to ${DEFAULT_REGION}: ${message}`);
|
|
13240
|
+
logEvent("region_resolve_failed", "error", {
|
|
13241
|
+
fallback_region: DEFAULT_REGION,
|
|
13242
|
+
reason: message
|
|
13243
|
+
});
|
|
13244
|
+
return DEFAULT_REGION;
|
|
13245
|
+
} finally {
|
|
13246
|
+
clearTimeout(timer);
|
|
13247
|
+
}
|
|
13248
|
+
}
|
|
12888
13249
|
class AvatarCoreMemoryManager {
|
|
12889
13250
|
constructor(wasmModule) {
|
|
12890
13251
|
__publicField(this, "module");
|
|
@@ -14322,17 +14683,23 @@ class AvatarSDK {
|
|
|
14322
14683
|
}
|
|
14323
14684
|
static async _initializeInternal(appId, configuration) {
|
|
14324
14685
|
try {
|
|
14325
|
-
|
|
14686
|
+
const requestedRegion = configuration.region || DEFAULT_REGION_REQUEST;
|
|
14687
|
+
logger.log(`[AvatarSDK] Initializing with appId: ${appId}, region: ${requestedRegion}`);
|
|
14326
14688
|
resetDeprecationWarnings();
|
|
14327
|
-
this._configuration = configuration;
|
|
14328
14689
|
setLogLevel(configuration.logLevel ?? LogLevel.off);
|
|
14329
14690
|
idManager.setAppId(appId);
|
|
14330
14691
|
logger.log(`[AvatarSDK] Client ID: ${idManager.getClientId()}`);
|
|
14331
|
-
const region = configuration.region || DEFAULT_REGION;
|
|
14332
14692
|
const dsm = configuration.drivingServiceMode || DrivingServiceMode.direct;
|
|
14333
14693
|
const appIdValue = idManager.getAppId() || "";
|
|
14694
|
+
const region = await resolveRegion({
|
|
14695
|
+
appId: appIdValue,
|
|
14696
|
+
sdkVersion: this._version,
|
|
14697
|
+
requestedRegion
|
|
14698
|
+
});
|
|
14699
|
+
this._configuration = { ...configuration, region };
|
|
14334
14700
|
initializePostHog(this._version, { region, dsm });
|
|
14335
14701
|
initializeOtel(this._version, { region, dsm, appId: appIdValue });
|
|
14702
|
+
clockSync.start({ appId: appIdValue, sdkVersion: this._version });
|
|
14336
14703
|
await this.initializeWASMModule();
|
|
14337
14704
|
await this.initializeTemplateResources();
|
|
14338
14705
|
this._initializationState = "initialized";
|
|
@@ -14594,6 +14961,7 @@ class AvatarSDK {
|
|
|
14594
14961
|
idManager.clear();
|
|
14595
14962
|
cleanupPostHog();
|
|
14596
14963
|
cleanupOtel();
|
|
14964
|
+
clockSync.cleanup();
|
|
14597
14965
|
logger.log("[AvatarSDK] Cleanup completed");
|
|
14598
14966
|
} catch (error) {
|
|
14599
14967
|
logger.error("Failed to cleanup AvatarSDK:", error instanceof Error ? error.message : String(error));
|
|
@@ -14712,7 +15080,7 @@ class AvatarSDK {
|
|
|
14712
15080
|
__publicField(AvatarSDK, "_initializationState", "uninitialized");
|
|
14713
15081
|
__publicField(AvatarSDK, "_initializingPromise", null);
|
|
14714
15082
|
__publicField(AvatarSDK, "_configuration", null);
|
|
14715
|
-
__publicField(AvatarSDK, "_version", "1.3.
|
|
15083
|
+
__publicField(AvatarSDK, "_version", "1.3.1-beta.2");
|
|
14716
15084
|
__publicField(AvatarSDK, "_avatarCore", null);
|
|
14717
15085
|
__publicField(AvatarSDK, "_cachedDeviceScore", null);
|
|
14718
15086
|
__publicField(AvatarSDK, "_rendererBackend", null);
|
|
@@ -15338,14 +15706,14 @@ class NetworkLayer {
|
|
|
15338
15706
|
}
|
|
15339
15707
|
const metrics = this.audioMetrics;
|
|
15340
15708
|
if (!metrics.taps.has(0)) {
|
|
15341
|
-
metrics.taps.set(0,
|
|
15709
|
+
metrics.taps.set(0, monoTimestamp());
|
|
15342
15710
|
}
|
|
15343
15711
|
metrics.accumulatedBytes += audioData.byteLength;
|
|
15344
15712
|
const currentDuration = metrics.accumulatedBytes / this.getAudioBytesPerSecond();
|
|
15345
15713
|
const maxTapKey = metrics.taps.size > 0 ? Math.max(...metrics.taps.keys()) : 0;
|
|
15346
15714
|
let s2 = maxTapKey + 1;
|
|
15347
15715
|
while (currentDuration >= s2) {
|
|
15348
|
-
metrics.taps.set(s2,
|
|
15716
|
+
metrics.taps.set(s2, monoTimestamp());
|
|
15349
15717
|
s2++;
|
|
15350
15718
|
}
|
|
15351
15719
|
if (audioData.byteLength === 0 && !isLast) {
|
|
@@ -15353,7 +15721,6 @@ class NetworkLayer {
|
|
|
15353
15721
|
}
|
|
15354
15722
|
if (this.isFallbackMode) {
|
|
15355
15723
|
if (this.currentConversationId) {
|
|
15356
|
-
logger.warn("[NetworkLayer] Fallback mode: skipping WebSocket send, triggering fallback");
|
|
15357
15724
|
this.dataController.yieldKeyframes([], this.currentConversationId);
|
|
15358
15725
|
}
|
|
15359
15726
|
return;
|
|
@@ -15502,7 +15869,7 @@ class NetworkLayer {
|
|
|
15502
15869
|
if (!this.dataController.getIsPlaying()) {
|
|
15503
15870
|
this.dataController.startStreamingPlayback();
|
|
15504
15871
|
}
|
|
15505
|
-
this.audioMetrics.anims.set(this.audioMetrics.animGroupCount,
|
|
15872
|
+
this.audioMetrics.anims.set(this.audioMetrics.animGroupCount, monoTimestamp());
|
|
15506
15873
|
this.audioMetrics.animGroupCount++;
|
|
15507
15874
|
} else {
|
|
15508
15875
|
logger.warn(`[NetworkLayer] Animation message has no animation data - conversationId: ${conversationId}`);
|
|
@@ -15581,26 +15948,24 @@ class NetworkLayer {
|
|
|
15581
15948
|
this.audioMetrics = this.createAudioMetrics();
|
|
15582
15949
|
}
|
|
15583
15950
|
/**
|
|
15584
|
-
*
|
|
15585
|
-
* 整轮播放结束(进入 idle)时由 AvatarController
|
|
15951
|
+
* 取本轮端到端延迟打点(tap_N=第N秒音频送入时刻,anim_N=第N组动画到达时刻)。
|
|
15952
|
+
* 整轮播放结束(进入 idle)时由 AvatarController 读取,并入 playback_quality 的
|
|
15953
|
+
* latency 成员统一上报(不再单发 driving_service_latency)。
|
|
15954
|
+
* 无打点数据(如未走 direct 驱动)时返回 null,playback_quality 不带 latency。
|
|
15586
15955
|
* @internal
|
|
15587
15956
|
*/
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
if (!conversationId) return;
|
|
15957
|
+
getDrivingServiceLatency() {
|
|
15958
|
+
if (!this.currentConversationId) return null;
|
|
15591
15959
|
const metrics = this.audioMetrics;
|
|
15592
|
-
if (metrics.taps.size === 0 || metrics.anims.size === 0) return;
|
|
15593
|
-
const props = {
|
|
15594
|
-
conversation_id: conversationId,
|
|
15595
|
-
dsm: "direct"
|
|
15596
|
-
};
|
|
15960
|
+
if (metrics.taps.size === 0 || metrics.anims.size === 0) return null;
|
|
15961
|
+
const props = {};
|
|
15597
15962
|
for (const [n2, t2] of metrics.taps) {
|
|
15598
15963
|
props[`tap_${n2}`] = t2;
|
|
15599
15964
|
}
|
|
15600
15965
|
for (const [n2, t2] of metrics.anims) {
|
|
15601
15966
|
props[`anim_${n2}`] = t2;
|
|
15602
15967
|
}
|
|
15603
|
-
|
|
15968
|
+
return props;
|
|
15604
15969
|
}
|
|
15605
15970
|
/**
|
|
15606
15971
|
* Start driving service heartbeat check (check connection status every 2 minutes)
|
|
@@ -15897,8 +16262,23 @@ class AvatarController {
|
|
|
15897
16262
|
__publicField(this, "isFallbackMode", false);
|
|
15898
16263
|
// 降级模式标志
|
|
15899
16264
|
// ========== Frame Starvation Tracking ==========
|
|
16265
|
+
// enterMono:进入卡顿那刻的单调读数(performance.now()),恢复时用来算 durationMs。
|
|
16266
|
+
// durationMs:这次卡顿持续多久(ms),帧恢复时回填;卡到播放结束都没恢复则为 null。
|
|
16267
|
+
// startTs:卡顿开始的时间线时刻(clockSync.timelineNow(),优先后台、退本地,与 tap/anim 同轴)。
|
|
16268
|
+
// framesAtEnter / framesAtRecover:卡顿开始/恢复时本轮已收到的服务端动画帧数
|
|
16269
|
+
// (差值 = 卡顿期间到帧量,区分"服务端没发"与"发了但不够消费")。未恢复则 framesAtRecover=null。
|
|
15900
16270
|
__publicField(this, "frameStarvationEvents", []);
|
|
15901
16271
|
__publicField(this, "isFrameStarved", false);
|
|
16272
|
+
// ========== Audio Buffer Stall Tracking ==========
|
|
16273
|
+
// 音频缓冲卡顿(缓冲耗尽自动暂停、有数据自动恢复),与帧饥饿对称记录。字段同义。
|
|
16274
|
+
__publicField(this, "audioStallEvents", []);
|
|
16275
|
+
__publicField(this, "isAudioBufferStalled", false);
|
|
16276
|
+
// 本轮播放的结束原因(供 playback_quality 上报):completed=正常播完,interrupted=中途打断。
|
|
16277
|
+
__publicField(this, "playbackEndReason", "completed");
|
|
16278
|
+
// 本轮已接收的音频总字节数(两模式统一累计)。总时长 = /audioBytesPerSecond。每轮重置。
|
|
16279
|
+
__publicField(this, "receivedAudioBytes", 0);
|
|
16280
|
+
// 本轮已接收的服务端下发动画帧总数(不含本地起播过渡帧)。每轮重置。
|
|
16281
|
+
__publicField(this, "receivedAnimationFrames", 0);
|
|
15902
16282
|
/**
|
|
15903
16283
|
* Whether this round's final animation batch (ServerResponseAnimation.end) has arrived.
|
|
15904
16284
|
* Frame starvation is only possible BEFORE this — once all frames are in, any remaining
|
|
@@ -16079,42 +16459,101 @@ class AvatarController {
|
|
|
16079
16459
|
/**
|
|
16080
16460
|
* Notify conversation state change and handle telemetry
|
|
16081
16461
|
* @internal
|
|
16082
|
-
* @param isFallback - true when entering fallback mode (suppresses playback stats)
|
|
16083
16462
|
*/
|
|
16084
|
-
notifyConversationState(avatarState
|
|
16085
|
-
var _a
|
|
16463
|
+
notifyConversationState(avatarState) {
|
|
16464
|
+
var _a;
|
|
16086
16465
|
switch (avatarState) {
|
|
16087
16466
|
case AvatarState.playing:
|
|
16088
16467
|
this.frameRateMonitor.resetPlaybackStats();
|
|
16089
|
-
this.shouldReportPlaybackStats =
|
|
16468
|
+
this.shouldReportPlaybackStats = true;
|
|
16090
16469
|
break;
|
|
16091
16470
|
case AvatarState.paused:
|
|
16092
16471
|
this.shouldReportPlaybackStats = false;
|
|
16093
16472
|
break;
|
|
16094
16473
|
case AvatarState.idle:
|
|
16095
|
-
(_a = this.networkLayer) == null ? void 0 : _a.reportDrivingServiceLatency();
|
|
16096
16474
|
if (this.shouldReportPlaybackStats) {
|
|
16097
|
-
|
|
16098
|
-
const bypassSampling = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("integration") === "1";
|
|
16099
|
-
if (stats.durationMs >= 2e3 && stats.frameCount > 0 && (bypassSampling || Math.random() < 0.3)) {
|
|
16100
|
-
const props = {
|
|
16101
|
-
avg_fps: Number(stats.avgFps.toFixed(1)),
|
|
16102
|
-
frame_count: stats.frameCount,
|
|
16103
|
-
duration_ms: stats.durationMs,
|
|
16104
|
-
jank_ratio: Number(stats.jankRatio.toFixed(2)),
|
|
16105
|
-
fps_cv: Number(stats.fpsCv.toFixed(3)),
|
|
16106
|
-
...this._getDeviceScoreProps()
|
|
16107
|
-
};
|
|
16108
|
-
if (AvatarSDK.getRendererBackend()) {
|
|
16109
|
-
props.renderer_backend = AvatarSDK.getRendererBackend();
|
|
16110
|
-
}
|
|
16111
|
-
logEvent("avatar_playback_fps", "info", props);
|
|
16112
|
-
}
|
|
16475
|
+
this.reportPlaybackQuality();
|
|
16113
16476
|
}
|
|
16114
16477
|
this.shouldReportPlaybackStats = false;
|
|
16115
16478
|
break;
|
|
16116
16479
|
}
|
|
16117
|
-
(
|
|
16480
|
+
(_a = this.onConversationState) == null ? void 0 : _a.call(this, this.mapToConversationState(avatarState));
|
|
16481
|
+
}
|
|
16482
|
+
/**
|
|
16483
|
+
* 上报本轮播放质量(playback_quality)。每轮播放结束都发一次(不采样、不设时长门槛),
|
|
16484
|
+
* 把帧率与卡顿两块合并为嵌套成员。end_reason 区分完整播完 / 中途打断。
|
|
16485
|
+
* 有卡顿时用 warning,否则 info(保留卡顿的告警语义)。
|
|
16486
|
+
* @internal
|
|
16487
|
+
*/
|
|
16488
|
+
reportPlaybackQuality() {
|
|
16489
|
+
var _a, _b;
|
|
16490
|
+
const stats = this.frameRateMonitor.getPlaybackStats();
|
|
16491
|
+
const settled = this.frameStarvationEvents.map((e2) => e2.durationMs).filter((d2) => d2 !== null);
|
|
16492
|
+
const totalStallMs = settled.reduce((a2, b2) => a2 + b2, 0);
|
|
16493
|
+
const stall = {
|
|
16494
|
+
count: this.frameStarvationEvents.length,
|
|
16495
|
+
max_ms: settled.length > 0 ? Math.max(...settled) : 0,
|
|
16496
|
+
total_ms: totalStallMs,
|
|
16497
|
+
// 卡顿率 = 卡顿总时长 / 已播放时长(fps.duration_ms;打断轮即播到打断那刻,
|
|
16498
|
+
// 上限为音频时长)。播放时长为 0 时记 0。
|
|
16499
|
+
ratio: stats.durationMs > 0 ? Number((totalStallMs / stats.durationMs).toFixed(4)) : 0,
|
|
16500
|
+
req_end: this.frameStarvationEvents.some((e2) => e2.reqEnd),
|
|
16501
|
+
// 每次卡顿明细:开始时间戳(timelineNow,与 tap/anim 同轴)/ 卡顿时的音频时刻 /
|
|
16502
|
+
// 等了多久(null=卡到播放结束没恢复)/ 卡顿开始与恢复时已收到的动画帧数
|
|
16503
|
+
// (frames_at_recover - frames_at_enter = 卡顿期间到帧量)。
|
|
16504
|
+
events: this.frameStarvationEvents.map((e2) => ({
|
|
16505
|
+
start_ts: Math.round(e2.startTs),
|
|
16506
|
+
audio_time: Number(e2.audioTime.toFixed(3)),
|
|
16507
|
+
duration_ms: e2.durationMs,
|
|
16508
|
+
frames_at_enter: e2.framesAtEnter,
|
|
16509
|
+
frames_at_recover: e2.framesAtRecover
|
|
16510
|
+
}))
|
|
16511
|
+
};
|
|
16512
|
+
const audioSettled = this.audioStallEvents.map((e2) => e2.durationMs).filter((d2) => d2 !== null);
|
|
16513
|
+
const audioStall = {
|
|
16514
|
+
count: this.audioStallEvents.length,
|
|
16515
|
+
max_ms: audioSettled.length > 0 ? Math.max(...audioSettled) : 0,
|
|
16516
|
+
total_ms: audioSettled.reduce((a2, b2) => a2 + b2, 0),
|
|
16517
|
+
events: this.audioStallEvents.map((e2) => ({
|
|
16518
|
+
start_ts: Math.round(e2.startTs),
|
|
16519
|
+
duration_ms: e2.durationMs,
|
|
16520
|
+
frames_at_enter: e2.framesAtEnter,
|
|
16521
|
+
frames_at_recover: e2.framesAtRecover
|
|
16522
|
+
}))
|
|
16523
|
+
};
|
|
16524
|
+
const fps = {
|
|
16525
|
+
avg_fps: Number(stats.avgFps.toFixed(1)),
|
|
16526
|
+
frame_count: stats.frameCount,
|
|
16527
|
+
duration_ms: stats.durationMs,
|
|
16528
|
+
jank_ratio: Number(stats.jankRatio.toFixed(2)),
|
|
16529
|
+
fps_cv: Number(stats.fpsCv.toFixed(3)),
|
|
16530
|
+
...this._getDeviceScoreProps()
|
|
16531
|
+
};
|
|
16532
|
+
if (AvatarSDK.getRendererBackend()) {
|
|
16533
|
+
fps.renderer_backend = AvatarSDK.getRendererBackend();
|
|
16534
|
+
}
|
|
16535
|
+
const latency = ((_a = this.networkLayer) == null ? void 0 : _a.getDrivingServiceLatency()) ?? null;
|
|
16536
|
+
logMetric("playback_quality", stall.count > 0 || audioStall.count > 0 ? "warning" : "info", {
|
|
16537
|
+
avatar_id: this.avatar.id,
|
|
16538
|
+
conversation_id: ((_b = this.networkLayer) == null ? void 0 : _b.getCurrentConversationId()) || void 0,
|
|
16539
|
+
end_reason: this.playbackEndReason,
|
|
16540
|
+
// 是否降级模式(audio-only 无动画帧):为 true 时 fps/animation_frame_count 空是预期
|
|
16541
|
+
is_fallback: this.isFallbackMode,
|
|
16542
|
+
// 本轮接收情况:总音频时长(ms) / 是否收到最后音频(end=true) /
|
|
16543
|
+
// 服务端下发总动画帧数(不含过渡帧) / 是否收到最后动画包(end=true)
|
|
16544
|
+
audio_duration_ms: Math.round(this.receivedAudioBytes / this.audioBytesPerSecond * 1e3),
|
|
16545
|
+
audio_ended: this.reqEnd,
|
|
16546
|
+
animation_frame_count: this.receivedAnimationFrames,
|
|
16547
|
+
animation_ended: this.animationEnded,
|
|
16548
|
+
fps,
|
|
16549
|
+
stall,
|
|
16550
|
+
audio_stall: audioStall,
|
|
16551
|
+
...latency ? { latency } : {}
|
|
16552
|
+
});
|
|
16553
|
+
this.frameStarvationEvents = [];
|
|
16554
|
+
this.isFrameStarved = false;
|
|
16555
|
+
this.audioStallEvents = [];
|
|
16556
|
+
this.isAudioBufferStalled = false;
|
|
16118
16557
|
}
|
|
16119
16558
|
/**
|
|
16120
16559
|
* Get animation player instance
|
|
@@ -16234,6 +16673,7 @@ class AvatarController {
|
|
|
16234
16673
|
if (this.reqEnd && this.isPlaying && networkConversationId) {
|
|
16235
16674
|
this.interrupt();
|
|
16236
16675
|
}
|
|
16676
|
+
this.receivedAudioBytes += audioData.byteLength;
|
|
16237
16677
|
this.addAudioChunkToBuffer(new Uint8Array(audioData), end);
|
|
16238
16678
|
this.networkLayer.sendAudioData(audioData, end);
|
|
16239
16679
|
if (end) {
|
|
@@ -16333,18 +16773,19 @@ class AvatarController {
|
|
|
16333
16773
|
if (isLast) {
|
|
16334
16774
|
this.reqEnd = true;
|
|
16335
16775
|
}
|
|
16776
|
+
this.receivedAudioBytes += data.length;
|
|
16336
16777
|
if (this.playbackMode === DrivingServiceMode.backend) {
|
|
16337
16778
|
const metrics = this.hostModeMetrics;
|
|
16338
16779
|
if (metrics.startTimestamp === 0) {
|
|
16339
|
-
metrics.startTimestamp =
|
|
16780
|
+
metrics.startTimestamp = clockSync.timelineNow();
|
|
16340
16781
|
}
|
|
16341
16782
|
metrics.accumulatedBytes += data.length;
|
|
16342
16783
|
const currentDuration = metrics.accumulatedBytes / this.audioBytesPerSecond;
|
|
16343
16784
|
if (currentDuration >= 1 && metrics.tap1Timestamp === 0) {
|
|
16344
|
-
metrics.tap1Timestamp =
|
|
16785
|
+
metrics.tap1Timestamp = clockSync.timelineNow();
|
|
16345
16786
|
}
|
|
16346
16787
|
if (currentDuration >= 2 && metrics.tap2Timestamp === 0) {
|
|
16347
|
-
metrics.tap2Timestamp =
|
|
16788
|
+
metrics.tap2Timestamp = clockSync.timelineNow();
|
|
16348
16789
|
}
|
|
16349
16790
|
}
|
|
16350
16791
|
if (this.isPlaying && ((_b = this.animationPlayer) == null ? void 0 : _b.isStreamingReady())) {
|
|
@@ -16461,11 +16902,12 @@ class AvatarController {
|
|
|
16461
16902
|
return;
|
|
16462
16903
|
}
|
|
16463
16904
|
const flameKeyframes = keyframes;
|
|
16905
|
+
this.receivedAnimationFrames += flameKeyframes.length;
|
|
16464
16906
|
if (this.currentKeyframes.length === 0) {
|
|
16465
16907
|
this.currentKeyframes = flameKeyframes;
|
|
16466
16908
|
if (this.playbackMode === DrivingServiceMode.backend && !this.hostModeMetrics.didRecvFirstFlame) {
|
|
16467
16909
|
this.hostModeMetrics.didRecvFirstFlame = true;
|
|
16468
|
-
this.hostModeMetrics.recvFirstFlameTimestamp =
|
|
16910
|
+
this.hostModeMetrics.recvFirstFlameTimestamp = clockSync.timelineNow();
|
|
16469
16911
|
}
|
|
16470
16912
|
} else {
|
|
16471
16913
|
this.currentKeyframes.push(...flameKeyframes);
|
|
@@ -16601,6 +17043,8 @@ class AvatarController {
|
|
|
16601
17043
|
this.lastSyncLogTime = 0;
|
|
16602
17044
|
this.lastOutOfBoundsState = false;
|
|
16603
17045
|
this.animationEnded = false;
|
|
17046
|
+
this.receivedAudioBytes = 0;
|
|
17047
|
+
this.receivedAnimationFrames = 0;
|
|
16604
17048
|
if (this.isAudioStalledForStarvation) {
|
|
16605
17049
|
this.isAudioStalledForStarvation = false;
|
|
16606
17050
|
(_a = this.onPlaybackStall) == null ? void 0 : _a.call(this, false);
|
|
@@ -16793,6 +17237,9 @@ class AvatarController {
|
|
|
16793
17237
|
await this.animationPlayer.prepareStreamingPlayer(() => {
|
|
16794
17238
|
this.onPlaybackEnded();
|
|
16795
17239
|
});
|
|
17240
|
+
this.animationPlayer.onAudioStall((stalled) => {
|
|
17241
|
+
this.trackAudioBufferStall(stalled);
|
|
17242
|
+
});
|
|
16796
17243
|
if (!this.isFallbackMode) {
|
|
16797
17244
|
this.emit("startRendering");
|
|
16798
17245
|
}
|
|
@@ -16810,7 +17257,7 @@ class AvatarController {
|
|
|
16810
17257
|
this.pendingAudioChunks = [];
|
|
16811
17258
|
this.isPlaying = true;
|
|
16812
17259
|
this.currentState = AvatarState.playing;
|
|
16813
|
-
this.notifyConversationState(AvatarState.playing
|
|
17260
|
+
this.notifyConversationState(AvatarState.playing);
|
|
16814
17261
|
this.isStartingPlayback = false;
|
|
16815
17262
|
logEvent("playback_started", "info", {
|
|
16816
17263
|
avatar_id: this.avatar.id,
|
|
@@ -16953,13 +17400,28 @@ class AvatarController {
|
|
|
16953
17400
|
const totalKeyframes = this.currentKeyframes.length + this.keyframesOffset;
|
|
16954
17401
|
const frameDiff = frameIndex - totalKeyframes;
|
|
16955
17402
|
if (!(this.reqEnd && frameDiff <= 1)) {
|
|
16956
|
-
this.frameStarvationEvents.push({
|
|
17403
|
+
this.frameStarvationEvents.push({
|
|
17404
|
+
audioTime,
|
|
17405
|
+
reqEnd: this.reqEnd,
|
|
17406
|
+
enterMono: performance.now(),
|
|
17407
|
+
durationMs: null,
|
|
17408
|
+
startTs: clockSync.timelineNow(),
|
|
17409
|
+
framesAtEnter: this.receivedAnimationFrames,
|
|
17410
|
+
framesAtRecover: null
|
|
17411
|
+
});
|
|
16957
17412
|
}
|
|
16958
17413
|
}
|
|
16959
17414
|
if (!this.animationEnded) {
|
|
16960
17415
|
this.pauseAudioForStarvation();
|
|
16961
17416
|
}
|
|
16962
17417
|
} else {
|
|
17418
|
+
if (this.isFrameStarved) {
|
|
17419
|
+
const last = this.frameStarvationEvents[this.frameStarvationEvents.length - 1];
|
|
17420
|
+
if (last && last.durationMs === null) {
|
|
17421
|
+
last.durationMs = Math.round(performance.now() - last.enterMono);
|
|
17422
|
+
last.framesAtRecover = this.receivedAnimationFrames;
|
|
17423
|
+
}
|
|
17424
|
+
}
|
|
16963
17425
|
this.isFrameStarved = false;
|
|
16964
17426
|
if (isOutOfBounds !== this.lastOutOfBoundsState) {
|
|
16965
17427
|
this.lastOutOfBoundsState = isOutOfBounds;
|
|
@@ -16967,6 +17429,33 @@ class AvatarController {
|
|
|
16967
17429
|
this.resumeAudioFromStarvation();
|
|
16968
17430
|
}
|
|
16969
17431
|
}
|
|
17432
|
+
/**
|
|
17433
|
+
* 记录音频缓冲卡顿(缓冲耗尽自动暂停 / 有数据自动恢复),并入 playback_quality.audio_stall。
|
|
17434
|
+
* 与 trackFrameStarvation 对称:进入 push 事件、恢复回填时长与动画帧数。
|
|
17435
|
+
* @param stalled true=进入卡顿,false=恢复
|
|
17436
|
+
* @internal
|
|
17437
|
+
*/
|
|
17438
|
+
trackAudioBufferStall(stalled) {
|
|
17439
|
+
if (stalled) {
|
|
17440
|
+
if (this.isAudioBufferStalled) return;
|
|
17441
|
+
this.isAudioBufferStalled = true;
|
|
17442
|
+
this.audioStallEvents.push({
|
|
17443
|
+
enterMono: performance.now(),
|
|
17444
|
+
durationMs: null,
|
|
17445
|
+
startTs: clockSync.timelineNow(),
|
|
17446
|
+
framesAtEnter: this.receivedAnimationFrames,
|
|
17447
|
+
framesAtRecover: null
|
|
17448
|
+
});
|
|
17449
|
+
} else {
|
|
17450
|
+
if (!this.isAudioBufferStalled) return;
|
|
17451
|
+
this.isAudioBufferStalled = false;
|
|
17452
|
+
const last = this.audioStallEvents[this.audioStallEvents.length - 1];
|
|
17453
|
+
if (last && last.durationMs === null) {
|
|
17454
|
+
last.durationMs = Math.round(performance.now() - last.enterMono);
|
|
17455
|
+
last.framesAtRecover = this.receivedAnimationFrames;
|
|
17456
|
+
}
|
|
17457
|
+
}
|
|
17458
|
+
}
|
|
16970
17459
|
/**
|
|
16971
17460
|
* strictSync: pause audio on frame starvation, waiting for new frames.
|
|
16972
17461
|
*
|
|
@@ -17042,7 +17531,6 @@ class AvatarController {
|
|
|
17042
17531
|
if (this.isFallbackMode) {
|
|
17043
17532
|
return;
|
|
17044
17533
|
}
|
|
17045
|
-
logger.warn("[AvatarController] Enabling fallback mode");
|
|
17046
17534
|
this.isFallbackMode = true;
|
|
17047
17535
|
this.resumeAudioFromStarvation();
|
|
17048
17536
|
logEvent("fallback_mode_entered", "warning", {
|
|
@@ -17076,26 +17564,15 @@ class AvatarController {
|
|
|
17076
17564
|
* @internal
|
|
17077
17565
|
*/
|
|
17078
17566
|
onPlaybackEnded() {
|
|
17079
|
-
var _a
|
|
17080
|
-
if (this.frameStarvationEvents.length > 0) {
|
|
17081
|
-
const hasReqEnd = this.frameStarvationEvents.some((e2) => e2.reqEnd);
|
|
17082
|
-
logEvent("frame_starvation", "warning", {
|
|
17083
|
-
avatar_id: this.avatar.id,
|
|
17084
|
-
conversation_id: ((_a = this.networkLayer) == null ? void 0 : _a.getCurrentConversationId()) || void 0,
|
|
17085
|
-
starvation_count: this.frameStarvationEvents.length,
|
|
17086
|
-
starvation_times: this.frameStarvationEvents.map((e2) => Number(e2.audioTime.toFixed(3))),
|
|
17087
|
-
...hasReqEnd ? { req_end: true } : {}
|
|
17088
|
-
});
|
|
17089
|
-
}
|
|
17090
|
-
this.frameStarvationEvents = [];
|
|
17091
|
-
this.isFrameStarved = false;
|
|
17567
|
+
var _a;
|
|
17092
17568
|
logEvent("playback_ended", "info", {
|
|
17093
17569
|
avatar_id: this.avatar.id,
|
|
17094
|
-
conversation_id: this.currentConversationId || ((
|
|
17570
|
+
conversation_id: this.currentConversationId || ((_a = this.networkLayer) == null ? void 0 : _a.getCurrentConversationId()) || void 0,
|
|
17095
17571
|
source: this.isFallbackMode ? "fallback" : "streaming"
|
|
17096
17572
|
});
|
|
17097
17573
|
this.isPlaying = false;
|
|
17098
17574
|
this.currentState = AvatarState.idle;
|
|
17575
|
+
this.playbackEndReason = "completed";
|
|
17099
17576
|
this.notifyConversationState(AvatarState.idle);
|
|
17100
17577
|
this.emit("stopRendering");
|
|
17101
17578
|
this.emit("stopFallback");
|
|
@@ -17113,6 +17590,7 @@ class AvatarController {
|
|
|
17113
17590
|
this.emit("stopRendering");
|
|
17114
17591
|
this.isPlaying = false;
|
|
17115
17592
|
this.currentState = AvatarState.idle;
|
|
17593
|
+
this.playbackEndReason = "interrupted";
|
|
17116
17594
|
this.notifyConversationState(AvatarState.idle);
|
|
17117
17595
|
}
|
|
17118
17596
|
/**
|
|
@@ -17490,7 +17968,7 @@ class AvatarDownloader {
|
|
|
17490
17968
|
if (cached) {
|
|
17491
17969
|
const duration = Date.now() - startTime;
|
|
17492
17970
|
logger.log(`✅ Unified template loaded from cache (${(cached.byteLength / 1024 / 1024).toFixed(1)} MB)`);
|
|
17493
|
-
|
|
17971
|
+
logMetric("template_resources_load_measure", "info", {
|
|
17494
17972
|
duration,
|
|
17495
17973
|
file_count: 1,
|
|
17496
17974
|
cache_hit: true,
|
|
@@ -17519,7 +17997,7 @@ class AvatarDownloader {
|
|
|
17519
17997
|
logger.warn(`[loadUnifiedTemplate] Failed to cache:`, err);
|
|
17520
17998
|
});
|
|
17521
17999
|
const duration = Date.now() - startTime;
|
|
17522
|
-
|
|
18000
|
+
logMetric("template_resources_load_measure", "info", {
|
|
17523
18001
|
duration,
|
|
17524
18002
|
file_count: 1,
|
|
17525
18003
|
cache_hit: false,
|
|
@@ -17672,7 +18150,7 @@ class AvatarDownloader {
|
|
|
17672
18150
|
const totalSize = Object.values(characterData).reduce((sum, buffer) => {
|
|
17673
18151
|
return sum + (buffer ? buffer.byteLength : 0);
|
|
17674
18152
|
}, 0);
|
|
17675
|
-
|
|
18153
|
+
logMetric("download_avatar_assets_latency", "info", {
|
|
17676
18154
|
resolution: "default",
|
|
17677
18155
|
// 目前写死 default
|
|
17678
18156
|
avatar_id: characterMeta.characterId ?? "unknown",
|
|
@@ -17866,7 +18344,7 @@ class AvatarDownloader {
|
|
|
17866
18344
|
);
|
|
17867
18345
|
}
|
|
17868
18346
|
const duration = Date.now() - startTime;
|
|
17869
|
-
|
|
18347
|
+
logMetric("fetch_avatar_metadata_latency", "info", {
|
|
17870
18348
|
avatar_id: characterId,
|
|
17871
18349
|
duration,
|
|
17872
18350
|
trace_id: traceId
|
|
@@ -18108,7 +18586,7 @@ const _AvatarManager = class _AvatarManager {
|
|
|
18108
18586
|
this.avatarCache.set(id, avatar);
|
|
18109
18587
|
logger.log("[AvatarManager] Avatar loaded successfully");
|
|
18110
18588
|
const totalDuration = Date.now() - startTime;
|
|
18111
|
-
|
|
18589
|
+
logMetric("fetch_avatar_latency", "info", {
|
|
18112
18590
|
resolution: "default",
|
|
18113
18591
|
// 目前写死 default
|
|
18114
18592
|
avatar_id: id,
|
|
@@ -20400,6 +20878,11 @@ class AvatarView {
|
|
|
20400
20878
|
* @internal
|
|
20401
20879
|
*/
|
|
20402
20880
|
createCanvas(container) {
|
|
20881
|
+
if (container.querySelector("canvas")) {
|
|
20882
|
+
logger.warn(
|
|
20883
|
+
"[AvatarView] The container already contains a <canvas>. This usually means another AvatarView was created in the same container without disposing the previous one, which will render two avatars side by side. Call dispose() on the previous AvatarView before creating a new one, or use a separate container."
|
|
20884
|
+
);
|
|
20885
|
+
}
|
|
20403
20886
|
const canvas = document.createElement("canvas");
|
|
20404
20887
|
const containerWidth = container.offsetWidth || 800;
|
|
20405
20888
|
const containerHeight = container.offsetHeight || 600;
|
|
@@ -21746,12 +22229,13 @@ export {
|
|
|
21746
22229
|
AvatarView as g,
|
|
21747
22230
|
RENDER_QUALITY_PARAMS as h,
|
|
21748
22231
|
DEFAULT_REGION as i,
|
|
21749
|
-
|
|
21750
|
-
|
|
22232
|
+
DEFAULT_REGION_REQUEST as j,
|
|
22233
|
+
LoadProgress as k,
|
|
21751
22234
|
logger as l,
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
22235
|
+
AnimationType as m,
|
|
22236
|
+
ConversationState as n,
|
|
22237
|
+
AvatarState as o,
|
|
22238
|
+
AvatarError as p,
|
|
22239
|
+
ResourceType as q,
|
|
22240
|
+
extractResourceUrls as r
|
|
21757
22241
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { m, b, c, p, f, d, o, g, C, n, i, j, D, E, F, k, L, h, R, q, T, r } from "./index-CPolDcOo.js";
|
|
2
2
|
export {
|
|
3
|
-
|
|
3
|
+
m as AnimationType,
|
|
4
4
|
b as Avatar,
|
|
5
5
|
c as AvatarController,
|
|
6
|
-
|
|
6
|
+
p as AvatarError,
|
|
7
7
|
f as AvatarManager,
|
|
8
8
|
d as AvatarSDK,
|
|
9
|
-
|
|
9
|
+
o as AvatarState,
|
|
10
10
|
g as AvatarView,
|
|
11
11
|
C as ConnectionState,
|
|
12
|
-
|
|
12
|
+
n as ConversationState,
|
|
13
13
|
i as DEFAULT_REGION,
|
|
14
|
+
j as DEFAULT_REGION_REQUEST,
|
|
14
15
|
D as DrivingServiceMode,
|
|
15
16
|
E as ErrorCode,
|
|
16
17
|
F as FrameStarvationMode,
|
|
17
|
-
|
|
18
|
+
k as LoadProgress,
|
|
18
19
|
L as LogLevel,
|
|
19
20
|
h as RENDER_QUALITY_PARAMS,
|
|
20
21
|
R as RenderQuality,
|
|
21
|
-
|
|
22
|
+
q as ResourceType,
|
|
22
23
|
T as TransitionType,
|
|
23
|
-
|
|
24
|
+
r as extractResourceUrls
|
|
24
25
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spatius/avatarkit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.1-beta.2",
|
|
5
5
|
"packageManager": "pnpm@10.18.2",
|
|
6
6
|
"description": "AvatarKit SDK - Real-time Avatar Rendering SDK for Web",
|
|
7
7
|
"author": "AvatarKit Team",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@types/node": "^20.11.30",
|
|
89
89
|
"@webgpu/types": "^0.1.65",
|
|
90
|
+
"fflate": "0.8.3",
|
|
90
91
|
"tsx": "^4.20.6",
|
|
91
92
|
"typescript": "^5.0.0",
|
|
92
93
|
"vite": "^5.0.0",
|