@spatius/avatarkit 1.3.1-beta.7 → 1.3.1
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 +22 -0
- package/dist/{AvatarDownloader-B1tJTnIb.js → AvatarDownloader-DyhenPKd.js} +98 -43
- package/dist/{AvatarSDK-CTprrfJk.js → AvatarSDK-DgPvHU4L.js} +162 -2560
- package/dist/{OpusCodec-PWv7ynPp.js → OpusCodec-CXU7ysrU.js} +43 -9
- package/dist/{OpusDecoderProxy-m0fVN_M2.js → OpusDecoderProxy-Cq8Z_MZY.js} +19 -6
- package/dist/{OpusEncoderProxy-DiW21sBK.js → OpusEncoderProxy-CzY-VckE.js} +42 -7
- package/dist/{StreamingAudioPlayer-C6hgkvbK.js → StreamingAudioPlayer-CIJmBp3z.js} +2 -2
- package/dist/assets/{AvatarDownloader-LO9uuYvm.js → AvatarDownloader-G5FO1bVl.js} +96 -41
- package/dist/assets/{AvatarSDK-CPEeTeLT.js → AvatarSDK-C_Anluwp.js} +149 -12
- package/dist/assets/{OpusDecoderWorker.worker-CSSWn75M.js → OpusDecoderWorker.worker-C2T0afkj.js} +21 -5
- package/dist/assets/{OpusEncoderWorker.worker-q4TdCVDZ.js → OpusEncoderWorker.worker-DLYO9vPK.js} +53 -11
- package/dist/assets/{logger-DCzHWd4N.js → logger-YPooEAbA.js} +319 -126
- package/dist/core/AvatarController.d.ts +10 -0
- package/dist/{error-utils-BCZDCrc6.js → error-utils-BogEllAd.js} +1 -1
- package/dist/index.js +274 -43
- package/dist/internal-telemetry.d.ts +45 -20
- package/dist/internal-telemetry.js +27 -2
- package/dist/{logger-DdmfSEn-.js → logger-BOpQ7u0w.js} +336 -126
- package/dist/otel-trace-Ct4T48nC.js +2565 -0
- package/dist/{pwa-cache-manager-BMKyJWXI.js → pwa-cache-manager-C7NbU52Q.js} +1 -1
- package/dist/types/index.d.ts +30 -4
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as __exportAll, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-B-1-B7_t.js";
|
|
2
|
-
import { t as logger } from "./logger-
|
|
2
|
+
import { t as logger } from "./logger-BOpQ7u0w.js";
|
|
3
3
|
/** Opus granule positions are always in 48 kHz samples, regardless of input rate. */
|
|
4
4
|
var OPUS_GRANULE_RATE = 48e3;
|
|
5
5
|
/**
|
|
@@ -41418,12 +41418,26 @@ var OggOpusEncoder = class {
|
|
|
41418
41418
|
* @param end true on the final chunk of the req_id; flushes the tail and emits the EOS page.
|
|
41419
41419
|
* @returns Ogg page byte arrays to send over the socket, in order.
|
|
41420
41420
|
*/
|
|
41421
|
-
|
|
41421
|
+
/**
|
|
41422
|
+
* @param onFrame 可选:每编完一帧(20ms)回调一次,参数是这帧的 PCM 字节数。
|
|
41423
|
+
* 给延迟打点用——编码内部逐帧进行,但结果打成页一起返回;只按整批记时会把
|
|
41424
|
+
* 一次调用的全部耗时算成一段(实测 1068ms 一条),看不出编码速率是否稳定。
|
|
41425
|
+
* @param onPages 可选:**每凑满 1 秒音频就吐一批页**,让调用方边编边发。
|
|
41426
|
+
*
|
|
41427
|
+
* 不给这个回调时退化为老行为(全部编完一次性返回)。宿主一次性灌入整段音频时,
|
|
41428
|
+
* 老行为要等全部编完才发出第一个包——实测 12 秒音频编码 1105ms,首包上行就白等
|
|
41429
|
+
* 这 1105ms(起播 startup_buffering 1804ms 里大半是它)。编 1 秒发 1 秒后,
|
|
41430
|
+
* 首包在几十毫秒内就能上线。
|
|
41431
|
+
*/
|
|
41432
|
+
encode(reqId, pcm, end, onFrame, onPages) {
|
|
41422
41433
|
const pages = [];
|
|
41434
|
+
const emit = onPages ?? ((built) => {
|
|
41435
|
+
pages.push(...built);
|
|
41436
|
+
});
|
|
41423
41437
|
if (reqId !== this.currentReqId) {
|
|
41424
41438
|
this.currentReqId = reqId;
|
|
41425
41439
|
this.pcmRemainder = /* @__PURE__ */ new Int16Array(0);
|
|
41426
|
-
|
|
41440
|
+
emit(this.muxer.startStream(reqId));
|
|
41427
41441
|
}
|
|
41428
41442
|
const incoming = this.bytesToInt16(pcm);
|
|
41429
41443
|
let samples = incoming;
|
|
@@ -41433,18 +41447,29 @@ var OggOpusEncoder = class {
|
|
|
41433
41447
|
samples.set(incoming, this.pcmRemainder.length);
|
|
41434
41448
|
}
|
|
41435
41449
|
const frameGranule = this.muxer.toGranule(this.frameSamples);
|
|
41436
|
-
const
|
|
41450
|
+
const FRAMES_PER_SECOND = Math.round(1e3 / FRAME_MS);
|
|
41451
|
+
let packets = [];
|
|
41437
41452
|
let offset = 0;
|
|
41453
|
+
const flushPackets = () => {
|
|
41454
|
+
if (packets.length === 0) return;
|
|
41455
|
+
const built = this.muxer.addPackets(packets, frameGranule);
|
|
41456
|
+
packets = [];
|
|
41457
|
+
emit(built);
|
|
41458
|
+
};
|
|
41438
41459
|
while (samples.length - offset >= this.frameSamples) {
|
|
41439
41460
|
const frame = samples.subarray(offset, offset + this.frameSamples);
|
|
41440
41461
|
offset += this.frameSamples;
|
|
41441
41462
|
const packet = this.encodeFrame(frame);
|
|
41442
|
-
if (packet)
|
|
41463
|
+
if (packet) {
|
|
41464
|
+
packets.push(packet);
|
|
41465
|
+
onFrame?.(frame.length * 2);
|
|
41466
|
+
}
|
|
41467
|
+
if (onPages && packets.length >= FRAMES_PER_SECOND) flushPackets();
|
|
41443
41468
|
}
|
|
41444
|
-
|
|
41469
|
+
flushPackets();
|
|
41445
41470
|
this.pcmRemainder = samples.slice(offset);
|
|
41446
41471
|
if (end) {
|
|
41447
|
-
|
|
41472
|
+
emit(this.flush());
|
|
41448
41473
|
this.currentReqId = null;
|
|
41449
41474
|
}
|
|
41450
41475
|
return pages;
|
|
@@ -41522,9 +41547,15 @@ var OggOpusDecoder = class {
|
|
|
41522
41547
|
* trailing page is buffered until the rest arrives.
|
|
41523
41548
|
*
|
|
41524
41549
|
* @param oggBytes raw Ogg stream bytes (any slice boundary).
|
|
41550
|
+
* @param onSegment 可选:每解出一个 packet 就回调一次那一段 PCM。
|
|
41551
|
+
*
|
|
41552
|
+
* 给延迟打点用。`tap_N` 的语义是「第 N 秒音频进入播放链路的时刻」,若只在
|
|
41553
|
+
* decode 返回后按整块累计,宿主一次性喂进整段 Ogg 时所有 tap 会挤在同一个
|
|
41554
|
+
* 时刻(实测 tap_1/2/4 三条同值),送入节奏这个信息就没了。解码本来就是逐包
|
|
41555
|
+
* 进行的,把分段暴露出来,打点才能落在真实的音频时间轴上。
|
|
41525
41556
|
* @returns decoded PCM16 bytes, possibly empty.
|
|
41526
41557
|
*/
|
|
41527
|
-
decode(oggBytes) {
|
|
41558
|
+
decode(oggBytes, onSegment) {
|
|
41528
41559
|
const incoming = oggBytes instanceof Uint8Array ? oggBytes : new Uint8Array(oggBytes);
|
|
41529
41560
|
let buf;
|
|
41530
41561
|
if (this.carry.length > 0) {
|
|
@@ -41540,7 +41571,10 @@ var OggOpusDecoder = class {
|
|
|
41540
41571
|
offset = page.nextOffset;
|
|
41541
41572
|
for (const packet of page.packets) {
|
|
41542
41573
|
const pcm = this.decodePacket(packet);
|
|
41543
|
-
if (pcm.length > 0)
|
|
41574
|
+
if (pcm.length > 0) {
|
|
41575
|
+
pcmChunks.push(pcm);
|
|
41576
|
+
onSegment?.(pcm);
|
|
41577
|
+
}
|
|
41544
41578
|
}
|
|
41545
41579
|
}
|
|
41546
41580
|
this.carry = offset < buf.length ? buf.slice(offset) : /* @__PURE__ */ new Uint8Array(0);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as logger } from "./logger-
|
|
1
|
+
import { t as logger } from "./logger-BOpQ7u0w.js";
|
|
2
2
|
//#region audio/OpusDecoderProxy.ts
|
|
3
3
|
var OpusDecoderProxy = class OpusDecoderProxy {
|
|
4
4
|
worker = null;
|
|
@@ -21,7 +21,7 @@ var OpusDecoderProxy = class OpusDecoderProxy {
|
|
|
21
21
|
} catch (err) {
|
|
22
22
|
logger.warn("[Opus][DecoderProxy] Worker unavailable, falling back to main-thread decode:", err instanceof Error ? err.message : String(err));
|
|
23
23
|
}
|
|
24
|
-
const { OggOpusDecoder } = await import("./OpusCodec-
|
|
24
|
+
const { OggOpusDecoder } = await import("./OpusCodec-CXU7ysrU.js").then((n) => n.n);
|
|
25
25
|
proxy.fallbackDecoder = new OggOpusDecoder();
|
|
26
26
|
proxy.backend = "main-thread";
|
|
27
27
|
return proxy;
|
|
@@ -32,7 +32,7 @@ var OpusDecoderProxy = class OpusDecoderProxy {
|
|
|
32
32
|
try {
|
|
33
33
|
worker = new Worker(new URL(
|
|
34
34
|
/* @vite-ignore */
|
|
35
|
-
"/assets/OpusDecoderWorker.worker-
|
|
35
|
+
"/assets/OpusDecoderWorker.worker-C2T0afkj.js",
|
|
36
36
|
"" + import.meta.url
|
|
37
37
|
), { type: "module" });
|
|
38
38
|
} catch (err) {
|
|
@@ -56,7 +56,11 @@ var OpusDecoderProxy = class OpusDecoderProxy {
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
if (d.type === "pcm") {
|
|
59
|
-
this.pending.get(d.seq)?.resolve(
|
|
59
|
+
this.pending.get(d.seq)?.resolve({
|
|
60
|
+
pcm: new Uint8Array(d.pcm),
|
|
61
|
+
segments: d.segments ?? [],
|
|
62
|
+
segmentTs: (d.segmentTs ?? []).map((ts) => ts - performance.timeOrigin)
|
|
63
|
+
});
|
|
60
64
|
this.pending.delete(d.seq);
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
@@ -103,8 +107,17 @@ var OpusDecoderProxy = class OpusDecoderProxy {
|
|
|
103
107
|
});
|
|
104
108
|
}
|
|
105
109
|
if (this.fallbackDecoder) try {
|
|
106
|
-
const
|
|
107
|
-
|
|
110
|
+
const segments = [];
|
|
111
|
+
const segmentTs = [];
|
|
112
|
+
const pcm = this.fallbackDecoder.decode(ogg, (seg) => {
|
|
113
|
+
segments.push(seg.length);
|
|
114
|
+
segmentTs.push(performance.now());
|
|
115
|
+
});
|
|
116
|
+
return Promise.resolve({
|
|
117
|
+
pcm,
|
|
118
|
+
segments,
|
|
119
|
+
segmentTs
|
|
120
|
+
});
|
|
108
121
|
} catch (err) {
|
|
109
122
|
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
110
123
|
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as __exportAll } from "./rolldown-runtime-B-1-B7_t.js";
|
|
2
|
+
import { t as logger } from "./logger-BOpQ7u0w.js";
|
|
2
3
|
//#region audio/OpusEncoderProxy.ts
|
|
4
|
+
var OpusEncoderProxy_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
OpusEncoderProxy: () => OpusEncoderProxy,
|
|
6
|
+
setOpusEncodeFrameSink: () => setOpusEncodeFrameSink
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* 逐帧编码时刻的接收钩子。由 `AnimationWebSocketClient` 装上,转交 trace 打点。
|
|
10
|
+
*
|
|
11
|
+
* 与解码那侧同理:编码在 Worker 里逐帧进行,结果打成页一起返回;只有把每帧的时刻
|
|
12
|
+
* 带回来,span 才能按音频秒数切,而不是把整批耗时算成一段。
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
var onEncodeFrames = null;
|
|
16
|
+
/** @internal */
|
|
17
|
+
function setOpusEncodeFrameSink(fn) {
|
|
18
|
+
onEncodeFrames = fn;
|
|
19
|
+
}
|
|
3
20
|
var OpusEncoderProxy = class OpusEncoderProxy {
|
|
4
21
|
worker = null;
|
|
5
22
|
fallbackEncoder = null;
|
|
@@ -26,7 +43,7 @@ var OpusEncoderProxy = class OpusEncoderProxy {
|
|
|
26
43
|
} catch (err) {
|
|
27
44
|
logger.warn("[Opus][Proxy] Worker unavailable, falling back to main-thread encode:", err instanceof Error ? err.message : String(err));
|
|
28
45
|
}
|
|
29
|
-
const { OggOpusEncoder } = await import("./OpusCodec-
|
|
46
|
+
const { OggOpusEncoder } = await import("./OpusCodec-CXU7ysrU.js").then((n) => n.n);
|
|
30
47
|
proxy.fallbackEncoder = new OggOpusEncoder(sampleRate, bitrate);
|
|
31
48
|
proxy.backend = "main-thread";
|
|
32
49
|
return proxy;
|
|
@@ -37,7 +54,7 @@ var OpusEncoderProxy = class OpusEncoderProxy {
|
|
|
37
54
|
try {
|
|
38
55
|
worker = new Worker(new URL(
|
|
39
56
|
/* @vite-ignore */
|
|
40
|
-
"/assets/OpusEncoderWorker.worker-
|
|
57
|
+
"/assets/OpusEncoderWorker.worker-DLYO9vPK.js",
|
|
41
58
|
"" + import.meta.url
|
|
42
59
|
), { type: "module" });
|
|
43
60
|
} catch (err) {
|
|
@@ -61,6 +78,11 @@ var OpusEncoderProxy = class OpusEncoderProxy {
|
|
|
61
78
|
return;
|
|
62
79
|
}
|
|
63
80
|
if (d.type === "pages") {
|
|
81
|
+
if (!d.last) {
|
|
82
|
+
this.pending.get(d.seq)?.onPages(d.pages);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
onEncodeFrames?.(d.frames ?? [], (d.frameTs ?? []).map((ts) => ts - performance.timeOrigin));
|
|
64
86
|
this.pending.get(d.seq)?.resolve(d.pages);
|
|
65
87
|
this.pending.delete(d.seq);
|
|
66
88
|
return;
|
|
@@ -92,13 +114,19 @@ var OpusEncoderProxy = class OpusEncoderProxy {
|
|
|
92
114
|
* worker path the main thread stays free while encoding runs; on the fallback
|
|
93
115
|
* path this blocks the main thread (same as the legacy behavior).
|
|
94
116
|
*/
|
|
95
|
-
|
|
117
|
+
/**
|
|
118
|
+
* @param onPages 每凑满 1 秒音频回调一次那一批页,调用方应立刻发出去。
|
|
119
|
+
* 返回的 Promise 只带**收尾批**(通常为空)——所有页都从这个回调出去,等
|
|
120
|
+
* Promise 再发会退回「整段编完才发」,白等一秒以上(见 OpusCodec.encode)。
|
|
121
|
+
*/
|
|
122
|
+
encode(reqId, pcm, end, onPages = () => {}) {
|
|
96
123
|
if (this.worker) {
|
|
97
124
|
const seq = this.seq++;
|
|
98
125
|
return new Promise((resolve, reject) => {
|
|
99
126
|
this.pending.set(seq, {
|
|
100
127
|
resolve,
|
|
101
|
-
reject
|
|
128
|
+
reject,
|
|
129
|
+
onPages
|
|
102
130
|
});
|
|
103
131
|
this.worker.postMessage({
|
|
104
132
|
type: "encode",
|
|
@@ -110,7 +138,14 @@ var OpusEncoderProxy = class OpusEncoderProxy {
|
|
|
110
138
|
});
|
|
111
139
|
}
|
|
112
140
|
if (this.fallbackEncoder) try {
|
|
113
|
-
|
|
141
|
+
const frames = [];
|
|
142
|
+
const frameTs = [];
|
|
143
|
+
const pages = this.fallbackEncoder.encode(reqId, pcm, end, (pcmBytes) => {
|
|
144
|
+
frames.push(pcmBytes);
|
|
145
|
+
frameTs.push(performance.now());
|
|
146
|
+
}, onPages);
|
|
147
|
+
onEncodeFrames?.(frames, frameTs);
|
|
148
|
+
return Promise.resolve(pages);
|
|
114
149
|
} catch (err) {
|
|
115
150
|
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
116
151
|
}
|
|
@@ -132,4 +167,4 @@ var OpusEncoderProxy = class OpusEncoderProxy {
|
|
|
132
167
|
}
|
|
133
168
|
};
|
|
134
169
|
//#endregion
|
|
135
|
-
export {
|
|
170
|
+
export { setOpusEncodeFrameSink as n, OpusEncoderProxy_exports as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as logEvent, t as logger } from "./logger-
|
|
2
|
-
import { n as APP_CONFIG, t as errorToMessage } from "./error-utils-
|
|
1
|
+
import { c as logEvent, t as logger } from "./logger-BOpQ7u0w.js";
|
|
2
|
+
import { n as APP_CONFIG, t as errorToMessage } from "./error-utils-BogEllAd.js";
|
|
3
3
|
//#region audio/StreamingAudioPlayer.ts
|
|
4
4
|
/**
|
|
5
5
|
* Streaming Audio Player
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as AvatarSDK } from "./AvatarSDK-
|
|
1
|
+
import { bt as ErrorCode, c as logEvent, gt as AvatarError, h as hostOf, ht as generateTraceId, l as logMetric, pt as isDebugMode, t as logger, y as recordHttpClientDuration } from "./logger-YPooEAbA.js";
|
|
2
|
+
import { t as AvatarSDK } from "./AvatarSDK-C_Anluwp.js";
|
|
3
3
|
//#region config/app-config.ts
|
|
4
4
|
const GLOBAL_FLAME_CDN_BASE = "https://cdn.spatialwalk.cloud/public";
|
|
5
5
|
const CN_FLAME_CDN_BASE = "https://cdn.spatialwalk.top/public";
|
|
@@ -273,20 +273,46 @@ function getCacheInfo(url, response) {
|
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
* 把整次角色加载的资产下载汇总成**一条** Transaction。
|
|
277
|
+
*
|
|
278
|
+
* 成败取「最差的一片」:任一片 transport error 即整次记 transport error;否则取
|
|
279
|
+
* 状态码最大的那个(4xx/5xx 会盖过 200)。这样后端按状态码切出的成功率,含义是
|
|
280
|
+
* 「这次加载的资产是否全部拿到」——与用户实际体验一致,一片挂了角色就出不来。
|
|
281
|
+
*
|
|
282
|
+
* duration 传整体耗时而非单片,与 `download_avatar_assets_latency` 同口径。
|
|
283
|
+
*/
|
|
284
|
+
function recordAssetTransaction(outcomes, durationMs, operation = ASSET_OPERATION) {
|
|
285
|
+
const real = outcomes.filter((o) => !o.skipped);
|
|
286
|
+
if (real.length === 0) {
|
|
287
|
+
recordHttpClientDuration({
|
|
288
|
+
operation,
|
|
289
|
+
method: "GET",
|
|
290
|
+
durationMs,
|
|
291
|
+
statusCode: 200,
|
|
292
|
+
cacheHit: true
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
recordHttpClientDuration({
|
|
297
|
+
operation,
|
|
298
|
+
method: "GET",
|
|
299
|
+
durationMs,
|
|
300
|
+
statusCode: real.some((o) => o.statusCode === void 0) ? void 0 : real.reduce((max, o) => Math.max(max, o.statusCode ?? 0), 0),
|
|
301
|
+
serverAddress: real[0].host,
|
|
302
|
+
cacheHit: false
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Transaction 的 `operation` 取值。必须低基数:资产 URL 带 avatar id 与文件名,
|
|
307
|
+
* 直接当维度会让时间序列随角色数无限增长。要定位「哪个文件挂了」用
|
|
308
|
+
* `download_avatar_assets_failed` log,它带 resource/url/error。
|
|
287
309
|
*/
|
|
310
|
+
const ASSET_OPERATION = "/assets/character";
|
|
311
|
+
const TEMPLATE_OPERATION = "/assets/template";
|
|
288
312
|
async function downloadResource(url, options) {
|
|
289
|
-
const { signal, characterId, resourceType, maxRetries = 3 } = options || {};
|
|
313
|
+
const { signal, characterId, resourceType, maxRetries = 3, outcomes } = options || {};
|
|
314
|
+
const outcomeSlotIndex = outcomes ? outcomes.length : -1;
|
|
315
|
+
if (outcomes) outcomes.push({ host: hostOf(url) });
|
|
290
316
|
if (signal?.aborted) throw new Error("Download cancelled");
|
|
291
317
|
try {
|
|
292
318
|
let cached = null;
|
|
@@ -305,6 +331,10 @@ async function downloadResource(url, options) {
|
|
|
305
331
|
cacheInfo.cacheHit = true;
|
|
306
332
|
cacheInfo.cacheType = "pwa";
|
|
307
333
|
cacheInfo.pwaCacheSubtype = pwaCacheSubtype;
|
|
334
|
+
if (outcomes && outcomeSlotIndex >= 0) outcomes[outcomeSlotIndex] = {
|
|
335
|
+
host: "",
|
|
336
|
+
skipped: true
|
|
337
|
+
};
|
|
308
338
|
return {
|
|
309
339
|
data: cached,
|
|
310
340
|
cacheInfo
|
|
@@ -313,8 +343,11 @@ async function downloadResource(url, options) {
|
|
|
313
343
|
let lastError = null;
|
|
314
344
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
315
345
|
if (signal?.aborted) throw new Error("Download cancelled");
|
|
346
|
+
const outcomeSlot = { host: hostOf(url) };
|
|
347
|
+
if (outcomes) outcomes[outcomeSlotIndex] = outcomeSlot;
|
|
316
348
|
try {
|
|
317
349
|
const response = await fetch(url, { signal });
|
|
350
|
+
outcomeSlot.statusCode = response.status;
|
|
318
351
|
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
319
352
|
const arrayBuffer = await response.arrayBuffer();
|
|
320
353
|
const contentLength = response.headers.get("content-length");
|
|
@@ -372,32 +405,41 @@ var AvatarDownloader = class {
|
|
|
372
405
|
cache_hit: true,
|
|
373
406
|
cache_type: "pwa"
|
|
374
407
|
});
|
|
408
|
+
recordAssetTransaction([], duration, TEMPLATE_OPERATION);
|
|
375
409
|
return { unifiedModel: cached };
|
|
376
410
|
}
|
|
377
411
|
const maxRetries = 3;
|
|
378
412
|
let lastError = null;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
413
|
+
const templateOutcomes = [];
|
|
414
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
415
|
+
const outcomeSlot = { host: hostOf(url) };
|
|
416
|
+
templateOutcomes[0] = outcomeSlot;
|
|
417
|
+
try {
|
|
418
|
+
const response = await fetch(url);
|
|
419
|
+
outcomeSlot.statusCode = response.status;
|
|
420
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
421
|
+
let buffer;
|
|
422
|
+
if (APP_CONFIG.flame.unifiedModelPath.endsWith(".gz")) {
|
|
423
|
+
const decompressedStream = response.body.pipeThrough(new DecompressionStream("gzip"));
|
|
424
|
+
buffer = await new Response(decompressedStream).arrayBuffer();
|
|
425
|
+
} else buffer = await response.arrayBuffer();
|
|
426
|
+
logger.log(`✅ Unified template loaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`);
|
|
427
|
+
PwaCacheManager.putTemplateResource(url, buffer).catch((err) => {
|
|
428
|
+
logger.warn(`[loadUnifiedTemplate] Failed to cache:`, err);
|
|
429
|
+
});
|
|
430
|
+
logMetric("template_resources_load_measure", Date.now() - startTime, {
|
|
431
|
+
file_count: 1,
|
|
432
|
+
cache_hit: false,
|
|
433
|
+
cache_type: "none"
|
|
434
|
+
});
|
|
435
|
+
recordAssetTransaction(templateOutcomes, Date.now() - startTime, TEMPLATE_OPERATION);
|
|
436
|
+
return { unifiedModel: buffer };
|
|
437
|
+
} catch (err) {
|
|
438
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
439
|
+
if (attempt < maxRetries) logger.warn(`[loadUnifiedTemplate] Attempt ${attempt}/${maxRetries} failed, retrying...`);
|
|
440
|
+
}
|
|
400
441
|
}
|
|
442
|
+
recordAssetTransaction(templateOutcomes, Date.now() - startTime, TEMPLATE_OPERATION);
|
|
401
443
|
throw lastError || /* @__PURE__ */ new Error(`Failed to download unified template after ${maxRetries} attempts`);
|
|
402
444
|
}
|
|
403
445
|
/**
|
|
@@ -478,6 +520,8 @@ var AvatarDownloader = class {
|
|
|
478
520
|
};
|
|
479
521
|
const characterData = {};
|
|
480
522
|
const cacheInfos = [];
|
|
523
|
+
/** 各片的 HTTP 结果,收尾时汇总成一条 Transaction(见 recordAssetTransaction)。 */
|
|
524
|
+
const httpOutcomes = [];
|
|
481
525
|
const parallelStartTime = Date.now();
|
|
482
526
|
const downloadPromises = filesToLoad.map(async ({ key, url, filename, optional }) => {
|
|
483
527
|
updateProgress(filename, false);
|
|
@@ -485,7 +529,8 @@ var AvatarDownloader = class {
|
|
|
485
529
|
const { data: arrayBuffer, cacheInfo } = await downloadResource(url, {
|
|
486
530
|
signal,
|
|
487
531
|
characterId: characterMeta.characterId ?? void 0,
|
|
488
|
-
resourceType: "character"
|
|
532
|
+
resourceType: "character",
|
|
533
|
+
outcomes: httpOutcomes
|
|
489
534
|
});
|
|
490
535
|
if (key === "shape") characterData.shape = arrayBuffer;
|
|
491
536
|
else if (key === "pointCloud") characterData.pointCloud = arrayBuffer;
|
|
@@ -523,7 +568,11 @@ var AvatarDownloader = class {
|
|
|
523
568
|
};
|
|
524
569
|
}
|
|
525
570
|
});
|
|
526
|
-
|
|
571
|
+
try {
|
|
572
|
+
await Promise.all(downloadPromises);
|
|
573
|
+
} finally {
|
|
574
|
+
recordAssetTransaction(httpOutcomes, Date.now() - totalStartTime);
|
|
575
|
+
}
|
|
527
576
|
const parallelDuration = Date.now() - parallelStartTime;
|
|
528
577
|
const totalDuration = Date.now() - totalStartTime;
|
|
529
578
|
if (!characterData.shape || !characterData.pointCloud) {
|
|
@@ -550,7 +599,11 @@ var AvatarDownloader = class {
|
|
|
550
599
|
parallel_duration: parallelDuration,
|
|
551
600
|
total_size: totalSize
|
|
552
601
|
});
|
|
553
|
-
return
|
|
602
|
+
return {
|
|
603
|
+
data: characterData,
|
|
604
|
+
cacheHit,
|
|
605
|
+
cacheType
|
|
606
|
+
};
|
|
554
607
|
}
|
|
555
608
|
/**
|
|
556
609
|
* Preload all resources (template + character data + camera info + settings)
|
|
@@ -559,7 +612,7 @@ var AvatarDownloader = class {
|
|
|
559
612
|
async preloadResources(characterMeta, options) {
|
|
560
613
|
const { progressCallback = null, signal, useCompressedModel = false } = options || {};
|
|
561
614
|
if (signal?.aborted) throw new Error("Preload cancelled");
|
|
562
|
-
const [
|
|
615
|
+
const [characterResult, preloadCameraSettings] = await Promise.all([this.loadCharacterData(characterMeta, {
|
|
563
616
|
signal,
|
|
564
617
|
useCompressedModel,
|
|
565
618
|
progressCallback: (info) => {
|
|
@@ -570,9 +623,11 @@ var AvatarDownloader = class {
|
|
|
570
623
|
}
|
|
571
624
|
}), this.loadCameraSettings(characterMeta, { signal })]);
|
|
572
625
|
return {
|
|
573
|
-
characterData,
|
|
626
|
+
characterData: characterResult.data,
|
|
574
627
|
preloadCameraSettings,
|
|
575
|
-
characterSettings: characterMeta.characterSettings
|
|
628
|
+
characterSettings: characterMeta.characterSettings,
|
|
629
|
+
cacheHit: characterResult.cacheHit,
|
|
630
|
+
cacheType: characterResult.cacheType
|
|
576
631
|
};
|
|
577
632
|
}
|
|
578
633
|
/**
|