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

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/README.md CHANGED
@@ -7,27 +7,50 @@
7
7
 
8
8
  ## 安装依赖
9
9
 
10
- 当前包尚未发布到 npm,通过 **GitLab 仓库地址**直接安装:
10
+ 当前包发布在公司 Nexus npm 镜像中。使用前请在业务项目根目录添加 `.npmrc`:
11
+
12
+ ```ini
13
+ @vivix:registry=https://nexus.vivi-x.ai/repository/ivi-sdk-npm/
14
+ ```
15
+
16
+ Nexus 仓库需要认证,请先配置只读账号:
17
+
18
+ Username:ivi-sdk-readonly
19
+
20
+ Password:ivi-sdk@2026
21
+
22
+ ```bash
23
+ npm login --registry=https://nexus.vivi-x.ai/repository/ivi-sdk-npm/
24
+ # 输入用户名和密码
25
+ ```
26
+
27
+ 验证配置是否生效:
28
+
29
+ ```bash
30
+ npm whoami --registry=https://nexus.vivi-x.ai/repository/ivi-sdk-npm/
31
+ ```
32
+
33
+ 显示 ivi-sdk-readonly 为成功
34
+
35
+ 然后安装 SDK:
11
36
 
12
37
  ```bash
13
38
  # npm
14
- npm install git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#main
39
+ npm install @vivix/ivi-frontend-sdk
15
40
 
16
41
  # pnpm
17
- pnpm add git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#main
42
+ pnpm add @vivix/ivi-frontend-sdk
18
43
 
19
44
  # yarn
20
- yarn add git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#main
45
+ yarn add @vivix/ivi-frontend-sdk
21
46
  ```
22
47
 
23
- > **推荐使用 `#main` 分支。** `dev` 分支可能引入尚未稳定的新特性,不保证向后兼容;`main` 分支仅包含经过验证的变更。如需锁定到更精确的版本,可替换为具体 tag 或 commit hash。
24
-
25
48
  安装后 `package.json` 中会出现类似条目:
26
49
 
27
50
  ```json
28
51
  {
29
52
  "dependencies": {
30
- "@vivix/ivi-frontend-sdk": "git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#main"
53
+ "@vivix/ivi-frontend-sdk": "^0.2.2"
31
54
  }
32
55
  }
33
56
  ```
@@ -35,19 +58,19 @@ yarn add git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#mai
35
58
  **更新到最新版本**:
36
59
 
37
60
  ```bash
38
- # npm(Git 依赖建议显式指定分支、tag 或 commit)
39
- npm install git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#main
61
+ # npm
62
+ npm update @vivix/ivi-frontend-sdk
40
63
 
41
64
  # pnpm
42
- pnpm add git+https://gitlab.vivix.work/vinf-2.0/ivi-sdk/ivi-frontend-sdk.git#main
65
+ pnpm update @vivix/ivi-frontend-sdk
43
66
 
44
- # 若 lockfile 固定了旧 commit,可重新安装或删除 lock 中该依赖条目后 install
45
- npm install
67
+ # yarn
68
+ yarn up @vivix/ivi-frontend-sdk
46
69
  ```
47
70
 
48
- ### `dist` 与 Git 直装
71
+ ### `dist` 与 npm 发布
49
72
 
50
- `package.json` 的 `main` / `module` / `types` / `exports` 均指向 `dist/`,且 npm 包通过 `"files": ["dist"]` 发布。从 **Git 引用安装**(如 `git+https://…#dev`)时,npm 只会打包 `files` 所列内容;因此 **`dist/` 已纳入版本库**,克隆默认分支即可得到可运行、可解析类型的产物。
73
+ `package.json` 的 `main` / `module` / `types` / `exports` 均指向 `dist/`,且 npm 包通过 `"files": ["dist"]` 发布。因此发布前必须先完成构建,确保 Nexus 中的包包含可运行、可解析类型的产物。
51
74
 
52
75
  **协作约定**:修改 `src/` 后必须在本地执行 `npm run build`(或 `pnpm run build`),并将**与本次源码变更对应的一次完整 `dist/` 输出**一并提交:可与功能改动放在**同一提交**,或在必要时单独提交,提交信息示例:`chore: 重新构建 dist`。禁止只改源码而不更新 `dist/`,避免仓库内源码与产物不一致。
