@vivix-ai/ivi-frontend-sdk 0.3.8 → 0.3.10

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/dist/index.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var iviSdkTs = require('@vivix-ai/ivi-sdk-ts');
4
+ var api = require('@opentelemetry/api');
5
+ var websocket = require('@vivix-ai/ivi-sdk-ts/transports/websocket');
4
6
  var react = require('react');
5
7
  var jsxRuntime = require('react/jsx-runtime');
6
8
 
@@ -622,7 +624,9 @@ var SourceManager = class {
622
624
  durationMs: previous?.durationMs,
623
625
  hasAudio: previous?.hasAudio,
624
626
  error: previous?.status === "failed" ? previous.error : void 0,
625
- preload: previous?.preload
627
+ preload: previous?.preload,
628
+ origin: previous?.origin,
629
+ provisional: previous?.provisional ? false : previous?.provisional
626
630
  });
627
631
  this.sources = next;
628
632
  }
@@ -637,7 +641,38 @@ var SourceManager = class {
637
641
  height: payload.height,
638
642
  durationMs: payload.durationMs,
639
643
  hasAudio: payload.hasAudio,
640
- preload: previous?.preload
644
+ preload: previous?.preload,
645
+ origin: previous?.origin,
646
+ provisional: previous?.provisional ? false : previous?.provisional
647
+ });
648
+ this.sources = next;
649
+ }
650
+ upsertBootstrapReady(payload) {
651
+ const previous = this.sources.get(payload.sourceId);
652
+ const next = new Map(this.sources);
653
+ const source = {
654
+ source_id: payload.sourceId,
655
+ kind: payload.source?.kind ?? (payload.streamId ? "generation_stream" : "stream"),
656
+ stream_id: payload.source?.stream_id ?? payload.streamId,
657
+ asset_type: payload.source?.asset_type ?? "video",
658
+ url: payload.source?.url,
659
+ generation: payload.source?.generation,
660
+ metadata: {
661
+ ...payload.source?.metadata ?? {},
662
+ label: payload.source?.metadata?.label ?? "prebuilt"
663
+ }
664
+ };
665
+ next.set(payload.sourceId, {
666
+ source,
667
+ status: "ready",
668
+ playback: previous?.playback ?? payload.playback,
669
+ width: previous?.width ?? payload.width,
670
+ height: previous?.height ?? payload.height,
671
+ durationMs: previous?.durationMs ?? payload.durationMs,
672
+ hasAudio: previous?.hasAudio ?? payload.hasAudio,
673
+ preload: previous?.preload ?? { autoclearAfterPlay: true },
674
+ origin: previous?.origin ?? "prebuilt-session-response",
675
+ provisional: previous?.provisional ?? true
641
676
  });
642
677
  this.sources = next;
643
678
  }
@@ -671,11 +706,14 @@ var SourceManager = class {
671
706
  const next = /* @__PURE__ */ new Map();
672
707
  sources.forEach((source) => {
673
708
  const previous = this.sources.get(source.source_id);
709
+ const playback = source.playback ?? previous?.playback;
674
710
  next.set(source.source_id, {
675
711
  source,
676
- status: source.playback ? "ready" : "created",
677
- playback: source.playback,
678
- preload: previous?.preload
712
+ status: playback ? "ready" : "created",
713
+ playback,
714
+ preload: previous?.preload,
715
+ origin: previous?.origin,
716
+ provisional: source.playback && previous?.provisional ? false : previous?.provisional
679
717
  });
680
718
  });
681
719
  this.sources = next;
@@ -1104,6 +1142,94 @@ var ConversationManager = class {
1104
1142
  ];
1105
1143
  }
1106
1144
  };
1145
+ var DEFAULT_TRACER_NAME = "@vivix-ai/ivi-frontend-sdk";
1146
+ function getIviTelemetryTracer(options) {
1147
+ return options?.tracer ?? api.trace.getTracer(DEFAULT_TRACER_NAME);
1148
+ }
1149
+ function getIviTelemetryContext(options) {
1150
+ return options?.context ?? api.context.active();
1151
+ }
1152
+ function startIviTelemetrySpan(name, options, attributes) {
1153
+ const tracer = getIviTelemetryTracer(options);
1154
+ const parentContext = getIviTelemetryContext(options);
1155
+ const span = tracer.startSpan(
1156
+ name,
1157
+ {
1158
+ kind: api.SpanKind.INTERNAL,
1159
+ attributes: mergeIviTelemetryAttributes(options?.attributes, attributes)
1160
+ },
1161
+ parentContext
1162
+ );
1163
+ return {
1164
+ span,
1165
+ context: api.trace.setSpan(parentContext, span)
1166
+ };
1167
+ }
1168
+ function endIviTelemetrySpan(span, attributes) {
1169
+ const cleaned = cleanIviTelemetryAttributes(attributes);
1170
+ if (cleaned) {
1171
+ span.setAttributes(cleaned);
1172
+ }
1173
+ span.setStatus({ code: api.SpanStatusCode.OK });
1174
+ span.end();
1175
+ }
1176
+ function failIviTelemetrySpan(span, error, attributes) {
1177
+ const cleaned = cleanIviTelemetryAttributes(attributes);
1178
+ if (cleaned) {
1179
+ span.setAttributes(cleaned);
1180
+ }
1181
+ recordIviTelemetryError(span, error);
1182
+ span.end();
1183
+ }
1184
+ function recordIviTelemetrySpan(name, options, attributes, error) {
1185
+ const { span } = startIviTelemetrySpan(name, options, attributes);
1186
+ if (error === void 0) {
1187
+ span.setStatus({ code: api.SpanStatusCode.OK });
1188
+ } else {
1189
+ recordIviTelemetryError(span, error);
1190
+ }
1191
+ span.end();
1192
+ }
1193
+ function withIviTelemetryContext(options, context, attributes) {
1194
+ return {
1195
+ ...options,
1196
+ context,
1197
+ attributes: mergeIviTelemetryAttributes(options?.attributes, attributes)
1198
+ };
1199
+ }
1200
+ function mergeIviTelemetryAttributes(base, next) {
1201
+ return cleanIviTelemetryAttributes({
1202
+ ...base ?? {},
1203
+ ...next ?? {}
1204
+ });
1205
+ }
1206
+ function cleanIviTelemetryAttributes(attributes) {
1207
+ const cleaned = {};
1208
+ Object.entries(attributes ?? {}).forEach(([key, value]) => {
1209
+ if (isIviTelemetryAttributeValue(value)) {
1210
+ cleaned[key] = value;
1211
+ }
1212
+ });
1213
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
1214
+ }
1215
+ function recordIviTelemetryError(span, error) {
1216
+ const message = error instanceof Error ? error.message : String(error);
1217
+ span.recordException(error instanceof Error ? error : message);
1218
+ span.setStatus({
1219
+ code: api.SpanStatusCode.ERROR,
1220
+ message
1221
+ });
1222
+ }
1223
+ function isIviTelemetryAttributeValue(value) {
1224
+ if (typeof value === "string" || typeof value === "boolean") return true;
1225
+ if (typeof value === "number") return Number.isFinite(value);
1226
+ if (!Array.isArray(value)) return false;
1227
+ const nonNullValues = value.filter((item) => item !== null && item !== void 0);
1228
+ if (nonNullValues.length === 0) return true;
1229
+ const firstType = typeof nonNullValues[0];
1230
+ if (firstType !== "string" && firstType !== "number" && firstType !== "boolean") return false;
1231
+ return nonNullValues.every((item) => typeof item === firstType && (typeof item !== "number" || Number.isFinite(item)));
1232
+ }
1107
1233
 
1108
1234
  // src/runtime/managers/trtc-source-manager.ts
1109
1235
  var TAG = "[IVI-TRTC]";
@@ -1114,10 +1240,11 @@ var DEFAULT_DENOISER_OPTIONS = {
1114
1240
  mode: "normal"
1115
1241
  };
