@spatius/avatarkit 1.3.7 → 1.3.8

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.
@@ -82,10 +82,6 @@ export declare class AvatarController {
82
82
  * Orthogonal to user pause — user pause/resume does not change it; only frame arrival does.
83
83
  */
84
84
  private isAudioStalledForStarvation;
85
- private playbackStuckCheckState;
86
- private readonly MAX_AUDIO_TIME_ZERO_COUNT;
87
- private readonly MAX_AUDIO_TIME_STUCK_COUNT;
88
- private readonly AUDIO_TIME_STUCK_THRESHOLD;
89
85
  private latencyMarks;
90
86
  /**
91
87
  * W3C `traceparent` the driving service stamped on this round's animation
@@ -52,6 +52,44 @@ export declare class AvatarView {
52
52
  width: number;
53
53
  height: number;
54
54
  };
55
+ /**
56
+ * Set once the first-frame callback has fired; the render loop samples the
57
+ * canvas on the first frame after this deadline and then clears it.
58
+ *
59
+ * The check exists because "the render loop is running" does not mean anything
60
+ * reached the screen. Failures along the render path mostly log and return, so
61
+ * a user seeing nothing produces no signal at all. This asks the only question
62
+ * that matters at the end: did this layer draw anything?
63
+ *
64
+ * Sampling has to happen inside the loop, right after renderFrame() — the
65
+ * context runs with preserveDrawingBuffer:false, so the buffer is only valid
66
+ * there. Reading from a timer would find it already cleared and report every
67
+ * healthy session as blank.
68
+ */
69
+ private _blankCheckDueAt;
70
+ private _blankCheckDone;
71
+ /** How long after the first frame to sample. Long enough for the pipeline to settle. */
72
+ private static readonly BLANK_CHECK_DELAY_MS;
73
+ /**
74
+ * Sample size. The browser scales the full canvas into this, so it covers the
75
+ * whole frame rather than a crop — the avatar is found wherever it sits. Kept
76
+ * tiny because the question is only "any opaque pixel at all"; reading the
77
+ * full canvas would copy megabytes for a yes/no answer.
78
+ */
79
+ private static readonly BLANK_CHECK_WIDTH;
80
+ private static readonly BLANK_CHECK_HEIGHT;
81
+ private scheduleBlankCanvasCheck;
82
+ /**
83
+ * Called from the render loop immediately after renderFrame(), while the
84
+ * drawing buffer is still valid.
85
+ */
86
+ private runBlankCanvasCheckIfDue;
87
+ /**
88
+ * Count pixels that are not fully transparent. Null when the sample could not
89
+ * be taken at all, which is not the same as a blank frame and must not be
90
+ * reported as one.
91
+ */
92
+ private countOpaquePixels;
55
93
  private _exportBitmapResolve;
56
94
  /**
57
95
  * Exports the current rendering as a Blob (PNG).
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as __toESM, t as __commonJSMin } from "./rolldown-runtime-FDOR9p9I.js";
2
- import { $ as DEFAULT_REGION, A as clockSync, B as OTEL_LOGS_ENDPOINT, C as logMetric, D as monoMarkFrom, E as initializeOtel, F as recordHttpClientDuration, G as idManager, H as OTEL_STREAM_NAME, I as recordMetric, J as AvatarError, K as generateTraceId, L as getSdkPackage, M as hostOf, N as cleanupOtelMetrics, O as monoTimestamp, P as initializeOtelMetrics, Q as DEFAULT_OPUS_BITRATE, R as resolveSdkVersion, S as logEvent, T as cleanupOtel, U as OTEL_USERNAME, V as OTEL_PASSWORD, W as isDebugMode, X as ConnectionState, Y as AvatarState, Z as ConversationState, _ as logSink, a as startLoadTrace, at as LogLevel, b as getLogsFeatureFlag, c as AudioFormat, ct as TransitionType, d as MessageType, et as DEFAULT_REGION_REQUEST, f as TransportCompression, g as setLogLevel, h as logger, i as startInitTrace, it as LoadProgress, j as fetchBootstrap, k as resolveMarks, l as EgressType, lt as requireSDK, m as BinaryWriter, n as initializeOtelTrace, nt as ErrorCode, o as startPlaybackTrace, ot as RENDER_QUALITY_PARAMS, p as BinaryReader, q as AnimationType, r as startConnectTrace, rt as FrameStarvationMode, s as buildTraceparent, st as RenderQuality, t as cleanupOtelTrace, tt as DrivingServiceMode, u as Message, ut as setSDKRef, v as cleanupPostHog, w as updatePostHogPersonPropertiesForFlags, x as initializePostHog, y as clientContextFields } from "./otel-trace-Y_BtH06z.js";
2
+ import { $ as DEFAULT_REGION, A as monoTimestamp, B as resolveSdkVersion, C as logMetric, D as initializeOtel, E as cleanupOtel, F as cleanupOtelMetrics, G as idManager, H as OTEL_LOGS_ENDPOINT, I as initializeOtelMetrics, J as AvatarError, K as generateTraceId, L as recordHttpClientDuration, M as clockSync, N as fetchBootstrap, P as hostOf, Q as DEFAULT_OPUS_BITRATE, R as recordMetric, S as logEvent, T as updatePostHogPersonPropertiesForFlags, U as OTEL_STREAM_NAME, W as isDebugMode, X as ConnectionState, Y as AvatarState, Z as ConversationState, _ as logSink, a as startLoadTrace, at as LogLevel, b as getLogsFeatureFlag, c as AudioFormat, ct as TransitionType, d as MessageType, et as DEFAULT_REGION_REQUEST, f as TransportCompression, g as setLogLevel, h as logger, i as startInitTrace, it as LoadProgress, j as resolveMarks, k as monoMarkFrom, l as EgressType, lt as requireSDK, m as BinaryWriter, n as initializeOtelTrace, nt as ErrorCode, o as startPlaybackTrace, ot as RENDER_QUALITY_PARAMS, p as BinaryReader, q as AnimationType, r as startConnectTrace, rt as FrameStarvationMode, s as buildTraceparent, st as RenderQuality, t as cleanupOtelTrace, tt as DrivingServiceMode, u as Message, ut as setSDKRef, v as cleanupPostHog, w as trackEvent, x as initializePostHog, y as clientContextFields, z as getSdkPackage } from "./otel-trace-CZJAGjGg.js";
3
3
  //#region core/Avatar.ts
4
4
  var Avatar = class {
5
5
  id;
@@ -41912,7 +41912,7 @@ var OpusDecoderProxy = class OpusDecoderProxy {
41912
41912
  try {
41913
41913
  worker = new Worker(new URL(
41914
41914
  /* @vite-ignore */
41915
- "" + new URL("assets/OpusDecoderWorker.worker-DbdfH4qX.js", import.meta.url).href,
41915
+ "" + new URL("assets/OpusDecoderWorker.worker-DVOREuTG.js", import.meta.url).href,
41916
41916
  "" + import.meta.url
41917
41917
  ), { type: "module" });
