@vivix-ai/ivi-frontend-sdk 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var iviSdkTs = require('@vivix/ivi-sdk-ts');
3
+ var iviSdkTs = require('@vivix-ai/ivi-sdk-ts');
4
4
  var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
@@ -28,7 +28,6 @@ function logIviEventReceived(event) {
28
28
  args: [message, event.raw],
29
29
  data: event.raw
30
30
  });
31
- console.log(message, event.raw);
32
31
  }
33
32
  function logIviStateChange(entity, key, eventType, before, after) {
34
33
  if (!didChange(before, after)) {
@@ -47,7 +46,6 @@ function logIviStateChange(entity, key, eventType, before, after) {
47
46
  args: [message, data],
48
47
  data
49
48
  });
50
- console.log(message, data);
51
49
  }
52
50
  function didChange(before, after) {
53
51
  if (before === after) return false;
@@ -110,7 +108,7 @@ var SessionEventHandler = class {
110
108
  this.callbacks = callbacks;
111
109
  }
112
110
  handle(event) {
113
- if (event instanceof iviSdkTs.SessionCreatedEvent) {
111
+ if (event instanceof iviSdkTs.ReceiveSessionCreatedEvent) {
114
112
  const before = this.sessionManager.getSession();
115
113
  this.sessionManager.setSession(event.session);
116
114
  logIviStateChange("session", null, event.type, before, this.sessionManager.getSession());
@@ -259,7 +257,7 @@ var SourceEventHandler = class {
259
257
  const before = this.sourceManager.get(sourceId);
260
258
  this.sourceManager.upsertCreated(source);
261
259
  this.sourceManager.applyPreload(sourceId, {
262
- autoclearAfterPlay: event.autoclearAfterPlay
260
+ autoclearAfterPlay: event.autoclearAfterPlay ?? true
263
261
  });
264
262
  logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
265
263
  }
@@ -301,7 +299,7 @@ var StreamEventHandler = class {
301
299
  this.callbacks = callbacks;
302
300
  }
303
301
  handle(event) {
304
- if (event instanceof iviSdkTs.SessionStreamCreatedEvent) {
302
+ if (event instanceof iviSdkTs.ReceiveSessionStreamCreatedEvent) {
305
303
  const streamId = event.stream.stream_id;
306
304
  const before = this.streamManager.getAll().get(streamId);
307
305
  this.streamManager.upsertCreated(event.stream);
@@ -362,7 +360,7 @@ var ConversationEventHandler = class {
362
360
  handle(event) {
363
361
  if (event instanceof iviSdkTs.ReceiveConversationListResponseEvent) {
364
362
  const before = snapshotMap4(this.conversationManager.getAllMap());
365
- this.conversationManager.replaceAll(event.items);
363
+ this.conversationManager.replaceAll(event.items.filter(hasConversationItemId));
366
364
  logIviStateChange(
367
365
  "conversations(list)",
368
366
  null,
@@ -374,6 +372,9 @@ var ConversationEventHandler = class {
374
372
  return { handled: true };
375
373
  }
376
374
  if (event instanceof iviSdkTs.ReceiveConversationItemAddedEvent) {
375
+ if (!hasConversationItemId(event.item)) {
376
+ return { handled: true };
377
+ }
377
378
  const before = this.conversationManager.getAllMap().get(event.item.id);
378
379
  this.conversationManager.upsertAdded(event.item);
379
380
  logIviStateChange(
@@ -387,6 +388,9 @@ var ConversationEventHandler = class {
387
388
  return { handled: true };
388
389
  }
389
390
  if (event instanceof iviSdkTs.ReceiveConversationItemDoneEvent) {
391
+ if (!hasConversationItemId(event.item)) {
392
+ return { handled: true };
393
+ }
390
394
  const before = this.conversationManager.getAllMap().get(event.item.id);
391
395
  this.conversationManager.markDone(event.item);
392
396
  logIviStateChange(
@@ -400,6 +404,9 @@ var ConversationEventHandler = class {
400
404
  return { handled: true };
401
405
  }
402
406
  if (event instanceof iviSdkTs.ReceiveResponseOutputTextDeltaEvent) {
407
+ if (!event.itemId) {
408
+ return { handled: true };
409
+ }
403
410
  const before = this.conversationManager.getAllMap().get(event.itemId);
404
411
  this.conversationManager.applyTextDelta(event.itemId, event.delta);
405
412
  logIviStateChange(
@@ -413,6 +420,9 @@ var ConversationEventHandler = class {
413
420
  return { handled: true };
414
421
  }
415
422
  if (event instanceof iviSdkTs.ReceiveResponseOutputTextDoneEvent) {
423
+ if (!event.itemId) {
424
+ return { handled: true };
425
+ }
416
426
  const before = this.conversationManager.getAllMap().get(event.itemId);
417
427
  this.conversationManager.applyTextDone(event.itemId, event.text);
418
428
  logIviStateChange(
@@ -426,6 +436,9 @@ var ConversationEventHandler = class {
426
436
  return { handled: true };
427
437
  }
428
438
  if (event instanceof iviSdkTs.ReceiveResponseOutputAudioTranscriptDeltaEvent) {
439
+ if (!event.itemId) {
440
+ return { handled: true };
441
+ }
429
442
  const before = this.conversationManager.getAllMap().get(event.itemId);
430
443
  this.conversationManager.applyTranscriptDelta(event.itemId, event.delta);
431
444
  logIviStateChange(
@@ -439,6 +452,9 @@ var ConversationEventHandler = class {
439
452
  return { handled: true };
440
453
  }
441
454
  if (event instanceof iviSdkTs.ReceiveResponseOutputAudioTranscriptDoneEvent) {
455
+ if (!event.itemId) {
456
+ return { handled: true };
457
+ }
442
458
  const before = this.conversationManager.getAllMap().get(event.itemId);
443
459
  this.conversationManager.applyTranscriptDone(event.itemId, event.transcript);
444
460
  logIviStateChange(
@@ -480,6 +496,9 @@ function snapshotMap4(map) {
480
496
  });
481
497
  return snap;
482
498
  }
499
+ function hasConversationItemId(item) {
500
+ return typeof item.id === "string" && item.id.length > 0;
501
+ }
483
502
 
484
503
  // src/runtime/managers/session-manager.ts
485
504
  var SessionManager = class {
@@ -550,13 +569,13 @@ var TrackManager = class {
550
569
  }
551
570
  this.patchTrack(trackId, {
552
571
  active_source_id: resolvedSourceId,
553
- next_source_id: null
572
+ next_source_id: void 0
554
573
  });
555
574
  }
556
575
  applyTrackCued(trackId, sourceId) {
557
576
  this.patchTrack(trackId, {
558
577
  active_source_id: sourceId,
559
- next_source_id: null
578
+ next_source_id: void 0
560
579
  });
561
580
  }
562
581
  applyTrackNextSet(trackId, sourceId) {
@@ -1120,7 +1139,7 @@ var TrtcSourceManager = class {
1120
1139
  upsertSource(sourceId, trtc) {
1121
1140
  const existing = this.sessions.get(sourceId);
1122
1141
  if (!existing) {
1123
- this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${trtc.room_id} user=${trtc.user_id}`);
1142
+ this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1124
1143
  const session2 = this.createSession(sourceId, trtc);
1125
1144
  this.sessions.set(sourceId, session2);
1126
1145
  void this.ensureConnected(session2);
@@ -1129,7 +1148,7 @@ var TrtcSourceManager = class {
1129
1148
  if (isSameTrtcConfig(existing.trtc, trtc)) {
1130
1149
  return;
1131
1150
  }
1132
- this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${trtc.room_id}`);
1151
+ this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")}`);
1133
1152
  void this.disposeSession(existing);
1134
1153
  const session = this.createSession(sourceId, trtc);
1135
1154
  this.sessions.set(sourceId, session);
@@ -1303,11 +1322,14 @@ var TrtcSourceManager = class {
1303
1322
  session.connectPromise = (async () => {
1304
1323
  const m = await import('trtc-sdk-v5');
1305
1324
  const TRTC = m.default ?? m;
1306
- const sdkAppId = Number(session.trtc.app_id);
1325
+ const roomId = getTrtcString(session.trtc, "room_id");
1326
+ const userId = getTrtcString(session.trtc, "user_id");
1327
+ const userSig = getTrtcString(session.trtc, "user_sig");
1328
+ const sdkAppId = Number(getTrtcString(session.trtc, "app_id"));
1307
1329
  if (!Number.isFinite(sdkAppId)) {
1308
1330
  throw new Error("TRTC app_id \u5FC5\u987B\u662F\u6570\u5B57\u5B57\u7B26\u4E32\u3002");
1309
1331
  }
1310
- const isStringRoomId = shouldUseStringRoomId(session.trtc.room_id);
1332
+ const isStringRoomId = shouldUseStringRoomId(roomId);
1311
1333
  const client = TRTC.create();
1312
1334
  const onRemoteVideoAvailable = (event) => {
1313
1335
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
@@ -1355,21 +1377,21 @@ var TrtcSourceManager = class {
1355
1377
  client.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, onRemoteVideoUnavailable);
1356
1378
  client.on(TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, onRemoteAudioAvailable);
1357
1379
  client.on(TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, onRemoteAudioUnavailable);
1358
- this.log("info", `\u6B63\u5728\u8FDB\u623F source=${session.sourceId} room=${session.trtc.room_id} sdkAppId=${sdkAppId} userId=${session.trtc.user_id}`);
1380
+ this.log("info", `\u6B63\u5728\u8FDB\u623F source=${session.sourceId} room=${roomId} sdkAppId=${sdkAppId} userId=${userId}`);
1359
1381
  await client.enterRoom({
1360
1382
  sdkAppId,
1361
- userId: session.trtc.user_id,
1362
- userSig: session.trtc.user_sig,
1383
+ userId,
1384
+ userSig,
1363
1385
  scene: TRTC.TYPE.SCENE_LIVE,
1364
1386
  role: TRTC.TYPE.ROLE_AUDIENCE,
1365
1387
  autoReceiveAudio: true,
1366
- ...isStringRoomId ? { strRoomId: session.trtc.room_id } : { roomId: Number(session.trtc.room_id) }
1388
+ ...isStringRoomId ? { strRoomId: roomId } : { roomId: Number(roomId) }
1367
1389
  });
1368
1390
  session.TRTC = TRTC;
1369
1391
  session.client = client;
1370
1392
  session.status = "connected";
1371
1393
  session.error = void 0;
1372
- this.log("info", `\u8FDB\u623F\u6210\u529F source=${session.sourceId} room=${session.trtc.room_id}`);
1394
+ this.log("info", `\u8FDB\u623F\u6210\u529F source=${session.sourceId} room=${roomId}`);
1373
1395
  this.emitSnapshot(session.sourceId, {
1374
1396
  status: session.status
1375
1397
  });
@@ -1517,19 +1539,10 @@ var TrtcSourceManager = class {
1517
1539
  args,
1518
1540
  data: extra.length > 0 ? { message, extra } : { message }
1519
1541
  });
1520
- if (level === "error") {
1521
- console.error(...args);
1522
- return;
1523
- }
1524
- if (level === "warn") {
1525
- console.warn(...args);
1526
- return;
1527
- }
1528
- console.log(...args);
1529
1542
  }
1530
1543
  };
1531
1544
  function isRuntimeTrtcSource(source) {
1532
- return source.status === "ready" && source.playback?.type === "trtc" && Boolean(source.playback.trtc);
1545
+ return source.status === "ready" && source.playback?.type === "trtc" && typeof source.playback.trtc === "object" && source.playback.trtc !== null;
1533
1546
  }
1534
1547
  function isSameTrtcConfig(a, b) {
1535
1548
  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;
@@ -1541,6 +1554,10 @@ function shouldUseStringRoomId(roomId) {
1541
1554
  const roomNumber = Number(roomId);
1542
1555
  return !Number.isInteger(roomNumber) || roomNumber < 1 || roomNumber > 4294967294;
1543
1556
  }
1557
+ function getTrtcString(trtc, key) {
1558
+ const value = trtc[key];
1559
+ return typeof value === "string" ? value : "";
1560
+ }
1544
1561
  function buildRemoteVideoKey(userId, streamType) {
1545
1562
  return `${userId}::${streamType}`;
1546
1563
  }
@@ -1579,6 +1596,433 @@ function enforceContainMedia(container) {
1579
1596
  });
1580
1597
  }
1581
1598
 
1599
+ // src/runtime/livekit-types.ts
1600
+ function isLivekitSourcePlayback(playback) {
1601
+ if (!playback) return false;
1602
+ const candidate = playback;
1603
+ if (candidate.type !== "livekit" || typeof candidate.livekit !== "object" || candidate.livekit === null) {
1604
+ return false;
1605
+ }
1606
+ const livekit = candidate.livekit;
1607
+ return typeof livekit.ws_url === "string" && typeof livekit.token === "string";
1608
+ }
1609
+ function isReadyLivekitRuntimeSource(source) {
1610
+ if (!source || source.status !== "ready") return false;
1611
+ return isLivekitSourcePlayback(source.playback);
1612
+ }
1613
+ function isSameLivekitConfig(a, b) {
1614
+ return a.ws_url === b.ws_url && a.token === b.token && a.room === b.room && a.identity === b.identity;
1615
+ }
1616
+ function describeLivekitRoom(livekit) {
1617
+ return livekit.room ?? livekit.identity ?? "unknown";
1618
+ }
1619
+
1620
+ // src/runtime/managers/livekit-source-manager.ts
1621
+ var TAG2 = "[IVI-LIVEKIT]";
1622
+ var LivekitSourceManager = class {
1623
+ constructor(onLog) {
1624
+ this.onLog = onLog;
1625
+ this.sessions = /* @__PURE__ */ new Map();
1626
+ this.listeners = /* @__PURE__ */ new Map();
1627
+ }
1628
+ /**
1629
+ * 与 runtime 当前 sources 对齐:
1630
+ * - 对 ready + livekit 的 source 进行 upsert(必要时建立连接);
1631
+ * - 清理已不在 runtime 中的会话(释放资源)。
1632
+ */
1633
+ syncRuntimeSources(sources) {
1634
+ const existingIds = new Set(sources.keys());
1635
+ sources.forEach((source, sourceId) => {
1636
+ if (!isReadyLivekitRuntimeSource(source)) {
1637
+ return;
1638
+ }
1639
+ this.upsertSource(sourceId, source.playback.livekit);
1640
+ });
1641
+ this.sessions.forEach((_session, sourceId) => {
1642
+ if (!existingIds.has(sourceId)) {
1643
+ this.removeSource(sourceId);
1644
+ }
1645
+ });
1646
+ }
1647
+ /**
1648
+ * 按 sourceId 注册/更新 LiveKit 配置。
1649
+ * 若 ws_url / token / room / identity 任一发生变化,会先销毁旧会话再按新参数重建连接。
1650
+ */
1651
+ upsertSource(sourceId, livekit) {
1652
+ const existing = this.sessions.get(sourceId);
1653
+ if (!existing) {
1654
+ this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${describeLivekitRoom(livekit)}`);
1655
+ const session2 = this.createSession(sourceId, livekit);
1656
+ this.sessions.set(sourceId, session2);
1657
+ void this.ensureConnected(session2);
1658
+ return;
1659
+ }
1660
+ if (isSameLivekitConfig(existing.livekit, livekit)) {
1661
+ return;
1662
+ }
1663
+ this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${describeLivekitRoom(livekit)}`);
1664
+ void this.disposeSession(existing);
1665
+ const session = this.createSession(sourceId, livekit);
1666
+ this.sessions.set(sourceId, session);
1667
+ void this.ensureConnected(session);
1668
+ }
1669
+ /**
1670
+ * 删除指定 source 的 LiveKit 会话并释放连接资源。
1671
+ */
1672
+ removeSource(sourceId) {
1673
+ const session = this.sessions.get(sourceId);
1674
+ if (!session) {
1675
+ return;
1676
+ }
1677
+ this.log("info", `\u79FB\u9664\u4F1A\u8BDD source=${sourceId}`);
1678
+ this.sessions.delete(sourceId);
1679
+ void this.disposeSession(session);
1680
+ this.emitSnapshot(sourceId, { status: "idle" });
1681
+ this.listeners.delete(sourceId);
1682
+ }
1683
+ reset() {
1684
+ this.log("info", `\u91CD\u7F6E\u5168\u90E8\u4F1A\u8BDD count=${this.sessions.size}`);
1685
+ const sessions = Array.from(this.sessions.values());
1686
+ this.sessions.clear();
1687
+ sessions.forEach((session) => {
1688
+ void this.disposeSession(session);
1689
+ });
1690
+ this.listeners.clear();
1691
+ }
1692
+ subscribe(sourceId, listener) {
1693
+ const set = this.listeners.get(sourceId) ?? /* @__PURE__ */ new Set();
1694
+ set.add(listener);
1695
+ this.listeners.set(sourceId, set);
1696
+ listener(this.getSnapshot(sourceId));
1697
+ return () => {
1698
+ const target = this.listeners.get(sourceId);
1699
+ if (!target) {
1700
+ return;
1701
+ }
1702
+ target.delete(listener);
1703
+ if (target.size === 0) {
1704
+ this.listeners.delete(sourceId);
1705
+ }
1706
+ };
1707
+ }
1708
+ getSnapshot(sourceId) {
1709
+ const session = this.sessions.get(sourceId);
1710
+ if (!session) {
1711
+ return { status: "idle" };
1712
+ }
1713
+ return { status: session.status, error: session.error };
1714
+ }
1715
+ hasRemoteVideoAvailable(sourceId) {
1716
+ const session = this.sessions.get(sourceId);
1717
+ return session?.hasEverReceivedRemoteVideo ?? false;
1718
+ }
1719
+ /**
1720
+ * 等待指定 source 的 LiveKit 会话首次拿到远端视频 track。
1721
+ * - 若已收到过,立即返回 true;
1722
+ * - 若会话被销毁或超时,返回 false。
1723
+ */
1724
+ waitForRemoteVideoAvailable(sourceId, timeoutMs = 3e4) {
1725
+ const session = this.sessions.get(sourceId);
1726
+ if (!session) return Promise.resolve(false);
1727
+ if (session.hasEverReceivedRemoteVideo) return Promise.resolve(true);
1728
+ return new Promise((resolve) => {
1729
+ let settled = false;
1730
+ const timer = setTimeout(() => {
1731
+ if (settled) return;
1732
+ settled = true;
1733
+ const idx = session.remoteVideoWaiters.indexOf(waiter);
1734
+ if (idx >= 0) session.remoteVideoWaiters.splice(idx, 1);
1735
+ resolve(false);
1736
+ }, timeoutMs);
1737
+ const waiter = (available) => {
1738
+ if (settled) return;
1739
+ settled = true;
1740
+ clearTimeout(timer);
1741
+ resolve(available);
1742
+ };
1743
+ session.remoteVideoWaiters.push(waiter);
1744
+ });
1745
+ }
1746
+ /**
1747
+ * 把一个渲染容器绑定到 source 会话:
1748
+ * - 确保已连接;
1749
+ * - 把已订阅的远端 video/audio 回放到该容器;
1750
+ * - 应用初始 muted 策略。
1751
+ */
1752
+ async attachView(sourceId, viewId, container, muted) {
1753
+ const session = this.sessions.get(sourceId);
1754
+ if (!session) {
1755
+ throw new Error(`LiveKit source session not found: ${sourceId}`);
1756
+ }
1757
+ session.views.set(viewId, {
1758
+ container,
1759
+ muted,
1760
+ attachedElements: /* @__PURE__ */ new Map()
1761
+ });
1762
+ await this.ensureConnected(session);
1763
+ const binding = session.views.get(viewId);
1764
+ if (!binding) {
1765
+ return;
1766
+ }
1767
+ this.attachKnownTracksToView(session, binding);
1768
+ this.applyMutedToBinding(binding);
1769
+ }
1770
+ detachView(sourceId, viewId) {
1771
+ const session = this.sessions.get(sourceId);
1772
+ if (!session) {
1773
+ return;
1774
+ }
1775
+ const binding = session.views.get(viewId);
1776
+ if (!binding) {
1777
+ return;
1778
+ }
1779
+ this.detachAllElementsFromView(session, binding);
1780
+ session.views.delete(viewId);
1781
+ }
1782
+ updateViewMuted(sourceId, viewId, muted) {
1783
+ const session = this.sessions.get(sourceId);
1784
+ if (!session) {
1785
+ return;
1786
+ }
1787
+ const binding = session.views.get(viewId);
1788
+ if (!binding) {
1789
+ return;
1790
+ }
1791
+ binding.muted = muted;
1792
+ this.applyMutedToBinding(binding);
1793
+ }
1794
+ createSession(sourceId, livekit) {
1795
+ return {
1796
+ sourceId,
1797
+ livekit,
1798
+ livekitModule: null,
1799
+ room: null,
1800
+ connectPromise: null,
1801
+ views: /* @__PURE__ */ new Map(),
1802
+ remoteTracks: /* @__PURE__ */ new Map(),
1803
+ status: "idle",
1804
+ hasEverReceivedRemoteVideo: false,
1805
+ remoteVideoWaiters: [],
1806
+ detachRoomListeners: () => void 0
1807
+ };
1808
+ }
1809
+ /**
1810
+ * 懒连接:单飞模式下复用同一个 connectPromise;建连后注册远端事件并把已订阅 track 推给 view。
1811
+ */
1812
+ async ensureConnected(session) {
1813
+ if (session.room && session.livekitModule) {
1814
+ return;
1815
+ }
1816
+ if (session.connectPromise) {
1817
+ await session.connectPromise;
1818
+ return;
1819
+ }
1820
+ session.status = "connecting";
1821
+ session.error = void 0;
1822
+ this.emitSnapshot(session.sourceId, { status: session.status });
1823
+ session.connectPromise = (async () => {
1824
+ const livekitModule = await import('livekit-client');
1825
+ const RoomCtor = livekitModule.Room;
1826
+ const RoomEvent = livekitModule.RoomEvent;
1827
+ const TrackKind = livekitModule.Track.Kind;
1828
+ const room = new RoomCtor({
1829
+ adaptiveStream: true,
1830
+ dynacast: true
1831
+ });
1832
+ const onTrackSubscribed = (track, _publication, participant) => {
1833
+ const kind = track.kind === TrackKind.Video ? "video" : track.kind === TrackKind.Audio ? "audio" : null;
1834
+ if (!kind) return;
1835
+ const trackKey = buildRemoteTrackKey(participant.identity, track.sid ?? track.kind);
1836
+ this.log(
1837
+ "info",
1838
+ `\u8BA2\u9605\u8FDC\u7AEF track source=${session.sourceId} participant=${participant.identity} kind=${kind} sid=${track.sid ?? "?"}`
1839
+ );
1840
+ session.remoteTracks.set(trackKey, {
1841
+ track,
1842
+ participantIdentity: participant.identity,
1843
+ kind
1844
+ });
1845
+ if (kind === "video" && !session.hasEverReceivedRemoteVideo) {
1846
+ session.hasEverReceivedRemoteVideo = true;
1847
+ for (const waiter of session.remoteVideoWaiters) {
1848
+ waiter(true);
1849
+ }
1850
+ session.remoteVideoWaiters.length = 0;
1851
+ }
1852
+ session.views.forEach((binding) => {
1853
+ this.attachTrackToView(track, kind, trackKey, binding);
1854
+ });
1855
+ };
1856
+ const onTrackUnsubscribed = (track, _publication, participant) => {
1857
+ const trackKey = buildRemoteTrackKey(participant.identity, track.sid ?? track.kind);
1858
+ this.log(
1859
+ "info",
1860
+ `\u8FDC\u7AEF track \u53D6\u6D88\u8BA2\u9605 source=${session.sourceId} participant=${participant.identity} sid=${track.sid ?? "?"}`
1861
+ );
1862
+ session.remoteTracks.delete(trackKey);
1863
+ session.views.forEach((binding) => {
1864
+ this.detachTrackFromBinding(track, trackKey, binding);
1865
+ });
1866
+ };
1867
+ const onDisconnected = () => {
1868
+ this.log("warn", `Room \u65AD\u5F00 source=${session.sourceId}`);
1869
+ };
1870
+ room.on(RoomEvent.TrackSubscribed, onTrackSubscribed);
1871
+ room.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed);
1872
+ room.on(RoomEvent.Disconnected, onDisconnected);
1873
+ session.detachRoomListeners = () => {
1874
+ room.off(RoomEvent.TrackSubscribed, onTrackSubscribed);
1875
+ room.off(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed);
1876
+ room.off(RoomEvent.Disconnected, onDisconnected);
1877
+ };
1878
+ this.log(
1879
+ "info",
1880
+ `\u6B63\u5728\u8FDB\u623F source=${session.sourceId} room=${describeLivekitRoom(session.livekit)} ws=${session.livekit.ws_url}`
1881
+ );
1882
+ await room.connect(session.livekit.ws_url, session.livekit.token);
1883
+ session.room = room;
1884
+ session.livekitModule = livekitModule;
1885
+ session.status = "connected";
1886
+ session.error = void 0;
1887
+ this.log("info", `\u8FDB\u623F\u6210\u529F source=${session.sourceId} room=${describeLivekitRoom(session.livekit)}`);
1888
+ this.emitSnapshot(session.sourceId, { status: session.status });
1889
+ })().catch((error) => {
1890
+ session.status = "error";
1891
+ session.error = error instanceof Error ? error.message : String(error);
1892
+ this.log("error", `\u8FDE\u63A5\u5931\u8D25 source=${session.sourceId} error=${session.error}`);
1893
+ this.emitSnapshot(session.sourceId, {
1894
+ status: session.status,
1895
+ error: session.error
1896
+ });
1897
+ throw error;
1898
+ }).finally(() => {
1899
+ session.connectPromise = null;
1900
+ });
1901
+ await session.connectPromise;
1902
+ }
1903
+ attachKnownTracksToView(session, binding) {
1904
+ session.remoteTracks.forEach((entry, trackKey) => {
1905
+ this.attachTrackToView(entry.track, entry.kind, trackKey, binding);
1906
+ });
1907
+ }
1908
+ attachTrackToView(track, kind, trackKey, binding) {
1909
+ if (binding.attachedElements.has(trackKey)) {
1910
+ return;
1911
+ }
1912
+ let element;
1913
+ try {
1914
+ element = track.attach();
1915
+ } catch (error) {
1916
+ this.log("warn", `attach track \u5931\u8D25 trackKey=${trackKey}`, error);
1917
+ return;
1918
+ }
1919
+ binding.attachedElements.set(trackKey, element);
1920
+ if (kind === "video") {
1921
+ enforceContainMedia2(element);
1922
+ } else {
1923
+ element.style.display = "none";
1924
+ }
1925
+ binding.container.appendChild(element);
1926
+ this.applyMutedToElement(element, binding.muted);
1927
+ }
1928
+ detachTrackFromBinding(track, trackKey, binding) {
1929
+ const element = binding.attachedElements.get(trackKey);
1930
+ if (!element) return;
1931
+ try {
1932
+ track.detach(element);
1933
+ } catch {
1934
+ }
1935
+ if (element.parentElement === binding.container) {
1936
+ binding.container.removeChild(element);
1937
+ }
1938
+ binding.attachedElements.delete(trackKey);
1939
+ }
1940
+ detachAllElementsFromView(session, binding) {
1941
+ binding.attachedElements.forEach((element, trackKey) => {
1942
+ const entry = session.remoteTracks.get(trackKey);
1943
+ if (entry) {
1944
+ try {
1945
+ entry.track.detach(element);
1946
+ } catch {
1947
+ }
1948
+ }
1949
+ if (element.parentElement === binding.container) {
1950
+ binding.container.removeChild(element);
1951
+ }
1952
+ });
1953
+ binding.attachedElements.clear();
1954
+ }
1955
+ applyMutedToBinding(binding) {
1956
+ binding.attachedElements.forEach((element) => {
1957
+ this.applyMutedToElement(element, binding.muted);
1958
+ });
1959
+ }
1960
+ applyMutedToElement(element, muted) {
1961
+ element.muted = muted;
1962
+ if (muted) {
1963
+ element.volume = 0;
1964
+ }
1965
+ }
1966
+ async disposeSession(session) {
1967
+ session.connectPromise = null;
1968
+ session.views.forEach((binding) => {
1969
+ this.detachAllElementsFromView(session, binding);
1970
+ });
1971
+ session.views.clear();
1972
+ session.remoteTracks.clear();
1973
+ for (const waiter of session.remoteVideoWaiters) {
1974
+ waiter(false);
1975
+ }
1976
+ session.remoteVideoWaiters.length = 0;
1977
+ session.hasEverReceivedRemoteVideo = false;
1978
+ session.status = "idle";
1979
+ session.error = void 0;
1980
+ const room = session.room;
1981
+ session.detachRoomListeners();
1982
+ session.detachRoomListeners = () => void 0;
1983
+ session.room = null;
1984
+ session.livekitModule = null;
1985
+ if (!room) {
1986
+ return;
1987
+ }
1988
+ this.log("info", `\u9500\u6BC1\u4F1A\u8BDD source=${session.sourceId}`);
1989
+ try {
1990
+ await room.disconnect();
1991
+ this.log("info", `\u79BB\u5F00\u623F\u95F4\u5B8C\u6210 source=${session.sourceId}`);
1992
+ } catch (err) {
1993
+ this.log("warn", `\u79BB\u5F00\u623F\u95F4\u5F02\u5E38 source=${session.sourceId}`, err);
1994
+ }
1995
+ }
1996
+ emitSnapshot(sourceId, snapshot) {
1997
+ const targetListeners = this.listeners.get(sourceId);
1998
+ if (!targetListeners) {
1999
+ return;
2000
+ }
2001
+ targetListeners.forEach((listener) => listener(snapshot));
2002
+ }
2003
+ log(level, message, ...extra) {
2004
+ const args = [TAG2, message, ...extra];
2005
+ this.onLog?.({
2006
+ level,
2007
+ tag: TAG2,
2008
+ message: `${TAG2} ${message}`,
2009
+ args,
2010
+ data: extra.length > 0 ? { message, extra } : { message }
2011
+ });
2012
+ }
2013
+ };
2014
+ function buildRemoteTrackKey(identity, trackId) {
2015
+ return `${identity}::${trackId}`;
2016
+ }
2017
+ function enforceContainMedia2(element) {
2018
+ element.style.setProperty("width", "100%", "important");
2019
+ element.style.setProperty("height", "100%", "important");
2020
+ element.style.setProperty("max-width", "100%", "important");
2021
+ element.style.setProperty("max-height", "100%", "important");
2022
+ element.style.setProperty("object-fit", "contain", "important");
2023
+ element.style.setProperty("display", "block", "important");
2024
+ }
2025
+
1582
2026
  // src/runtime/runtime-coordinator.ts
1583
2027
  var IviRuntimeCoordinator = class {
1584
2028
  /**
@@ -1614,6 +2058,7 @@ var IviRuntimeCoordinator = class {
1614
2058
  ...config
1615
2059
  };
1616
2060
  this.trtcSourceManager = new TrtcSourceManager(this.config.onLog);
2061
+ this.livekitSourceManager = new LivekitSourceManager(this.config.onLog);
1617
2062
  this.sessionHandler = new SessionEventHandler(this.sessionManager, {
1618
2063
  onSessionCreated: (event) => this.onSessionCreated(event),
1619
2064
  onSessionEnded: (event) => this.onSessionEnded(event)
@@ -1685,13 +2130,16 @@ var IviRuntimeCoordinator = class {
1685
2130
  this.eventListeners.delete(listener);
1686
2131
  };
1687
2132
  }
2133
+ emitLog(entry) {
2134
+ this.config.onLog?.(entry);
2135
+ }
1688
2136
  /**
1689
2137
  * 编排一次"用户文本输入 -> 触发模型回复"链路。
1690
2138
  *
1691
2139
  * 流程:
1692
2140
  * 1) 发送 conversation.item.create(可指定 itemId);
1693
2141
  * 2) 等待 conversation.item.added + conversation.item.done;
1694
- * 3) 发送 response.create(携带 item_reference,可选绑定 stream);
2142
+ * 3) 发送 response.create(默认不携带 input,使用 IVI 内部上下文;可选显式绑定 item_reference);
1695
2143
  * 4) 等待 response.created 并结束。
1696
2144
  */
1697
2145
  sendUserTextAndTriggerResponse(options) {
@@ -1713,12 +2161,13 @@ var IviRuntimeCoordinator = class {
1713
2161
  itemId,
1714
2162
  streamId: normalizedStreamId,
1715
2163
  response: options.response,
2164
+ responseInput: options.responseInput ?? "context",
1716
2165
  callbacks: options.callbacks,
1717
2166
  responseRequested: false,
1718
2167
  resolve,
1719
2168
  reject
1720
2169
  });
1721
- this.client.sendConversationUserText(normalizedText, itemId).catch((error) => {
2170
+ this.client.sendUserText(normalizedText, itemId).catch((error) => {
1722
2171
  this.pendingUserTextToResponseFlows.delete(itemId);
1723
2172
  reject(error instanceof Error ? error : new Error(String(error)));
1724
2173
  });
@@ -1749,6 +2198,10 @@ var IviRuntimeCoordinator = class {
1749
2198
  void this.deferredTrtcTakeCompleteAndTake(resolvedTrackId, track.next_source_id, sourceId, trackId);
1750
2199
  return;
1751
2200
  }
2201
+ if (isVideoPlaybackSource(currentSource) && isLivekitPlaybackSource(nextSource)) {
2202
+ void this.deferredLivekitTakeCompleteAndTake(resolvedTrackId, track.next_source_id, sourceId, trackId);
2203
+ return;
2204
+ }
1752
2205
  this.applyLocalTrackTake(resolvedTrackId);
1753
2206
  this.client.sendSessionSourcePlaybackCompleted(sourceId, trackId);
1754
2207
  this.sendSessionTrackTake(resolvedTrackId);
@@ -1759,6 +2212,9 @@ var IviRuntimeCoordinator = class {
1759
2212
  getTrtcSourceManager() {
1760
2213
  return this.trtcSourceManager;
1761
2214
  }
2215
+ getLivekitSourceManager() {
2216
+ return this.livekitSourceManager;
2217
+ }
1762
2218
  onConnectionChange(connected) {
1763
2219
  if (connected || this.state.status === "idle") {
1764
2220
  return;
@@ -1798,6 +2254,7 @@ var IviRuntimeCoordinator = class {
1798
2254
  this.sourceManager.reset();
1799
2255
  this.streamManager.reset();
1800
2256
  this.trtcSourceManager.reset();
2257
+ this.livekitSourceManager.reset();
1801
2258
  this.conversationManager.reset();
1802
2259
  this.reset();
1803
2260
  const nextStatus = this.config.syncStageOnSessionCreated !== false ? "syncing" : "running";
@@ -1838,6 +2295,7 @@ var IviRuntimeCoordinator = class {
1838
2295
  const nextStage = this.stageManager.getStage();
1839
2296
  this.sourceManager.syncWithTracks(this.trackManager.getAll());
1840
2297
  this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2298
+ this.livekitSourceManager.syncRuntimeSources(this.sourceManager.getAll());
1841
2299
  this.setState({
1842
2300
  ...this.state,
1843
2301
  tracks: this.trackManager.getAll(),
@@ -1852,14 +2310,27 @@ var IviRuntimeCoordinator = class {
1852
2310
  this.trackManager.applyTrackTook(trackId);
1853
2311
  this.sourceManager.syncWithTracks(this.trackManager.getAll());
1854
2312
  this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2313
+ this.livekitSourceManager.syncRuntimeSources(this.sourceManager.getAll());
1855
2314
  this.setState({
1856
2315
  ...this.state,
1857
2316
  tracks: this.trackManager.getAll(),
1858
2317
  sources: this.sourceManager.getAll()
1859
2318
  });
1860
2319
  }
1861
- async deferredTrtcTakeCompleteAndTake(trackId, nextSourceId, completedSourceId, completedTrackIdArg) {
1862
- const remoteVideoAvailable = await this.trtcSourceManager.waitForRemoteVideoAvailable(nextSourceId);
2320
+ async deferredTrtcTakeCompleteAndTake(trackId, nextSourceId, completedSourceId, completedTrackIdArg) {
2321
+ const remoteVideoAvailable = await this.trtcSourceManager.waitForRemoteVideoAvailable(nextSourceId);
2322
+ if (!remoteVideoAvailable) return;
2323
+ if (this.state.status !== "running") return;
2324
+ const currentTrack = this.trackManager.getAll().get(trackId);
2325
+ if (!currentTrack || currentTrack.next_source_id !== nextSourceId) {
2326
+ return;
2327
+ }
2328
+ this.applyLocalTrackTake(trackId);
2329
+ this.client.sendSessionSourcePlaybackCompleted(completedSourceId, completedTrackIdArg);
2330
+ this.sendSessionTrackTake(trackId);
2331
+ }
2332
+ async deferredLivekitTakeCompleteAndTake(trackId, nextSourceId, completedSourceId, completedTrackIdArg) {
2333
+ const remoteVideoAvailable = await this.livekitSourceManager.waitForRemoteVideoAvailable(nextSourceId);
1863
2334
  if (!remoteVideoAvailable) return;
1864
2335
  if (this.state.status !== "running") return;
1865
2336
  const currentTrack = this.trackManager.getAll().get(trackId);
@@ -1880,6 +2351,7 @@ var IviRuntimeCoordinator = class {
1880
2351
  onSourcesChanged(listRefreshed) {
1881
2352
  this.sourceManager.syncWithTracks(this.trackManager.getAll());
1882
2353
  this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
2354
+ this.livekitSourceManager.syncRuntimeSources(this.sourceManager.getAll());
1883
2355
  this.setState({
1884
2356
  ...this.state,
1885
2357
  sources: this.sourceManager.getAll()
@@ -1922,6 +2394,7 @@ var IviRuntimeCoordinator = class {
1922
2394
  this.sourceManager.reset();
1923
2395
  this.streamManager.reset();
1924
2396
  this.trtcSourceManager.reset();
2397
+ this.livekitSourceManager.reset();
1925
2398
  this.conversationManager.reset();
1926
2399
  this.reset();
1927
2400
  this.setState({
@@ -1960,7 +2433,7 @@ var IviRuntimeCoordinator = class {
1960
2433
  return [];
1961
2434
  }
1962
2435
  const missing = /* @__PURE__ */ new Set();
1963
- stage.composition.forEach((item) => {
2436
+ (stage.composition ?? []).forEach((item) => {
1964
2437
  if (!hasTrack(item.track_id)) {
1965
2438
  missing.add(item.track_id);
1966
2439
  }
@@ -1995,6 +2468,9 @@ var IviRuntimeCoordinator = class {
1995
2468
  }
1996
2469
  progressUserTextToResponseFlows(event) {
1997
2470
  if (event instanceof iviSdkTs.ReceiveConversationItemAddedEvent) {
2471
+ if (!event.item.id) {
2472
+ return;
2473
+ }
1998
2474
  const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
1999
2475
  if (!flow) {
2000
2476
  return;
@@ -2005,6 +2481,9 @@ var IviRuntimeCoordinator = class {
2005
2481
  return;
2006
2482
  }
2007
2483
  if (event instanceof iviSdkTs.ReceiveConversationItemDoneEvent) {
2484
+ if (!event.item.id) {
2485
+ return;
2486
+ }
2008
2487
  const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
2009
2488
  if (!flow) {
2010
2489
  return;
@@ -2040,7 +2519,11 @@ var IviRuntimeCoordinator = class {
2040
2519
  addedEvent: flow.addedEvent,
2041
2520
  doneEvent: flow.doneEvent
2042
2521
  });
2043
- this.client.sendResponseCreateByItemId(flow.itemId, flow.streamId, flow.response);
2522
+ if (flow.responseInput === "item_reference") {
2523
+ this.client.sendResponseCreateByItemId(flow.itemId, flow.streamId, flow.response);
2524
+ } else {
2525
+ this.client.sendResponseCreate(flow.streamId, flow.response);
2526
+ }
2044
2527
  flow.responseRequested = true;
2045
2528
  }
2046
2529
  buildUserTextItemId() {
@@ -2053,13 +2536,18 @@ var IviRuntimeCoordinator = class {
2053
2536
  function isVideoPlaybackSource(source) {
2054
2537
  if (!source || source.status !== "ready" || !source.playback) return false;
2055
2538
  if (source.playback.type === "trtc") return false;
2539
+ if (isLivekitSourcePlayback(source.playback)) return false;
2056
2540
  if (source.source.asset_type === "image") return false;
2057
- return Boolean(source.playback.url);
2541
+ return source.playback.type === "url" && Boolean(source.playback.url);
2058
2542
  }
2059
2543
  function isTrtcPlaybackSource(source) {
2060
2544
  if (!source || !source.playback) return false;
2061
2545
  return source.playback.type === "trtc" && Boolean(source.playback.trtc);
2062
2546
  }
2547
+ function isLivekitPlaybackSource(source) {
2548
+ if (!source || !source.playback) return false;
2549
+ return isLivekitSourcePlayback(source.playback);
2550
+ }
2063
2551
  var IviFrontendSdk = class {
2064
2552
  createClient(config) {
2065
2553
  return new iviSdkTs.IviClient(config);
@@ -2146,14 +2634,14 @@ function IVIStageView(props) {
2146
2634
  }
2147
2635
  function buildSlotTrackMapFromState(state) {
2148
2636
  const map = /* @__PURE__ */ new Map();
2149
- state.stage?.composition.forEach((item) => {
2637
+ (state.stage?.composition ?? []).forEach((item) => {
2150
2638
  map.set(item.slot, item.track_id);
2151
2639
  });
2152
2640
  return map;
2153
2641
  }
2154
2642
  function buildSlotBindingsFromState(state) {
2155
2643
  const bindings = [];
2156
- state.stage?.composition.forEach((item) => {
2644
+ (state.stage?.composition ?? []).forEach((item) => {
2157
2645
  const track = state.tracks.get(item.track_id);
2158
2646
  const sourceId = track?.active_source_id ?? null;
2159
2647
  const source = sourceId ? state.sources.get(sourceId) : void 0;
@@ -2180,12 +2668,18 @@ function loadVolumePreferences() {
2180
2668
  return {
2181
2669
  trtc: clampVolume(parsed.trtc ?? DEFAULT_VOLUME),
2182
2670
  video: clampVolume(parsed.video ?? DEFAULT_VOLUME),
2183
- hls: clampVolume(parsed.hls ?? DEFAULT_VOLUME)
2671
+ hls: clampVolume(parsed.hls ?? DEFAULT_VOLUME),
2672
+ livekit: clampVolume(parsed.livekit ?? DEFAULT_VOLUME)
2184
2673
  };
2185
2674
  }
2186
2675
  } catch {
2187
2676
  }
2188
- return { trtc: DEFAULT_VOLUME, video: DEFAULT_VOLUME, hls: DEFAULT_VOLUME };
2677
+ return {
2678
+ trtc: DEFAULT_VOLUME,
2679
+ video: DEFAULT_VOLUME,
2680
+ hls: DEFAULT_VOLUME,
2681
+ livekit: DEFAULT_VOLUME
2682
+ };
2189
2683
  }
2190
2684
  function saveVolumePreferences(prefs) {
2191
2685
  try {
@@ -2443,101 +2937,133 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
2443
2937
  return () => observer.disconnect();
2444
2938
  }, [containerRef, volume, enabled, activeSourceId]);
2445
2939
  }
2446
- function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
2447
- const [visibleIds, setVisibleIds] = react.useState([]);
2448
- const timersRef = react.useRef(/* @__PURE__ */ new Map());
2449
- const seenRef = react.useRef(/* @__PURE__ */ new Set());
2450
- const dismissedRef = react.useRef(/* @__PURE__ */ new Set());
2940
+ function useIviSubtitles(runtime, options = {}) {
2941
+ const roles = options.roles ?? "user";
2942
+ const maxItems = normalizeMaxItems(options.maxItems);
2943
+ const roleKey = Array.isArray(roles) ? roles.join("\0") : roles;
2944
+ const roleSet = react.useMemo(
2945
+ () => new Set(roleKey.split("\0")),
2946
+ [roleKey]
2947
+ );
2948
+ const [subtitles, setSubtitles] = react.useState([]);
2949
+ const seenIdsRef = react.useRef(/* @__PURE__ */ new Set());
2451
2950
  const initializedRef = react.useRef(false);
2452
2951
  react.useEffect(() => {
2453
- const seen = seenRef.current;
2454
- const dismissed = dismissedRef.current;
2455
- const timers = timersRef.current;
2456
- if (!initializedRef.current) {
2457
- initializedRef.current = true;
2458
- for (const item of conversations) {
2459
- if (item.lifecycle === "done" || !(item.text || item.transcript)) {
2460
- seen.add(item.id);
2461
- dismissed.add(item.id);
2952
+ seenIdsRef.current = /* @__PURE__ */ new Set();
2953
+ initializedRef.current = false;
2954
+ setSubtitles([]);
2955
+ if (!runtime) {
2956
+ return;
2957
+ }
2958
+ const syncConversations = (conversations) => {
2959
+ const now = Date.now();
2960
+ const seenIds = seenIdsRef.current;
2961
+ if (!initializedRef.current) {
2962
+ initializedRef.current = true;
2963
+ for (const item of conversations) {
2964
+ if (item.lifecycle === "done" || !getDisplayText(item) || !roleSet.has(item.role)) {
2965
+ seenIds.add(item.id);
2966
+ }
2462
2967
  }
2463
2968
  }
2464
- }
2465
- const newIds = [];
2466
- for (const item of conversations) {
2467
- const displayText = item.text || item.transcript;
2468
- if (!displayText) continue;
2469
- if (!seen.has(item.id)) {
2470
- seen.add(item.id);
2471
- newIds.push(item.id);
2472
- }
2473
- if (item.lifecycle === "done" && !dismissed.has(item.id)) {
2474
- dismissed.add(item.id);
2475
- const timer = setTimeout(() => {
2476
- timers.delete(item.id);
2477
- setVisibleIds((prev) => prev.filter((id) => id !== item.id));
2478
- }, dismissAfterMs);
2479
- timers.set(item.id, timer);
2480
- }
2481
- }
2482
- if (newIds.length > 0) {
2483
- setVisibleIds((prev) => {
2484
- const next = [...prev, ...newIds];
2485
- while (next.length > maxVisible) {
2486
- const removedId = next.shift();
2487
- if (timers.has(removedId)) {
2488
- clearTimeout(timers.get(removedId));
2489
- timers.delete(removedId);
2969
+ setSubtitles((previous) => {
2970
+ const conversationMap = new Map(conversations.map((item) => [item.id, item]));
2971
+ const nextById = /* @__PURE__ */ new Map();
2972
+ for (const previousItem of previous) {
2973
+ const conversation = conversationMap.get(previousItem.id);
2974
+ if (!conversation || !roleSet.has(conversation.role) || !getDisplayText(conversation)) {
2975
+ continue;
2490
2976
  }
2491
- dismissed.add(removedId);
2977
+ nextById.set(
2978
+ previousItem.id,
2979
+ buildSubtitleItem(
2980
+ conversation,
2981
+ previousItem.timestamp,
2982
+ hasSubtitleChanged(previousItem, conversation) ? now : previousItem.updatedAt
2983
+ )
2984
+ );
2492
2985
  }
2493
- return next;
2986
+ for (const conversation of conversations) {
2987
+ if (!roleSet.has(conversation.role) || !getDisplayText(conversation)) {
2988
+ continue;
2989
+ }
2990
+ if (seenIds.has(conversation.id)) {
2991
+ continue;
2992
+ }
2993
+ seenIds.add(conversation.id);
2994
+ nextById.set(conversation.id, buildSubtitleItem(conversation, now, now));
2995
+ }
2996
+ const next = Array.from(nextById.values());
2997
+ if (maxItems === 0) {
2998
+ return [];
2999
+ }
3000
+ return next.length > maxItems ? next.slice(next.length - maxItems) : next;
2494
3001
  });
2495
- }
2496
- }, [conversations, maxVisible, dismissAfterMs]);
2497
- react.useEffect(() => {
2498
- const timers = timersRef.current;
2499
- return () => {
2500
- timers.forEach((t) => clearTimeout(t));
2501
- timers.clear();
2502
3002
  };
2503
- }, []);
2504
- const conversationMap = react.useMemo(() => {
2505
- const map = /* @__PURE__ */ new Map();
2506
- for (const item of conversations) {
2507
- map.set(item.id, item);
2508
- }
2509
- return map;
2510
- }, [conversations]);
2511
- return react.useMemo(() => {
2512
- return visibleIds.map((id) => {
2513
- const item = conversationMap.get(id);
2514
- if (!item) return null;
2515
- const text = item.text || item.transcript;
2516
- if (!text) return null;
2517
- return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
2518
- }).filter((entry) => entry !== null);
2519
- }, [visibleIds, conversationMap]);
3003
+ syncConversations(runtime.getState().conversations);
3004
+ return runtime.onEvent((event, state) => {
3005
+ if (event.type === "session.ended") {
3006
+ seenIdsRef.current = /* @__PURE__ */ new Set();
3007
+ initializedRef.current = false;
3008
+ setSubtitles([]);
3009
+ return;
3010
+ }
3011
+ if (!isSubtitleRelatedEvent(event.type)) {
3012
+ return;
3013
+ }
3014
+ syncConversations(state.conversations);
3015
+ });
3016
+ }, [runtime, roleSet, maxItems]);
3017
+ return subtitles;
3018
+ }
3019
+ function normalizeMaxItems(maxItems) {
3020
+ if (maxItems === void 0) {
3021
+ return 2;
3022
+ }
3023
+ if (!Number.isFinite(maxItems)) {
3024
+ return 2;
3025
+ }
3026
+ return Math.max(0, Math.floor(maxItems));
3027
+ }
3028
+ function getDisplayText(item) {
3029
+ return item.text || item.transcript;
3030
+ }
3031
+ function buildSubtitleItem(item, timestamp, updatedAt) {
3032
+ return {
3033
+ id: item.id,
3034
+ role: item.role,
3035
+ lifecycle: item.lifecycle,
3036
+ status: item.status,
3037
+ text: item.text,
3038
+ transcript: item.transcript,
3039
+ displayText: getDisplayText(item),
3040
+ content: item.content,
3041
+ item: item.item,
3042
+ timestamp,
3043
+ updatedAt
3044
+ };
3045
+ }
3046
+ function hasSubtitleChanged(previous, next) {
3047
+ return previous.text !== next.text || previous.transcript !== next.transcript || previous.lifecycle !== next.lifecycle || previous.status !== next.status || previous.role !== next.role;
3048
+ }
3049
+ function isSubtitleRelatedEvent(type) {
3050
+ return type.startsWith("conversation.") || type.startsWith("response.");
2520
3051
  }
2521
3052
  var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
2522
3053
  function IVISubtitleOverlay(props) {
2523
3054
  const {
2524
- conversations,
3055
+ runtime,
2525
3056
  roles = "user",
2526
- maxVisible = 2,
2527
- dismissAfterMs = 5e3,
3057
+ maxItems,
3058
+ maxVisible,
2528
3059
  subtitleStyle,
2529
3060
  className,
2530
3061
  style
2531
3062
  } = props;
2532
- const roleSet = react.useMemo(
2533
- () => new Set(Array.isArray(roles) ? roles : [roles]),
2534
- [roles]
2535
- );
2536
- const filtered = react.useMemo(
2537
- () => conversations.filter((c) => roleSet.has(c.role)),
2538
- [conversations, roleSet]
2539
- );
2540
- const entries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
3063
+ const entries = useIviSubtitles(runtime, {
3064
+ roles,
3065
+ maxItems: maxItems ?? maxVisible
3066
+ });
2541
3067
  if (entries.length === 0) return null;
2542
3068
  const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
2543
3069
  const fontSize = subtitleStyle?.fontSize ?? 14;
@@ -2575,7 +3101,7 @@ function IVISubtitleOverlay(props) {
2575
3101
  whiteSpace: "nowrap",
2576
3102
  animation: entry.lifecycle === "added" ? "ivi-subtitle-breathe 1.5s ease-in-out infinite" : void 0
2577
3103
  },
2578
- children: entry.text
3104
+ children: entry.displayText
2579
3105
  },
2580
3106
  entry.id
2581
3107
  ))
@@ -2678,9 +3204,224 @@ function IVITrtcPlayer(props) {
2678
3204
  }
2679
3205
  );
2680
3206
  }
3207
+ var standaloneLivekitSourceManager = new LivekitSourceManager();
3208
+ function IVILivekitPlayer(props) {
3209
+ const {
3210
+ livekit,
3211
+ sourceId,
3212
+ runtime,
3213
+ className,
3214
+ style,
3215
+ loadingFallback = null,
3216
+ errorFallback = null,
3217
+ muted = false
3218
+ } = props;
3219
+ const containerRef = react.useRef(null);
3220
+ const viewIdRef = react.useRef(`livekit-view-${Math.random().toString(36).slice(2, 10)}`);
3221
+ const manager = runtime?.getLivekitSourceManager() ?? standaloneLivekitSourceManager;
3222
+ const resolvedSourceId = react.useMemo(
3223
+ () => sourceId ?? `adhoc:${livekit.ws_url}:${describeLivekitRoom(livekit)}:${livekit.token}`,
3224
+ [sourceId, livekit]
3225
+ );
3226
+ const shouldManageSourceLifecycle = !runtime || !sourceId;
3227
+ const [loading, setLoading] = react.useState(true);
3228
+ const [error, setError] = react.useState(null);
3229
+ const mutedRef = react.useRef(muted);
3230
+ mutedRef.current = muted;
3231
+ react.useEffect(() => {
3232
+ const container = containerRef.current;
3233
+ if (!container) {
3234
+ return;
3235
+ }
3236
+ let disposed = false;
3237
+ if (shouldManageSourceLifecycle) {
3238
+ manager.upsertSource(resolvedSourceId, livekit);
3239
+ }
3240
+ const unsubscribe = manager.subscribe(resolvedSourceId, (snapshot) => {
3241
+ if (disposed) {
3242
+ return;
3243
+ }
3244
+ setLoading(snapshot.status === "idle" || snapshot.status === "connecting");
3245
+ setError(snapshot.status === "error" ? snapshot.error ?? "LiveKit \u62C9\u6D41\u5931\u8D25" : null);
3246
+ });
3247
+ void manager.attachView(resolvedSourceId, viewIdRef.current, container, mutedRef.current).catch((caughtError) => {
3248
+ if (disposed) {
3249
+ return;
3250
+ }
3251
+ setError(caughtError instanceof Error ? caughtError.message : "LiveKit \u62C9\u6D41\u5931\u8D25");
3252
+ setLoading(false);
3253
+ });
3254
+ return () => {
3255
+ disposed = true;
3256
+ unsubscribe();
3257
+ manager.detachView(resolvedSourceId, viewIdRef.current);
3258
+ if (shouldManageSourceLifecycle) {
3259
+ manager.removeSource(resolvedSourceId);
3260
+ }
3261
+ };
3262
+ }, [
3263
+ manager,
3264
+ resolvedSourceId,
3265
+ shouldManageSourceLifecycle,
3266
+ livekit.ws_url,
3267
+ livekit.token,
3268
+ livekit.room,
3269
+ livekit.identity
3270
+ ]);
3271
+ react.useEffect(() => {
3272
+ manager.updateViewMuted(resolvedSourceId, viewIdRef.current, muted);
3273
+ }, [manager, muted, resolvedSourceId]);
3274
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3275
+ "div",
3276
+ {
3277
+ className,
3278
+ style: {
3279
+ width: "100%",
3280
+ height: "100%",
3281
+ minWidth: 0,
3282
+ minHeight: 0,
3283
+ backgroundColor: "#000",
3284
+ position: "relative",
3285
+ ...style
3286
+ },
3287
+ children: [
3288
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
3289
+ loading ? loadingFallback : null,
3290
+ error ? errorFallback ?? /* @__PURE__ */ jsxRuntime.jsx("div", { children: error }) : null
3291
+ ]
3292
+ }
3293
+ );
3294
+ }
3295
+ var loadModulesPromise = null;
3296
+ async function loadXgplayerModules() {
3297
+ if (!loadModulesPromise) {
3298
+ loadModulesPromise = (async () => {
3299
+ try {
3300
+ await import('xgplayer/dist/index.min.css');
3301
+ } catch (err) {
3302
+ console.warn("[IVIFlvVideo] \u52A0\u8F7D xgplayer \u6837\u5F0F\u5931\u8D25\uFF0C\u64AD\u653E\u5668\u63A7\u4EF6\u53EF\u80FD\u663E\u793A\u5F02\u5E38", err);
3303
+ }
3304
+ const [playerMod, flvMod] = await Promise.all([import('xgplayer'), import('xgplayer-flv')]);
3305
+ const playerModule = playerMod;
3306
+ const flvModule = flvMod;
3307
+ const Player = playerModule.default ?? playerModule.Player;
3308
+ const FlvPlugin = flvModule.default ?? flvModule.FlvPlugin;
3309
+ if (!Player || !FlvPlugin) {
3310
+ throw new Error("xgplayer \u6216 xgplayer-flv \u6A21\u5757\u5BFC\u51FA\u65E0\u6548");
3311
+ }
3312
+ return { Player, FlvPlugin };
3313
+ })().catch((err) => {
3314
+ loadModulesPromise = null;
3315
+ throw err;
3316
+ });
3317
+ }
3318
+ return loadModulesPromise;
3319
+ }
3320
+ function IVIFlvVideo(props) {
3321
+ const { url, videoProps, style, isLive = true, paused = false } = props;
3322
+ const containerRef = react.useRef(null);
3323
+ const playerRef = react.useRef(null);
3324
+ const pausedRef = react.useRef(paused);
3325
+ const videoPropsRef = react.useRef(videoProps);
3326
+ videoPropsRef.current = videoProps;
3327
+ const reactId = react.useId();
3328
+ const containerId = `ivi-flv-${reactId.replace(/:/g, "")}`;
3329
+ react.useEffect(() => {
3330
+ pausedRef.current = paused;
3331
+ const player = playerRef.current;
3332
+ if (!player) return;
3333
+ if (paused) {
3334
+ player.pause();
3335
+ } else {
3336
+ Promise.resolve(player.play()).catch((err) => {
3337
+ console.warn(
3338
+ "[IVIFlvVideo] paused\u2192active \u5207\u6362\u65F6 play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD\uFF08\u591A\u534A\u53D7\u81EA\u52A8\u64AD\u653E\u7B56\u7565\u9650\u5236\uFF09",
3339
+ err
3340
+ );
3341
+ });
3342
+ }
3343
+ }, [paused]);
3344
+ react.useEffect(() => {
3345
+ const container = containerRef.current;
3346
+ if (!container) return;
3347
+ let disposed = false;
3348
+ let endedHandler = null;
3349
+ let errorHandler = null;
3350
+ const setup = async () => {
3351
+ try {
3352
+ const { Player, FlvPlugin } = await loadXgplayerModules();
3353
+ if (disposed || !containerRef.current) return;
3354
+ playerRef.current?.destroy();
3355
+ playerRef.current = null;
3356
+ container.innerHTML = "";
3357
+ const shouldAutoplay = !pausedRef.current;
3358
+ const currentVideoProps = videoPropsRef.current;
3359
+ const muted = Boolean(currentVideoProps?.muted);
3360
+ const player = new Player({
3361
+ id: containerId,
3362
+ el: containerRef.current,
3363
+ url,
3364
+ plugins: [FlvPlugin],
3365
+ isLive,
3366
+ autoplay: shouldAutoplay,
3367
+ autoplayMuted: muted,
3368
+ muted,
3369
+ controls: currentVideoProps?.controls ?? true,
3370
+ width: "100%",
3371
+ height: "100%",
3372
+ fluid: true,
3373
+ videoFillMode: "contain",
3374
+ flv: {
3375
+ retryCount: Number.MAX_SAFE_INTEGER,
3376
+ retryDelay: 500,
3377
+ loadTimeout: 1e4
3378
+ }
3379
+ });
3380
+ playerRef.current = player;
3381
+ errorHandler = (...args) => {
3382
+ console.warn("[IVIFlvVideo] \u64AD\u653E\u5668 error \u4E8B\u4EF6", { url, args });
3383
+ };
3384
+ player.on("error", errorHandler);
3385
+ const onEnded = currentVideoProps?.onEnded;
3386
+ if (onEnded) {
3387
+ endedHandler = () => {
3388
+ videoPropsRef.current?.onEnded?.({});
3389
+ };
3390
+ player.on("ended", endedHandler);
3391
+ }
3392
+ if (!pausedRef.current) {
3393
+ Promise.resolve(player.play()).catch((err) => {
3394
+ console.warn("[IVIFlvVideo] \u521D\u59CB play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD", err);
3395
+ });
3396
+ }
3397
+ } catch (err) {
3398
+ console.warn("[IVIFlvVideo] \u521D\u59CB\u5316 xgplayer-flv \u5931\u8D25", err);
3399
+ }
3400
+ };
3401
+ void setup();
3402
+ return () => {
3403
+ disposed = true;
3404
+ const player = playerRef.current;
3405
+ if (player) {
3406
+ if (endedHandler) player.off("ended", endedHandler);
3407
+ if (errorHandler) player.off("error", errorHandler);
3408
+ player.destroy();
3409
+ }
3410
+ playerRef.current = null;
3411
+ if (containerRef.current) {
3412
+ containerRef.current.innerHTML = "";
3413
+ }
3414
+ };
3415
+ }, [url, isLive, containerId]);
3416
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { id: containerId, ref: containerRef, style: { width: "100%", height: "100%", ...style } });
3417
+ }
3418
+ function isFlvUrl(url) {
3419
+ return /\.flv(?:$|[?#])/i.test(url);
3420
+ }
2681
3421
  var RETRY_DELAY_MS = 500;
2682
3422
  var UNLIMITED_RETRIES = Number.MAX_SAFE_INTEGER;
2683
- function makeRetryConfig(label, kind) {
3423
+ var HLS_LOG_TAG = "[IVI-HLS]";
3424
+ function makeRetryConfig(label, kind, onLog) {
2684
3425
  return {
2685
3426
  maxNumRetry: UNLIMITED_RETRIES,
2686
3427
  retryDelayMs: RETRY_DELAY_MS,
@@ -2688,25 +3429,27 @@ function makeRetryConfig(label, kind) {
2688
3429
  backoff: "linear",
2689
3430
  shouldRetry: (_config, retryCount, isTimeout) => {
2690
3431
  const reason = kind === "timeout" || isTimeout ? "\u52A0\u8F7D\u8D85\u65F6" : "\u52A0\u8F7D\u5931\u8D25";
2691
- console.warn(
2692
- `[IVIHlsVideo] ${label} ${reason}\uFF0C${RETRY_DELAY_MS}ms \u540E\u8FDB\u884C\u7B2C ${retryCount + 1} \u6B21\u91CD\u8BD5\uFF08\u65E0\u4E0A\u9650\uFF09`
3432
+ emitHlsLog(
3433
+ onLog,
3434
+ "warn",
3435
+ `${label} ${reason}\uFF0C${RETRY_DELAY_MS}ms \u540E\u8FDB\u884C\u7B2C ${retryCount + 1} \u6B21\u91CD\u8BD5\uFF08\u65E0\u4E0A\u9650\uFF09`
2693
3436
  );
2694
3437
  return true;
2695
3438
  }
2696
3439
  };
2697
3440
  }
2698
- function makeLoadPolicy(label) {
3441
+ function makeLoadPolicy(label, onLog) {
2699
3442
  return {
2700
3443
  default: {
2701
3444
  maxTimeToFirstByteMs: 1e4,
2702
3445
  maxLoadTimeMs: 2e4,
2703
- timeoutRetry: makeRetryConfig(label, "timeout"),
2704
- errorRetry: makeRetryConfig(label, "error")
3446
+ timeoutRetry: makeRetryConfig(label, "timeout", onLog),
3447
+ errorRetry: makeRetryConfig(label, "error", onLog)
2705
3448
  }
2706
3449
  };
2707
3450
  }
2708
3451
  function IVIHlsVideo(props) {
2709
- const { url, videoProps, style, aggressivePreload = false, paused = false } = props;
3452
+ const { url, videoProps, style, aggressivePreload = false, paused = false, onLog } = props;
2710
3453
  const videoRef = react.useRef(null);
2711
3454
  const pausedRef = react.useRef(paused);
2712
3455
  react.useEffect(() => {
@@ -2717,13 +3460,15 @@ function IVIHlsVideo(props) {
2717
3460
  video.pause();
2718
3461
  } else {
2719
3462
  video.play().catch((err) => {
2720
- console.warn(
2721
- "[IVIHlsVideo] paused\u2192active \u5207\u6362\u65F6 play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD\uFF08\u591A\u534A\u53D7\u81EA\u52A8\u64AD\u653E\u7B56\u7565\u9650\u5236\uFF09",
3463
+ emitHlsLog(
3464
+ onLog,
3465
+ "warn",
3466
+ "paused\u2192active \u5207\u6362\u65F6 play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD\uFF08\u591A\u534A\u53D7\u81EA\u52A8\u64AD\u653E\u7B56\u7565\u9650\u5236\uFF09",
2722
3467
  err
2723
3468
  );
2724
3469
  });
2725
3470
  }
2726
- }, [paused]);
3471
+ }, [onLog, paused]);
2727
3472
  react.useEffect(() => {
2728
3473
  const video = videoRef.current;
2729
3474
  if (!video) {
@@ -2741,7 +3486,7 @@ function IVIHlsVideo(props) {
2741
3486
  const onVideoError = () => {
2742
3487
  const el = videoRef.current;
2743
3488
  const mediaErr = el?.error;
2744
- console.warn("[IVIHlsVideo] <video> \u5143\u7D20\u62A5\u9519", {
3489
+ emitHlsLog(onLog, "warn", "<video> \u5143\u7D20\u62A5\u9519", {
2745
3490
  code: mediaErr?.code,
2746
3491
  message: mediaErr?.message,
2747
3492
  currentSrc: el?.currentSrc,
@@ -2750,7 +3495,7 @@ function IVIHlsVideo(props) {
2750
3495
  });
2751
3496
  };
2752
3497
  const onVideoStalled = () => {
2753
- console.warn("[IVIHlsVideo] <video> stalled\uFF08\u7F13\u51B2\u505C\u6EDE\uFF09", {
3498
+ emitHlsLog(onLog, "warn", "<video> stalled\uFF08\u7F13\u51B2\u505C\u6EDE\uFF09", {
2754
3499
  currentTime: videoRef.current?.currentTime,
2755
3500
  readyState: videoRef.current?.readyState
2756
3501
  });
@@ -2763,17 +3508,17 @@ function IVIHlsVideo(props) {
2763
3508
  const el = videoRef.current;
2764
3509
  if (!el) return;
2765
3510
  el.play().catch((err) => {
2766
- console.warn(
2767
- "[IVIHlsVideo] \u6062\u590D\u540E play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD\uFF08\u53EF\u80FD\u53D7\u81EA\u52A8\u64AD\u653E\u7B56\u7565\u9650\u5236\uFF09",
3511
+ emitHlsLog(
3512
+ onLog,
3513
+ "warn",
3514
+ "\u6062\u590D\u540E play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD\uFF08\u53EF\u80FD\u53D7\u81EA\u52A8\u64AD\u653E\u7B56\u7565\u9650\u5236\uFF09",
2768
3515
  err
2769
3516
  );
2770
3517
  });
2771
3518
  };
2772
3519
  const scheduleFullReload = (reason) => {
2773
3520
  if (disposed || fullReloadTimer) return;
2774
- console.warn(
2775
- `[IVIHlsVideo] ${reason}\uFF0C${RETRY_DELAY_MS}ms \u540E\u91CD\u65B0\u62C9\u53D6\u6E90\uFF08\u65E0\u9650\u91CD\u8BD5\uFF09`
2776
- );
3521
+ emitHlsLog(onLog, "warn", `${reason}\uFF0C${RETRY_DELAY_MS}ms \u540E\u91CD\u65B0\u62C9\u53D6\u6E90\uFF08\u65E0\u9650\u91CD\u8BD5\uFF09`);
2777
3522
  fullReloadTimer = setTimeout(() => {
2778
3523
  fullReloadTimer = null;
2779
3524
  if (disposed || !hlsInstance || !videoRef.current) return;
@@ -2783,7 +3528,7 @@ function IVIHlsVideo(props) {
2783
3528
  hlsInstance.startLoad();
2784
3529
  resumePlayback();
2785
3530
  } catch (err) {
2786
- console.warn("[IVIHlsVideo] \u91CD\u65B0\u62C9\u53D6\u6E90\u629B\u51FA\u5F02\u5E38\uFF0C\u7EE7\u7EED\u91CD\u8BD5", err);
3531
+ emitHlsLog(onLog, "warn", "\u91CD\u65B0\u62C9\u53D6\u6E90\u629B\u51FA\u5F02\u5E38\uFF0C\u7EE7\u7EED\u91CD\u8BD5", err);
2787
3532
  scheduleFullReload("\u91CD\u65B0\u62C9\u53D6\u6E90\u5F02\u5E38");
2788
3533
  }
2789
3534
  }, RETRY_DELAY_MS);
@@ -2799,8 +3544,10 @@ function IVIHlsVideo(props) {
2799
3544
  const events = hlsModule.Events ?? Hls.Events ?? {};
2800
3545
  const errorTypes = hlsModule.ErrorTypes ?? Hls.ErrorTypes ?? {};
2801
3546
  if (!Hls.isSupported()) {
2802
- console.warn(
2803
- "[IVIHlsVideo] \u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 hls.js\uFF0C\u964D\u7EA7\u5230\u539F\u751F HLS\uFF08\u5931\u8D25\u5C06\u7531 <video> error \u4E8B\u4EF6\u6253\u5370\uFF09",
3547
+ emitHlsLog(
3548
+ onLog,
3549
+ "warn",
3550
+ "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 hls.js\uFF0C\u964D\u7EA7\u5230\u539F\u751F HLS\uFF08\u5931\u8D25\u5C06\u7531 <video> error \u4E8B\u4EF6\u4E0A\u62A5\uFF09",
2804
3551
  { url }
2805
3552
  );
2806
3553
  videoRef.current.src = url;
@@ -2822,9 +3569,9 @@ function IVIHlsVideo(props) {
2822
3569
  capLevelToPlayerSize: true,
2823
3570
  // 以下三项是核心:列表与分片的拉取全部使用 hls.js 内置重试,
2824
3571
  // 无上限次数、间隔上限 0.5s、每次重试通过 shouldRetry 打印警告。
2825
- manifestLoadPolicy: makeLoadPolicy("manifest \u4E3B\u5217\u8868"),
2826
- playlistLoadPolicy: makeLoadPolicy("level \u5B50\u7801\u7387\u5217\u8868"),
2827
- fragLoadPolicy: makeLoadPolicy("fragment \u5A92\u4F53\u5206\u7247")
3572
+ manifestLoadPolicy: makeLoadPolicy("manifest \u4E3B\u5217\u8868", onLog),
3573
+ playlistLoadPolicy: makeLoadPolicy("level \u5B50\u7801\u7387\u5217\u8868", onLog),
3574
+ fragLoadPolicy: makeLoadPolicy("fragment \u5A92\u4F53\u5206\u7247", onLog)
2828
3575
  });
2829
3576
  hlsInstance = instance;
2830
3577
  const errorEvent = events.ERROR ?? "hlsError";
@@ -2832,52 +3579,38 @@ function IVIHlsVideo(props) {
2832
3579
  const mediaErrorType = errorTypes.MEDIA_ERROR ?? "mediaError";
2833
3580
  instance.on(errorEvent, (_eventName, data) => {
2834
3581
  if (!data) {
2835
- console.warn("[IVIHlsVideo] HLS ERROR \u4E8B\u4EF6 data \u4E3A\u7A7A");
3582
+ emitHlsLog(onLog, "warn", "HLS ERROR \u4E8B\u4EF6 data \u4E3A\u7A7A");
2836
3583
  return;
2837
3584
  }
2838
3585
  if (!data.fatal) {
2839
3586
  if (data.type !== networkErrorType) {
2840
- console.warn(
2841
- "[IVIHlsVideo] HLS \u975E\u81F4\u547D\u9519\u8BEF\uFF08\u4E0D\u8D70\u5185\u7F6E\u91CD\u8BD5\uFF09",
2842
- data.type,
2843
- data.details
2844
- );
3587
+ emitHlsLog(onLog, "warn", "HLS \u975E\u81F4\u547D\u9519\u8BEF\uFF08\u4E0D\u8D70\u5185\u7F6E\u91CD\u8BD5\uFF09", data.type, data.details);
2845
3588
  }
2846
3589
  return;
2847
3590
  }
2848
3591
  switch (data.type) {
2849
3592
  case networkErrorType:
2850
- console.warn(
2851
- "[IVIHlsVideo] HLS \u81F4\u547D\u7F51\u7EDC\u9519\u8BEF\uFF0C\u8C03\u7528 startLoad() \u91CD\u542F\u62C9\u6D41",
2852
- data.details
2853
- );
3593
+ emitHlsLog(onLog, "warn", "HLS \u81F4\u547D\u7F51\u7EDC\u9519\u8BEF\uFF0C\u8C03\u7528 startLoad() \u91CD\u542F\u62C9\u6D41", data.details);
2854
3594
  try {
2855
3595
  hlsInstance?.startLoad();
2856
3596
  resumePlayback();
2857
3597
  } catch (err) {
2858
- console.warn("[IVIHlsVideo] startLoad() \u5F02\u5E38\uFF0C\u8D70\u515C\u5E95\u91CD\u8F7D", err);
3598
+ emitHlsLog(onLog, "warn", "startLoad() \u5F02\u5E38\uFF0C\u8D70\u515C\u5E95\u91CD\u8F7D", err);
2859
3599
  scheduleFullReload("startLoad \u5F02\u5E38");
2860
3600
  }
2861
3601
  break;
2862
3602
  case mediaErrorType:
2863
- console.warn(
2864
- "[IVIHlsVideo] HLS \u81F4\u547D\u5A92\u4F53\u9519\u8BEF\uFF0C\u8C03\u7528 recoverMediaError() \u6062\u590D",
2865
- data.details
2866
- );
3603
+ emitHlsLog(onLog, "warn", "HLS \u81F4\u547D\u5A92\u4F53\u9519\u8BEF\uFF0C\u8C03\u7528 recoverMediaError() \u6062\u590D", data.details);
2867
3604
  try {
2868
3605
  hlsInstance?.recoverMediaError();
2869
3606
  resumePlayback();
2870
3607
  } catch (err) {
2871
- console.warn("[IVIHlsVideo] recoverMediaError() \u5F02\u5E38\uFF0C\u8D70\u515C\u5E95\u91CD\u8F7D", err);
3608
+ emitHlsLog(onLog, "warn", "recoverMediaError() \u5F02\u5E38\uFF0C\u8D70\u515C\u5E95\u91CD\u8F7D", err);
2872
3609
  scheduleFullReload("recoverMediaError \u5F02\u5E38");
2873
3610
  }
2874
3611
  break;
2875
3612
  default:
2876
- console.warn(
2877
- "[IVIHlsVideo] HLS \u5176\u4ED6\u81F4\u547D\u9519\u8BEF\uFF0C\u51C6\u5907\u91CD\u65B0\u62C9\u53D6\u6E90",
2878
- data.type,
2879
- data.details
2880
- );
3613
+ emitHlsLog(onLog, "warn", "HLS \u5176\u4ED6\u81F4\u547D\u9519\u8BEF\uFF0C\u51C6\u5907\u91CD\u65B0\u62C9\u53D6\u6E90", data.type, data.details);
2881
3614
  scheduleFullReload("\u5176\u4ED6\u81F4\u547D\u9519\u8BEF");
2882
3615
  break;
2883
3616
  }
@@ -2885,8 +3618,10 @@ function IVIHlsVideo(props) {
2885
3618
  instance.loadSource(url);
2886
3619
  instance.attachMedia(videoRef.current);
2887
3620
  } catch (err) {
2888
- console.warn(
2889
- "[IVIHlsVideo] \u52A8\u6001\u52A0\u8F7D hls.js \u5931\u8D25\uFF0C\u964D\u7EA7\u5230\u539F\u751F HLS\uFF08\u5931\u8D25\u5C06\u7531 <video> error \u4E8B\u4EF6\u6253\u5370\uFF09",
3621
+ emitHlsLog(
3622
+ onLog,
3623
+ "warn",
3624
+ "\u52A8\u6001\u52A0\u8F7D hls.js \u5931\u8D25\uFF0C\u964D\u7EA7\u5230\u539F\u751F HLS\uFF08\u5931\u8D25\u5C06\u7531 <video> error \u4E8B\u4EF6\u4E0A\u62A5\uFF09",
2890
3625
  err
2891
3626
  );
2892
3627
  if (disposed || !videoRef.current) return;
@@ -2902,7 +3637,7 @@ function IVIHlsVideo(props) {
2902
3637
  hlsInstance?.destroy();
2903
3638
  hlsInstance = null;
2904
3639
  };
2905
- }, [url, aggressivePreload]);
3640
+ }, [url, aggressivePreload, onLog]);
2906
3641
  return /* @__PURE__ */ jsxRuntime.jsx(
2907
3642
  "video",
2908
3643
  {
@@ -2918,159 +3653,16 @@ function IVIHlsVideo(props) {
2918
3653
  function isM3u8Url(url) {
2919
3654
  return /\.m3u8(?:$|[?#])/i.test(url);
2920
3655
  }
2921
- function resolveBlurMode(bg) {
2922
- if (bg === "blur") return "live";
2923
- if (typeof bg === "object" && "blur" in bg) return bg.blur;
2924
- return false;
2925
- }
2926
- function TrackSlotBlurLayer(props) {
2927
- const { source, mode, children } = props;
2928
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: CONTAINER_STYLE, children: [
2929
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: BG_LAYER_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx(BlurBackgroundLayer, { source, mode }) }),
2930
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: MAIN_LAYER_STYLE, children })
2931
- ] });
2932
- }
2933
- function BlurBackgroundLayer({
2934
- source,
2935
- mode
2936
- }) {
2937
- const { playback } = source;
2938
- if (source.source.asset_type === "image" && playback.url) {
2939
- return /* @__PURE__ */ jsxRuntime.jsx("img", { src: playback.url, alt: "", style: BLUR_MEDIA_STYLE });
2940
- }
2941
- if (playback.type === "trtc") {
2942
- return /* @__PURE__ */ jsxRuntime.jsx(SlotVideoBlurCanvas, { staticOnly: mode === "static" });
2943
- }
2944
- const url = playback.url;
2945
- if (!url) return null;
2946
- if (mode === "static") {
2947
- return isM3u8Url(url) ? /* @__PURE__ */ jsxRuntime.jsx(HlsStaticBlurFrame, { url }) : /* @__PURE__ */ jsxRuntime.jsx(StaticBlurFrame, { url });
2948
- }
2949
- if (isM3u8Url(url)) {
2950
- return /* @__PURE__ */ jsxRuntime.jsx(
2951
- IVIHlsVideo,
2952
- {
2953
- url,
2954
- videoProps: { muted: true, autoPlay: true, playsInline: true },
2955
- style: BLUR_MEDIA_STYLE,
2956
- paused: false
2957
- }
2958
- );
2959
- }
2960
- return /* @__PURE__ */ jsxRuntime.jsx(
2961
- "video",
2962
- {
2963
- src: url,
2964
- muted: true,
2965
- autoPlay: true,
2966
- playsInline: true,
2967
- style: BLUR_MEDIA_STYLE
2968
- }
2969
- );
2970
- }
2971
- function SlotVideoBlurCanvas({ staticOnly }) {
2972
- const canvasRef = react.useRef(null);
2973
- react.useEffect(() => {
2974
- const canvas = canvasRef.current;
2975
- if (!canvas) return;
2976
- const container = canvas.closest("[data-ivi-source-id]");
2977
- if (!container) return;
2978
- let animId;
2979
- let lastDrawTime = 0;
2980
- let captured = false;
2981
- const intervalMs = 1e3 / 5;
2982
- const draw = (time) => {
2983
- if (staticOnly && captured) return;
2984
- animId = requestAnimationFrame(draw);
2985
- if (time - lastDrawTime < intervalMs) return;
2986
- lastDrawTime = time;
2987
- const video = container.querySelector("video");
2988
- if (!video || video.readyState < 2) return;
2989
- const ctx = canvas.getContext("2d");
2990
- if (!ctx) return;
2991
- if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
2992
- canvas.width = video.videoWidth || 640;
2993
- canvas.height = video.videoHeight || 360;
2994
- }
2995
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2996
- captured = true;
2997
- };
2998
- animId = requestAnimationFrame(draw);
2999
- return () => cancelAnimationFrame(animId);
3000
- }, [staticOnly]);
3001
- return /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, style: BLUR_MEDIA_STYLE });
3002
- }
3003
- function StaticBlurFrame({ url }) {
3004
- const canvasRef = react.useRef(null);
3005
- react.useEffect(() => {
3006
- const canvas = canvasRef.current;
3007
- if (!canvas) return;
3008
- const video = document.createElement("video");
3009
- video.muted = true;
3010
- video.playsInline = true;
3011
- video.preload = "auto";
3012
- video.crossOrigin = "anonymous";
3013
- video.src = url;
3014
- const onReady = () => {
3015
- const ctx = canvas.getContext("2d");
3016
- if (!ctx) return;
3017
- canvas.width = video.videoWidth || 640;
3018
- canvas.height = video.videoHeight || 360;
3019
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
3020
- cleanup();
3021
- };
3022
- const cleanup = () => {
3023
- video.removeEventListener("loadeddata", onReady);
3024
- video.pause();
3025
- video.removeAttribute("src");
3026
- video.load();
3027
- };
3028
- video.addEventListener("loadeddata", onReady);
3029
- video.load();
3030
- return cleanup;
3031
- }, [url]);
3032
- return /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, style: BLUR_MEDIA_STYLE });
3033
- }
3034
- function HlsStaticBlurFrame({ url }) {
3035
- return /* @__PURE__ */ jsxRuntime.jsx(
3036
- IVIHlsVideo,
3037
- {
3038
- url,
3039
- videoProps: { muted: true, autoPlay: true, playsInline: true },
3040
- style: BLUR_MEDIA_STYLE,
3041
- paused: true
3042
- }
3043
- );
3656
+ function emitHlsLog(onLog, level, message, ...extra) {
3657
+ const args = [HLS_LOG_TAG, message, ...extra];
3658
+ onLog?.({
3659
+ level,
3660
+ tag: HLS_LOG_TAG,
3661
+ message: `${HLS_LOG_TAG} ${message}`,
3662
+ args,
3663
+ data: extra.length > 0 ? { message, extra } : { message }
3664
+ });
3044
3665
  }
3045
- var CONTAINER_STYLE = {
3046
- width: "100%",
3047
- height: "100%",
3048
- position: "relative",
3049
- overflow: "hidden"
3050
- };
3051
- var BG_LAYER_STYLE = {
3052
- position: "absolute",
3053
- top: 0,
3054
- left: 0,
3055
- width: "100%",
3056
- height: "100%",
3057
- zIndex: 0,
3058
- overflow: "hidden"
3059
- };
3060
- var MAIN_LAYER_STYLE = {
3061
- position: "relative",
3062
- zIndex: 1,
3063
- width: "100%",
3064
- height: "100%"
3065
- };
3066
- var BLUR_MEDIA_STYLE = {
3067
- width: "100%",
3068
- height: "100%",
3069
- objectFit: "cover",
3070
- filter: "blur(20px)",
3071
- transform: "scale(1.15)",
3072
- display: "block"
3073
- };
3074
3666
  function toReadyRuntimeSource(source) {
3075
3667
  if (!source || source.status !== "ready" || !source.playback) {
3076
3668
  return null;
@@ -3084,9 +3676,10 @@ function supportsSubtitleOverlay(source) {
3084
3676
  }
3085
3677
  function detectMediaVolumeType(source) {
3086
3678
  if (!source) return null;
3087
- if (source.playback.type === "trtc") return "trtc";
3679
+ if (getPlaybackType(source.playback) === "trtc") return "trtc";
3680
+ if (isLivekitSourcePlayback(source.playback)) return "livekit";
3088
3681
  if (source.source.asset_type === "image") return null;
3089
- const url = source.playback.url;
3682
+ const url = getPlaybackUrl(source.playback);
3090
3683
  if (!url) return null;
3091
3684
  return isM3u8Url(url) ? "hls" : "video";
3092
3685
  }
@@ -3099,13 +3692,15 @@ function TrackSlotMediaContent(props) {
3099
3692
  isActive,
3100
3693
  runtime,
3101
3694
  renderTrtc,
3695
+ renderLivekit,
3102
3696
  renderMedia,
3103
3697
  imageProps,
3104
3698
  videoProps,
3105
3699
  trtcPlayerProps,
3700
+ livekitPlayerProps,
3106
3701
  adaptToSourceSize,
3107
3702
  fitStrategy,
3108
- background
3703
+ onLog
3109
3704
  } = props;
3110
3705
  const renderContext = {
3111
3706
  slot: slot ?? "",
@@ -3113,17 +3708,18 @@ function TrackSlotMediaContent(props) {
3113
3708
  source,
3114
3709
  isPreloading: !isActive
3115
3710
  };
3116
- const mediaStyle = buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background);
3711
+ const mediaStyle = buildAdaptiveMediaStyle(adaptToSourceSize, fitStrategy);
3117
3712
  const shouldMute = !isActive;
3118
3713
  if (renderMedia) return renderMedia(renderContext);
3119
- if (source.playback.type === "trtc") {
3120
- if (!source.playback.trtc) return null;
3714
+ if (getPlaybackType(source.playback) === "trtc") {
3715
+ const trtc = getTrtcPlayback(source.playback);
3716
+ if (!trtc) return null;
3121
3717
  if (renderTrtc) return renderTrtc(renderContext);
3122
3718
  const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
3123
3719
  return /* @__PURE__ */ jsxRuntime.jsx(
3124
3720
  IVITrtcPlayer,
3125
3721
  {
3126
- trtc: source.playback.trtc,
3722
+ trtc,
3127
3723
  sourceId: source.source.source_id,
3128
3724
  runtime,
3129
3725
  ...trtcPlayerProps,
@@ -3134,18 +3730,37 @@ function TrackSlotMediaContent(props) {
3134
3730
  }
3135
3731
  );
3136
3732
  }
3733
+ if (isLivekitSourcePlayback(source.playback)) {
3734
+ if (renderLivekit) return renderLivekit(renderContext);
3735
+ const livekitMuted = shouldMute || Boolean(livekitPlayerProps?.muted);
3736
+ return /* @__PURE__ */ jsxRuntime.jsx(
3737
+ IVILivekitPlayer,
3738
+ {
3739
+ livekit: source.playback.livekit,
3740
+ sourceId: source.source.source_id,
3741
+ runtime,
3742
+ ...livekitPlayerProps,
3743
+ muted: livekitMuted,
3744
+ loadingFallback: isActive ? livekitPlayerProps?.loadingFallback : null,
3745
+ errorFallback: isActive ? livekitPlayerProps?.errorFallback : null,
3746
+ style: { ...mediaStyle, ...livekitPlayerProps?.style ?? {} }
3747
+ }
3748
+ );
3749
+ }
3137
3750
  if (source.source.asset_type === "image") {
3751
+ const imageUrl = getPlaybackUrl(source.playback);
3752
+ if (!imageUrl) return null;
3138
3753
  return /* @__PURE__ */ jsxRuntime.jsx(
3139
3754
  "img",
3140
3755
  {
3141
- src: source.playback.url,
3756
+ src: imageUrl,
3142
3757
  alt: "",
3143
3758
  ...imageProps,
3144
3759
  style: { ...mediaStyle, ...imageProps?.style ?? {} }
3145
3760
  }
3146
3761
  );
3147
3762
  }
3148
- const playbackUrl = source.playback.url;
3763
+ const playbackUrl = getPlaybackUrl(source.playback);
3149
3764
  if (!playbackUrl) return null;
3150
3765
  const videoStyle = { ...mediaStyle, ...videoProps?.style ?? {} };
3151
3766
  const mergedVideoProps = {
@@ -3159,15 +3774,32 @@ function TrackSlotMediaContent(props) {
3159
3774
  ) : void 0
3160
3775
  };
3161
3776
  const shouldPause = !isActive;
3162
- return isM3u8Url(playbackUrl) ? /* @__PURE__ */ jsxRuntime.jsx(
3163
- IVIHlsVideo,
3164
- {
3165
- url: playbackUrl,
3166
- videoProps: mergedVideoProps,
3167
- style: videoStyle,
3168
- paused: shouldPause
3169
- }
3170
- ) : /* @__PURE__ */ jsxRuntime.jsx(
3777
+ if (isM3u8Url(playbackUrl)) {
3778
+ return /* @__PURE__ */ jsxRuntime.jsx(
3779
+ IVIHlsVideo,
3780
+ {
3781
+ url: playbackUrl,
3782
+ videoProps: mergedVideoProps,
3783
+ style: videoStyle,
3784
+ paused: shouldPause
3785
+ }
3786
+ );
3787
+ }
3788
+ if (isFlvUrl(playbackUrl)) {
3789
+ return /* @__PURE__ */ jsxRuntime.jsx(
3790
+ IVIFlvVideo,
3791
+ {
3792
+ url: playbackUrl,
3793
+ videoProps: {
3794
+ ...mergedVideoProps,
3795
+ controls: isActive ? videoProps?.controls ?? true : false
3796
+ },
3797
+ style: videoStyle,
3798
+ paused: shouldPause
3799
+ }
3800
+ );
3801
+ }
3802
+ return /* @__PURE__ */ jsxRuntime.jsx(
3171
3803
  SlotVideo,
3172
3804
  {
3173
3805
  src: playbackUrl,
@@ -3182,7 +3814,7 @@ function TrackSlotMediaContent(props) {
3182
3814
  }
3183
3815
  );
3184
3816
  }
3185
- function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
3817
+ function buildAdaptiveMediaStyle(adaptToSourceSize, fitStrategy) {
3186
3818
  const objectFitStyle = fitStrategy === "auto" ? {} : {
3187
3819
  objectFit: fitStrategy ?? "contain"
3188
3820
  };
@@ -3193,20 +3825,23 @@ function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, backgro
3193
3825
  width: "100%",
3194
3826
  height: "100%",
3195
3827
  display: "block",
3196
- ...objectFitStyle,
3197
- backgroundColor: resolveBackgroundColor(background)
3828
+ ...objectFitStyle
3198
3829
  };
3199
3830
  }
3200
- function resolveBackgroundColor(bg) {
3201
- if (!bg || bg === "black") return "#000";
3202
- if (bg === "white") return "#fff";
3203
- if (bg === "transparent") return "transparent";
3204
- if (bg === "blur") return "transparent";
3205
- if (typeof bg === "object") {
3206
- if ("color" in bg) return bg.color;
3207
- if ("blur" in bg) return "transparent";
3831
+ function getPlaybackType(playback) {
3832
+ const type = playback.type;
3833
+ return typeof type === "string" ? type : void 0;
3834
+ }
3835
+ function getPlaybackUrl(playback) {
3836
+ const url = playback.url;
3837
+ return typeof url === "string" ? url : void 0;
3838
+ }
3839
+ function getTrtcPlayback(playback) {
3840
+ const trtc = playback.trtc;
3841
+ if (typeof trtc !== "object" || trtc === null) {
3842
+ return null;
3208
3843
  }
3209
- return "#000";
3844
+ return trtc;
3210
3845
  }
3211
3846
  function createAutoTakeOnEndedHandler(runtime, sourceId, trackId, userOnEnded) {
3212
3847
  return (event) => {
@@ -3268,19 +3903,26 @@ function IVITrackSlot(props) {
3268
3903
  style,
3269
3904
  emptyFallback = null,
3270
3905
  renderTrtc,
3906
+ renderLivekit,
3271
3907
  renderMedia,
3272
3908
  videoProps,
3273
3909
  imageProps,
3274
3910
  adaptToSourceSize = true,
3275
3911
  fitStrategy = "contain",
3276
3912
  trtcPlayerProps,
3913
+ livekitPlayerProps,
3277
3914
  showVolumeControl,
3278
3915
  volumeControlProps,
3279
3916
  showSubtitle,
3280
3917
  subtitleProps,
3281
- background = "black"
3918
+ onLog
3282
3919
  } = props;
3283
3920
  const context = useIviStageView();
3921
+ const fallbackLogCallback = react.useCallback(
3922
+ (entry) => context.runtime?.emitLog(entry),
3923
+ [context.runtime]
3924
+ );
3925
+ const mediaLogCallback = onLog ?? fallbackLogCallback;
3284
3926
  const containerRef = react.useRef(null);
3285
3927
  const resolvedTrackId = trackId ?? (slot ? context.slotTrackMap.get(slot) : void 0);
3286
3928
  const track = resolvedTrackId ? context.state.tracks.get(resolvedTrackId) : void 0;
@@ -3302,7 +3944,6 @@ function IVITrackSlot(props) {
3302
3944
  minHeight: 0,
3303
3945
  ...style ?? {}
3304
3946
  };
3305
- const blurMode = resolveBlurMode(background);
3306
3947
  return /* @__PURE__ */ jsxRuntime.jsxs(
3307
3948
  "div",
3308
3949
  {
@@ -3315,33 +3956,33 @@ function IVITrackSlot(props) {
3315
3956
  !activeSource && emptyFallback,
3316
3957
  preloadEntries.map((entry) => {
3317
3958
  const isActive = entry.isActive;
3318
- const showBlur = blurMode !== false && isActive;
3319
- const content = /* @__PURE__ */ jsxRuntime.jsx(
3320
- TrackSlotMediaContent,
3321
- {
3322
- slot,
3323
- track,
3324
- source: entry.source,
3325
- slotSourceId: entry.sourceId,
3326
- isActive,
3327
- runtime: context.runtime,
3328
- renderTrtc,
3329
- renderMedia,
3330
- imageProps,
3331
- videoProps,
3332
- trtcPlayerProps,
3333
- adaptToSourceSize,
3334
- fitStrategy,
3335
- background
3336
- }
3337
- );
3338
3959
  return /* @__PURE__ */ jsxRuntime.jsx(
3339
3960
  "div",
3340
3961
  {
3341
3962
  style: isActive ? ACTIVE_SLOT_STYLE : STANDBY_SLOT_STYLE,
3342
3963
  "data-ivi-source-id": entry.sourceId,
3343
3964
  "data-ivi-slot-role": isActive ? "active" : "standby",
3344
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: SLOT_CONTENT_STYLE, children: showBlur ? /* @__PURE__ */ jsxRuntime.jsx(TrackSlotBlurLayer, { source: entry.source, mode: blurMode, children: content }) : content })
3965
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: SLOT_CONTENT_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx(
3966
+ TrackSlotMediaContent,
3967
+ {
3968
+ slot,
3969
+ track,
3970
+ source: entry.source,
3971
+ slotSourceId: entry.sourceId,
3972
+ isActive,
3973
+ runtime: context.runtime,
3974
+ renderTrtc,
3975
+ renderLivekit,
3976
+ renderMedia,
3977
+ imageProps,
3978
+ videoProps,
3979
+ trtcPlayerProps,
3980
+ livekitPlayerProps,
3981
+ adaptToSourceSize,
3982
+ fitStrategy,
3983
+ onLog: mediaLogCallback
3984
+ }
3985
+ ) })
3345
3986
  },
3346
3987
  entry.sourceId
3347
3988
  );
@@ -3349,7 +3990,7 @@ function IVITrackSlot(props) {
3349
3990
  showSubtitle && activeSource && supportsSubtitleOverlay(activeSource) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: SUBTITLE_OVERLAY_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx(
3350
3991
  IVISubtitleOverlay,
3351
3992
  {
3352
- conversations: context.state.conversations,
3993
+ runtime: context.runtime,
3353
3994
  ...subtitleProps
3354
3995
  }
3355
3996
  ) }),
@@ -3411,17 +4052,16 @@ function useManagedIviRuntime(config) {
3411
4052
  onRuntimeInitError,
3412
4053
  onLog
3413
4054
  } = config;
3414
- const { url, sessionId } = clientConfig;
3415
4055
  const runtime = react.useMemo(() => {
3416
- if (typeof window === "undefined" || !sessionId) {
4056
+ if (typeof window === "undefined") {
3417
4057
  return null;
3418
4058
  }
3419
- if (!url) {
3420
- throw new Error("useManagedIviRuntime: `url` is required when `sessionId` is provided.");
4059
+ if (!clientConfig.transport) {
4060
+ return null;
3421
4061
  }
3422
4062
  const mergedClientConfig = {
3423
4063
  ...clientConfig,
3424
- url: normalizeUrlToWsUrl(url),
4064
+ transport: clientConfig.transport,
3425
4065
  onLog: (entry) => {
3426
4066
  onLog?.(normalizeClientLogEntry(entry));
3427
4067
  clientConfig.onLog?.(entry);
@@ -3436,7 +4076,7 @@ function useManagedIviRuntime(config) {
3436
4076
  }
3437
4077
  };
3438
4078
  return new IviRuntimeCoordinator(client, mergedRuntimeConfig);
3439
- }, [url, sessionId, clientConfig, runtimeConfig, onLog]);
4079
+ }, [clientConfig, runtimeConfig, onLog]);
3440
4080
  react.useEffect(() => {
3441
4081
  if (!autoStart || !runtime) {
3442
4082
  return;
@@ -3459,19 +4099,6 @@ function useManagedIviRuntime(config) {
3459
4099
  }, [autoStart, runtime, onRuntimeInitError]);
3460
4100
  return runtime;
3461
4101
  }
3462
- function normalizeUrlToWsUrl(url) {
3463
- if (/^wss?:\/\//.test(url)) {
3464
- return url;
3465
- }
3466
- if (/^https?:\/\//.test(url)) {
3467
- const normalized = new URL(url);
3468
- normalized.protocol = normalized.protocol === "https:" ? "wss:" : "ws:";
3469
- return normalized.toString();
3470
- }
3471
- throw new Error(
3472
- `useManagedIviRuntime: invalid url "${url}". Expected a ws://, wss://, http://, or https:// URL.`
3473
- );
3474
- }
3475
4102
  function normalizeClientLogEntry(entry) {
3476
4103
  const tag = getClientLogTag(entry.category);
3477
4104
  return {
@@ -3497,18 +4124,27 @@ function normalizeRuntimeLogEntry(entry) {
3497
4124
  }
3498
4125
  function getClientLogTag(category) {
3499
4126
  if (category === "send") return "[IVI-SEND]";
3500
- if (category === "ws") return "[IVI-WS]";
4127
+ if (category === "transport") return "[IVI-TRANSPORT]";
3501
4128
  if (category === "reconnect") return "[IVI-RECONNECT]";
3502
4129
  return "[IVI-CLIENT]";
3503
4130
  }
3504
4131
 
3505
4132
  exports.EMPTY_RUNTIME_STATE = EMPTY_RUNTIME_STATE;
4133
+ exports.IVILivekitPlayer = IVILivekitPlayer;
3506
4134
  exports.IVIStageView = IVIStageView;
4135
+ exports.IVISubtitleOverlay = IVISubtitleOverlay;
3507
4136
  exports.IVITrackSlot = IVITrackSlot;
4137
+ exports.IVITrtcPlayer = IVITrtcPlayer;
3508
4138
  exports.IviFrontendSdk = IviFrontendSdk;
3509
4139
  exports.IviRuntimeCoordinator = IviRuntimeCoordinator;
3510
4140
  exports.IviRuntimeDispatcher = IviRuntimeDispatcher;
4141
+ exports.LivekitSourceManager = LivekitSourceManager;
4142
+ exports.TrtcSourceManager = TrtcSourceManager;
4143
+ exports.isLivekitSourcePlayback = isLivekitSourcePlayback;
4144
+ exports.isReadyLivekitRuntimeSource = isReadyLivekitRuntimeSource;
4145
+ exports.isSameLivekitConfig = isSameLivekitConfig;
3511
4146
  exports.useIviStageView = useIviStageView;
4147
+ exports.useIviSubtitles = useIviSubtitles;
3512
4148
  exports.useManagedIviRuntime = useManagedIviRuntime;
3513
4149
  exports.useRuntimeState = useRuntimeState;
3514
4150
  //# sourceMappingURL=index.cjs.map