@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.
Files changed (22) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/{AvatarDownloader-B1tJTnIb.js → AvatarDownloader-DyhenPKd.js} +98 -43
  3. package/dist/{AvatarSDK-CTprrfJk.js → AvatarSDK-DgPvHU4L.js} +162 -2560
  4. package/dist/{OpusCodec-PWv7ynPp.js → OpusCodec-CXU7ysrU.js} +43 -9
  5. package/dist/{OpusDecoderProxy-m0fVN_M2.js → OpusDecoderProxy-Cq8Z_MZY.js} +19 -6
  6. package/dist/{OpusEncoderProxy-DiW21sBK.js → OpusEncoderProxy-CzY-VckE.js} +42 -7
  7. package/dist/{StreamingAudioPlayer-C6hgkvbK.js → StreamingAudioPlayer-CIJmBp3z.js} +2 -2
  8. package/dist/assets/{AvatarDownloader-LO9uuYvm.js → AvatarDownloader-G5FO1bVl.js} +96 -41
  9. package/dist/assets/{AvatarSDK-CPEeTeLT.js → AvatarSDK-C_Anluwp.js} +149 -12
  10. package/dist/assets/{OpusDecoderWorker.worker-CSSWn75M.js → OpusDecoderWorker.worker-C2T0afkj.js} +21 -5
  11. package/dist/assets/{OpusEncoderWorker.worker-q4TdCVDZ.js → OpusEncoderWorker.worker-DLYO9vPK.js} +53 -11
  12. package/dist/assets/{logger-DCzHWd4N.js → logger-YPooEAbA.js} +319 -126
  13. package/dist/core/AvatarController.d.ts +10 -0
  14. package/dist/{error-utils-BCZDCrc6.js → error-utils-BogEllAd.js} +1 -1
  15. package/dist/index.js +274 -43
  16. package/dist/internal-telemetry.d.ts +45 -20
  17. package/dist/internal-telemetry.js +27 -2
  18. package/dist/{logger-DdmfSEn-.js → logger-BOpQ7u0w.js} +336 -126
  19. package/dist/otel-trace-Ct4T48nC.js +2565 -0
  20. package/dist/{pwa-cache-manager-BMKyJWXI.js → pwa-cache-manager-C7NbU52Q.js} +1 -1
  21. package/dist/types/index.d.ts +30 -4
  22. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { n as __exportAll } from "./rolldown-runtime-B-1-B7_t.js";