53
76
 
@@ -64,34 +87,6 @@ React 为可选 peer dependency,仅使用 React 组件时需要:
64
87
 
65
88
  如果只使用 runtime 能力、不接入 React 组件与 hooks,可忽略本文中的 React 用法部分。
66
89
 
67
- ## 包导出与导入方式
68
-
69
- 当前仅支持单一主入口:
70
-
71
- | 入口 | 路径 | 说明 |
72
- |------|------|------|
73
- | 主入口 | `@vivix/ivi-frontend-sdk` | 导出 runtime 能力、`IviFrontendSdk`、以及当前公开的 React API:`IVIStageView`、`IVITrackSlot`、`useManagedIviRuntime`、`useIviStageView` |
74
-
75
- **导入示例**:
76
-
77
- ```ts
78
- // 底层 client 由 @vivix/ivi-sdk-ts 提供
79
- import { IviClient } from "@vivix/ivi-sdk-ts";
80
-
81
- // 本包主入口
82
- import {
83
- IviRuntimeCoordinator,
84
- IviRuntimeDispatcher,
85
- IviFrontendSdk,
86
- IVIStageView,
87
- IVITrackSlot,
88
- useManagedIviRuntime,
89
- useIviStageView
90
- } from "@vivix/ivi-frontend-sdk";
91
- ```
92
-
93
- > 当前包没有 `@vivix/ivi-frontend-sdk/core` / `@vivix/ivi-frontend-sdk/react` 子入口;底层 WebSocket client、事件解析与协议类型请直接从 `@vivix/ivi-sdk-ts` 导入。
94
-
95
90
  ---
96
91
 
97
92
  ## 快速开始
@@ -198,8 +193,8 @@ runtime.sendSessionSourcePlaybackCompleted("source_001", "track_001");
198
193
  | 字段 | 类型 | 说明 |
199
194
  |------|------|------|
200
195
  | `status` | `"idle" \| "connecting" \| "syncing" \| "running" \| "stopped"` | 生命周期状态 |
201
- | `session` | `IviRealtimeSession \| null` | 当前会话 |
202
- | `stage` | `IviStage \| null` | 当前 stage(slot → track 映射) |
196
+ | `session` | `IviRealtimeSessionConfig \| null` | 当前会话 |
197
+ | `stage` | `IviStageComposition \| null` | 当前 stage(slot → track 映射) |
203
198
  | `tracks` | `Map<string, IviTrack>` | 所有 track |
204
199
  | `sources` | `Map<string, IviRuntimeSource>` | 所有 source(含 `status: created \| ready \| failed`) |
205
200
  | `conversationItems` | `Map<string, IviRuntimeConversationItem>` | 对话条目映射 |
package/dist/index.cjs CHANGED
@@ -110,7 +110,7 @@ var SessionEventHandler = class {
110
110
  this.callbacks = callbacks;
111
111
  }
