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