2
- import { $ as ROOT_CONTEXT, A as hrTimeDuration, B as getStringFromEnv, C as OTLPExporterBase, D as ExportResultCode, E as BindOnceFuture, F as ATTR_EXCEPTION_MESSAGE, G as diag$1, H as isTracingSuppressed, I as ATTR_EXCEPTION_STACKTRACE, J as isValidTraceId, K as context, L as ATTR_EXCEPTION_TYPE, M as isTimeInputHrTime, N as millisToHrTime, O as addHrTimes, P as otperformance, Q as createNoopMeter, R as ATTR_SERVICE_NAME, S as toAttributes, T as resourceFromAttributes, U as suppressTracing, V as globalErrorHandler, W as trace, X as TraceFlags, Y as INVALID_SPAN_CONTEXT, Z as ContextAPI, _t as DrivingServiceMode, a as clientContextFields, at as OTEL_LOGS_ENDPOINT, b as createInstrumentationScope, c as logEvent, ct as OTEL_TRACES_ENDPOINT, d as cleanupOtel, et as createContextKey, f as initializeOtel, ft as idManager, g as initializeOtelMetrics, h as cleanupOtelMetrics, i as cleanupPostHog, it as unregisterGlobal, j as isTimeInput, k as hrTime, lt as OTEL_TRACES_STREAM_NAME, m as fetchBootstrap, n as setLogLevel, nt as getGlobal, o as getLogsFeatureFlag, ot as OTEL_PASSWORD, p as clockSync, q as isSpanContextValid, r as logSink, rt as registerGlobal, s as initializePostHog, st as OTEL_STREAM_NAME, t as logger, tt as DiagAPI, u as updatePostHogPersonPropertiesForFlags, ut as OTEL_USERNAME, v as createLegacyOtlpBrowserExportDelegate, w as defaultResource, x as createResource, y as JSON_ENCODER, yt as LogLevel, z as getNumberFromEnv } from "./logger-DCzHWd4N.js";
2
+ import { $ as ContextAPI, A as addHrTimes, B as ATTR_SERVICE_NAME, C as createResource, D as resourceFromAttributes, E as defaultResource, F as millisToHrTime, G as suppressTracing, H as getStringFromEnv, I as otperformance, J as context, K as trace, L as ATTR_EXCEPTION_MESSAGE, M as hrTimeDuration, N as isTimeInput, O as BindOnceFuture, P as isTimeInputHrTime, Q as TraceFlags, R as ATTR_EXCEPTION_STACKTRACE, S as createInstrumentationScope, T as OTLPExporterBase, U as globalErrorHandler, V as getNumberFromEnv, W as isTracingSuppressed, X as isValidTraceId, Y as isSpanContextValid, Z as INVALID_SPAN_CONTEXT, _ as cleanupOtelMetrics, a as clientContextFields, at as registerGlobal, b as createLegacyOtlpBrowserExportDelegate, c as logEvent, ct as OTEL_PASSWORD, d as cleanupOtel, dt as OTEL_TRACES_STREAM_NAME, et as createNoopMeter, f as initializeOtel, ft as OTEL_USERNAME, g as observeExporter, i as cleanupPostHog, it as getGlobal, j as hrTime, k as ExportResultCode, lt as OTEL_STREAM_NAME, m as fetchBootstrap, mt as idManager, n as setLogLevel, nt as createContextKey, o as getLogsFeatureFlag, ot as unregisterGlobal, p as clockSync, q as diag$1, r as logSink, rt as DiagAPI, s as initializePostHog, st as OTEL_LOGS_ENDPOINT, t as logger, tt as ROOT_CONTEXT, u as updatePostHogPersonPropertiesForFlags, ut as OTEL_TRACES_ENDPOINT, v as initializeOtelMetrics, w as toAttributes, x as JSON_ENCODER, xt as LogLevel, yt as DrivingServiceMode, z as ATTR_EXCEPTION_TYPE } from "./logger-YPooEAbA.js";
3
3
  //#region node_modules/.pnpm/@opentelemetry+api@1.9.1/node_modules/@opentelemetry/api/build/esm/baggage/internal/baggage-impl.js