41918
41918
  } catch (err) {
@@ -44372,29 +44372,26 @@ function emitInitErrorLog(params) {
44372
44372
  ];
44373
44373
  const client = clientContextFields();
44374
44374
  for (const [k, v] of Object.entries(client)) logAttrs.push(kv(k, v));
44375
- const payload = { resourceLogs: [{
44376
- resource: { attributes: resourceAttrs },
44377
- scopeLogs: [{
44378
- scope: { name: OTEL_LOGGER_NAME },
44379
- logRecords: [{
44380
- timeUnixNano: nowNanos,
44381
- severityNumber: SEVERITY_ERROR,
44382
- severityText: "ERROR",
44383
- body: { stringValue: event },
44384
- attributes: logAttrs
44385
- }]
44386
- }]
44387
- }] };
44388
- const authToken = btoa(`${OTEL_USERNAME}:${OTEL_PASSWORD}`);
44389
44375
  fetch(OTEL_LOGS_ENDPOINT, {
44390
44376
  method: "POST",
44391
44377
  keepalive: true,
44392
44378
  headers: {
44393
44379
  "Content-Type": "application/json",
44394
- Authorization: `Basic ${authToken}`,
44395
44380
  "stream-name": OTEL_STREAM_NAME
44396
44381
  },
44397
- body: JSON.stringify(payload)
44382
+ body: JSON.stringify({ resourceLogs: [{
44383
+ resource: { attributes: resourceAttrs },
44384
+ scopeLogs: [{
44385
+ scope: { name: OTEL_LOGGER_NAME },
44386
+ logRecords: [{
44387
+ timeUnixNano: nowNanos,
44388
+ severityNumber: SEVERITY_ERROR,
44389
+ severityText: "ERROR",
44390
+ body: { stringValue: event },
44391
+ attributes: logAttrs
44392
+ }]
44393
+ }]
44394
+ }] })
44398
44395
  }).catch(() => {});
44399
44396
  } catch {}
44400
44397
  }
@@ -45730,7 +45727,7 @@ var AvatarSDK = class {
45730
45727
  static _initializationState = "uninitialized";
45731
45728
  static _initializingPromise = null;
45732
45729
  static _configuration = null;
45733
- static _version = "1.3.7";
45730
+ static _version = "1.3.8";
45734
45731
  static _avatarCore = null;
45735
45732
  static _cachedDeviceScore = null;
45736
45733
  static _rendererBackend = null;
@@ -46295,7 +46292,7 @@ var OpusEncoderProxy = class OpusEncoderProxy {
46295
46292
  try {
46296
46293
  worker = new Worker(new URL(
46297
46294
  /* @vite-ignore */
46298
- "" + new URL("assets/OpusEncoderWorker.worker-DVnsr0L5.js", import.meta.url).href,
46295
+ "" + new URL("assets/OpusEncoderWorker.worker-fkLsXEK1.js", import.meta.url).href,
46299
46296
  "" + import.meta.url
46300
46297
  ), { type: "module" });
46301
46298
  } catch (err) {
@@ -47830,15 +47827,6 @@ var AvatarController = class {
47830
47827
  * Orthogonal to user pause — user pause/resume does not change it; only frame arrival does.
47831
47828
  */
47832
47829
  isAudioStalledForStarvation = false;
47833
- playbackStuckCheckState = {
47834
- audioTimeZeroCount: 0,
47835
- lastAudioTime: 0,
47836
- audioTimeStuckCount: 0,
47837
- reported: false
47838
- };
47839
- MAX_AUDIO_TIME_ZERO_COUNT = 25;
47840
- MAX_AUDIO_TIME_STUCK_COUNT = 25;
47841
- AUDIO_TIME_STUCK_THRESHOLD = .001;
47842
47830
  latencyMarks = {
47843
47831
  accumulatedBytes: 0,
47844
47832
  taps: /* @__PURE__ */ new Map(),
@@ -49141,79 +49129,6 @@ var AvatarController = class {
49141
49129
  * Playback loop: Calculate animation frame based on audio time, notify render layer to render
49142
49130
  */
49143
49131
  /**
49144
- * 检测播放是否卡住(在过渡动画完成后)
49145
- * 注意:AudioContext suspended 的情况现在由 StreamingAudioPlayer 自动处理,不再作为触发条件
49146
- * 此函数主要用于检测其他原因导致的播放卡住(如网络问题、音频数据问题等)
49147
- *
49148
- * @param audioTime 当前音频时间
49149
- * @returns true 如果检测到卡住并已上报,false 否则
49150
- * @internal
49151
- */
49152
- checkPlaybackStuck(audioTime) {
49153
- const streamingPlayer = this.animationPlayer?.getStreamingPlayer();
49154
- if (!streamingPlayer) return false;
49155
- const state = this.playbackStuckCheckState;
49156
- const hasAnimationData = this.currentKeyframes.length > 0;
49157
- const hasAudioData = streamingPlayer.audioChunks?.length > 0;
49158
- const isNotPaused = this.currentState !== AvatarState.paused;
49159
- if (this.isAudioStalledForStarvation) {
49160
- state.audioTimeZeroCount = 0;
49161
- state.audioTimeStuckCount = 0;
49162
- state.lastAudioTime = 0;
49163
- state.reported = false;
49164
- return false;
49165
- }
49166
- if (!hasAnimationData || !hasAudioData || this.currentState === AvatarState.paused) {
49167
- state.audioTimeZeroCount = 0;
49168
- state.audioTimeStuckCount = 0;
49169
- state.lastAudioTime = 0;
49170
- state.reported = false;
49171
- return false;
49172
- }
49173
- if (state.reported) return true;
49174
- const audioContextState = streamingPlayer.audioContext?.state || "unknown";
49175
- const activeSourcesCount = streamingPlayer.activeSources?.size || 0;
49176
- const scheduledChunks = streamingPlayer.scheduledChunks || 0;
49177
- const hasScheduledButNoActive = scheduledChunks > 0 && activeSourcesCount === 0;
49178
- if (audioTime === 0) state.audioTimeZeroCount++;
49179
- else state.audioTimeZeroCount = 0;
49180
- if (Math.abs(audioTime - state.lastAudioTime) < this.AUDIO_TIME_STUCK_THRESHOLD) state.audioTimeStuckCount++;
49181
- else state.audioTimeStuckCount = 0;
49182
- state.lastAudioTime = audioTime;
49183
- if ((state.audioTimeZeroCount > this.MAX_AUDIO_TIME_ZERO_COUNT || state.audioTimeStuckCount > this.MAX_AUDIO_TIME_STUCK_COUNT || hasScheduledButNoActive && this.isPlaying) && isNotPaused) {
49184
- state.reported = true;
49185
- logEvent("playback_stuck_after_transition", "error", {
49186
- avatar_id: this.avatar.id,
49187
- conversation_id: this.currentConversationId || void 0,
49188
- audio_context_state: audioContextState,
49189
- audio_time: audioTime,
49190
- audio_time_zero: audioTime === 0,
49191
- audio_time_zero_count: state.audioTimeZeroCount,
49192
- audio_time_stuck: state.audioTimeStuckCount > this.MAX_AUDIO_TIME_STUCK_COUNT,
49193
- audio_time_stuck_count: state.audioTimeStuckCount,
49194
- has_scheduled_but_no_active: hasScheduledButNoActive,
49195
- is_playing: this.isPlaying,
49196
- current_state: this.currentState,
49197
- animation_frames_count: this.currentKeyframes.length,
49198
- audio_chunks_count: streamingPlayer.audioChunks?.length || 0,
49199
- scheduled_chunks: scheduledChunks,
49200
- active_sources_count: activeSourcesCount
49201
- });
49202
- return true;
49203
- }
49204
- return false;
49205
- }
49206
- /**
49207
- * Reset playback stuck detection counters
49208
- * @internal
49209
- */
49210
- resetPlaybackStuckCheckState() {
49211
- this.playbackStuckCheckState.audioTimeZeroCount = 0;
49212
- this.playbackStuckCheckState.audioTimeStuckCount = 0;
49213
- this.playbackStuckCheckState.lastAudioTime = 0;
49214
- this.playbackStuckCheckState.reported = false;
49215
- }
49216
- /**
49217
49132
  * Memory optimization: clean old keyframes if array is too long.
49218
49133
  * Extracted from the former computeAndRenderFrame so that the monitoring-only
49219
49134
  * playback loop can still manage memory without doing WASM computation.
@@ -49342,11 +49257,6 @@ var AvatarController = class {
49342
49257
  monitorPlaybackTick(audioTime, frameIndex) {
49343
49258
  if (!this.isPlaying || this.currentState === AvatarState.paused) return;
49344
49259
  try {
49345
- this.checkPlaybackStuck(audioTime);
49346
- if (audioTime > 0 && Math.abs(audioTime - this.playbackStuckCheckState.lastAudioTime) >= this.AUDIO_TIME_STUCK_THRESHOLD) {
49347
- this.playbackStuckCheckState.audioTimeZeroCount = 0;
49348
- this.playbackStuckCheckState.audioTimeStuckCount = 0;
49349
- }
49350
49260
  if (audioTime > 0) {
49351
49261
  if (frameIndex > this.lastRenderedFrameIndex) this.lastRenderedFrameIndex = frameIndex;
49352
49262
  this.cleanupKeyframesIfNeeded(frameIndex);
@@ -49416,7 +49326,6 @@ var AvatarController = class {
49416
49326
  this.resetConversationIdState();
49417
49327
  }
49418
49328
  stopPlayback() {
49419
- this.resetPlaybackStuckCheckState();
49420
49329
  this.isStartingPlayback = false;
49421
49330
  if (this.animationPlayer) {
49422
49331
  this.animationPlayer.stop();
@@ -51794,7 +51703,7 @@ function generateTransitionFrames(from, to, durationMs, fps = 25) {
51794
51703
  }
51795
51704
  //#endregion
51796
51705
  //#region core/AvatarView.ts
51797
- var AvatarView = class {
51706
+ var AvatarView = class AvatarView {
51798
51707
  avatarController;
51799
51708
  avatar;
51800
51709
  onFirstRendering;
@@ -51982,6 +51891,83 @@ var AvatarView = class {
51982
51891
  height: this.canvas.height
51983
51892
  };
51984
51893
  }
51894
+ /**
51895
+ * Set once the first-frame callback has fired; the render loop samples the
51896
+ * canvas on the first frame after this deadline and then clears it.
51897
+ *
51898
+ * The check exists because "the render loop is running" does not mean anything
51899
+ * reached the screen. Failures along the render path mostly log and return, so
51900
+ * a user seeing nothing produces no signal at all. This asks the only question
51901
+ * that matters at the end: did this layer draw anything?
51902
+ *
51903
+ * Sampling has to happen inside the loop, right after renderFrame() — the
51904
+ * context runs with preserveDrawingBuffer:false, so the buffer is only valid
51905
+ * there. Reading from a timer would find it already cleared and report every
51906
+ * healthy session as blank.
51907
+ */
51908
+ _blankCheckDueAt = null;
51909
+ _blankCheckDone = false;
51910
+ /** How long after the first frame to sample. Long enough for the pipeline to settle. */
51911
+ static BLANK_CHECK_DELAY_MS = 1e3;
51912
+ /**
51913
+ * Sample size. The browser scales the full canvas into this, so it covers the
51914
+ * whole frame rather than a crop — the avatar is found wherever it sits. Kept
51915
+ * tiny because the question is only "any opaque pixel at all"; reading the
51916
+ * full canvas would copy megabytes for a yes/no answer.
51917
+ */
51918
+ static BLANK_CHECK_WIDTH = 64;
51919
+ static BLANK_CHECK_HEIGHT = 128;
51920
+ scheduleBlankCanvasCheck() {
51921
+ if (this._blankCheckDone || this._blankCheckDueAt !== null) return;
51922
+ this._blankCheckDueAt = performance.now() + AvatarView.BLANK_CHECK_DELAY_MS;
51923
+ }
51924
+ /**
51925
+ * Called from the render loop immediately after renderFrame(), while the
51926
+ * drawing buffer is still valid.
51927
+ */
51928
+ runBlankCanvasCheckIfDue() {
51929
+ const dueAt = this._blankCheckDueAt;
51930
+ if (dueAt === null || performance.now() < dueAt) return;
51931
+ this._blankCheckDueAt = null;
51932
+ this._blankCheckDone = true;
51933
+ const sampleStart = performance.now();
51934
+ const opaquePixels = this.countOpaquePixels();
51935
+ const sampleMs = performance.now() - sampleStart;
51936
+ if (opaquePixels === null || opaquePixels > 0) return;
51937
+ logger.error(`[AvatarView] Blank canvas: nothing was drawn ${AvatarView.BLANK_CHECK_DELAY_MS}ms after the first frame`);
51938
+ trackEvent("render_blank_canvas", "error", {
51939
+ canvas_width: this.canvas?.width ?? 0,
51940
+ canvas_height: this.canvas?.height ?? 0,
51941
+ sampled_pixels: AvatarView.BLANK_CHECK_WIDTH * AvatarView.BLANK_CHECK_HEIGHT,
51942
+ delay_ms: AvatarView.BLANK_CHECK_DELAY_MS,
51943
+ sample_ms: Number(sampleMs.toFixed(1))
51944
+ });
51945
+ }
51946
+ /**
51947
+ * Count pixels that are not fully transparent. Null when the sample could not
51948
+ * be taken at all, which is not the same as a blank frame and must not be
51949
+ * reported as one.
51950
+ */
51951
+ countOpaquePixels() {
51952
+ const canvas = this.canvas;
51953
+ if (!canvas || canvas.width === 0 || canvas.height === 0) return null;
51954
+ const w = AvatarView.BLANK_CHECK_WIDTH;
51955
+ const h = AvatarView.BLANK_CHECK_HEIGHT;
51956
+ try {
51957
+ const scratch = document.createElement("canvas");
51958
+ scratch.width = w;
51959
+ scratch.height = h;
51960
+ const ctx = scratch.getContext("2d", { willReadFrequently: true });
51961
+ if (!ctx) return null;
51962
+ ctx.drawImage(canvas, 0, 0, w, h);
51963
+ const data = ctx.getImageData(0, 0, w, h).data;
51964
+ let count = 0;
51965
+ for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) count++;
51966
+ return count;
51967
+ } catch {
51968
+ return null;
51969
+ }
51970
+ }
51985
51971
  _exportBitmapResolve = null;
51986
51972
  /**
51987
51973
  * Exports the current rendering as a Blob (PNG).
@@ -52210,6 +52196,7 @@ var AvatarView = class {
52210
52196
  this.renderSystem.renderFrame();
52211
52197
  if (APP_CONFIG.debug) logger.log("[AvatarView] First frame rendered successfully");
52212
52198
  this.onFirstRendering?.();
52199
+ this.scheduleBlankCanvasCheck();
52213
52200
  } else throw new Error("Failed to compute first frame splat data");
52214
52201
  }
52215
52202
  /**
@@ -52289,8 +52276,10 @@ var AvatarView = class {
52289
52276
  this.currentFrame = frame;
52290
52277
  const wasmParams = convertProtoFlameToWasmParams(frame);
52291
52278
  computeInFlight = true;
52279
+ const computeStart = performance.now();
52292
52280
  avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0).then((splatData) => {
52293
- if (splatData && !this.isPureRenderingMode) this.doRender(splatData);
52281
+ const computeMs = performance.now() - computeStart;
52282
+ if (splatData && !this.isPureRenderingMode) this.doRender(splatData, computeMs);
52294
52283
  }).catch((e) => logger.warn("[AvatarView] Transition frame compute failed:", e instanceof Error ? e.message : String(e))).finally(() => {
52295
52284
  computeInFlight = false;
52296
52285
  });
@@ -52312,8 +52301,10 @@ var AvatarView = class {
52312
52301
  this.currentFrame = currentFrame;
52313
52302
  const wasmParams = convertProtoFlameToWasmParams(currentFrame);
52314
52303
  computeInFlight = true;
52304
+ const computeStart = performance.now();
52315
52305
  avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0).then((splatData) => {
52316
- if (splatData && !this.isPureRenderingMode) this.doRender(splatData);
52306
+ const computeMs = performance.now() - computeStart;
52307
+ if (splatData && !this.isPureRenderingMode) this.doRender(splatData, computeMs);
52317
52308
  }).catch((e) => logger.warn("[AvatarView] Conversation frame compute failed:", e instanceof Error ? e.message : String(e))).finally(() => {
52318
52309
  computeInFlight = false;
52319
52310
  });
@@ -52345,13 +52336,15 @@ var AvatarView = class {
52345
52336
  computeInFlight = true;
52346
52337
  (async () => {
52347
52338
  try {
52339
+ const computeStart = performance.now();
52348
52340
  const splatData = await avatarCore.computeCompleteFrameFlat({
52349
52341
  frameIndex: fi,
52350
52342
  animationInfo: animInfo
52351
52343
  }, this.characterHandle ?? void 0);
52344
+ const computeMs = performance.now() - computeStart;
52352
52345
  const currentParams = await avatarCore.getCurrentFrameParams(fi, this.characterId, animInfo);
52353
52346
  this.currentFrame = convertWasmParamsToProtoFlame(currentParams);
52354
- if (splatData && !this.isPureRenderingMode) this.doRender(splatData);
52347
+ if (splatData && !this.isPureRenderingMode) this.doRender(splatData, computeMs);
52355
52348
  } catch (e) {
52356
52349
  logger.warn("[AvatarView] Idle frame compute failed:", e instanceof Error ? e.message : String(e));
52357
52350
  } finally {
@@ -52481,12 +52474,13 @@ var AvatarView = class {
52481
52474
  * This is the single point of control for renderingEnabled flag
52482
52475
  * @internal
52483
52476
  */
52484
- doRender(splatData) {
52477
+ doRender(splatData, computeMs = 0) {
52485
52478
  if (!this.renderSystem || !this._renderingEnabled) return;
52486
- const startTime = performance.now();
52479
+ const startTime = performance.now() - computeMs;
52487
52480
  this.updateFPS();
52488
52481
  this.renderSystem.loadSplatsFromPackedData(splatData);
52489
52482
  this.renderSystem.renderFrame();
52483
+ this.runBlankCanvasCheckIfDue();
52490
52484
  if (this._exportBitmapResolve) {
52491
52485
  const resolve = this._exportBitmapResolve;
52492
52486
  this._exportBitmapResolve = null;
@@ -52676,8 +52670,10 @@ var AvatarView = class {
52676
52670
  const wasmParams = convertProtoFlameToWasmParams(flame);
52677
52671
  const avatarCore = AvatarSDK.getAvatarCore();
52678
52672
  if (!avatarCore) throw new Error("AvatarCore not available");
52673
+ const computeStart = performance.now();
52679
52674
  const splatData = await avatarCore.computeFrameFlatFromParams(wasmParams, this.characterHandle ?? void 0);
52680
- if (splatData) this.doRender(splatData);
52675
+ const computeMs = performance.now() - computeStart;
52676
+ if (splatData) this.doRender(splatData, computeMs);
52681
52677
  } catch (error) {
52682
52678
  logger.error("[AvatarView] Failed to render flame:", error instanceof Error ? error.message : String(error));
52683
52679
  throw error;
@@ -52983,8 +52979,10 @@ var AvatarView = class {
52983
52979
  const avatarCore = AvatarSDK.getAvatarCore();
52984
52980
  if (!avatarCore || !this.renderSystem) return;
52985
52981
  try {
52982
+ const computeStart = performance.now();
52986
52983
  const splatData = await avatarCore.computeCompleteFrameFlat({ frameIndex: this.activeAnimationState.frameIndex }, this.characterHandle ?? void 0);
52987
- if (splatData) this.doRender(splatData);
52984
+ const computeMs = performance.now() - computeStart;
52985
+ if (splatData) this.doRender(splatData, computeMs);
52988
52986
  } catch (error) {
52989
52987
  logger.warn("[AvatarView] Failed to render current idle frame:", error);
52990
52988
  }
@@ -79,6 +79,23 @@ export declare function peekTraceparent(rawMessage: Uint8Array): string | undefi
79
79
  * dropped before it is sent.
80
80
  */
81
81
  export declare function recordMetric(name: string, value: number, attributes?: Attributes): void;
82
+ /**
83
+ * Emit one event onto avatarkit's OTel log channel.
84
+ *
85
+ * Must go through here rather than the RTC SDK calling `logs.getLogger().emit()`
86
+ * itself. Sharing the global LoggerProvider only carries Resource-level
87
+ * attributes (app_id / region / sdk.version); the record-level identity fields
88
+ * — `user_id`, `client_id` — are stamped inside this SDK's emit path, so a
89
+ * record emitted directly through the provider carries none of them. Routing
90
+ * through here is what the Android / iOS RTC SDKs already do via their
91
+ * `RTCTelemetry.track` entry, and it additionally picks up crash-replay
92
+ * persistence and the pre-calibration queue.
93
+ *
94
+ * The timestamp is taken here rather than accepted from the caller: this path
95
+ * resolves it against the calibrated clock and stamps `server_timestamp`
96
+ * alongside, which a caller-supplied `Date.now()` cannot do.
97
+ */
98
+ export declare function trackEvent(event: string, level?: 'debug' | 'info' | 'warning' | 'error', contents?: Record<string, unknown>): void;
82
99
  /**
83
100
  * The client environment fields this SDK stamps on every OTel log record —
84
101
  * `host`, `domain`, `url`, `pathname`, `referrer`, `user_agent`, `locale`,
@@ -1,4 +1,4 @@
1
- import { A as clockSync, I as recordMetric$1, o as startPlaybackTrace$1, u as Message, y as clientContextFields$1, z as setSdkIdentity$1 } from "./otel-trace-Y_BtH06z.js";
1
+ import { M as clockSync, O as trackEventOtel, R as recordMetric$1, V as setSdkIdentity$1, o as startPlaybackTrace$1, u as Message, y as clientContextFields$1 } from "./otel-trace-CZJAGjGg.js";
2
2
  //#region internal-telemetry.ts
3
3
  /**
4
4
  * Internal telemetry surface for the companion RTC SDK.
@@ -91,6 +91,25 @@ function recordMetric(name, value, attributes = {}) {
91
91
  recordMetric$1(name, value, attributes);
92
92
  }
93
93
  /**
94
+ * Emit one event onto avatarkit's OTel log channel.
95
+ *
96
+ * Must go through here rather than the RTC SDK calling `logs.getLogger().emit()`
97
+ * itself. Sharing the global LoggerProvider only carries Resource-level
98
+ * attributes (app_id / region / sdk.version); the record-level identity fields
99
+ * — `user_id`, `client_id` — are stamped inside this SDK's emit path, so a
100
+ * record emitted directly through the provider carries none of them. Routing
101
+ * through here is what the Android / iOS RTC SDKs already do via their
102
+ * `RTCTelemetry.track` entry, and it additionally picks up crash-replay
103
+ * persistence and the pre-calibration queue.
104
+ *
105
+ * The timestamp is taken here rather than accepted from the caller: this path
106
+ * resolves it against the calibrated clock and stamps `server_timestamp`
107
+ * alongside, which a caller-supplied `Date.now()` cannot do.
108
+ */
109
+ function trackEvent(event, level = "info", contents = {}) {
110
+ trackEventOtel(event, level, contents);
111
+ }
112
+ /**
94
113
  * The client environment fields this SDK stamps on every OTel log record —
95
114
  * `host`, `domain`, `url`, `pathname`, `referrer`, `user_agent`, `locale`,
96
115
  * screen/viewport size and `timezone`.
@@ -137,4 +156,4 @@ function setSdkIdentity(identity) {
137
156
  setSdkIdentity$1(identity);
138
157
  }
139
158
  //#endregion
140
- export { clientContextFields, isClockCalibrated, peekTraceparent, recordMetric, resolveMonoTimestamp, setSdkIdentity, startPlaybackTrace };
159
+ export { clientContextFields, isClockCalibrated, peekTraceparent, recordMetric, resolveMonoTimestamp, setSdkIdentity, startPlaybackTrace, trackEvent };