1116
1242
  var TrtcSourceManager = class {
1117
- constructor(onLog, onTrtcEvent, aiDenoiser) {
1243
+ constructor(onLog, onTrtcEvent, aiDenoiser, telemetry) {
1118
1244
  this.onLog = onLog;
1119
1245
  this.onTrtcEvent = onTrtcEvent;
1120
1246
  this.aiDenoiser = aiDenoiser;
1247
+ this.telemetry = telemetry;
1121
1248
  this.sessions = /* @__PURE__ */ new Map();
1122
1249
  this.sourceSessionKeys = /* @__PURE__ */ new Map();
1123
1250
  this.listeners = /* @__PURE__ */ new Map();
@@ -1279,6 +1406,7 @@ var TrtcSourceManager = class {
1279
1406
  startedVideoKeys: /* @__PURE__ */ new Set(),
1280
1407
  startingVideoKeys: /* @__PURE__ */ new Set()
1281
1408
  });
1409
+ this.log("info", `\u7ED1\u5B9A\u89C6\u56FE source=${sourceId} view=${viewId} views=${session.views.size}`);
1282
1410
  await this.ensureConnected(session);
1283
1411
  const binding = session.views.get(viewId);
1284
1412
  if (!binding) {
@@ -1294,6 +1422,7 @@ var TrtcSourceManager = class {
1294
1422
  }
1295
1423
  const binding = session.views.get(viewId);
1296
1424
  if (binding?.sourceId === sourceId) {
1425
+ this.log("info", `\u89E3\u7ED1\u89C6\u56FE source=${sourceId} view=${viewId}`);
1297
1426
  binding.container.removeAttribute(TRTC_VIEW_ATTR);
1298
1427
  session.views.delete(viewId);
1299
1428
  }
@@ -1311,6 +1440,24 @@ var TrtcSourceManager = class {
1311
1440
  binding.muted = muted;
1312
1441
  void this.applyAudioPolicy(session);
1313
1442
  }
1443
+ reassignView(fromSourceId, toSourceId, viewId) {
1444
+ if (fromSourceId === toSourceId) {
1445
+ return true;
1446
+ }
1447
+ const fromSession = this.getSession(fromSourceId);
1448
+ const toSession = this.getSession(toSourceId);
1449
+ if (!fromSession || !toSession || fromSession !== toSession) {
1450
+ return false;
1451
+ }
1452
+ const binding = fromSession.views.get(viewId);
1453
+ if (!binding || binding.sourceId !== fromSourceId) {
1454
+ return false;
1455
+ }
1456
+ this.log("info", `\u8FC1\u79FB\u89C6\u56FE source=${fromSourceId}->${toSourceId} view=${viewId}`);
1457
+ binding.sourceId = toSourceId;
1458
+ void this.applyAudioPolicy(fromSession);
1459
+ return true;
1460
+ }
1314
1461
  unlinkSourceFromSession(sourceId, session) {
1315
1462
  session.sourceIds.delete(sourceId);
1316
1463
  this.sourceSessionKeys.delete(sourceId);
@@ -1393,6 +1540,11 @@ var TrtcSourceManager = class {
1393
1540
  const client = TRTC.create();
1394
1541
  const onRemoteVideoAvailable = (event) => {
1395
1542
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
1543
+ recordIviTelemetrySpan(
1544
+ "ivi.media.trtc.remote_video_available",
1545
+ this.telemetry,
1546
+ buildTrtcMediaTelemetryAttributes(session.sourceId, event.userId, event.streamType)
1547
+ );
1396
1548
  const remoteVideoKey = buildRemoteVideoKey(event.userId, String(event.streamType));
1397
1549
  session.remoteVideoStreams.add(remoteVideoKey);
1398
1550
  if (!session.hasEverReceivedRemoteVideo) {
@@ -1485,6 +1637,7 @@ var TrtcSourceManager = class {
1485
1637
  if (!parsed) {
1486
1638
  continue;
1487
1639
  }
1640
+ this.log("info", `\u56DE\u653E\u5DF2\u77E5\u8FDC\u7AEF\u89C6\u9891 source=${binding.sourceId} userId=${parsed.userId} streamType=${parsed.streamType}`);
1488
1641
  await this.startRemoteVideoForBinding(
1489
1642
  client,
1490
1643
  parsed.userId,
@@ -1499,20 +1652,42 @@ var TrtcSourceManager = class {
1499
1652
  return;
1500
1653
  }
1501
1654
  binding.startingVideoKeys.add(remoteVideoKey);
1655
+ const telemetryAttributes = buildTrtcMediaTelemetryAttributes(
1656
+ binding.sourceId,
1657
+ userId,
1658
+ streamType
1659
+ );
1660
+ const telemetrySpan = startIviTelemetrySpan(
1661
+ "ivi.media.trtc.start_remote_video",
1662
+ this.telemetry,
1663
+ telemetryAttributes
1664
+ );
1502
1665
  try {
1666
+ this.log("info", `\u5F00\u59CB\u6E32\u67D3\u8FDC\u7AEF\u89C6\u9891 source=${binding.sourceId} view=${remoteVideoKey} userId=${userId} streamType=${streamType}`);
1503
1667
  await client.startRemoteVideo({
1504
1668
  userId,
1505
1669
  streamType,
1506
1670
  view: binding.container,
1507
1671
  option: { fillMode: "contain" }
1508
1672
  });
1673
+ endIviTelemetrySpan(telemetrySpan.span);
1674
+ recordIviTelemetrySpan(
1675
+ "ivi.media.trtc.remote_video_rendered",
1676
+ this.telemetry,
1677
+ telemetryAttributes
1678
+ );
1679
+ this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u6E32\u67D3\u5B8C\u6210 source=${binding.sourceId} userId=${userId} streamType=${streamType}`);
1509
1680
  binding.startedVideoKeys.add(remoteVideoKey);
1510
1681
  enforceContainMedia(binding.container);
1511
1682
  } catch (error) {
1512
1683
  if (error instanceof Error && /already started|OPERATION_ABORT/i.test(error.message)) {
1513
1684
  binding.startedVideoKeys.add(remoteVideoKey);
1685
+ endIviTelemetrySpan(telemetrySpan.span, {
1686
+ "ivi.media.trtc.start_remote_video.result": "already_started"
1687
+ });
1514
1688
  return;
1515
1689
  }
1690
+ failIviTelemetrySpan(telemetrySpan.span, error);
1516
1691
  this.log("warn", `\u8FDC\u7AEF\u89C6\u9891\u6E32\u67D3\u5931\u8D25 userId=${userId} streamType=${streamType}`, error);
1517
1692
  } finally {
1518
1693
  binding.startingVideoKeys.delete(remoteVideoKey);
@@ -1672,7 +1847,7 @@ function isRuntimeTrtcSource(source) {
1672
1847
  return source.status === "ready" && source.playback?.type === "trtc" && typeof source.playback.trtc === "object" && source.playback.trtc !== null;
1673
1848
  }
1674
1849
  function isSameTrtcConfig(a, b) {
1675
- return a.app_id === b.app_id && a.user_id === b.user_id && a.user_sig === b.user_sig && a.room_id === b.room_id;
1850
+ return a.app_id === b.app_id && a.user_id === b.user_id && a.room_id === b.room_id;
1676
1851
  }
1677
1852
  function buildTrtcSessionKey(trtc, aiDenoiser) {
1678
1853
  const denoiser = resolveAIDenoiserOptions(aiDenoiser);
@@ -1680,7 +1855,6 @@ function buildTrtcSessionKey(trtc, aiDenoiser) {
1680
1855
  appId: trtc.app_id,
1681
1856
  roomId: trtc.room_id,
1682
1857
  userId: trtc.user_id,
1683
- userSig: trtc.user_sig,
1684
1858
  aiDenoiser: {
1685
1859
  enabled: denoiser.enabled,
1686
1860
  mode: denoiser.mode,
@@ -1727,6 +1901,13 @@ function getAIDenoiserModeValue(mode) {
1727
1901
  function buildRemoteVideoKey(userId, streamType) {
1728
1902
  return `${userId}::${streamType}`;
1729
1903
  }
1904
+ function buildTrtcMediaTelemetryAttributes(sourceId, userId, streamType) {
1905
+ return {
1906
+ "ivi.source.id": sourceId,
1907
+ "ivi.trtc.user_id": userId,
1908
+ "ivi.trtc.stream_type": String(streamType)
1909
+ };
1910
+ }
1730
1911
  function parseRemoteVideoKey(key) {
1731
1912
  const separatorIndex = key.indexOf("::");
1732
1913
  if (separatorIndex < 0) {
@@ -1789,8 +1970,10 @@ function describeLivekitRoom(livekit) {
1789
1970
  // src/runtime/managers/livekit-source-manager.ts
1790
1971
  var TAG2 = "[IVI-LIVEKIT]";
1791
1972
  var LivekitSourceManager = class {
1792
- constructor(onLog) {
1973
+ constructor(onLog, onRemoteVideoAvailable, telemetry) {
1793
1974
  this.onLog = onLog;
1975
+ this.onRemoteVideoAvailable = onRemoteVideoAvailable;
1976
+ this.telemetry = telemetry;
1794
1977
  this.sessions = /* @__PURE__ */ new Map();
1795
1978
  this.listeners = /* @__PURE__ */ new Map();
1796
1979
  }
@@ -2012,11 +2195,21 @@ var LivekitSourceManager = class {
2012
2195
  kind
2013
2196
  });
2014
2197
  if (kind === "video" && !session.hasEverReceivedRemoteVideo) {
2198
+ recordIviTelemetrySpan(
2199
+ "ivi.media.livekit.remote_video_available",
2200
+ this.telemetry,
2201
+ {
2202
+ "ivi.source.id": session.sourceId,
2203
+ "ivi.livekit.participant_identity": participant.identity,
2204
+ "ivi.livekit.track_sid": track.sid
2205
+ }
2206
+ );
2015
2207
  session.hasEverReceivedRemoteVideo = true;
2016
2208
  for (const waiter of session.remoteVideoWaiters) {
2017
2209
  waiter(true);
2018
2210
  }
2019
2211
  session.remoteVideoWaiters.length = 0;
2212
+ this.onRemoteVideoAvailable?.(session.sourceId);
2020
2213
  }
2021
2214
  session.views.forEach((binding) => {
2022
2215
  this.attachTrackToView(track, kind, trackKey, binding);
@@ -2212,6 +2405,7 @@ var IviRuntimeCoordinator = class {
2212
2405
  this.userTextFlowCounter = 0;
2213
2406
  this.waitingTracksListValidation = false;
2214
2407
  this.waitingSourcesListValidation = false;
2408
+ this.bootstrapSource = null;
2215
2409
  this.state = {
2216
2410
  status: "idle",
2217
2411
  session: null,
@@ -2220,7 +2414,8 @@ var IviRuntimeCoordinator = class {
2220
2414
  sources: /* @__PURE__ */ new Map(),
2221
2415
  streams: /* @__PURE__ */ new Map(),
2222
2416
  conversationItems: /* @__PURE__ */ new Map(),
2223
- conversations: []
2417
+ conversations: [],
2418
+ bootstrap: null
2224
2419
  };
2225
2420
  this.client = client;
2226
2421
  this.config = {
@@ -2229,10 +2424,17 @@ var IviRuntimeCoordinator = class {
2229
2424
  };
2230
2425
  this.trtcSourceManager = new TrtcSourceManager(
2231
2426
  this.config.onLog,
2232
- (event) => this.emitTrtcEvent(event),
2233
- this.config.trtcAIDenoiser
2427
+ (event) => this.onTrtcManagerEvent(event),
2428
+ this.config.trtcAIDenoiser,
2429
+ this.config.telemetry
2430
+ );
2431
+ this.livekitSourceManager = new LivekitSourceManager(
2432
+ this.config.onLog,
2433
+ () => {
2434
+ this.recomposeBootstrapState();
2435
+ },
2436
+ this.config.telemetry
2234
2437
  );
2235
- this.livekitSourceManager = new LivekitSourceManager(this.config.onLog);
2236
2438
  this.sessionHandler = new SessionEventHandler(this.sessionManager, {
2237
2439
  onSessionCreated: (event) => this.onSessionCreated(event),
2238
2440
  onSessionEnded: (event) => this.onSessionEnded(event)
@@ -2269,15 +2471,27 @@ var IviRuntimeCoordinator = class {
2269
2471
  * 启动后状态会从 idle/stopped 进入 connecting,连接成功后由事件驱动进入后续状态。
2270
2472
  */
2271
2473
  async start() {
2272
- this.setState({
2273
- ...this.state,
2274
- status: "connecting"
2474
+ const telemetrySpan = startIviTelemetrySpan("ivi.runtime.start", this.config.telemetry, {
2475
+ "ivi.runtime.status": this.state.status,
2476
+ "ivi.session.id": this.state.session?.id
2275
2477
  });
2276
- this.dispatcher.start();
2277
2478
  try {
2479
+ this.setState({
2480
+ ...this.state,
2481
+ status: "connecting"
2482
+ });
2483
+ this.dispatcher.start();
2278
2484
  await this.client.connect();
2485
+ endIviTelemetrySpan(telemetrySpan.span, {
2486
+ "ivi.runtime.status": this.state.status,
2487
+ "ivi.session.id": this.state.session?.id
2488
+ });
2279
2489
  } catch (error) {
2280
2490
  this.stop();
2491
+ failIviTelemetrySpan(telemetrySpan.span, error, {
2492
+ "ivi.runtime.status": this.state.status,
2493
+ "ivi.session.id": this.state.session?.id
2494
+ });
2281
2495
  throw error;
2282
2496
  }
2283
2497
  }
@@ -2285,13 +2499,31 @@ var IviRuntimeCoordinator = class {
2285
2499
  * 停止协调器并断开实时连接,同时重置运行态数据为 stopped。
2286
2500
  */
2287
2501
  stop() {
2288
- this.dispatcher.stop();
2289
- this.client.disconnect();
2290
- this.resetStoppedState();
2502
+ const telemetrySpan = startIviTelemetrySpan("ivi.runtime.stop", this.config.telemetry, {
2503
+ "ivi.runtime.status": this.state.status,
2504
+ "ivi.session.id": this.state.session?.id
2505
+ });
2506
+ try {
2507
+ this.dispatcher.stop();
2508
+ this.client.disconnect();
2509
+ this.resetStoppedState();
2510
+ endIviTelemetrySpan(telemetrySpan.span, {
2511
+ "ivi.runtime.status": this.state.status
2512
+ });
2513
+ } catch (error) {
2514
+ failIviTelemetrySpan(telemetrySpan.span, error, {
2515
+ "ivi.runtime.status": this.state.status,
2516
+ "ivi.session.id": this.state.session?.id
2517
+ });
2518
+ throw error;
2519
+ }
2291
2520
  }
2292
2521
  getState() {
2293
2522
  return this.state;
2294
2523
  }
2524
+ getTelemetryOptions() {
2525
+ return this.config.telemetry;
2526
+ }
2295
2527
  onStateChange(listener) {
2296
2528
  this.stateListeners.add(listener);
2297
2529
  return () => {
@@ -2313,6 +2545,22 @@ var IviRuntimeCoordinator = class {
2313
2545
  emitLog(entry) {
2314
2546
  this.config.onLog?.(entry);
2315
2547
  }
2548
+ setBootstrapSource(source) {
2549
+ const previous = this.bootstrapSource;
2550
+ if (previous && (!source || previous.sourceId !== source.sourceId)) {
2551
+ const existing = this.sourceManager.get(previous.sourceId);
2552
+ if (existing?.origin === "prebuilt-session-response" && existing.provisional) {
2553
+ this.sourceManager.remove(previous.sourceId);
2554
+ }
2555
+ }
2556
+ this.bootstrapSource = source;
2557
+ this.ensureBootstrapSourceRegistered();
2558
+ this.syncPlaybackManagers();
2559
+ this.setState({
2560
+ ...this.state,
2561
+ sources: this.sourceManager.getAll()
2562
+ });
2563
+ }
2316
2564
  /**
2317
2565
  * 编排一次"用户文本输入 -> 触发模型回复"链路。
2318
2566
  *
@@ -2433,10 +2681,10 @@ var IviRuntimeCoordinator = class {
2433
2681
  this.trackManager.reset();
2434
2682
  this.sourceManager.reset();
2435
2683
  this.streamManager.reset();
2436
- this.trtcSourceManager.reset();
2437
- this.livekitSourceManager.reset();
2438
2684
  this.conversationManager.reset();
2439
2685
  this.reset();
2686
+ this.ensureBootstrapSourceRegistered();
2687
+ this.syncPlaybackManagers();
2440
2688
  const nextStatus = this.config.syncStageOnSessionCreated !== false ? "syncing" : "running";
2441
2689
  this.setState({
2442
2690
  status: nextStatus,
@@ -2474,8 +2722,8 @@ var IviRuntimeCoordinator = class {
2474
2722
  onTracksChanged(listRefreshed) {
2475
2723
  const nextStage = this.stageManager.getStage();
2476
2724
  this.sourceManager.syncWithTracks(this.trackManager.getAll());
2477
- this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2478
- this.livekitSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2725
+ this.ensureBootstrapSourceRegistered();
2726
+ this.syncPlaybackManagers();
2479
2727
  this.setState({
2480
2728
  ...this.state,
2481
2729
  tracks: this.trackManager.getAll(),
@@ -2489,8 +2737,8 @@ var IviRuntimeCoordinator = class {
2489
2737
  applyLocalTrackTake(trackId) {
2490
2738
  this.trackManager.applyTrackTook(trackId);
2491
2739
  this.sourceManager.syncWithTracks(this.trackManager.getAll());
2492
- this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2493
- this.livekitSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2740
+ this.ensureBootstrapSourceRegistered();
2741
+ this.syncPlaybackManagers();
2494
2742
  this.setState({
2495
2743
  ...this.state,
2496
2744
  tracks: this.trackManager.getAll(),
@@ -2530,8 +2778,8 @@ var IviRuntimeCoordinator = class {
2530
2778
  }
2531
2779
  onSourcesChanged(listRefreshed) {
2532
2780
  this.sourceManager.syncWithTracks(this.trackManager.getAll());
2533
- this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2534
- this.livekitSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2781
+ this.ensureBootstrapSourceRegistered();
2782
+ this.syncPlaybackManagers();
2535
2783
  this.setState({
2536
2784
  ...this.state,
2537
2785
  sources: this.sourceManager.getAll()
@@ -2554,12 +2802,110 @@ var IviRuntimeCoordinator = class {
2554
2802
  });
2555
2803
  }
2556
2804
  setState(nextState) {
2557
- if (this.state.status === nextState.status && this.state.session === nextState.session && this.state.stage === nextState.stage && this.state.tracks === nextState.tracks && this.state.sources === nextState.sources && this.state.streams === nextState.streams && this.state.conversationItems === nextState.conversationItems && this.state.conversations === nextState.conversations) {
2805
+ let resolvedNextState = nextState;
2806
+ if (this.bootstrapSource && nextState.status !== "stopped" && !nextState.sources.has(this.bootstrapSource.sourceId)) {
2807
+ this.ensureBootstrapSourceRegistered();
2808
+ resolvedNextState = {
2809
+ ...nextState,
2810
+ sources: this.sourceManager.getAll()
2811
+ };
2812
+ }
2813
+ const composedNextState = this.composeBootstrapState(resolvedNextState);
2814
+ if (this.state.status === composedNextState.status && this.state.session === composedNextState.session && this.state.stage === composedNextState.stage && this.state.tracks === composedNextState.tracks && this.state.sources === composedNextState.sources && this.state.streams === composedNextState.streams && this.state.conversationItems === composedNextState.conversationItems && this.state.conversations === composedNextState.conversations && isSameBootstrapState(this.state.bootstrap, composedNextState.bootstrap)) {
2558
2815
  return;
2559
2816
  }
2560
- this.state = nextState;
2817
+ const previousState = this.state;
2818
+ this.state = composedNextState;
2819
+ if (previousState.status !== composedNextState.status) {
2820
+ recordIviTelemetrySpan("ivi.runtime.state_change", this.config.telemetry, {
2821
+ "ivi.runtime.status.previous": previousState.status,
2822
+ "ivi.runtime.status.next": composedNextState.status,
2823
+ "ivi.runtime.state_change.reason": "state_update",
2824
+ "ivi.session.id": composedNextState.session?.id ?? previousState.session?.id
2825
+ });
2826
+ }
2561
2827
  this.stateListeners.forEach((listener) => listener(this.state));
2562
2828
  }
2829
+ composeBootstrapState(nextState) {
2830
+ const bootstrap = this.bootstrapSource;
2831
+ if (!bootstrap) {
2832
+ return nextState.bootstrap === null || nextState.bootstrap === void 0 ? nextState : { ...nextState, bootstrap: null };
2833
+ }
2834
+ const shouldExpose = this.shouldExposeBootstrap(nextState, bootstrap);
2835
+ const bootstrapState = {
2836
+ active: shouldExpose,
2837
+ slot: bootstrap.slot,
2838
+ trackId: bootstrap.trackId,
2839
+ sourceId: bootstrap.sourceId,
2840
+ streamId: bootstrap.streamId
2841
+ };
2842
+ if (!shouldExpose) {
2843
+ return {
2844
+ ...nextState,
2845
+ bootstrap: bootstrapState
2846
+ };
2847
+ }
2848
+ const tracks = new Map(nextState.tracks);
2849
+ tracks.set(bootstrap.trackId, {
2850
+ track_id: bootstrap.trackId,
2851
+ active_source_id: bootstrap.sourceId
2852
+ });
2853
+ return {
2854
+ ...nextState,
2855
+ tracks,
2856
+ bootstrap: bootstrapState
2857
+ };
2858
+ }
2859
+ shouldExposeBootstrap(state, bootstrap) {
2860
+ if (state.status === "stopped") {
2861
+ return false;
2862
+ }
2863
+ const bootstrapRuntimeSource = state.sources.get(bootstrap.sourceId);
2864
+ if (!isReadyRenderableSource(bootstrapRuntimeSource)) {
2865
+ return false;
2866
+ }
2867
+ const layer = (state.stage?.composition ?? []).find((item) => item.slot === bootstrap.slot);
2868
+ if (!layer) {
2869
+ return true;
2870
+ }
2871
+ const track = state.tracks.get(layer.track_id);
2872
+ const activeSourceId = track?.active_source_id;
2873
+ if (!activeSourceId || activeSourceId === bootstrap.sourceId) {
2874
+ return true;
2875
+ }
2876
+ return !this.isReadyForBootstrapHandoff(activeSourceId, state.sources.get(activeSourceId));
2877
+ }
2878
+ isReadyForBootstrapHandoff(sourceId, source) {
2879
+ if (!isReadyRenderableSource(source)) {
2880
+ return false;
2881
+ }
2882
+ if (isTrtcPlaybackSource(source)) {
2883
+ return this.trtcSourceManager.hasRemoteVideoAvailable(sourceId);
2884
+ }
2885
+ if (isLivekitPlaybackSource(source)) {
2886
+ return this.livekitSourceManager.hasRemoteVideoAvailable(sourceId);
2887
+ }
2888
+ return true;
2889
+ }
2890
+ recomposeBootstrapState() {
2891
+ if (!this.bootstrapSource) {
2892
+ return;
2893
+ }
2894
+ this.setState({ ...this.state });
2895
+ }
2896
+ ensureBootstrapSourceRegistered() {
2897
+ if (!this.bootstrapSource) {
2898
+ return;
2899
+ }
2900
+ if (this.sourceManager.has(this.bootstrapSource.sourceId)) {
2901
+ return;
2902
+ }
2903
+ this.sourceManager.upsertBootstrapReady(this.bootstrapSource);
2904
+ }
2905
+ syncPlaybackManagers() {
2906
+ this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2907
+ this.livekitSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2908
+ }
2563
2909
  emitEvent(event) {
2564
2910
  this.progressUserTextToResponseFlows(event);
2565
2911
  this.eventListeners.forEach((listener) => listener(event, this.state));
@@ -2568,6 +2914,12 @@ var IviRuntimeCoordinator = class {
2568
2914
  this.config.onTrtcEvent?.(event);
2569
2915
  this.trtcEventListeners.forEach((listener) => listener(event));
2570
2916
  }
2917
+ onTrtcManagerEvent(event) {
2918
+ this.emitTrtcEvent(event);
2919
+ if (event.type === "remote_video_available") {
2920
+ this.recomposeBootstrapState();
2921
+ }
2922
+ }
2571
2923
  resetStoppedState() {
2572
2924
  this.rejectPendingUserTextToResponseFlows(
2573
2925
  new Error("Runtime stopped before user text to response flow completed.")
@@ -2732,15 +3084,481 @@ function isLivekitPlaybackSource(source) {
2732
3084
  if (!source || !source.playback) return false;
2733
3085
  return isLivekitSourcePlayback(source.playback);
2734
3086
  }
3087
+ function isReadyRenderableSource(source) {
3088
+ return Boolean(source && source.status === "ready" && source.playback);
3089
+ }
3090
+ function isSameBootstrapState(a, b) {
3091
+ if (!a && !b) return true;
3092
+ if (!a || !b) return false;
3093
+ return a.active === b.active && a.slot === b.slot && a.trackId === b.trackId && a.sourceId === b.sourceId && a.streamId === b.streamId;
3094
+ }
3095
+ var IviCreateIVISessionError = class extends Error {
3096
+ constructor(response, body) {
3097
+ super(`CreateIVISession failed with ${response.status} ${response.statusText}`);
3098
+ this.name = "IviCreateIVISessionError";
3099
+ this.status = response.status;
3100
+ this.statusText = response.statusText;
3101
+ this.body = body;
3102
+ }
3103
+ };
3104
+ async function createIVISession(request, options) {
3105
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3106
+ if (!fetchImpl) {
3107
+ throw new Error("fetch is not available. Pass options.fetch to createIVISession.");
3108
+ }
3109
+ const telemetrySpan = startIviTelemetrySpan(
3110
+ "ivi.session.create",
3111
+ options.telemetry,
3112
+ buildCreateSessionTelemetryAttributes(request)
3113
+ );
3114
+ try {
3115
+ const requestBody = toCpCreateIVISessionRequestBody(request);
3116
+ const headers = await buildHeaders(options.headers);
3117
+ injectIviTelemetryHeaders(headers, telemetrySpan.context);
3118
+ const response = await fetchImpl(resolveCreateIVISessionUrl(options), {
3119
+ method: "POST",
3120
+ headers,
3121
+ body: JSON.stringify(requestBody),
3122
+ credentials: options.credentials,
3123
+ signal: options.signal
3124
+ });
3125
+ const responseBody = await readJsonResponse(response);
3126
+ if (!response.ok) {
3127
+ const error = new IviCreateIVISessionError(response, responseBody);
3128
+ failIviTelemetrySpan(telemetrySpan.span, error, {
3129
+ "http.response.status_code": response.status,
3130
+ "http.response.status_text": response.statusText
3131
+ });
3132
+ throw error;
3133
+ }
3134
+ const result = normalizeCreateIVISessionResponse(responseBody, request, requestBody);
3135
+ endIviTelemetrySpan(telemetrySpan.span, {
3136
+ "http.response.status_code": response.status,
3137
+ "ivi.session.id": result.iviSessionId,
3138
+ "ivi.prebuild.playback.kind": result.prebuiltPlayback?.kind
3139
+ });
3140
+ return result;
3141
+ } catch (error) {
3142
+ if (!(error instanceof IviCreateIVISessionError)) {
3143
+ failIviTelemetrySpan(telemetrySpan.span, error);
3144
+ }
3145
+ throw error;
3146
+ }
3147
+ }
3148
+ function createRuntimeFromSession(session, options = {}) {
3149
+ const telemetrySpan = startIviTelemetrySpan("ivi.runtime.create", options.telemetry, {
3150
+ "ivi.session.id": session.iviSessionId,
3151
+ "ivi.prebuild.playback.kind": session.prebuiltPlayback?.kind
3152
+ });
3153
+ try {
3154
+ const websocketUrl = buildWebSocketUrl(session.endpoint, options.websocket);
3155
+ const childTelemetry = withIviTelemetryContext(options.telemetry, telemetrySpan.context, {
3156
+ "ivi.session.id": session.iviSessionId
3157
+ });
3158
+ const clientTelemetry = mergeIviTelemetryOptions(
3159
+ options.clientConfig?.telemetry,
3160
+ childTelemetry
3161
+ );
3162
+ const runtimeTelemetry = mergeIviTelemetryOptions(
3163
+ options.runtimeConfig?.telemetry,
3164
+ childTelemetry
3165
+ );
3166
+ const client = new iviSdkTs.IviClient({
3167
+ ...options.clientConfig ?? {},
3168
+ telemetry: clientTelemetry,
3169
+ transport: new websocket.WebSocketTransport({
3170
+ url: websocketUrl,
3171
+ sessionId: session.iviSessionId,
3172
+ protocols: options.websocket?.protocols,
3173
+ socketFactory: options.websocket?.socketFactory
3174
+ })
3175
+ });
3176
+ const runtime = new IviRuntimeCoordinator(client, {
3177
+ ...options.runtimeConfig ?? {},
3178
+ telemetry: runtimeTelemetry
3179
+ });
3180
+ if (options.fastStart !== false && session.prebuiltPlayback) {
3181
+ runtime.setBootstrapSource(toBootstrapSource(session.prebuiltPlayback, options));
3182
+ }
3183
+ endIviTelemetrySpan(telemetrySpan.span);
3184
+ return {
3185
+ session,
3186
+ client,
3187
+ runtime
3188
+ };
3189
+ } catch (error) {
3190
+ failIviTelemetrySpan(telemetrySpan.span, error);
3191
+ throw error;
3192
+ }
3193
+ }
3194
+ async function createIVISessionRuntime(request, options) {
3195
+ const telemetry = mergeIviTelemetryOptions(options.telemetry, options.http.telemetry);
3196
+ const session = await createIVISession(request, {
3197
+ ...options.http,
3198
+ telemetry
3199
+ });
3200
+ return createRuntimeFromSession(session, {
3201
+ ...options,
3202
+ telemetry
3203
+ });
3204
+ }
3205
+ function toCpCreateIVISessionRequestBody(request) {
3206
+ const body = { ...request };
3207
+ delete body.idempotencyKey;
3208
+ delete body.streamType;
3209
+ delete body.iviVersion;
3210
+ delete body.sessionParameters;
3211
+ delete body.enableDecoderPublish;
3212
+ delete body.sessionRecording;
3213
+ delete body.prebuiltCharacters;
3214
+ delete body.prebuiltStream;
3215
+ if (request.idempotencyKey !== void 0) body.idempotency_key = request.idempotencyKey;
3216
+ if (request.streamType !== void 0) body.stream_type = request.streamType;
3217
+ if (request.iviVersion !== void 0) body.ivi_version = request.iviVersion;
3218
+ if (request.sessionParameters !== void 0) body.session_parameters = request.sessionParameters;
3219
+ if (request.enableDecoderPublish !== void 0) body.enable_decoder_publish = request.enableDecoderPublish;
3220
+ if (request.sessionRecording !== void 0) {
3221
+ body.session_recording = {
3222
+ enabled: request.sessionRecording.enabled,
3223
+ aspect_ratio: request.sessionRecording.aspectRatio
3224
+ };
3225
+ }
3226
+ if (request.prebuiltCharacters !== void 0) {
3227
+ body.prebuilt_characters = request.prebuiltCharacters.map((character) => ({
3228
+ character_id: character.characterId,
3229
+ character_payload: serializePrebuiltPayload(character.characterPayload)
3230
+ }));
3231
+ }
3232
+ if (request.prebuiltStream !== void 0) {
3233
+ body.prebuilt_stream = {
3234
+ stream_id: request.prebuiltStream.streamId,
3235
+ mode: request.prebuiltStream.mode,
3236
+ stream_config: serializePrebuiltPayload(request.prebuiltStream.streamConfig)
3237
+ };
3238
+ }
3239
+ return body;
3240
+ }
3241
+ function normalizeCreateIVISessionResponse(responseBody, request = {}, requestBody = toCpCreateIVISessionRequestBody(request)) {
3242
+ const payload = unwrapResponsePayload(responseBody);
3243
+ const iviSessionId = getString(payload, "ivi_session_id") ?? getString(payload, "iviSessionId");
3244
+ const endpoint = getString(payload, "endpoint");
3245
+ if (!iviSessionId) {
3246
+ throw new Error("CreateIVISession response missing ivi_session_id.");
3247
+ }
3248
+ if (!endpoint) {
3249
+ throw new Error("CreateIVISession response missing endpoint.");
3250
+ }
3251
+ const trtcInfo = normalizeTrtcInfo(getObjectValue(payload, "trtc_info") ?? getObjectValue(payload, "trtcInfo"));
3252
+ const livekitInfo = normalizeLivekitInfo(getObjectValue(payload, "livekit_info") ?? getObjectValue(payload, "livekitInfo"));
3253
+ const prebuildInfo = getObjectValue(payload, "prebuild_trtc_info") ?? getObjectValue(payload, "prebuildTrtcInfo");
3254
+ const prebuildTrtcInfo = normalizeTrtcInfo(
3255
+ getObjectValue(prebuildInfo, "trtc_info") ?? getObjectValue(prebuildInfo, "trtcInfo")
3256
+ );
3257
+ const prebuildLivekitInfo = normalizeLivekitInfo(
3258
+ getObjectValue(prebuildInfo, "livekit_info") ?? getObjectValue(prebuildInfo, "livekitInfo")
3259
+ );
3260
+ const prebuiltPlayback = buildPrebuiltPlayback(
3261
+ request,
3262
+ requestBody,
3263
+ prebuildTrtcInfo,
3264
+ prebuildLivekitInfo
3265
+ );
3266
+ return {
3267
+ iviSessionId,
3268
+ endpoint,
3269
+ tokenInfo: getObjectValue(payload, "token_info") ?? getObjectValue(payload, "tokenInfo"),
3270
+ trtcInfo,
3271
+ livekitInfo,
3272
+ prebuildTrtcInfo: prebuildTrtcInfo || prebuildLivekitInfo ? {
3273
+ trtcInfo: prebuildTrtcInfo,
3274
+ livekitInfo: prebuildLivekitInfo
3275
+ } : void 0,
3276
+ prebuiltPlayback,
3277
+ raw: responseBody,
3278
+ requestBody
3279
+ };
3280
+ }
3281
+ function resolveCreateIVISessionUrl(options) {
3282
+ if (options.url) return options.url;
3283
+ if (!options.baseUrl) {
3284
+ throw new Error("CreateIVISession requires options.baseUrl or options.url.");
3285
+ }
3286
+ const path = options.path ?? "/v1/ivi/sessions";
3287
+ return new URL(path, ensureTrailingSlash(options.baseUrl)).toString();
3288
+ }
3289
+ function buildCreateSessionTelemetryAttributes(request) {
3290
+ return {
3291
+ "ivi.version": request.iviVersion,
3292
+ "ivi.stream.type": request.streamType,
3293
+ "ivi.prebuild.enabled": Boolean(
3294
+ request.prebuiltStream || request.prebuiltCharacters && request.prebuiltCharacters.length > 0
3295
+ )
3296
+ };
3297
+ }
3298
+ function mergeIviTelemetryOptions(explicit, inherited) {
3299
+ if (!explicit && !inherited) {
3300
+ return void 0;
3301
+ }
3302
+ return {
3303
+ tracer: explicit?.tracer ?? inherited?.tracer,
3304
+ context: explicit?.context ?? inherited?.context,
3305
+ attributes: mergeIviTelemetryAttributes(inherited?.attributes, explicit?.attributes)
3306
+ };
3307
+ }
3308
+ async function buildHeaders(headers) {
3309
+ const resolvedHeaders = typeof headers === "function" ? await headers() : headers;
3310
+ const next = new Headers(resolvedHeaders);
3311
+ if (!next.has("content-type")) {
3312
+ next.set("content-type", "application/json");
3313
+ }
3314
+ return next;
3315
+ }
3316
+ var headersTextMapSetter = {
3317
+ set(carrier, key, value) {
3318
+ carrier.set(key, value);
3319
+ }
3320
+ };
3321
+ function injectIviTelemetryHeaders(headers, context) {
3322
+ api.propagation.inject(context, headers, headersTextMapSetter);
3323
+ }
3324
+ async function readJsonResponse(response) {
3325
+ const text = await response.text();
3326
+ if (!text) {
3327
+ return null;
3328
+ }
3329
+ try {
3330
+ return JSON.parse(text);
3331
+ } catch {
3332
+ return text;
3333
+ }
3334
+ }
3335
+ function buildWebSocketUrl(endpoint, options) {
3336
+ const rawUrl = options?.url ?? endpoint;
3337
+ if (!options?.query) {
3338
+ return rawUrl;
3339
+ }
3340
+ const url = new URL(rawUrl);
3341
+ Object.entries(options.query).forEach(([key, value]) => {
3342
+ if (value === void 0 || value === null) return;
3343
+ url.searchParams.set(key, value);
3344
+ });
3345
+ return url.toString();
3346
+ }
3347
+ function toBootstrapSource(prebuiltPlayback, options) {
3348
+ return {
3349
+ sourceId: prebuiltPlayback.sourceId,
3350
+ streamId: prebuiltPlayback.streamId,
3351
+ slot: options.bootstrapSlot ?? "main",
3352
+ trackId: options.bootstrapTrackId ?? "__ivi_bootstrap_track",
3353
+ playback: prebuiltPlayback.playback,
3354
+ source: {
3355
+ source_id: prebuiltPlayback.sourceId,
3356
+ kind: prebuiltPlayback.streamId ? "generation_stream" : "stream",
3357
+ stream_id: prebuiltPlayback.streamId,
3358
+ asset_type: "video",
3359
+ metadata: {
3360
+ label: "prebuilt"
3361
+ }
3362
+ }
3363
+ };
3364
+ }
3365
+ function buildPrebuiltPlayback(request, requestBody, trtcInfo, livekitInfo) {
3366
+ if (!trtcInfo && !livekitInfo) {
3367
+ return void 0;
3368
+ }
3369
+ const streamId = resolvePrebuiltStreamId(request, requestBody, trtcInfo, livekitInfo);
3370
+ const sourceId = request.prebuiltStream?.sourceId ?? streamId;
3371
+ if (!sourceId) {
3372
+ return void 0;
3373
+ }
3374
+ if (trtcInfo) {
3375
+ return {
3376
+ sourceId,
3377
+ streamId,
3378
+ kind: "trtc",
3379
+ playback: {
3380
+ type: "trtc",
3381
+ trtc: trtcInfo
3382
+ },
3383
+ provisional: true
3384
+ };
3385
+ }
3386
+ if (livekitInfo) {
3387
+ return {
3388
+ sourceId,
3389
+ streamId,
3390
+ kind: "livekit",
3391
+ playback: {
3392
+ type: "livekit",
3393
+ livekit: livekitInfo
3394
+ },
3395
+ provisional: true
3396
+ };
3397
+ }
3398
+ return void 0;
3399
+ }
3400
+ function resolvePrebuiltStreamId(request, requestBody, trtcInfo, livekitInfo) {
3401
+ const prebuiltStream = getObjectValue(requestBody, "prebuilt_stream");
3402
+ return request.prebuiltStream?.streamId ?? getString(prebuiltStream, "stream_id") ?? getString(prebuiltStream, "streamId") ?? trtcInfo?.room_id ?? livekitInfo?.room;
3403
+ }
3404
+ function normalizeTrtcInfo(value) {
3405
+ if (!isRecord(value)) {
3406
+ return void 0;
3407
+ }
3408
+ const appId = getString(value, "app_id") ?? getString(value, "appId");
3409
+ const roomId = getString(value, "room_id") ?? getString(value, "roomId");
3410
+ const userId = getString(value, "user_id") ?? getString(value, "userId");
3411
+ const userSig = getString(value, "user_sig") ?? getString(value, "userSig");
3412
+ if (!appId || !roomId || !userId || !userSig) {
3413
+ return void 0;
3414
+ }
3415
+ const normalized = {
3416
+ ...value,
3417
+ app_id: appId,
3418
+ room_id: roomId,
3419
+ user_id: userId,
3420
+ user_sig: userSig
3421
+ };
3422
+ delete normalized.secret_key;
3423
+ delete normalized.secretKey;
3424
+ return normalized;
3425
+ }
3426
+ function normalizeLivekitInfo(value) {
3427
+ if (!isRecord(value)) {
3428
+ return void 0;
3429
+ }
3430
+ const wsUrl = getString(value, "ws_url") ?? getString(value, "url") ?? getString(value, "wsUrl");
3431
+ const token = getString(value, "token") ?? getString(value, "access_token") ?? getString(value, "accessToken");
3432
+ if (!wsUrl || !token) {
3433
+ return void 0;
3434
+ }
3435
+ return {
3436
+ ws_url: wsUrl,
3437
+ token,
3438
+ room: getString(value, "room") ?? getString(value, "room_name") ?? getString(value, "roomName"),
3439
+ identity: getString(value, "identity")
3440
+ };
3441
+ }
3442
+ function serializePrebuiltPayload(value) {
3443
+ if (typeof value === "string") {
3444
+ return value;
3445
+ }
3446
+ return encodeJsonBase64(value);
3447
+ }
3448
+ function encodeJsonBase64(value) {
3449
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
3450
+ let binary = "";
3451
+ bytes.forEach((byte) => {
3452
+ binary += String.fromCharCode(byte);
3453
+ });
3454
+ if (typeof btoa === "function") {
3455
+ return btoa(binary);
3456
+ }
3457
+ const maybeBuffer = globalThis.Buffer;
3458
+ if (maybeBuffer) {
3459
+ return maybeBuffer.from(bytes).toString("base64");
3460
+ }
3461
+ throw new Error("Base64 encoder is not available in this runtime.");
3462
+ }
3463
+ function unwrapResponsePayload(value) {
3464
+ if (!isRecord(value)) {
3465
+ return {};
3466
+ }
3467
+ const data = getObjectValue(value, "data");
3468
+ return data ?? value;
3469
+ }
3470
+ function getObjectValue(value, key) {
3471
+ if (!isRecord(value)) {
3472
+ return void 0;
3473
+ }
3474
+ const nested = value[key];
3475
+ return isRecord(nested) ? nested : void 0;
3476
+ }
3477
+ function getString(value, key) {
3478
+ if (!isRecord(value)) {
3479
+ return void 0;
3480
+ }
3481
+ const nested = value[key];
3482
+ return typeof nested === "string" && nested.trim().length > 0 ? nested.trim() : void 0;
3483
+ }
3484
+ function isRecord(value) {
3485
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3486
+ }
3487
+ function ensureTrailingSlash(value) {
3488
+ return value.endsWith("/") ? value : `${value}/`;
3489
+ }
3490
+
3491
+ // src/sdk.ts
2735
3492
  var IviFrontendSdk = class {
3493
+ constructor(config = {}) {
3494
+ this.config = config;
3495
+ }
2736
3496
  createClient(config) {
2737
- return new iviSdkTs.IviClient(config);
3497
+ return new iviSdkTs.IviClient({
3498
+ ...config,
3499
+ telemetry: mergeSdkTelemetryOptions(config.telemetry, this.config.telemetry)
3500
+ });
2738
3501
  }
2739
3502
  createRuntimeCoordinator(clientConfig, runtimeConfig) {
2740
3503
  const client = this.createClient(clientConfig);
2741
- return new IviRuntimeCoordinator(client, runtimeConfig);
3504
+ return new IviRuntimeCoordinator(client, {
3505
+ ...runtimeConfig ?? {},
3506
+ telemetry: mergeSdkTelemetryOptions(runtimeConfig?.telemetry, this.config.telemetry)
3507
+ });
3508
+ }
3509
+ createIVISession(request, options) {
3510
+ const telemetry = mergeSdkTelemetryChain(
3511
+ this.config.telemetry,
3512
+ this.config.http?.telemetry,
3513
+ options?.telemetry
3514
+ );
3515
+ return createIVISession(request, {
3516
+ ...this.config.http ?? {},
3517
+ ...options ?? {},
3518
+ telemetry
3519
+ });
3520
+ }
3521
+ createRuntimeFromSession(session, options) {
3522
+ return createRuntimeFromSession(session, {
3523
+ ...options ?? {},
3524
+ telemetry: mergeSdkTelemetryChain(this.config.telemetry, options?.telemetry)
3525
+ });
3526
+ }
3527
+ createIVISessionRuntime(request, options) {
3528
+ const telemetry = mergeSdkTelemetryChain(
3529
+ this.config.telemetry,
3530
+ this.config.http?.telemetry,
3531
+ options?.http?.telemetry,
3532
+ options?.telemetry
3533
+ );
3534
+ const http = {
3535
+ ...this.config.http ?? {},
3536
+ ...options?.http ?? {},
3537
+ telemetry
3538
+ };
3539
+ return createIVISessionRuntime(request, {
3540
+ ...options ?? {},
3541
+ telemetry,
3542
+ http
3543
+ });
2742
3544
  }
2743
3545
  };
3546
+ function mergeSdkTelemetryChain(...items) {
3547
+ return items.reduce(
3548
+ (merged, item) => mergeSdkTelemetryOptions(item, merged),
3549
+ void 0
3550
+ );
3551
+ }
3552
+ function mergeSdkTelemetryOptions(explicit, inherited) {
3553
+ if (!explicit && !inherited) {
3554
+ return void 0;
3555
+ }
3556
+ return {
3557
+ tracer: explicit?.tracer ?? inherited?.tracer,
3558
+ context: explicit?.context ?? inherited?.context,
3559
+ attributes: mergeIviTelemetryAttributes(inherited?.attributes, explicit?.attributes)
3560
+ };
3561
+ }
2744
3562
  var IviStageViewContext = react.createContext(null);
2745
3563
  var EMPTY_RUNTIME_STATE = {
2746
3564
  status: "idle",
@@ -2750,7 +3568,8 @@ var EMPTY_RUNTIME_STATE = {
2750
3568
  sources: /* @__PURE__ */ new Map(),
2751
3569
  streams: /* @__PURE__ */ new Map(),
2752
3570
  conversationItems: /* @__PURE__ */ new Map(),
2753
- conversations: []
3571
+ conversations: [],
3572
+ bootstrap: null
2754
3573
  };
2755
3574
  function useRuntimeState(runtime) {
2756
3575
  const [state, setState] = react.useState(() => runtime?.getState() ?? EMPTY_RUNTIME_STATE);
@@ -2778,10 +3597,10 @@ function IVIStageView(props) {
2778
3597
  const state = useRuntimeState(runtime);
2779
3598
  const slotTrackMap = react.useMemo(() => {
2780
3599
  return buildSlotTrackMapFromState(state);
2781
- }, [state.stage]);
3600
+ }, [state.stage, state.bootstrap]);
2782
3601
  const slotBindings = react.useMemo(() => {
2783
3602
  return buildSlotBindingsFromState(state);
2784
- }, [state.stage, state.tracks, state.sources]);
3603
+ }, [state.stage, state.tracks, state.sources, state.bootstrap]);
2785
3604
  react.useEffect(() => {
2786
3605
  onBindingsChange?.(slotBindings);
2787
3606
  }, [slotBindings, onBindingsChange]);
@@ -2821,15 +3640,18 @@ function buildSlotTrackMapFromState(state) {
2821
3640
  (state.stage?.composition ?? []).forEach((item) => {
2822
3641
  map.set(item.slot, item.track_id);
2823
3642
  });
3643
+ if (state.bootstrap?.active) {
3644
+ map.set(state.bootstrap.slot, state.bootstrap.trackId);
3645
+ }
2824
3646
  return map;
2825
3647
  }
2826
3648
  function buildSlotBindingsFromState(state) {
2827
- const bindings = [];
3649
+ const bindingsBySlot = /* @__PURE__ */ new Map();
2828
3650
  (state.stage?.composition ?? []).forEach((item) => {
2829
3651
  const track = state.tracks.get(item.track_id);
2830
3652
  const sourceId = track?.active_source_id ?? null;
2831
3653
  const source = sourceId ? state.sources.get(sourceId) : void 0;
2832
- bindings.push({
3654
+ bindingsBySlot.set(item.slot, {
2833
3655
  slot: item.slot,
2834
3656
  trackId: item.track_id,
2835
3657
  track,
@@ -2837,7 +3659,18 @@ function buildSlotBindingsFromState(state) {
2837
3659
  source
2838
3660
  });
2839
3661
  });
2840
- return bindings;
3662
+ if (state.bootstrap?.active) {
3663
+ const track = state.tracks.get(state.bootstrap.trackId);
3664
+ const source = state.sources.get(state.bootstrap.sourceId);
3665
+ bindingsBySlot.set(state.bootstrap.slot, {
3666
+ slot: state.bootstrap.slot,
3667
+ trackId: state.bootstrap.trackId,
3668
+ track,
3669
+ sourceId: state.bootstrap.sourceId,
3670
+ source
3671
+ });
3672
+ }
3673
+ return Array.from(bindingsBySlot.values());
2841
3674
  }
2842
3675
  var VOLUME_STORAGE_KEY = "ivi-volume-preferences";
2843
3676
  var DEFAULT_VOLUME = 80;
@@ -3605,23 +4438,25 @@ function IVITrtcPlayer(props) {
3605
4438
  const [loading, setLoading] = react.useState(true);
3606
4439
  const [error, setError] = react.useState(null);
3607
4440
  const mutedRef = react.useRef(muted);
4441
+ const attachedSourceIdRef = react.useRef(null);
3608
4442
  mutedRef.current = muted;
4443
+ react.useEffect(() => {
4444
+ const unsubscribe = manager.subscribe(resolvedSourceId, (snapshot) => {
4445
+ setLoading(snapshot.status === "idle" || snapshot.status === "connecting");
4446
+ setError(snapshot.status === "error" ? snapshot.error ?? "TRTC \u62C9\u6D41\u5931\u8D25" : null);
4447
+ });
4448
+ return unsubscribe;
4449
+ }, [manager, resolvedSourceId]);
3609
4450
  react.useEffect(() => {
3610
4451
  const container = containerRef.current;
3611
4452
  if (!container) {
3612
4453
  return;
3613
4454
  }
3614
4455
  let disposed = false;
4456
+ attachedSourceIdRef.current = resolvedSourceId;
3615
4457
  if (shouldManageSourceLifecycle) {
3616
4458
  manager.upsertSource(resolvedSourceId, trtc, trtcAIDenoiser);
3617
4459
  }
3618
- const unsubscribe = manager.subscribe(resolvedSourceId, (snapshot) => {
3619
- if (disposed) {
3620
- return;
3621
- }
3622
- setLoading(snapshot.status === "idle" || snapshot.status === "connecting");
3623
- setError(snapshot.status === "error" ? snapshot.error ?? "TRTC \u62C9\u6D41\u5931\u8D25" : null);
3624
- });
3625
4460
  void manager.attachView(resolvedSourceId, viewIdRef.current, container, mutedRef.current).catch((caughtError) => {
3626
4461
  if (disposed) {
3627
4462
  return;
@@ -3631,15 +4466,15 @@ function IVITrtcPlayer(props) {
3631
4466
  });
3632
4467
  return () => {
3633
4468
  disposed = true;
3634
- unsubscribe();
3635
- manager.detachView(resolvedSourceId, viewIdRef.current);
4469
+ const attachedSourceId = attachedSourceIdRef.current ?? resolvedSourceId;
4470
+ manager.detachView(attachedSourceId, viewIdRef.current);
3636
4471
  if (shouldManageSourceLifecycle) {
3637
- manager.removeSource(resolvedSourceId);
4472
+ manager.removeSource(attachedSourceId);
3638
4473
  }
4474
+ attachedSourceIdRef.current = null;
3639
4475
  };
3640
4476
  }, [
3641
4477
  manager,
3642
- resolvedSourceId,
3643
4478
  shouldManageSourceLifecycle,
3644
4479
  trtc.app_id,
3645
4480
  trtc.room_id,
@@ -3647,6 +4482,15 @@ function IVITrtcPlayer(props) {
3647
4482
  trtc.user_sig,
3648
4483
  trtcAIDenoiser
3649
4484
  ]);
4485
+ react.useEffect(() => {
4486
+ const previousSourceId = attachedSourceIdRef.current;
4487
+ if (!previousSourceId || previousSourceId === resolvedSourceId) {
4488
+ return;
4489
+ }
4490
+ if (manager.reassignView(previousSourceId, resolvedSourceId, viewIdRef.current)) {
4491
+ attachedSourceIdRef.current = resolvedSourceId;
4492
+ }
4493
+ }, [manager, resolvedSourceId]);
3650
4494
  react.useEffect(() => {
3651
4495
  manager.updateViewMuted(resolvedSourceId, viewIdRef.current, muted);
3652
4496
  }, [manager, muted, resolvedSourceId]);
@@ -3915,7 +4759,16 @@ function makeLoadPolicy(label, onLog) {
3915
4759
  };
3916
4760
  }
3917
4761
  function IVIHlsVideo(props) {
3918
- const { url, videoProps, style, aggressivePreload = false, paused = false, onLog } = props;
4762
+ const {
4763
+ url,
4764
+ sourceId,
4765
+ videoProps,
4766
+ style,
4767
+ aggressivePreload = false,
4768
+ paused = false,
4769
+ onLog,
4770
+ telemetry
4771
+ } = props;
3919
4772
  const videoRef = react.useRef(null);
3920
4773
  const pausedRef = react.useRef(paused);
3921
4774
  react.useEffect(() => {
@@ -3952,6 +4805,7 @@ function IVIHlsVideo(props) {
3952
4805
  const onVideoError = () => {
3953
4806
  const el = videoRef.current;
3954
4807
  const mediaErr = el?.error;
4808
+ recordVideoTelemetry("ivi.media.video.error", telemetry, sourceId, "hls", mediaErr);
3955
4809
  emitHlsLog(onLog, "warn", "<video> \u5143\u7D20\u62A5\u9519", {
3956
4810
  code: mediaErr?.code,
3957
4811
  message: mediaErr?.message,
@@ -3961,6 +4815,13 @@ function IVIHlsVideo(props) {
3961
4815
  });
3962
4816
  };
3963
4817
  const onVideoStalled = () => {
4818
+ recordVideoTelemetry(
4819
+ "ivi.media.video.stalled",
4820
+ telemetry,
4821
+ sourceId,
4822
+ "hls",
4823
+ new Error("<video> stalled")
4824
+ );
3964
4825
  emitHlsLog(onLog, "warn", "<video> stalled\uFF08\u7F13\u51B2\u505C\u6EDE\uFF09", {
3965
4826
  currentTime: videoRef.current?.currentTime,
3966
4827
  readyState: videoRef.current?.readyState
@@ -4103,7 +4964,7 @@ function IVIHlsVideo(props) {
4103
4964
  hlsInstance?.destroy();
4104
4965
  hlsInstance = null;
4105
4966
  };
4106
- }, [url, aggressivePreload, onLog]);
4967
+ }, [url, sourceId, aggressivePreload, onLog, telemetry]);
4107
4968
  return /* @__PURE__ */ jsxRuntime.jsx(
4108
4969
  "video",
4109
4970
  {
@@ -4116,6 +4977,17 @@ function IVIHlsVideo(props) {
4116
4977
  }
4117
4978
  );
4118
4979
  }
4980
+ function recordVideoTelemetry(name, telemetry, sourceId, kind, error) {
4981
+ recordIviTelemetrySpan(
4982
+ name,
4983
+ telemetry,
4984
+ {
4985
+ "ivi.source.id": sourceId,
4986
+ "ivi.media.kind": kind
4987
+ },
4988
+ error
4989
+ );
4990
+ }
4119
4991
  function isM3u8Url(url) {
4120
4992
  return /\.m3u8(?:$|[?#])/i.test(url);
4121
4993
  }
@@ -4154,13 +5026,7 @@ function getSourceRenderKey(sourceId, source) {
4154
5026
  if (!trtc) {
4155
5027
  return sourceId;
4156
5028
  }
4157
- return [
4158
- "trtc",
4159
- trtc.app_id ?? "",
4160
- trtc.room_id ?? "",
4161
- trtc.user_id ?? "",
4162
- trtc.user_sig ?? ""
4163
- ].join(":");
5029
+ return ["trtc", trtc.app_id ?? "", trtc.room_id ?? ""].join(":");
4164
5030
  }
4165
5031
  function TrackSlotMediaContent(props) {
4166
5032
  const {
@@ -4187,6 +5053,7 @@ function TrackSlotMediaContent(props) {
4187
5053
  source,
4188
5054
  isPreloading: !isActive
4189
5055
  };
5056
+ const telemetry = runtime?.getTelemetryOptions();
4190
5057
  const mediaStyle = buildAdaptiveMediaStyle(adaptToSourceSize, fitStrategy);
4191
5058
  const shouldMute = !isActive;
4192
5059
  if (renderMedia) return renderMedia(renderContext);
@@ -4258,9 +5125,11 @@ function TrackSlotMediaContent(props) {
4258
5125
  IVIHlsVideo,
4259
5126
  {
4260
5127
  url: playbackUrl,
5128
+ sourceId: slotSourceId,
4261
5129
  videoProps: mergedVideoProps,
4262
5130
  style: videoStyle,
4263
- paused: shouldPause
5131
+ paused: shouldPause,
5132
+ telemetry
4264
5133
  }
4265
5134
  );
4266
5135
  }
@@ -4282,6 +5151,7 @@ function TrackSlotMediaContent(props) {
4282
5151
  SlotVideo,
4283
5152
  {
4284
5153
  src: playbackUrl,
5154
+ sourceId: slotSourceId,
4285
5155
  paused: shouldPause,
4286
5156
  videoProps: {
4287
5157
  ...mergedVideoProps,
@@ -4289,7 +5159,8 @@ function TrackSlotMediaContent(props) {
4289
5159
  playsInline: videoProps?.playsInline ?? true,
4290
5160
  controls: isActive ? videoProps?.controls ?? true : false
4291
5161
  },
4292
- style: videoStyle
5162
+ style: videoStyle,
5163
+ telemetry
4293
5164
  }
4294
5165
  );
4295
5166
  }
@@ -4339,17 +5210,48 @@ function SlotVideo(props) {
4339
5210
  video.play().catch(() => void 0);
4340
5211
  }
4341
5212
  }, [props.paused]);
5213
+ const onError = (event) => {
5214
+ recordPlainVideoTelemetry(
5215
+ "ivi.media.video.error",
5216
+ props.telemetry,
5217
+ props.sourceId,
5218
+ event.currentTarget.error
5219
+ );
5220
+ props.videoProps.onError?.(event);
5221
+ };
5222
+ const onStalled = (event) => {
5223
+ recordPlainVideoTelemetry(
5224
+ "ivi.media.video.stalled",
5225
+ props.telemetry,
5226
+ props.sourceId,
5227
+ new Error("<video> stalled")
5228
+ );
5229
+ props.videoProps.onStalled?.(event);
5230
+ };
4342
5231
  return /* @__PURE__ */ jsxRuntime.jsx(
4343
5232
  "video",
4344
5233
  {
4345
5234
  ref: videoRef,
4346
5235
  src: props.src,
4347
5236
  ...props.videoProps,
5237
+ onError,
5238
+ onStalled,
4348
5239
  autoPlay: props.paused ? false : props.videoProps.autoPlay ?? true,
4349
5240
  style: props.style
4350
5241
  }
4351
5242
  );
4352
5243
  }
5244
+ function recordPlainVideoTelemetry(name, telemetry, sourceId, error) {
5245
+ recordIviTelemetrySpan(
5246
+ name,
5247
+ telemetry,
5248
+ {
5249
+ "ivi.source.id": sourceId,
5250
+ "ivi.media.kind": "video"
5251
+ },
5252
+ error
5253
+ );
5254
+ }
4353
5255
 
4354
5256
  // src/react/internal/use-multi-preload-sources.ts
4355
5257
  function useMultiPreloadSources(sources, activeSourceId) {
@@ -4616,6 +5518,82 @@ function getClientLogTag(category) {
4616
5518
  if (category === "reconnect") return "[IVI-RECONNECT]";
4617
5519
  return "[IVI-CLIENT]";
4618
5520
  }
5521
+ function useIviSessionRuntime(config) {
5522
+ const [status, setStatus] = react.useState("idle");
5523
+ const [session, setSession] = react.useState(null);
5524
+ const [runtime, setRuntime] = react.useState(null);
5525
+ const [client, setClient] = react.useState(null);
5526
+ const [error, setError] = react.useState(null);
5527
+ react.useEffect(() => {
5528
+ const {
5529
+ request,
5530
+ enabled = true,
5531
+ autoStart = true,
5532
+ onCreated,
5533
+ onRuntimeReady,
5534
+ onError,
5535
+ ...options
5536
+ } = config;
5537
+ if (!enabled || !request) {
5538
+ setStatus("idle");
5539
+ setSession(null);
5540
+ setRuntime(null);
5541
+ setClient(null);
5542
+ setError(null);
5543
+ return;
5544
+ }
5545
+ let disposed = false;
5546
+ let activeRuntime = null;
5547
+ setStatus("creating");
5548
+ setSession(null);
5549
+ setRuntime(null);
5550
+ setClient(null);
5551
+ setError(null);
5552
+ void createIVISessionRuntime(request, options).then(async (created) => {
5553
+ if (disposed) {
5554
+ created.runtime.stop();
5555
+ return;
5556
+ }
5557
+ activeRuntime = created.runtime;
5558
+ setSession(created.session);
5559
+ setRuntime(created.runtime);
5560
+ setClient(created.client);
5561
+ onCreated?.(created.session);
5562
+ onRuntimeReady?.(created.runtime, created.client);
5563
+ if (!autoStart) {
5564
+ setStatus("idle");
5565
+ return;
5566
+ }
5567
+ setStatus("connecting");
5568
+ await created.runtime.start();
5569
+ if (disposed) {
5570
+ created.runtime.stop();
5571
+ return;
5572
+ }
5573
+ setStatus("running");
5574
+ }).catch((caughtError) => {
5575
+ if (disposed) {
5576
+ return;
5577
+ }
5578
+ const normalizedError = caughtError instanceof Error ? caughtError : new Error(String(caughtError));
5579
+ setError(normalizedError);
5580
+ setStatus("error");
5581
+ onError?.(caughtError);
5582
+ });
5583
+ return () => {
5584
+ disposed = true;
5585
+ activeRuntime?.stop();
5586
+ setStatus("stopped");
5587
+ };
5588
+ }, [config]);
5589
+ return {
5590
+ status,
5591
+ session,
5592
+ runtime,
5593
+ client,
5594
+ error
5595
+ };
5596
+ }
4619
5597
 
4620
5598
  exports.DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS;
4621
5599
  exports.EMPTY_RUNTIME_STATE = EMPTY_RUNTIME_STATE;
@@ -4624,14 +5602,21 @@ exports.IVIStageView = IVIStageView;
4624
5602
  exports.IVISubtitleOverlay = IVISubtitleOverlay;
4625
5603
  exports.IVITrackSlot = IVITrackSlot;
4626
5604
  exports.IVITrtcPlayer = IVITrtcPlayer;
5605
+ exports.IviCreateIVISessionError = IviCreateIVISessionError;
4627
5606
  exports.IviFrontendSdk = IviFrontendSdk;
4628
5607
  exports.IviRuntimeCoordinator = IviRuntimeCoordinator;
4629
5608
  exports.IviRuntimeDispatcher = IviRuntimeDispatcher;
4630
5609
  exports.LivekitSourceManager = LivekitSourceManager;
4631
5610
  exports.TrtcSourceManager = TrtcSourceManager;
5611
+ exports.createIVISession = createIVISession;
5612
+ exports.createIVISessionRuntime = createIVISessionRuntime;
5613
+ exports.createRuntimeFromSession = createRuntimeFromSession;
4632
5614
  exports.isLivekitSourcePlayback = isLivekitSourcePlayback;
4633
5615
  exports.isReadyLivekitRuntimeSource = isReadyLivekitRuntimeSource;
4634
5616
  exports.isSameLivekitConfig = isSameLivekitConfig;
5617
+ exports.normalizeCreateIVISessionResponse = normalizeCreateIVISessionResponse;
5618
+ exports.toCpCreateIVISessionRequestBody = toCpCreateIVISessionRequestBody;
5619
+ exports.useIviSessionRuntime = useIviSessionRuntime;
4635
5620
  exports.useIviStageView = useIviStageView;
4636
5621
  exports.useIviSubtitles = useIviSubtitles;
4637
5622
  exports.useManagedIviRuntime = useManagedIviRuntime;