4
4
  var BaggageImpl = class BaggageImpl {
5
5
  constructor(entries) {
@@ -1040,13 +1040,20 @@ const avatarViewRegistry = new AvatarViewRegistry();
1040
1040
  //#endregion
1041
1041
  //#region utils/region-resolver.ts
1042
1042
  /**
1043
- * Region 解析(区域自动调度)
1043
+ * Region 解析(区域自动调度)—— stale-while-revalidate
1044
1044
  *
1045
- * - region 为 'auto':调一次 bootstrap,用后台推荐的 region.current。
1046
1045
  * - region 为具体值(用户手动指定):直接用,跳过调度。
1047
- * - auto bootstrap 失败:降级到默认 region(us-west),并上报一条 error 埋点
1046
+ * - region 为 'auto' 且**有缓存**:立即返回缓存值,bootstrap 转后台异步跑、只更新缓存。
1047
+ * - region 为 'auto' 且**无缓存**(首次启动):同步等 bootstrap,拿到即用并写缓存。
1048
+ * - auto 无缓存且 bootstrap 失败:降级到默认 region(us-west),并上报一条 error 埋点
1048
1049
  * (严重事件,供监控观测),不阻断初始化。
1049
1050
  *
1051
+ * 为什么缓存优先:bootstrap 是一次网络往返,此前每次 init 都要等它(弱网下最多 5s
1052
+ * 超时)才能继续。实际上用户的接入区极少变动,用上次结果几乎总是对的。
1053
+ *
1054
+ * **后台刷新到的新值不影响本次会话**,只写缓存、下次启动生效。会话中途换接入域名
1055
+ * 会让已建立的 WS / 下到一半的资产指向另一个接入点,风险远大于「晚一次启动才切区」。
1056
+ *
1050
1057
  * 与时钟校准(clock-sync)解耦:各自独立调 bootstrap,互不影响。
1051
1058
  * @internal
1052
1059
  */
@@ -1071,6 +1078,32 @@ function readRegionCache() {
1071
1078
  }
1072
1079
  }
1073
1080
  /**
1081
+ * 后台刷新 region 缓存:缓存命中时发起,**结果只写缓存、不影响本次会话**(见文件头)。
1082
+ *
1083
+ * 失败一律静默:本次会话已经拿着缓存值正常跑了,这条刷新失败不构成用户可见问题,
1084
+ * 报 error 埋点只会污染 `region_resolve_failed` 的告警语义(那条是「真的没 region 可用」)。
1085
+ * @internal
1086
+ */
1087
+ async function refreshRegionCache(opts) {
1088
+ const controller = new AbortController();
1089
+ const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
1090
+ try {
1091
+ const current = (await fetchBootstrap({
1092
+ appId: opts.appId,
1093
+ sdkVersion: opts.sdkVersion,
1094
+ region: "auto"
1095
+ }, controller.signal)).region?.current;
1096
+ if (typeof current === "string" && current.length > 0) {
1097
+ writeRegionCache(current);
1098
+ logger.log(`[RegionResolver] background refresh → ${current} (applies next launch)`);
1099
+ }
1100
+ } catch (error) {
1101
+ logger.log(`[RegionResolver] background refresh failed (cached value stays): ${error instanceof Error ? error.message : String(error)}`);
1102
+ } finally {
1103
+ clearTimeout(timer);
1104
+ }
1105
+ }
1106
+ /**
1074
1107
  * 把请求的 region 解析成实际接入 region。
1075
1108
  * @param requestedRegion 用户传入的 region('auto' 或具体值)
1076
1109
  * @returns 实际接入 region(always a concrete value,不会是 'auto')
@@ -1079,6 +1112,15 @@ function readRegionCache() {
1079
1112
  async function resolveRegion(opts) {
1080
1113
  const { appId, sdkVersion, requestedRegion } = opts;
1081
1114
  if (requestedRegion !== "auto") return requestedRegion;
1115
+ const cachedRegion = readRegionCache();
1116
+ if (cachedRegion !== null) {
1117
+ logger.log(`[RegionResolver] auto → ${cachedRegion} (cached, refreshing in background)`);
1118
+ refreshRegionCache({
1119
+ appId,
1120
+ sdkVersion
1121
+ });
1122
+ return cachedRegion;
1123
+ }
1082
1124
  const controller = new AbortController();
1083
1125
  const timer = setTimeout(() => controller.abort(), RESOLVE_TIMEOUT_MS);
1084
1126
  try {
@@ -2318,6 +2360,7 @@ const OTEL_TRACER_NAME = "spatius-avatarkit";
2318
2360
  let sdkVersion = "1.0.0";
2319
2361
  let isInitialized = false;
2320
2362
  let tracerProvider = null;
2363
+ let tracer = null;
2321
2364
  const idGenerator = new PinnableIdGenerator();
2322
2365
  function buildBasicAuthHeader() {
2323
2366
  const credentials = `${OTEL_USERNAME}:${OTEL_PASSWORD}`;
@@ -2347,23 +2390,93 @@ function initializeOtelTrace(version, resourceAttrs) {
2347
2390
  "dsm": resourceAttrs.dsm,
2348
2391
  ...clientContextFields()
2349
2392
  }),
2350
- spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
2393
+ spanProcessors: [new BatchSpanProcessor(observeExporter(new OTLPTraceExporter({
2351
2394
  url: OTEL_TRACES_ENDPOINT,
2352
2395
  headers: {
2353
2396
  "Authorization": buildBasicAuthHeader(),
2354
2397
  "stream-name": OTEL_TRACES_STREAM_NAME
2355
2398
  }
2356
- }))],
2399
+ }), "/v1/traces", OTEL_TRACES_ENDPOINT))],
2357
2400
  idGenerator
2358
2401
  });
2359
2402
  trace.setGlobalTracerProvider(tracerProvider);
2360
- trace.getTracer(OTEL_TRACER_NAME, sdkVersion);
2403
+ tracer = trace.getTracer(OTEL_TRACER_NAME, sdkVersion);
2361
2404
  isInitialized = true;
2362
2405
  logger.log(`[OTel-Trace] Initialized - endpoint: ${OTEL_TRACES_ENDPOINT}, stream: ${OTEL_TRACES_STREAM_NAME}`);
2363
2406
  } catch (error) {
2364
2407
  logger.warn("[OTel-Trace] Failed to initialize:", error instanceof Error ? error.message : String(error));
2365
2408
  }
2366
2409
  }
2410
+ /** 空实现:未初始化 / 被门禁拦截时返回,调用方无需判空。 */
2411
+ const NOOP_GROUP = {
2412
+ span: () => {},
2413
+ end: () => {}
2414
+ };
2415
+ const NOOP_TRACE = {
2416
+ span: () => {},
2417
+ group: () => NOOP_GROUP,
2418
+ end: () => {}
2419
+ };
2420
+ /**
2421
+ * 开一条**加载角色**的 trace。root span 名 `load_avatar`,覆盖 `AvatarManager` 加载
2422
+ * 全过程,内部按四段切子 span(见调用处)。
2423
+ *
2424
+ * 与播放 trace 的两点不同:
2425
+ * - **trace_id 随机**,不从业务 id 派生。加载链路上后端只参与元数据接口一处,串联需求
2426
+ * 弱;而按 avatar_id 派生会让同一角色的多次加载撞进同一条 trace,在 viewer 里叠成
2427
+ * 一条、时间轴错乱。avatar_id 作 span 属性照样能筛。
2428
+ * - **无 anchor / 不钉 span_id**:那两样是为了匹配已在线上的 traceparent,加载没有这个约束。
2429
+ *
2430
+ * 量级上远小于播放 trace:一次会话通常只加载一次角色,而对话是几十轮。
2431
+ * @internal
2432
+ */
2433
+ /**
2434
+ * 开一条 **SDK 初始化** 的 trace。root span 名 `sdk_init`。
2435
+ *
2436
+ * 与 load / playback 是三条**独立** trace:它们本就是独立流程(初始化一次、加载可多次、
2437
+ * 对话更多次),硬串成一条会得到横跨整个 app 生命周期的怪 waterfall。三者靠 `session_id`
2438
+ * 关联——后台按它一查就能还原「这次启动做了什么」。
2439
+ *
2440
+ * 注意调用时机:trace 通道本身要到 `initializeOtelTrace` 之后才就绪,所以初始化早期
2441
+ * 阶段(region 解析等)只能先记时刻、等通道起来再用历史时间戳补发 span。
2442
+ * @internal
2443
+ */
2444
+ function startInitTrace(sessionId, startTimeMs, attrs = {}) {
2445
+ if (!tracer || !tracerProvider) return NOOP_TRACE;
2446
+ try {
2447
+ const root = tracer.startSpan("sdk_init", {
2448
+ startTime: startTimeMs,
2449
+ attributes: {
2450
+ session_id: sessionId,
2451
+ ...attrs
2452
+ }
2453
+ }, ROOT_CONTEXT);
2454
+ const rootCtx = trace.setSpan(ROOT_CONTEXT, root);
2455
+ return {
2456
+ span(name, startMs, endMs, spanAttrs = {}, isError = false) {
2457
+ try {
2458
+ const sp = tracer.startSpan(name, {
2459
+ startTime: startMs,
2460
+ attributes: spanAttrs
2461
+ }, rootCtx);
2462
+ if (isError) sp.setStatus({ code: SpanStatusCode.ERROR });
2463
+ sp.end(endMs);
2464
+ } catch {}
2465
+ },
2466
+ group: () => NOOP_GROUP,
2467
+ end(ok = true, endTimeMs, endAttrs = {}) {
2468
+ try {
2469
+ for (const [k, v] of Object.entries(endAttrs)) root.setAttribute(k, v);
2470
+ root.setStatus({ code: ok ? SpanStatusCode.OK : SpanStatusCode.ERROR });
2471
+ root.end(endTimeMs);
2472
+ } catch {}
2473
+ }
2474
+ };
2475
+ } catch (error) {
2476
+ logger.warn("[OTel-Trace] Failed to start init trace:", error instanceof Error ? error.message : String(error));
2477
+ return NOOP_TRACE;
2478
+ }
2479
+ }
2367
2480
  /**
2368
2481
  * Cleanup:flush 剩余 span 并关闭 provider。
2369
2482
  * @internal
@@ -2380,6 +2493,7 @@ function cleanupOtelTrace() {
2380
2493
  } finally {
2381
2494
  isInitialized = false;
2382
2495
  tracerProvider = null;
2496
+ tracer = null;
2383
2497
  }
2384
2498
  }
2385
2499
  //#endregion
@@ -6109,7 +6223,7 @@ var AvatarSDK = class {
6109
6223
  static _initializationState = "uninitialized";
6110
6224
  static _initializingPromise = null;
6111
6225
  static _configuration = null;
6112
- static _version = "1.3.1-beta.7";
6226
+ static _version = "1.3.1";
6113
6227
  static _avatarCore = null;
6114
6228
  static _cachedDeviceScore = null;
6115
6229
  static _rendererBackend = null;
@@ -6157,8 +6271,9 @@ var AvatarSDK = class {
6157
6271
  return this._initializingPromise;
6158
6272
  }
6159
6273
  static async _initializeInternal(appId, configuration) {
6274
+ const initStart = Date.now();
6275
+ const requestedRegion = configuration.region || "auto";
6160
6276
  try {
6161
- const requestedRegion = configuration.region || "auto";
6162
6277
  logger.log(`[AvatarSDK] Initializing with appId: ${appId}, region: ${requestedRegion}`);
6163
6278
  resetDeprecationWarnings();
6164
6279
  setLogLevel(configuration.logLevel ?? LogLevel.off);
@@ -6166,6 +6281,7 @@ var AvatarSDK = class {
6166
6281
  logger.log(`[AvatarSDK] Client ID: ${idManager.getClientId()}`);
6167
6282
  const dsm = configuration.drivingServiceMode || DrivingServiceMode.direct;
6168
6283
  const appIdValue = idManager.getAppId() || "";
6284
+ const regionStart = Date.now();
6169
6285
  const region = await resolveRegion({
6170
6286
  appId: appIdValue,
6171
6287
  sdkVersion: this._version,
@@ -6176,8 +6292,8 @@ var AvatarSDK = class {
6176
6292
  region
6177
6293
  };
6178
6294
  const af = this._configuration.audioFormat;
6179
- if (af?.opusUplinkEnabled === true && af.inputAudioFormat !== "opus" && !OPUS_UPLINK_SAMPLE_RATES.includes(af.sampleRate)) {
6180
- logger.warn(`[AvatarSDK] opusUplinkEnabled=true but sampleRate=${af.sampleRate} is not Opus-compatible (supported: ${OPUS_UPLINK_SAMPLE_RATES}); disabling Opus uplink, sending raw PCM.`);
6295
+ if (af && af.opusUplinkEnabled !== false && af.inputAudioFormat !== "opus" && !OPUS_UPLINK_SAMPLE_RATES.includes(af.sampleRate)) {
6296
+ logger.warn(`[AvatarSDK] Opus uplink is on but sampleRate=${af.sampleRate} is not Opus-compatible (supported: ${OPUS_UPLINK_SAMPLE_RATES}); disabling Opus uplink, sending raw PCM.`);
6181
6297
  this._configuration = {
6182
6298
  ...this._configuration,
6183
6299
  audioFormat: {
@@ -6190,6 +6306,7 @@ var AvatarSDK = class {
6190
6306
  supported_rates: OPUS_UPLINK_SAMPLE_RATES.join(",")
6191
6307
  });
6192
6308
  }
6309
+ const regionEnd = Date.now();
6193
6310
  initializePostHog(this._version, {
6194
6311
  region,
6195
6312
  dsm
@@ -6213,9 +6330,22 @@ var AvatarSDK = class {
6213
6330
  appId: appIdValue,
6214
6331
  sdkVersion: this._version
6215
6332
  });
6333
+ const wasmStart = Date.now();
6216
6334
  await this.initializeWASMModule();
6335
+ const templateStart = Date.now();
6217
6336
  await this.initializeTemplateResources();
6218
6337
  this._initializationState = "initialized";
6338
+ const initEnd = Date.now();
6339
+ const initTrace = startInitTrace(idManager.getSessionId(), initStart, {
6340
+ region,
6341
+ dsm
6342
+ });
6343
+ initTrace.span("prepare", initStart, regionStart);
6344
+ initTrace.span("resolve_region", regionStart, regionEnd);
6345
+ initTrace.span("init_telemetry", regionEnd, wasmStart);
6346
+ initTrace.span("init_wasm", wasmStart, templateStart);
6347
+ initTrace.span("load_template", templateStart, initEnd);
6348
+ initTrace.end(true, initEnd);
6219
6349
  logEvent("sdk_initialized", "info", {});
6220
6350
  logger.log(`[AvatarSDK] Successfully initialized`);
6221
6351
  this._ensureDeviceScoreCached();
@@ -6224,6 +6354,13 @@ var AvatarSDK = class {
6224
6354
  }, 0);
6225
6355
  } catch (error) {
6226
6356
  const errorMessage = error instanceof Error ? error.message : String(error);
6357
+ try {
6358
+ const failEnd = Date.now();
6359
+ startInitTrace(idManager.getSessionId(), initStart, {
6360
+ region: requestedRegion,
6361
+ dsm: configuration.drivingServiceMode || DrivingServiceMode.direct
6362
+ }).end(false, failEnd, { error: errorMessage });
6363
+ } catch {}
6227
6364
  logger.error("Failed to initialize AvatarSDK:", errorMessage);
6228
6365
  logEvent("sdk_startup", "error", { reason: errorMessage });
6229
6366
  this._initializationState = "uninitialized";
@@ -6275,7 +6412,7 @@ var AvatarSDK = class {
6275
6412
  logger.log("[AvatarSDK] Loading template resources...");
6276
6413
  logEvent("template_init_start", "info", { stage: "sdk_init" });
6277
6414
  try {
6278
- const { AvatarDownloader } = await import("./AvatarDownloader-LO9uuYvm.js");
6415
+ const { AvatarDownloader } = await import("./AvatarDownloader-G5FO1bVl.js");
6279
6416
  const templateResources = await new AvatarDownloader().loadUnifiedTemplate();
6280
6417
  if (await this._avatarCore.loadTemplateResourcesFromBuffers(templateResources)) logger.log("[AvatarSDK] Template resources initialized successfully");
6281
6418
  else logger.log("[AvatarSDK] Template resources already initialized, skipping...");
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./rolldown-runtime-B-1-B7_t.js";
2
- import { t as logger } from "./logger-DCzHWd4N.js";
2
+ import { t as logger } from "./logger-YPooEAbA.js";
3
3
  //#region __vite-browser-external
4
4
  var require___vite_browser_external = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5
5
  module.exports = {};
@@ -41185,9 +41185,15 @@ var OggOpusDecoder = class {
41185
41185
  * trailing page is buffered until the rest arrives.
41186
41186
  *
41187
41187
  * @param oggBytes raw Ogg stream bytes (any slice boundary).
41188
+ * @param onSegment 可选:每解出一个 packet 就回调一次那一段 PCM。
41189
+ *
41190
+ * 给延迟打点用。`tap_N` 的语义是「第 N 秒音频进入播放链路的时刻」,若只在
41191
+ * decode 返回后按整块累计,宿主一次性喂进整段 Ogg 时所有 tap 会挤在同一个
41192
+ * 时刻(实测 tap_1/2/4 三条同值),送入节奏这个信息就没了。解码本来就是逐包
41193
+ * 进行的,把分段暴露出来,打点才能落在真实的音频时间轴上。
41188
41194
  * @returns decoded PCM16 bytes, possibly empty.
41189
41195
  */
41190
- decode(oggBytes) {
41196
+ decode(oggBytes, onSegment) {
41191
41197
  const incoming = oggBytes instanceof Uint8Array ? oggBytes : new Uint8Array(oggBytes);
41192
41198
  let buf;
41193
41199
  if (this.carry.length > 0) {
@@ -41203,7 +41209,10 @@ var OggOpusDecoder = class {
41203
41209
  offset = page.nextOffset;
41204
41210
  for (const packet of page.packets) {
41205
41211
  const pcm = this.decodePacket(packet);
41206
- if (pcm.length > 0) pcmChunks.push(pcm);
41212
+ if (pcm.length > 0) {
41213
+ pcmChunks.push(pcm);
41214
+ onSegment?.(pcm);
41215
+ }
41207
41216
  }
41208
41217
  }
41209
41218
  this.carry = offset < buf.length ? buf.slice(offset) : /* @__PURE__ */ new Uint8Array(0);
@@ -41347,11 +41356,18 @@ self.onmessage = (e) => {
41347
41356
  });
41348
41357
  return;
41349
41358
  }
41350
- const pcm = decoder.decode(msg.ogg);
41359
+ const segments = [];
41360
+ const segmentTs = [];
41361
+ const pcm = decoder.decode(msg.ogg, (seg) => {
41362
+ segments.push(seg.length);
41363
+ segmentTs.push(performance.timeOrigin + performance.now());
41364
+ });
41351
41365
  post({
41352
41366
  type: "pcm",
41353
41367
  seq: msg.seq,
41354
- pcm: pcm.buffer
41368
+ pcm: pcm.buffer,
41369
+ segments,
41370
+ segmentTs
41355
41371
  }, [pcm.buffer]);
41356
41372
  break;
41357
41373
  }
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./rolldown-runtime-B-1-B7_t.js";
2
- import { t as logger } from "./logger-DCzHWd4N.js";
2
+ import { t as logger } from "./logger-YPooEAbA.js";
3
3
  //#region __vite-browser-external
4
4
  var require___vite_browser_external = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5
5
  module.exports = {};
@@ -41423,12 +41423,26 @@ var OggOpusEncoder = class {
41423
41423
  * @param end true on the final chunk of the req_id; flushes the tail and emits the EOS page.
41424
41424
  * @returns Ogg page byte arrays to send over the socket, in order.
41425
41425
  */
41426
- encode(reqId, pcm, end) {
41426
+ /**
41427
+ * @param onFrame 可选:每编完一帧(20ms)回调一次,参数是这帧的 PCM 字节数。
41428
+ * 给延迟打点用——编码内部逐帧进行,但结果打成页一起返回;只按整批记时会把
41429
+ * 一次调用的全部耗时算成一段(实测 1068ms 一条),看不出编码速率是否稳定。
41430
+ * @param onPages 可选:**每凑满 1 秒音频就吐一批页**,让调用方边编边发。
41431
+ *
41432
+ * 不给这个回调时退化为老行为(全部编完一次性返回)。宿主一次性灌入整段音频时,
41433
+ * 老行为要等全部编完才发出第一个包——实测 12 秒音频编码 1105ms,首包上行就白等
41434
+ * 这 1105ms(起播 startup_buffering 1804ms 里大半是它)。编 1 秒发 1 秒后,
41435
+ * 首包在几十毫秒内就能上线。
41436
+ */
41437
+ encode(reqId, pcm, end, onFrame, onPages) {
41427
41438
  const pages = [];
41439
+ const emit = onPages ?? ((built) => {
41440
+ pages.push(...built);
41441
+ });
41428
41442
  if (reqId !== this.currentReqId) {
41429
41443
  this.currentReqId = reqId;
41430
41444
  this.pcmRemainder = /* @__PURE__ */ new Int16Array(0);
41431
- pages.push(...this.muxer.startStream(reqId));
41445
+ emit(this.muxer.startStream(reqId));
41432
41446
  }
41433
41447
  const incoming = this.bytesToInt16(pcm);
41434
41448
  let samples = incoming;
@@ -41438,18 +41452,29 @@ var OggOpusEncoder = class {
41438
41452
  samples.set(incoming, this.pcmRemainder.length);
41439
41453
  }
41440
41454
  const frameGranule = this.muxer.toGranule(this.frameSamples);
41441
- const packets = [];
41455
+ const FRAMES_PER_SECOND = Math.round(1e3 / FRAME_MS);
41456
+ let packets = [];
41442
41457
  let offset = 0;
41458
+ const flushPackets = () => {
41459
+ if (packets.length === 0) return;
41460
+ const built = this.muxer.addPackets(packets, frameGranule);
41461
+ packets = [];
41462
+ emit(built);
41463
+ };
41443
41464
  while (samples.length - offset >= this.frameSamples) {
41444
41465
  const frame = samples.subarray(offset, offset + this.frameSamples);
41445
41466
  offset += this.frameSamples;
41446
41467
  const packet = this.encodeFrame(frame);
41447
- if (packet) packets.push(packet);
41468
+ if (packet) {
41469
+ packets.push(packet);
41470
+ onFrame?.(frame.length * 2);
41471
+ }
41472
+ if (onPages && packets.length >= FRAMES_PER_SECOND) flushPackets();
41448
41473
  }
41449
- pages.push(...this.muxer.addPackets(packets, frameGranule));
41474
+ flushPackets();
41450
41475
  this.pcmRemainder = samples.slice(offset);
41451
41476
  if (end) {
41452
- pages.push(...this.flush());
41477
+ emit(this.flush());
41453
41478
  this.currentReqId = null;
41454
41479
  }
41455
41480
  return pages;
@@ -41538,15 +41563,32 @@ self.onmessage = (e) => {
41538
41563
  });
41539
41564
  return;
41540
41565
  }
41541
- const pages = encoder.encode(msg.reqId, msg.pcm, msg.end);
41542
- const transfer = pages.map((p) => p.buffer);
41566
+ const frames = [];
41567
+ const frameTs = [];
41568
+ encoder.encode(msg.reqId, msg.pcm, msg.end, (pcmBytes) => {
41569
+ frames.push(pcmBytes);
41570
+ frameTs.push(performance.timeOrigin + performance.now());
41571
+ }, (batch) => {
41572
+ const transfer = batch.map((p) => p.buffer);
41573
+ post({
41574
+ type: "pages",
41575
+ reqId: msg.reqId,
41576
+ seq: msg.seq,
41577
+ end: msg.end,
41578
+ pages: batch,
41579
+ last: false
41580
+ }, transfer);
41581
+ });
41543
41582
  post({
41544
41583
  type: "pages",
41545
41584
  reqId: msg.reqId,
41546
41585
  seq: msg.seq,
41547
41586
  end: msg.end,
41548
- pages
41549
- }, transfer);
41587
+ pages: [],
41588
+ frames,
41589
+ frameTs,
41590
+ last: true
41591
+ });
41550
41592
  break;
41551
41593
  }
41552
41594
  case "destroy":