112
112
  handle(event) {
113
- if (event instanceof iviSdkTs.SessionCreatedEvent) {
113
+ if (event instanceof iviSdkTs.ReceiveSessionCreatedEvent) {
114
114
  const before = this.sessionManager.getSession();
115
115
  this.sessionManager.setSession(event.session);
116
116
  logIviStateChange("session", null, event.type, before, this.sessionManager.getSession());
@@ -259,7 +259,7 @@ var SourceEventHandler = class {
259
259
  const before = this.sourceManager.get(sourceId);
260
260
  this.sourceManager.upsertCreated(source);
261
261
  this.sourceManager.applyPreload(sourceId, {
262
- autoclearAfterPlay: event.autoclearAfterPlay
262
+ autoclearAfterPlay: event.autoclearAfterPlay ?? true
263
263
  });
264
264
  logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
265
265
  }
@@ -301,7 +301,7 @@ var StreamEventHandler = class {
301
301
  this.callbacks = callbacks;
302
302
  }
303
303
  handle(event) {
304
- if (event instanceof iviSdkTs.SessionStreamCreatedEvent) {
304
+ if (event instanceof iviSdkTs.ReceiveSessionStreamCreatedEvent) {
305
305
  const streamId = event.stream.stream_id;
306
306
  const before = this.streamManager.getAll().get(streamId);
307
307
  this.streamManager.upsertCreated(event.stream);
@@ -374,32 +374,37 @@ var ConversationEventHandler = class {
374
374
  return { handled: true };
375
375
  }
376
376
  if (event instanceof iviSdkTs.ReceiveConversationItemAddedEvent) {
377
- const before = this.conversationManager.getAllMap().get(event.item.id);
378
- this.conversationManager.upsertAdded(event.item);
377
+ if (!event.item.id) return { handled: true };
378
+ const item = { ...event.item, id: event.item.id };
379
+ const before = this.conversationManager.getAllMap().get(item.id);
380
+ this.conversationManager.upsertAdded(item);
379
381
  logIviStateChange(
380
382
  "conversationItem",
381
- event.item.id,
383
+ item.id,
382
384
  event.type,
383
385
  before,
384
- this.conversationManager.getAllMap().get(event.item.id)
386
+ this.conversationManager.getAllMap().get(item.id)
385
387
  );
386
388
  this.callbacks.onConversationsChanged();
387
389
  return { handled: true };
388
390
  }
389
391
  if (event instanceof iviSdkTs.ReceiveConversationItemDoneEvent) {
390
- const before = this.conversationManager.getAllMap().get(event.item.id);
391
- this.conversationManager.markDone(event.item);
392
+ if (!event.item.id) return { handled: true };
393
+ const item = { ...event.item, id: event.item.id };
394
+ const before = this.conversationManager.getAllMap().get(item.id);
395
+ this.conversationManager.markDone(item);
392
396
  logIviStateChange(
393
397
  "conversationItem",
394
- event.item.id,
398
+ item.id,
395
399
  event.type,
396
400
  before,
397
- this.conversationManager.getAllMap().get(event.item.id)
401
+ this.conversationManager.getAllMap().get(item.id)
398
402
  );
399
403
  this.callbacks.onConversationsChanged();
400
404
  return { handled: true };
401
405
  }
402
406
  if (event instanceof iviSdkTs.ReceiveResponseOutputTextDeltaEvent) {
407
+ if (!event.itemId) return { handled: true };
403
408
  const before = this.conversationManager.getAllMap().get(event.itemId);
404
409
  this.conversationManager.applyTextDelta(event.itemId, event.delta);
405
410
  logIviStateChange(
@@ -413,6 +418,7 @@ var ConversationEventHandler = class {
413
418
  return { handled: true };
414
419
  }
415
420
  if (event instanceof iviSdkTs.ReceiveResponseOutputTextDoneEvent) {
421
+ if (!event.itemId) return { handled: true };
416
422
  const before = this.conversationManager.getAllMap().get(event.itemId);
417
423
  this.conversationManager.applyTextDone(event.itemId, event.text);
418
424
  logIviStateChange(
@@ -426,6 +432,7 @@ var ConversationEventHandler = class {
426
432
  return { handled: true };
427
433
  }
428
434
  if (event instanceof iviSdkTs.ReceiveResponseOutputAudioTranscriptDeltaEvent) {
435
+ if (!event.itemId) return { handled: true };
429
436
  const before = this.conversationManager.getAllMap().get(event.itemId);
430
437
  this.conversationManager.applyTranscriptDelta(event.itemId, event.delta);
431
438
  logIviStateChange(
@@ -439,6 +446,7 @@ var ConversationEventHandler = class {
439
446
  return { handled: true };
440
447
  }
441
448
  if (event instanceof iviSdkTs.ReceiveResponseOutputAudioTranscriptDoneEvent) {
449
+ if (!event.itemId) return { handled: true };
442
450
  const before = this.conversationManager.getAllMap().get(event.itemId);
443
451
  this.conversationManager.applyTranscriptDone(event.itemId, event.transcript);
444
452
  logIviStateChange(
@@ -550,13 +558,13 @@ var TrackManager = class {
550
558
  }
551
559
  this.patchTrack(trackId, {
552
560
  active_source_id: resolvedSourceId,
553
- next_source_id: null
561
+ next_source_id: void 0
554
562
  });
555
563
  }
556
564
  applyTrackCued(trackId, sourceId) {
557
565
  this.patchTrack(trackId, {
558
566
  active_source_id: sourceId,
559
- next_source_id: null
567
+ next_source_id: void 0
560
568
  });
561
569
  }
562
570
  applyTrackNextSet(trackId, sourceId) {
@@ -853,6 +861,7 @@ var ConversationManager = class {
853
861
  const nextItems = /* @__PURE__ */ new Map();
854
862
  const nextOrder = [];
855
863
  items.forEach((item) => {
864
+ if (!hasItemId(item)) return;
856
865
  const runtimeItem = this.buildRuntimeItem(item, item.status === "in_progress" ? "added" : "done");
857
866
  nextItems.set(runtimeItem.id, runtimeItem);
858
867
  nextOrder.push(runtimeItem.id);
@@ -1085,6 +1094,9 @@ var ConversationManager = class {
1085
1094
  ];
1086
1095
  }
1087
1096
  };
1097
+ function hasItemId(item) {
1098
+ return typeof item.id === "string" && item.id.length > 0;
1099
+ }
1088
1100
 
1089
1101
  // src/runtime/managers/trtc-source-manager.ts
1090
1102
  var TAG = "[IVI-TRTC]";
@@ -1360,8 +1372,7 @@ var TrtcSourceManager = class {
1360
1372
  sdkAppId,
1361
1373
  userId: session.trtc.user_id,
1362
1374
  userSig: session.trtc.user_sig,
1363
- scene: TRTC.TYPE.SCENE_LIVE,
1364
- role: TRTC.TYPE.ROLE_AUDIENCE,
1375
+ scene: TRTC.TYPE.SCENE_RTC,
1365
1376
  autoReceiveAudio: true,
1366
1377
  ...isStringRoomId ? { strRoomId: session.trtc.room_id } : { roomId: Number(session.trtc.room_id) }
1367
1378
  });
@@ -1529,7 +1540,8 @@ var TrtcSourceManager = class {
1529
1540
  }
1530
1541
  };
1531
1542
  function isRuntimeTrtcSource(source) {
1532
- return source.status === "ready" && source.playback?.type === "trtc" && Boolean(source.playback.trtc);
1543
+ const trtc = source.playback?.trtc;
1544
+ return source.status === "ready" && source.playback?.type === "trtc" && typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
1533
1545
  }
1534
1546
  function isSameTrtcConfig(a, b) {
1535
1547
  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;
@@ -1960,7 +1972,7 @@ var IviRuntimeCoordinator = class {
1960
1972
  return [];
1961
1973
  }
1962
1974
  const missing = /* @__PURE__ */ new Set();
1963
- stage.composition.forEach((item) => {
1975
+ stage.composition?.forEach((item) => {
1964
1976
  if (!hasTrack(item.track_id)) {
1965
1977
  missing.add(item.track_id);
1966
1978
  }
@@ -1995,7 +2007,11 @@ var IviRuntimeCoordinator = class {
1995
2007
  }
1996
2008
  progressUserTextToResponseFlows(event) {
1997
2009
  if (event instanceof iviSdkTs.ReceiveConversationItemAddedEvent) {
1998
- const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
2010
+ const itemId = event.item.id;
2011
+ if (!itemId) {
2012
+ return;
2013
+ }
2014
+ const flow = this.pendingUserTextToResponseFlows.get(itemId);
1999
2015
  if (!flow) {
2000
2016
  return;
2001
2017
  }
@@ -2005,7 +2021,11 @@ var IviRuntimeCoordinator = class {
2005
2021
  return;
2006
2022
  }
2007
2023
  if (event instanceof iviSdkTs.ReceiveConversationItemDoneEvent) {
2008
- const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
2024
+ const itemId = event.item.id;
2025
+ if (!itemId) {
2026
+ return;
2027
+ }
2028
+ const flow = this.pendingUserTextToResponseFlows.get(itemId);
2009
2029
  if (!flow) {
2010
2030
  return;
2011
2031
  }
@@ -2146,14 +2166,14 @@ function IVIStageView(props) {
2146
2166
  }
2147
2167
  function buildSlotTrackMapFromState(state) {
2148
2168
  const map = /* @__PURE__ */ new Map();
2149
- state.stage?.composition.forEach((item) => {
2169
+ state.stage?.composition?.forEach((item) => {
2150
2170
  map.set(item.slot, item.track_id);
2151
2171
  });
2152
2172
  return map;
2153
2173
  }
2154
2174
  function buildSlotBindingsFromState(state) {
2155
2175
  const bindings = [];
2156
- state.stage?.composition.forEach((item) => {
2176
+ state.stage?.composition?.forEach((item) => {
2157
2177
  const track = state.tracks.get(item.track_id);
2158
2178
  const sourceId = track?.active_source_id ?? null;
2159
2179
  const source = sourceId ? state.sources.get(sourceId) : void 0;
@@ -2443,6 +2463,7 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
2443
2463
  return () => observer.disconnect();
2444
2464
  }, [containerRef, volume, enabled, activeSourceId]);
2445
2465
  }
2466
+ var ALL_CONVERSATION_ROLES = ["user", "director", "model", "vlm"];
2446
2467
  function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
2447
2468
  const [visibleIds, setVisibleIds] = react.useState([]);
2448
2469
  const timersRef = react.useRef(/* @__PURE__ */ new Map());
@@ -2518,26 +2539,82 @@ function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
2518
2539
  }).filter((entry) => entry !== null);
2519
2540
  }, [visibleIds, conversationMap]);
2520
2541
  }
2542
+ function useSourceWindowSubtitleEntries(conversations, sourceKey) {
2543
+ const normalizedSourceKey = sourceKey ?? null;
2544
+ const [windowState, setWindowState] = react.useState(() => ({
2545
+ sourceKey: normalizedSourceKey,
2546
+ baselineIds: new Set(conversations.map((item) => item.id)),
2547
+ visibleIds: []
2548
+ }));
2549
+ react.useEffect(() => {
2550
+ setWindowState((prev) => {
2551
+ if (prev.sourceKey !== normalizedSourceKey) {
2552
+ return {
2553
+ sourceKey: normalizedSourceKey,
2554
+ baselineIds: new Set(conversations.map((item) => item.id)),
2555
+ visibleIds: []
2556
+ };
2557
+ }
2558
+ if (!normalizedSourceKey) {
2559
+ return prev.visibleIds.length === 0 ? prev : { ...prev, visibleIds: [] };
2560
+ }
2561
+ const nextVisibleIds = conversations.filter((item) => {
2562
+ const displayText = item.text || item.transcript;
2563
+ return Boolean(displayText) && !prev.baselineIds.has(item.id);
2564
+ }).map((item) => item.id);
2565
+ if (nextVisibleIds.length === prev.visibleIds.length && nextVisibleIds.every((id, index) => id === prev.visibleIds[index])) {
2566
+ return prev;
2567
+ }
2568
+ return {
2569
+ ...prev,
2570
+ visibleIds: nextVisibleIds
2571
+ };
2572
+ });
2573
+ }, [conversations, normalizedSourceKey]);
2574
+ const conversationMap = react.useMemo(() => {
2575
+ const map = /* @__PURE__ */ new Map();
2576
+ for (const item of conversations) {
2577
+ map.set(item.id, item);
2578
+ }
2579
+ return map;
2580
+ }, [conversations]);
2581
+ return react.useMemo(() => {
2582
+ if (windowState.sourceKey !== normalizedSourceKey) {
2583
+ return [];
2584
+ }
2585
+ return windowState.visibleIds.map((id) => {
2586
+ const item = conversationMap.get(id);
2587
+ if (!item) return null;
2588
+ const text = item.text || item.transcript;
2589
+ if (!text) return null;
2590
+ return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
2591
+ }).filter((entry) => entry !== null);
2592
+ }, [windowState.sourceKey, windowState.visibleIds, conversationMap, normalizedSourceKey]);
2593
+ }
2521
2594
  var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
2522
2595
  function IVISubtitleOverlay(props) {
2523
2596
  const {
2524
2597
  conversations,
2525
- roles = "user",
2598
+ roles,
2526
2599
  maxVisible = 2,
2527
2600
  dismissAfterMs = 5e3,
2601
+ stickySourceKey,
2528
2602
  subtitleStyle,
2529
2603
  className,
2530
2604
  style
2531
2605
  } = props;
2606
+ const resolvedRoles = roles ?? (stickySourceKey ? ALL_CONVERSATION_ROLES : "user");
2532
2607
  const roleSet = react.useMemo(
2533
- () => new Set(Array.isArray(roles) ? roles : [roles]),
2534
- [roles]
2608
+ () => new Set(Array.isArray(resolvedRoles) ? resolvedRoles : [resolvedRoles]),
2609
+ [resolvedRoles]
2535
2610
  );
2536
2611
  const filtered = react.useMemo(
2537
2612
  () => conversations.filter((c) => roleSet.has(c.role)),
2538
2613
  [conversations, roleSet]
2539
2614
  );
2540
- const entries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
2615
+ const queueEntries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
2616
+ const sourceWindowEntries = useSourceWindowSubtitleEntries(filtered, stickySourceKey);
2617
+ const entries = stickySourceKey ? sourceWindowEntries : queueEntries;
2541
2618
  if (entries.length === 0) return null;
2542
2619
  const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
2543
2620
  const fontSize = subtitleStyle?.fontSize ?? 14;
@@ -3082,6 +3159,14 @@ function supportsSubtitleOverlay(source) {
3082
3159
  if (source.source.asset_type === "image") return false;
3083
3160
  return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
3084
3161
  }
3162
+ function supportsGeneratedClipStickySubtitles(source) {
3163
+ if (!source) return false;
3164
+ if (source.source.kind !== "generated_clip") return false;
3165
+ if (source.source.asset_type === "image") return false;
3166
+ if (source.playback.type === "trtc") return false;
3167
+ const playbackUrl = source.playback.url;
3168
+ return typeof playbackUrl === "string" && isM3u8Url(playbackUrl);
3169
+ }
3085
3170
  function detectMediaVolumeType(source) {
3086
3171
  if (!source) return null;
3087
3172
  if (source.playback.type === "trtc") return "trtc";
@@ -3117,7 +3202,7 @@ function TrackSlotMediaContent(props) {
3117
3202
  const shouldMute = !isActive;
3118
3203
  if (renderMedia) return renderMedia(renderContext);
3119
3204
  if (source.playback.type === "trtc") {
3120
- if (!source.playback.trtc) return null;
3205
+ if (!isRuntimeTrtcPlayback(source.playback.trtc)) return null;
3121
3206
  if (renderTrtc) return renderTrtc(renderContext);
3122
3207
  const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
3123
3208
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -3182,6 +3267,9 @@ function TrackSlotMediaContent(props) {
3182
3267
  }
3183
3268
  );
3184
3269
  }
3270
+ function isRuntimeTrtcPlayback(trtc) {
3271
+ return typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
3272
+ }
3185
3273
  function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
3186
3274
  const objectFitStyle = fitStrategy === "auto" ? {} : {
3187
3275
  objectFit: fitStrategy ?? "contain"
@@ -3278,6 +3366,7 @@ function IVITrackSlot(props) {
3278
3366
  volumeControlProps,
3279
3367
  showSubtitle,
3280
3368
  subtitleProps,
3369
+ keepGeneratedClipSubtitlesUntilEnded = false,
3281
3370
  background = "black"
3282
3371
  } = props;
3283
3372
  const context = useIviStageView();
@@ -3288,6 +3377,7 @@ function IVITrackSlot(props) {
3288
3377
  const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
3289
3378
  const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
3290
3379
  const activeSource = activeEntry?.source ?? null;
3380
+ const stickySubtitleSourceKey = keepGeneratedClipSubtitlesUntilEnded && supportsGeneratedClipStickySubtitles(activeSource) ? activeSourceId : null;
3291
3381
  const mediaType = detectMediaVolumeType(activeSource);
3292
3382
  const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
3293
3383
  useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
@@ -3350,7 +3440,8 @@ function IVITrackSlot(props) {
3350
3440
  IVISubtitleOverlay,
3351
3441
  {
3352
3442
  conversations: context.state.conversations,
3353
- ...subtitleProps
3443
+ ...subtitleProps,
3444
+ stickySourceKey: stickySubtitleSourceKey
3354
3445
  }
3355
3446
  ) }),
3356
3447
  showVolumeControl && mediaType !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx(