@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
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { At as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
1
|
+
import { a as EgressType, c as TransportCompression, i as AudioFormat, l as BinaryReader, o as Message, r as avatarViewRegistry, s as MessageType, t as AvatarSDK, u as BinaryWriter } from "./AvatarSDK-DgPvHU4L.js";
|
|
2
|
+
import { At as FrameStarvationMode, C as recordMetric, Ct as ConnectionState, Dt as DEFAULT_REGION_REQUEST, Et as DEFAULT_REGION, Ft as TransitionType, Mt as LogLevel, Nt as RENDER_QUALITY_PARAMS, Ot as DrivingServiceMode, Pt as RenderQuality, St as AvatarState, Tt as DEFAULT_OPUS_BITRATE, bt as AnimationType, c as logEvent, g as clockSync, h as resolveMarks, jt as LoadProgress, kt as ErrorCode, l as logMetric, m as monoTimestamp, p as monoMarkFrom, t as logger, vt as idManager, wt as ConversationState, xt as AvatarError } from "./logger-BOpQ7u0w.js";
|
|
3
|
+
import { a as startPlaybackTrace, i as startLoadTrace, o as buildTraceparent } from "./otel-trace-Ct4T48nC.js";
|
|
4
|
+
import { n as APP_CONFIG } from "./error-utils-BogEllAd.js";
|
|
5
|
+
import { r as OggMuxer, t as OggPageSplitter } from "./OpusCodec-CXU7ysrU.js";
|
|
6
|
+
import { n as setOpusEncodeFrameSink } from "./OpusEncoderProxy-CzY-VckE.js";
|
|
7
|
+
import { t as AvatarDownloader } from "./AvatarDownloader-DyhenPKd.js";
|
|
6
8
|
//#region core/Avatar.ts
|
|
7
9
|
var Avatar = class {
|
|
8
10
|
id;
|
|
@@ -309,8 +311,8 @@ var AnimationPlayer = class AnimationPlayer {
|
|
|
309
311
|
*/
|
|
310
312
|
async createAndInitializeStreamingPlayer() {
|
|
311
313
|
if (this.streamingPlayer) return;
|
|
312
|
-
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-
|
|
313
|
-
const { AvatarSDK } = await import("./AvatarSDK-
|
|
314
|
+
const { StreamingAudioPlayer } = await import("./StreamingAudioPlayer-CIJmBp3z.js");
|
|
315
|
+
const { AvatarSDK } = await import("./AvatarSDK-DgPvHU4L.js").then((n) => n.n);
|
|
314
316
|
const audioFormat = AvatarSDK.getAudioFormat();
|
|
315
317
|
this.streamingPlayer = new StreamingAudioPlayer({
|
|
316
318
|
sampleRate: audioFormat.sampleRate,
|
|
@@ -930,9 +932,11 @@ var PendingAudioBuffer = class {
|
|
|
930
932
|
};
|
|
931
933
|
//#endregion
|
|
932
934
|
//#region animation/AnimationWebSocketClient.ts
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
935
|
+
var onOpusEncodeSegment = null;
|
|
936
|
+
/** @internal 由 AvatarController 在每轮播放开始时装上、结束时卸下。 */
|
|
937
|
+
function setOpusEncodeSegmentSink(fn) {
|
|
938
|
+
onOpusEncodeSegment = fn;
|
|
939
|
+
}
|
|
936
940
|
var AnimationWebSocketClient = class extends EventEmitter {
|
|
937
941
|
wsUrl;
|
|
938
942
|
reconnectAttempts;
|
|
@@ -1194,26 +1198,84 @@ var AnimationWebSocketClient = class extends EventEmitter {
|
|
|
1194
1198
|
* message so the server sees the stream close. Caller ensures socket/session
|
|
1195
1199
|
* are ready.
|
|
1196
1200
|
*/
|
|
1201
|
+
/** 1 秒 PCM 的字节数(16-bit 单声道),用于把编码分段按音频秒数切开。 */
|
|
1202
|
+
get audioBytesPerSecond() {
|
|
1203
|
+
return (AvatarSDK.getAudioFormat().sampleRate || 16e3) * 2;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* 编码 span 的跨**调用**累计状态(不足 1 秒的余量留到下一次 encode 调用)。
|
|
1207
|
+
*
|
|
1208
|
+
* `encodeSpanReqId` 用来判轮次切换:这几个字段是实例级的,不跟着 controller 每轮
|
|
1209
|
+
* 重置的 traceActions 走,上一轮的余量留到下一轮会让段数虚增、时刻串到几秒前
|
|
1210
|
+
* (实测 12 秒音频报出 24 段,且落在 send_audio 之前 4 秒)。
|
|
1211
|
+
*/
|
|
1212
|
+
encodeSpanReqId = null;
|
|
1213
|
+
encodeSpanStart = null;
|
|
1214
|
+
encodeSpanBytes = 0;
|
|
1215
|
+
/**
|
|
1216
|
+
* 把逐帧的编码时刻按**音频秒数**切成段,报给 trace。
|
|
1217
|
+
*
|
|
1218
|
+
* 与解码侧同一套口径:累计满 1 秒 PCM 收一段,边界用那一帧编完的真实时刻;
|
|
1219
|
+
* 尾巴不足 1 秒的余量跨调用累计(流式喂时一块往往远不足 1 秒)。
|
|
1220
|
+
*/
|
|
1221
|
+
emitEncodeSegments(reqId, fallbackStart, frames, frameTs) {
|
|
1222
|
+
if (frames.length === 0) return;
|
|
1223
|
+
if (this.encodeSpanReqId !== reqId) {
|
|
1224
|
+
this.encodeSpanReqId = reqId;
|
|
1225
|
+
this.encodeSpanStart = null;
|
|
1226
|
+
this.encodeSpanBytes = 0;
|
|
1227
|
+
}
|
|
1228
|
+
const oneSecondBytes = this.audioBytesPerSecond;
|
|
1229
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1230
|
+
this.encodeSpanStart ??= fallbackStart;
|
|
1231
|
+
this.encodeSpanBytes += frames[i];
|
|
1232
|
+
if (this.encodeSpanBytes < oneSecondBytes) continue;
|
|
1233
|
+
const endTs = monoMarkFrom(frameTs[i]);
|
|
1234
|
+
onOpusEncodeSegment?.({
|
|
1235
|
+
startTs: this.encodeSpanStart,
|
|
1236
|
+
endTs,
|
|
1237
|
+
pcmBytes: this.encodeSpanBytes
|
|
1238
|
+
});
|
|
1239
|
+
this.encodeSpanStart = endTs;
|
|
1240
|
+
this.encodeSpanBytes = 0;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1197
1243
|
async encodeAndSendOpus(conversationId, audioData, end) {
|
|
1198
1244
|
try {
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1245
|
+
const encodeStartTs = monoTimestamp();
|
|
1246
|
+
let pendingFrames = [];
|
|
1247
|
+
let pendingTs = [];
|
|
1248
|
+
setOpusEncodeFrameSink((frames, ts) => {
|
|
1249
|
+
pendingFrames = frames;
|
|
1250
|
+
pendingTs = ts;
|
|
1251
|
+
});
|
|
1252
|
+
const sendPage = (page, isLast) => {
|
|
1253
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
1203
1254
|
const message = {
|
|
1204
1255
|
type: MessageType.MESSAGE_CLIENT_AUDIO_INPUT,
|
|
1205
1256
|
clientAudioInput: {
|
|
1206
1257
|
reqId: conversationId,
|
|
1207
|
-
audio:
|
|
1258
|
+
audio: page,
|
|
1208
1259
|
end: isLast,
|
|
1209
1260
|
traceContext: this.takeTraceContext(conversationId)
|
|
1210
1261
|
}
|
|
1211
1262
|
};
|
|
1212
1263
|
const writer = new BinaryWriter();
|
|
1213
1264
|
Message.encode(message, writer);
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1265
|
+
this.ws.send(writer.finish());
|
|
1266
|
+
};
|
|
1267
|
+
let held = null;
|
|
1268
|
+
const pushPage = (page) => {
|
|
1269
|
+
if (held) sendPage(held, false);
|
|
1270
|
+
held = page;
|
|
1271
|
+
};
|
|
1272
|
+
await this.opusEncoder.encode(conversationId, audioData, end, (batch) => {
|
|
1273
|
+
for (const page of batch) pushPage(page);
|
|
1274
|
+
});
|
|
1275
|
+
setOpusEncodeFrameSink(null);
|
|
1276
|
+
this.emitEncodeSegments(conversationId, encodeStartTs, pendingFrames, pendingTs);
|
|
1277
|
+
if (held === null && end) held = /* @__PURE__ */ new Uint8Array(0);
|
|
1278
|
+
if (held) sendPage(held, end);
|
|
1217
1279
|
} catch (error) {
|
|
1218
1280
|
const message = error instanceof Error ? error.message : String(error);
|
|
1219
1281
|
logger.error("[AnimationWebSocketClient] Failed to send Opus audio:", message);
|
|
@@ -1229,8 +1291,8 @@ var AnimationWebSocketClient = class extends EventEmitter {
|
|
|
1229
1291
|
* Internal decision for the SDK's upstream audio format when the host feeds PCM:
|
|
1230
1292
|
* whether to encode PCM→Opus (true) or send raw PCM (false).
|
|
1231
1293
|
*
|
|
1232
|
-
* Opus uplink is opt-
|
|
1233
|
-
*
|
|
1294
|
+
* Opus uplink is opt-OUT: it is on unless the host passes
|
|
1295
|
+
* `opusUplinkEnabled: false`, which keeps the raw PCM uplink.
|
|
1234
1296
|
*
|
|
1235
1297
|
* Once opted in, the decision is still the SDK's: the backend (v2) supports Opus
|
|
1236
1298
|
* unconditionally and the encoder has a two-tier fallback (worker → main-thread
|
|
@@ -1240,7 +1302,7 @@ var AnimationWebSocketClient = class extends EventEmitter {
|
|
|
1240
1302
|
*/
|
|
1241
1303
|
shouldUseOpusUplink() {
|
|
1242
1304
|
const { opusUplinkEnabled } = AvatarSDK.getAudioFormat();
|
|
1243
|
-
if (opusUplinkEnabled
|
|
1305
|
+
if (opusUplinkEnabled === false) return false;
|
|
1244
1306
|
return true;
|
|
1245
1307
|
}
|
|
1246
1308
|
/**
|
|
@@ -1281,7 +1343,7 @@ var AnimationWebSocketClient = class extends EventEmitter {
|
|
|
1281
1343
|
return;
|
|
1282
1344
|
}
|
|
1283
1345
|
try {
|
|
1284
|
-
const { OpusEncoderProxy } = await import("./OpusEncoderProxy-
|
|
1346
|
+
const { OpusEncoderProxy } = await import("./OpusEncoderProxy-CzY-VckE.js").then((n) => n.t);
|
|
1285
1347
|
const bitrate = opusBitrate ?? 48e3;
|
|
1286
1348
|
this.opusEncoder = await OpusEncoderProxy.create(sampleRate, bitrate);
|
|
1287
1349
|
this.uplinkIsOpus = true;
|
|
@@ -2056,7 +2118,7 @@ function resolveInputAudioFormat() {
|
|
|
2056
2118
|
*/
|
|
2057
2119
|
function resolveUplinkAudioFormat() {
|
|
2058
2120
|
const fmt = AvatarSDK.getAudioFormat();
|
|
2059
|
-
return fmt.inputAudioFormat === "opus" || fmt.opusUplinkEnabled
|
|
2121
|
+
return fmt.inputAudioFormat === "opus" || fmt.opusUplinkEnabled !== false ? "opus" : "pcm";
|
|
2060
2122
|
}
|
|
2061
2123
|
var AvatarController = class {
|
|
2062
2124
|
networkLayer;
|
|
@@ -2185,8 +2247,23 @@ var AvatarController = class {
|
|
|
2185
2247
|
*/
|
|
2186
2248
|
traceActions = {
|
|
2187
2249
|
sends: [],
|
|
2188
|
-
recvs: []
|
|
2250
|
+
recvs: [],
|
|
2251
|
+
decodes: [],
|
|
2252
|
+
encodes: []
|
|
2189
2253
|
};
|
|
2254
|
+
/** 上行编码分段的接收钩子,构造时装上、dispose 时卸下(见 setOpusEncodeSegmentSink)。 */
|
|
2255
|
+
onEncodeSegment = (seg) => {
|
|
2256
|
+
this.traceActions.encodes.push(seg);
|
|
2257
|
+
this.markAudioTapAt(seg.pcmBytes, seg.endTs);
|
|
2258
|
+
};
|
|
2259
|
+
/**
|
|
2260
|
+
* 解码 span 的聚合状态:累计满 1 秒 PCM 收一段。
|
|
2261
|
+
*
|
|
2262
|
+
* 跨 chunk 累计——宿主流式喂时一块可能远不足 1 秒,按块收段会碎成几百个 span。
|
|
2263
|
+
* `decodeSpanStart` 是上一段的收尾时刻(null = 用本次 decode 的发起时刻当起点)。
|
|
2264
|
+
*/
|
|
2265
|
+
decodeSpanStart = null;
|
|
2266
|
+
decodeSpanBytes = 0;
|
|
2190
2267
|
/**
|
|
2191
2268
|
* Highest Ogg granule forwarded so far this round, for Opus input. The granule
|
|
2192
2269
|
* accumulates across a stream, so a send's audio seconds is its own end granule
|
|
@@ -2208,6 +2285,7 @@ var AvatarController = class {
|
|
|
2208
2285
|
if (this.playbackMode === DrivingServiceMode.direct) this.networkLayer = new NetworkLayer(this);
|
|
2209
2286
|
if (typeof document !== "undefined") document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
2210
2287
|
this.ensureInputOpusDecoder();
|
|
2288
|
+
setOpusEncodeSegmentSink(this.onEncodeSegment);
|
|
2211
2289
|
}
|
|
2212
2290
|
handleVisibilityChange = () => {
|
|
2213
2291
|
if (document.visibilityState === "hidden") this.shouldReportPlaybackStats = false;
|
|
@@ -2457,9 +2535,14 @@ var AvatarController = class {
|
|
|
2457
2535
|
const resolved = resolveMarks(latency);
|
|
2458
2536
|
const tap0 = resolved["tap_0"];
|
|
2459
2537
|
if (typeof tap0 === "number") {
|
|
2538
|
+
const latencyLabels = {
|
|
2539
|
+
...labels,
|
|
2540
|
+
input_audio_format: resolveInputAudioFormat(),
|
|
2541
|
+
uplink_audio_format: resolveUplinkAudioFormat()
|
|
2542
|
+
};
|
|
2460
2543
|
const emitDelta = (name, key) => {
|
|
2461
2544
|
const t = resolved[key];
|
|
2462
|
-
if (typeof t === "number") recordMetric(name, t - tap0,
|
|
2545
|
+
if (typeof t === "number") recordMetric(name, t - tap0, latencyLabels);
|
|
2463
2546
|
};
|
|
2464
2547
|
emitDelta("playback_latency_tap_1_ms", "tap_1");
|
|
2465
2548
|
emitDelta("playback_latency_tap_2_ms", "tap_2");
|
|
@@ -2497,6 +2580,14 @@ var AvatarController = class {
|
|
|
2497
2580
|
actionMarks[`recv_start_${i}`] = r.startTs;
|
|
2498
2581
|
actionMarks[`recv_end_${i}`] = r.endTs;
|
|
2499
2582
|
});
|
|
2583
|
+
this.traceActions.decodes.forEach((d, i) => {
|
|
2584
|
+
actionMarks[`decode_start_${i}`] = d.startTs;
|
|
2585
|
+
actionMarks[`decode_end_${i}`] = d.endTs;
|
|
2586
|
+
});
|
|
2587
|
+
this.traceActions.encodes.forEach((e, i) => {
|
|
2588
|
+
actionMarks[`encode_start_${i}`] = e.startTs;
|
|
2589
|
+
actionMarks[`encode_end_${i}`] = e.endTs;
|
|
2590
|
+
});
|
|
2500
2591
|
const rm = resolveMarks(actionMarks);
|
|
2501
2592
|
const allEnds = [...this.traceActions.sends.map((_, i) => rm[`send_end_${i}`]), ...this.traceActions.recvs.map((_, i) => rm[`recv_end_${i}`])].filter((v) => typeof v === "number");
|
|
2502
2593
|
const allStarts = [...this.traceActions.sends.map((_, i) => rm[`send_start_${i}`]), ...this.traceActions.recvs.map((_, i) => rm[`recv_start_${i}`])].filter((v) => typeof v === "number");
|
|
@@ -2524,6 +2615,44 @@ var AvatarController = class {
|
|
|
2524
2615
|
});
|
|
2525
2616
|
});
|
|
2526
2617
|
arrival.end(arrivalEnd);
|
|
2618
|
+
const decodeMarks = this.traceActions.decodes;
|
|
2619
|
+
if (decodeMarks.length > 0) {
|
|
2620
|
+
const firstStart = rm[`decode_start_0`];
|
|
2621
|
+
if (typeof firstStart === "number") {
|
|
2622
|
+
const decodeGroup = t.group("decode", firstStart, { view: "decode" });
|
|
2623
|
+
let decodeEnd = firstStart;
|
|
2624
|
+
decodeMarks.forEach((d, i) => {
|
|
2625
|
+
const start = rm[`decode_start_${i}`];
|
|
2626
|
+
const end = rm[`decode_end_${i}`];
|
|
2627
|
+
if (typeof start !== "number" || typeof end !== "number") return;
|
|
2628
|
+
decodeEnd = Math.max(decodeEnd, end);
|
|
2629
|
+
decodeGroup.span(`decode_audio_${i + 1}`, start, floorEnd(start, end), {
|
|
2630
|
+
dur_ms: Math.round(end - start),
|
|
2631
|
+
audio_ms: Math.round(d.pcmBytes / this.audioBytesPerSecond * 1e3)
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
decodeGroup.end(decodeEnd);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const encodeMarks = this.traceActions.encodes;
|
|
2638
|
+
if (encodeMarks.length > 0) {
|
|
2639
|
+
const firstStart = rm[`encode_start_0`];
|
|
2640
|
+
if (typeof firstStart === "number") {
|
|
2641
|
+
const encodeGroup = t.group("encode", firstStart, { view: "encode" });
|
|
2642
|
+
let encodeEnd = firstStart;
|
|
2643
|
+
encodeMarks.forEach((e, i) => {
|
|
2644
|
+
const start = rm[`encode_start_${i}`];
|
|
2645
|
+
const end = rm[`encode_end_${i}`];
|
|
2646
|
+
if (typeof start !== "number" || typeof end !== "number") return;
|
|
2647
|
+
encodeEnd = Math.max(encodeEnd, end);
|
|
2648
|
+
encodeGroup.span(`encode_audio_${i + 1}`, start, floorEnd(start, end), {
|
|
2649
|
+
dur_ms: Math.round(end - start),
|
|
2650
|
+
audio_ms: Math.round(e.pcmBytes / this.audioBytesPerSecond * 1e3)
|
|
2651
|
+
});
|
|
2652
|
+
});
|
|
2653
|
+
encodeGroup.end(encodeEnd);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2527
2656
|
const renderEnd = this.playbackEndedAt > tap0 ? this.playbackEndedAt : tap0 + durationMs;
|
|
2528
2657
|
const emitPlaybackChain = (into, playLabel, stallLabel, chainStart, chainEnd, events) => {
|
|
2529
2658
|
const stalls = events.filter((e) => e.startTs >= chainStart && e.startTs <= chainEnd).sort((a, b) => a.startTs - b.startTs);
|
|
@@ -2630,7 +2759,7 @@ var AvatarController = class {
|
|
|
2630
2759
|
const { inputAudioFormat } = AvatarSDK.getAudioFormat();
|
|
2631
2760
|
if (inputAudioFormat !== "opus" || this.inputOpusDecoderProxy) return;
|
|
2632
2761
|
try {
|
|
2633
|
-
const { OpusDecoderProxy } = await import("./OpusDecoderProxy-
|
|
2762
|
+
const { OpusDecoderProxy } = await import("./OpusDecoderProxy-Cq8Z_MZY.js");
|
|
2634
2763
|
this.inputOpusDecoderProxy = await OpusDecoderProxy.create();
|
|
2635
2764
|
this.inputDecodeChain = Promise.resolve();
|
|
2636
2765
|
} catch (error) {
|
|
@@ -2649,15 +2778,28 @@ var AvatarController = class {
|
|
|
2649
2778
|
if (!this.inputOpusDecoderProxy) {
|
|
2650
2779
|
if (resolveInputAudioFormat() === "opus") {
|
|
2651
2780
|
logger.error("[AvatarController] Opus input but decoder unavailable; dropping chunk to avoid playing raw Opus as PCM");
|
|
2652
|
-
return
|
|
2781
|
+
return {
|
|
2782
|
+
pcm: /* @__PURE__ */ new Uint8Array(0),
|
|
2783
|
+
segments: [],
|
|
2784
|
+
segmentTs: []
|
|
2785
|
+
};
|
|
2653
2786
|
}
|
|
2654
|
-
|
|
2787
|
+
const raw = new Uint8Array(data);
|
|
2788
|
+
return {
|
|
2789
|
+
pcm: raw,
|
|
2790
|
+
segments: raw.length > 0 ? [raw.length] : [],
|
|
2791
|
+
segmentTs: raw.length > 0 ? [performance.now()] : []
|
|
2792
|
+
};
|
|
2655
2793
|
}
|
|
2656
2794
|
try {
|
|
2657
2795
|
return await this.inputOpusDecoderProxy.decode(data);
|
|
2658
2796
|
} catch (error) {
|
|
2659
2797
|
logger.error("[AvatarController] Failed to decode input Opus audio:", error instanceof Error ? error.message : String(error));
|
|
2660
|
-
return
|
|
2798
|
+
return {
|
|
2799
|
+
pcm: /* @__PURE__ */ new Uint8Array(0),
|
|
2800
|
+
segments: [],
|
|
2801
|
+
segmentTs: []
|
|
2802
|
+
};
|
|
2661
2803
|
}
|
|
2662
2804
|
}
|
|
2663
2805
|
/**
|
|
@@ -2705,6 +2847,7 @@ var AvatarController = class {
|
|
|
2705
2847
|
this.currentConversationId = this.generateAndLogNewConversationId();
|
|
2706
2848
|
this.reqEnd = false;
|
|
2707
2849
|
}
|
|
2850
|
+
this.markFirstAudioTap();
|
|
2708
2851
|
const sendStartTs = monoTimestamp();
|
|
2709
2852
|
const isOpusInput = resolveInputAudioFormat() === "opus";
|
|
2710
2853
|
let sendAudioMs = isOpusInput ? 0 : audioData.byteLength / this.audioBytesPerSecond * 1e3;
|
|
@@ -2732,17 +2875,41 @@ var AvatarController = class {
|
|
|
2732
2875
|
const oggEnd = end;
|
|
2733
2876
|
this.inputDecodeChain = this.inputDecodeChain.then(async () => {
|
|
2734
2877
|
await this.ensureInputOpusDecoder();
|
|
2735
|
-
const
|
|
2878
|
+
const decodeStartTs = monoTimestamp();
|
|
2879
|
+
const { pcm, segments, segmentTs } = await this.normalizeInputAudioToPcm(oggForDecode);
|
|
2736
2880
|
this.receivedAudioBytes += pcm.length;
|
|
2737
|
-
|
|
2881
|
+
for (let i = 0; i < segments.length; i++) {
|
|
2882
|
+
this.decodeSpanBytes += segments[i];
|
|
2883
|
+
if (this.decodeSpanBytes < this.audioBytesPerSecond) continue;
|
|
2884
|
+
const endTs = monoMarkFrom(segmentTs[i]);
|
|
2885
|
+
this.traceActions.decodes.push({
|
|
2886
|
+
startTs: this.decodeSpanStart ?? decodeStartTs,
|
|
2887
|
+
endTs,
|
|
2888
|
+
pcmBytes: this.decodeSpanBytes
|
|
2889
|
+
});
|
|
2890
|
+
this.decodeSpanStart = endTs;
|
|
2891
|
+
this.decodeSpanBytes = 0;
|
|
2892
|
+
}
|
|
2893
|
+
if (this.decodeSpanBytes > 0) {
|
|
2894
|
+
const endTs = monoMarkFrom(segmentTs[segments.length - 1]);
|
|
2895
|
+
this.traceActions.decodes.push({
|
|
2896
|
+
startTs: this.decodeSpanStart ?? decodeStartTs,
|
|
2897
|
+
endTs,
|
|
2898
|
+
pcmBytes: this.decodeSpanBytes
|
|
2899
|
+
});
|
|
2900
|
+
this.decodeSpanStart = endTs;
|
|
2901
|
+
this.decodeSpanBytes = 0;
|
|
2902
|
+
}
|
|
2738
2903
|
this.addAudioChunkToBuffer(pcm, oggEnd);
|
|
2739
2904
|
}).catch((error) => {
|
|
2740
2905
|
logger.error("[AvatarController] Opus decode→buffer chain error:", error instanceof Error ? error.message : String(error));
|
|
2741
2906
|
});
|
|
2742
2907
|
this.networkLayer.sendAudioData(this.currentConversationId, audioData, end);
|
|
2908
|
+
this.markAudioTap(sendAudioMs / 1e3 * this.audioBytesPerSecond);
|
|
2743
2909
|
} else {
|
|
2744
2910
|
this.receivedAudioBytes += audioData.byteLength;
|
|
2745
|
-
this.
|
|
2911
|
+
if (resolveUplinkAudioFormat() === "opus") this.markFirstAudioTap();
|
|
2912
|
+
else this.markAudioTap(audioData.byteLength);
|
|
2746
2913
|
this.addAudioChunkToBuffer(new Uint8Array(audioData), end);
|
|
2747
2914
|
this.networkLayer.sendAudioData(this.currentConversationId, audioData, end);
|
|
2748
2915
|
}
|
|
@@ -2854,7 +3021,7 @@ var AvatarController = class {
|
|
|
2854
3021
|
const oggLast = isLast;
|
|
2855
3022
|
this.inputDecodeChain = this.inputDecodeChain.then(async () => {
|
|
2856
3023
|
await this.ensureInputOpusDecoder();
|
|
2857
|
-
const pcm = await this.normalizeInputAudioToPcm(ogg);
|
|
3024
|
+
const { pcm } = await this.normalizeInputAudioToPcm(ogg);
|
|
2858
3025
|
this.bufferHostPcm(pcm, oggLast);
|
|
2859
3026
|
}).catch((error) => {
|
|
2860
3027
|
logger.error("[AvatarController] Opus decode→buffer chain error:", error instanceof Error ? error.message : String(error));
|
|
@@ -2868,24 +3035,50 @@ var AvatarController = class {
|
|
|
2868
3035
|
return this.currentConversationId;
|
|
2869
3036
|
}
|
|
2870
3037
|
/**
|
|
3038
|
+
* 记下 tap_0 —— 宿主送入首包音频的时刻,只抢零点、不参与分秒累计。
|
|
3039
|
+
*
|
|
3040
|
+
* 语义是「用户把第一块音频交给 SDK 的那一刻」,PCM 与 Opus 一视同仁:它是端到端
|
|
3041
|
+
* 延迟的零点,上行发送、本地解码都发生在它之后,本就该被算进后续各段 Δ 里。
|
|
3042
|
+
*
|
|
3043
|
+
* 分秒(tap_N≥1)在各路径的「发出去」那一刻推进,见 markAudioTap / markAudioTapAt
|
|
3044
|
+
* 的调用点。字节口径恒为 PCM(opus 输入由 granule 秒数折算),因为分母
|
|
3045
|
+
* audioBytesPerSecond 按真实采样率算,直接用 Ogg 压缩字节会让 tap_N 几乎不递增。
|
|
3046
|
+
* taps.set(0) 是 set-once,故这里先占、后面那次不会覆盖。
|
|
3047
|
+
* @internal
|
|
3048
|
+
*/
|
|
3049
|
+
markFirstAudioTap() {
|
|
3050
|
+
const marks = this.latencyMarks;
|
|
3051
|
+
if (!marks.taps.has(0)) marks.taps.set(0, monoTimestamp());
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
2871
3054
|
* 记一次音频 tap:tap_0=首包送入,之后每累计满整秒记一个 tap_N。
|
|
2872
3055
|
*
|
|
2873
3056
|
* **两种模式都走这里**(direct 由 send() 调、backend 由 bufferHostPcm 调),
|
|
2874
3057
|
* 所以只有这一份分秒逻辑。
|
|
2875
3058
|
*
|
|
2876
|
-
* @param pcmByteLength
|
|
2877
|
-
*
|
|
2878
|
-
*
|
|
3059
|
+
* @param pcmByteLength 本块的 **PCM 口径**字节数。分母 audioBytesPerSecond 按真实
|
|
3060
|
+
* 采样率算,直接传 Opus 压缩字节(比 PCM 小 5-10 倍)会让 tap_N 几乎不递增;
|
|
3061
|
+
* opus 输入不解码,故由 Ogg granule 的秒数折算成等价 PCM 字节再传进来。
|
|
2879
3062
|
* @internal
|
|
2880
3063
|
*/
|
|
2881
3064
|
markAudioTap(pcmByteLength) {
|
|
3065
|
+
this.markAudioTapAt(pcmByteLength, monoTimestamp());
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* markAudioTap 的指定时刻版:这一秒音频「就绪」的时刻由调用方给,而不是取当下。
|
|
3069
|
+
*
|
|
3070
|
+
* 上行 Opus 的编码分段用它——那一秒的就绪时刻是它编完的时刻(回调触发时已过去
|
|
3071
|
+
* 一小会儿),取 monoTimestamp() 会把编码后的排队/回调延迟算进 tap。
|
|
3072
|
+
* @internal
|
|
3073
|
+
*/
|
|
3074
|
+
markAudioTapAt(pcmByteLength, at) {
|
|
2882
3075
|
const marks = this.latencyMarks;
|
|
2883
|
-
if (!marks.taps.has(0)) marks.taps.set(0,
|
|
3076
|
+
if (!marks.taps.has(0)) marks.taps.set(0, at);
|
|
2884
3077
|
marks.accumulatedBytes += pcmByteLength;
|
|
2885
3078
|
const currentDuration = marks.accumulatedBytes / this.audioBytesPerSecond;
|
|
2886
3079
|
let s = (marks.taps.size > 0 ? Math.max(...marks.taps.keys()) : 0) + 1;
|
|
2887
3080
|
while (currentDuration >= s) {
|
|
2888
|
-
marks.taps.set(s,
|
|
3081
|
+
marks.taps.set(s, at);
|
|
2889
3082
|
s++;
|
|
2890
3083
|
}
|
|
2891
3084
|
}
|
|
@@ -3149,8 +3342,12 @@ var AvatarController = class {
|
|
|
3149
3342
|
};
|
|
3150
3343
|
this.traceActions = {
|
|
3151
3344
|
sends: [],
|
|
3152
|
-
recvs: []
|
|
3345
|
+
recvs: [],
|
|
3346
|
+
decodes: [],
|
|
3347
|
+
encodes: []
|
|
3153
3348
|
};
|
|
3349
|
+
this.decodeSpanStart = null;
|
|
3350
|
+
this.decodeSpanBytes = 0;
|
|
3154
3351
|
this.lastOpusGranule = -1;
|
|
3155
3352
|
}
|
|
3156
3353
|
/**
|
|
@@ -3410,7 +3607,7 @@ var AvatarController = class {
|
|
|
3410
3607
|
const stateChanged = isOutOfBounds !== this.lastOutOfBoundsState;
|
|
3411
3608
|
const timeSinceLastLog = now - this.lastSyncLogTime;
|
|
3412
3609
|
if (stateChanged || timeSinceLastLog >= 1e3) {
|
|
3413
|
-
logger.warn(`[
|
|
3610
|
+
logger.warn(`[AvatarKit] Animation is running behind the audio: playback has reached ${(frameIndex / 25).toFixed(1)}s but only ${((this.currentKeyframes.length + this.keyframesOffset) / 25).toFixed(1)}s of animation has arrived, so the last available frame is being held. Send audio to the SDK faster than real time — at least 2s of audio per second of wall clock, ideally more — so animation frames can stay ahead of playback.`);
|
|
3414
3611
|
this.lastSyncLogTime = now;
|
|
3415
3612
|
}
|
|
3416
3613
|
this.lastOutOfBoundsState = isOutOfBounds;
|
|
@@ -3614,6 +3811,7 @@ var AvatarController = class {
|
|
|
3614
3811
|
this.animationPlayer.dispose();
|
|
3615
3812
|
this.animationPlayer = null;
|
|
3616
3813
|
}
|
|
3814
|
+
setOpusEncodeSegmentSink(null);
|
|
3617
3815
|
if (this.inputOpusDecoderProxy) {
|
|
3618
3816
|
this.inputOpusDecoderProxy.destroy();
|
|
3619
3817
|
this.inputOpusDecoderProxy = null;
|
|
@@ -3799,6 +3997,7 @@ var AvatarManager = class AvatarManager {
|
|
|
3799
3997
|
if (task.status === "cancelled") return;
|
|
3800
3998
|
task.status = "failed";
|
|
3801
3999
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4000
|
+
task._loadTrace?.end(false, Date.now(), { error: err.message });
|
|
3802
4001
|
this.notifyAllRequests(task, {
|
|
3803
4002
|
type: LoadProgress.failed,
|
|
3804
4003
|
error: err
|
|
@@ -3819,10 +4018,19 @@ var AvatarManager = class AvatarManager {
|
|
|
3819
4018
|
const { id, abortController } = task;
|
|
3820
4019
|
const signal = abortController.signal;
|
|
3821
4020
|
const startTime = Date.now();
|
|
4021
|
+
const loadTrace = startLoadTrace(id, startTime, {
|
|
4022
|
+
use_compressed_model: task.useCompressedModel,
|
|
4023
|
+
source: "network"
|
|
4024
|
+
});
|
|
4025
|
+
task._loadTrace = loadTrace;
|
|
3822
4026
|
if (signal.aborted) throw new Error("Task cancelled");
|
|
3823
4027
|
task.status = "fetching-meta";
|
|
3824
4028
|
logger.log(`[AvatarManager] Fetching metadata for avatar ${id}...`);
|
|
4029
|
+
const metaStart = Date.now();
|
|
4030
|
+
loadTrace.span("prepare", startTime, metaStart);
|
|
3825
4031
|
const characterMeta = await this.avatarDownloader.getCharacterById(id, { signal });
|
|
4032
|
+
const metaEnd = Date.now();
|
|
4033
|
+
loadTrace.span("fetch_metadata", metaStart, metaEnd);
|
|
3826
4034
|
task.characterMeta = characterMeta;
|
|
3827
4035
|
if (signal.aborted) throw new Error("Task cancelled");
|
|
3828
4036
|
const cached = this.avatarCache.get(id);
|
|
@@ -3839,11 +4047,19 @@ var AvatarManager = class AvatarManager {
|
|
|
3839
4047
|
type: LoadProgress.downloading,
|
|
3840
4048
|
progress: 1
|
|
3841
4049
|
});
|
|
4050
|
+
const hitEnd = Date.now();
|
|
4051
|
+
loadTrace.span("prepare_assets", metaEnd, hitEnd, { cache_hit: true });
|
|
4052
|
+
loadTrace.end(true, hitEnd, { cache_hit: true });
|
|
4053
|
+
logMetric("fetch_avatar_latency", Date.now() - startTime, {
|
|
4054
|
+
resolution: "default",
|
|
4055
|
+
use_compressed_model: task.useCompressedModel,
|
|
4056
|
+
cache_hit: true
|
|
4057
|
+
}, { avatar_id: id });
|
|
3842
4058
|
return cached;
|
|
3843
4059
|
} else {
|
|
3844
4060
|
logger.log(`[AvatarManager] Avatar ${id} cache invalid: updatedAt=${cachedUpdatedAt}->${newUpdatedAt}, modelType=${cachedModelType}->${requestedModelType}, reloading...`);
|
|
3845
4061
|
this.avatarCache.delete(id);
|
|
3846
|
-
const { PwaCacheManager } = await import("./pwa-cache-manager-
|
|
4062
|
+
const { PwaCacheManager } = await import("./pwa-cache-manager-C7NbU52Q.js").then((n) => n.n);
|
|
3847
4063
|
await PwaCacheManager.clearCharacterCache(id);
|
|
3848
4064
|
}
|
|
3849
4065
|
}
|
|
@@ -3855,6 +4071,8 @@ var AvatarManager = class AvatarManager {
|
|
|
3855
4071
|
});
|
|
3856
4072
|
if (signal.aborted) throw new Error("Task cancelled");
|
|
3857
4073
|
task.status = "downloading";
|
|
4074
|
+
const assetsStart = Date.now();
|
|
4075
|
+
loadTrace.span("prepare_assets", metaEnd, assetsStart);
|
|
3858
4076
|
logger.log("[AvatarManager] Downloading resources...");
|
|
3859
4077
|
let downloadedCount = 0;
|
|
3860
4078
|
const resources = await this.avatarDownloader.preloadResources(characterMeta, {
|
|
@@ -3874,11 +4092,18 @@ var AvatarManager = class AvatarManager {
|
|
|
3874
4092
|
if (signal.aborted) throw new Error("Task cancelled");
|
|
3875
4093
|
logger.log("[AvatarManager] Creating Avatar instance...");
|
|
3876
4094
|
const avatar = new Avatar(id, characterMeta, resources, requestedModelType, false);
|
|
4095
|
+
const loadEnd = Date.now();
|
|
4096
|
+
loadTrace.span("load_assets", assetsStart, loadEnd, {
|
|
4097
|
+
cache_hit: resources.cacheHit,
|
|
4098
|
+
cache_type: resources.cacheType
|
|
4099
|
+
});
|
|
4100
|
+
loadTrace.end(true, loadEnd, { cache_hit: resources.cacheHit });
|
|
3877
4101
|
this.avatarCache.set(id, avatar);
|
|
3878
4102
|
logger.log("[AvatarManager] Avatar loaded successfully");
|
|
3879
4103
|
logMetric("fetch_avatar_latency", Date.now() - startTime, {
|
|
3880
4104
|
resolution: "default",
|
|
3881
|
-
use_compressed_model: task.useCompressedModel
|
|
4105
|
+
use_compressed_model: task.useCompressedModel,
|
|
4106
|
+
cache_hit: resources.cacheHit
|
|
3882
4107
|
}, { avatar_id: id });
|
|
3883
4108
|
return avatar;
|
|
3884
4109
|
}
|
|
@@ -3913,7 +4138,13 @@ var AvatarManager = class AvatarManager {
|
|
|
3913
4138
|
* @returns Avatar instance, or undefined if not in cache
|
|
3914
4139
|
*/
|
|
3915
4140
|
retrieve(id) {
|
|
3916
|
-
|
|
4141
|
+
const trace = startLoadTrace(id, Date.now(), { source: "cache" });
|
|
4142
|
+
const cached = this.avatarCache.get(id);
|
|
4143
|
+
trace.end(true, Date.now(), {
|
|
4144
|
+
result: cached ? "hit" : "miss",
|
|
4145
|
+
cache_hit: cached !== void 0
|
|
4146
|
+
});
|
|
4147
|
+
return cached;
|
|
3917
4148
|
}
|
|
3918
4149
|
/**
|
|
3919
4150
|
* Clear cached avatar for specified ID
|
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Internal telemetry surface for the companion RTC SDK.
|
|
3
|
-
*
|
|
4
|
-
* `@spatius/avatarkit-rtc` reports into the same OpenObserve backend and shares
|
|
5
|
-
* this SDK's OTel providers, so its spans must sit on the same calibrated
|
|
6
|
-
* timeline. Without that, a client whose wall clock is off puts RTC spans at a
|
|
7
|
-
* time that does not line up with the server-side records they need to be read
|
|
8
|
-
* against.
|
|
9
|
-
*
|
|
10
|
-
* Deliberately NOT re-exported from the package root: this is a bridge between
|
|
11
|
-
* two first-party packages, not API for integrators. It exposes conversion only
|
|
12
|
-
* — nothing here can alter the clock baseline.
|
|
13
|
-
*
|
|
14
|
-
* NOTE: nothing in this file may be tagged `@internal`. The dts plugin runs with
|
|
15
|
-
* `stripInternal: true`, which erases such declarations from the emitted `.d.ts`
|
|
16
|
-
* — the entry would still export both functions at runtime while shipping an
|
|
17
|
-
* empty `export {}` type file, leaving the RTC package with no types.
|
|
18
|
-
*
|
|
19
|
-
* @packageDocumentation
|
|
20
|
-
*/
|
|
1
|
+
import { Attributes } from '@opentelemetry/api';
|
|
21
2
|
/**
|
|
22
3
|
* Convert a `performance.now()` reading taken earlier into a timeline
|
|
23
4
|
* timestamp (epoch ms), preferring the server-calibrated baseline and falling
|
|
@@ -35,3 +16,47 @@ export declare function resolveMonoTimestamp(mono: number): number;
|
|
|
35
16
|
* resolve either way; this only reports whether they are server-aligned yet.
|
|
36
17
|
*/
|
|
37
18
|
export declare function isClockCalibrated(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Handle for one round's trace.
|
|
21
|
+
*
|
|
22
|
+
* Declared here rather than re-exported from `utils/otel-trace`: the dts build
|
|
23
|
+
* excludes `utils/**`, so a type imported from there resolves to a file absent
|
|
24
|
+
* from the published package — which fails the whole module's type resolution,
|
|
25
|
+
* not just that one name.
|
|
26
|
+
*
|
|
27
|
+
* Structurally identical to the internal `PlaybackTrace`; the compiler checks
|
|
28
|
+
* the two match at `startPlaybackTrace`'s return below.
|
|
29
|
+
*/
|
|
30
|
+
export interface PlaybackTrace {
|
|
31
|
+
/** Add one interval span under the round's root. */
|
|
32
|
+
span(name: string, startMs: number, endMs: number, attrs?: Attributes, isError?: boolean): void;
|
|
33
|
+
/** Open a container span; spans added to the returned handle nest under it. */
|
|
34
|
+
group(name: string, startMs: number, attrs?: Attributes): SpanGroup;
|
|
35
|
+
/** Close the round's root span. */
|
|
36
|
+
end(ok?: boolean, endTimeMs?: number, attrs?: Attributes): void;
|
|
37
|
+
}
|
|
38
|
+
/** Sub-handle returned by {@link PlaybackTrace.group}. */
|
|
39
|
+
export interface SpanGroup {
|
|
40
|
+
span(name: string, startMs: number, endMs: number, attrs?: Attributes, isError?: boolean): void;
|
|
41
|
+
end(endTimeMs?: number, attrs?: Attributes): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Open one round's trace: `driven.request` → `playback` → the caller's spans.
|
|
45
|
+
*
|
|
46
|
+
* Shared with the RTC SDK rather than reimplemented there, so both packages
|
|
47
|
+
* emit the same tree. The root's ids derive from the conversation id, which is
|
|
48
|
+
* what lets the backend's spans join a round without threading state through;
|
|
49
|
+
* reproducing that separately would mean two definitions of one wire contract.
|
|
50
|
+
*/
|
|
51
|
+
export declare function startPlaybackTrace(conversationId: string, startTimeMs?: number, attrs?: Attributes): PlaybackTrace;
|
|
52
|
+
/**
|
|
53
|
+
* Record one measurement onto avatarkit's metric pipeline.
|
|
54
|
+
*
|
|
55
|
+
* Must go through here rather than the RTC SDK reaching for the global meter
|
|
56
|
+
* itself: a metrics export is skipped entirely unless that round carried real
|
|
57
|
+
* business data, and the flag saying so lives in this module. A histogram
|
|
58
|
+
* written directly through the global provider lands in the same instrument but
|
|
59
|
+
* leaves the flag clear, so the whole batch — RTC's readings included — is
|
|
60
|
+
* dropped before it is sent.
|
|
61
|
+
*/
|
|
62
|
+
export declare function recordMetric(name: string, value: number, attributes?: Attributes): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as recordMetric$1, g as clockSync } from "./logger-BOpQ7u0w.js";
|
|
2
|
+
import { a as startPlaybackTrace$1 } from "./otel-trace-Ct4T48nC.js";
|
|
2
3
|
//#region internal-telemetry.ts
|
|
3
4
|
/**
|
|
4
5
|
* Internal telemetry surface for the companion RTC SDK.
|
|
@@ -41,5 +42,29 @@ function resolveMonoTimestamp(mono) {
|
|
|
41
42
|
function isClockCalibrated() {
|
|
42
43
|
return clockSync.isReady();
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Open one round's trace: `driven.request` → `playback` → the caller's spans.
|
|
47
|
+
*
|
|
48
|
+
* Shared with the RTC SDK rather than reimplemented there, so both packages
|
|
49
|
+
* emit the same tree. The root's ids derive from the conversation id, which is
|
|
50
|
+
* what lets the backend's spans join a round without threading state through;
|
|
51
|
+
* reproducing that separately would mean two definitions of one wire contract.
|
|
52
|
+
*/
|
|
53
|
+
function startPlaybackTrace(conversationId, startTimeMs, attrs = {}) {
|
|
54
|
+
return startPlaybackTrace$1(conversationId, startTimeMs, attrs);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Record one measurement onto avatarkit's metric pipeline.
|
|
58
|
+
*
|
|
59
|
+
* Must go through here rather than the RTC SDK reaching for the global meter
|
|
60
|
+
* itself: a metrics export is skipped entirely unless that round carried real
|
|
61
|
+
* business data, and the flag saying so lives in this module. A histogram
|
|
62
|
+
* written directly through the global provider lands in the same instrument but
|
|
63
|
+
* leaves the flag clear, so the whole batch — RTC's readings included — is
|
|
64
|
+
* dropped before it is sent.
|
|
65
|
+
*/
|
|
66
|
+
function recordMetric(name, value, attributes = {}) {
|
|
67
|
+
recordMetric$1(name, value, attributes);
|
|
68
|
+
}
|
|
44
69
|
//#endregion
|
|
45
|
-
export { isClockCalibrated, resolveMonoTimestamp };
|
|
70
|
+
export { isClockCalibrated, recordMetric, resolveMonoTimestamp, startPlaybackTrace };
|