@vivix-ai/ivi-frontend-sdk 0.2.3 → 0.2.4
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 +39 -44
- package/dist/index.cjs +117 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -11
- package/dist/index.d.ts +33 -11
- package/dist/index.js +118 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,27 +7,50 @@
|
|
|
7
7
|
|
|
8
8
|
## 安装依赖
|
|
9
9
|
|
|
10
|
-
|
|
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
|
|
39
|
+
npm install @vivix/ivi-frontend-sdk
|
|
15
40
|
|
|
16
41
|
# pnpm
|
|
17
|
-
pnpm add
|
|
42
|
+
pnpm add @vivix/ivi-frontend-sdk
|
|
18
43
|
|
|
19
44
|
# yarn
|
|
20
|
-
yarn add
|
|
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": "
|
|
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
|
|
39
|
-
npm
|
|
61
|
+
# npm
|
|
62
|
+
npm update @vivix/ivi-frontend-sdk
|
|
40
63
|
|
|
41
64
|
# pnpm
|
|
42
|
-
pnpm
|
|
65
|
+
pnpm update @vivix/ivi-frontend-sdk
|
|
43
66
|
|
|
44
|
-
#
|
|
45
|
-
|
|
67
|
+
# yarn
|
|
68
|
+
yarn up @vivix/ivi-frontend-sdk
|
|
46
69
|
```
|
|
47
70
|
|
|
48
|
-
### `dist` 与
|
|
71
|
+
### `dist` 与 npm 发布
|
|
49
72
|
|
|
50
|
-
`package.json` 的 `main` / `module` / `types` / `exports` 均指向 `dist/`,且 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` | `
|
|
202
|
-
| `stage` | `
|
|
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.
|
|
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.
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
383
|
+
item.id,
|
|
382
384
|
event.type,
|
|
383
385
|
before,
|
|
384
|
-
this.conversationManager.getAllMap().get(
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
398
|
+
item.id,
|
|
395
399
|
event.type,
|
|
396
400
|
before,
|
|
397
|
-
this.conversationManager.getAllMap().get(
|
|
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:
|
|
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:
|
|
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]";
|
|
@@ -1529,7 +1541,8 @@ var TrtcSourceManager = class {
|
|
|
1529
1541
|
}
|
|
1530
1542
|
};
|
|
1531
1543
|
function isRuntimeTrtcSource(source) {
|
|
1532
|
-
|
|
1544
|
+
const trtc = source.playback?.trtc;
|
|
1545
|
+
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
1546
|
}
|
|
1534
1547
|
function isSameTrtcConfig(a, b) {
|
|
1535
1548
|
return a.app_id === b.app_id && a.user_id === b.user_id && a.user_sig === b.user_sig && a.room_id === b.room_id;
|
|
@@ -1960,7 +1973,7 @@ var IviRuntimeCoordinator = class {
|
|
|
1960
1973
|
return [];
|
|
1961
1974
|
}
|
|
1962
1975
|
const missing = /* @__PURE__ */ new Set();
|
|
1963
|
-
stage.composition
|
|
1976
|
+
stage.composition?.forEach((item) => {
|
|
1964
1977
|
if (!hasTrack(item.track_id)) {
|
|
1965
1978
|
missing.add(item.track_id);
|
|
1966
1979
|
}
|
|
@@ -1995,7 +2008,11 @@ var IviRuntimeCoordinator = class {
|
|
|
1995
2008
|
}
|
|
1996
2009
|
progressUserTextToResponseFlows(event) {
|
|
1997
2010
|
if (event instanceof iviSdkTs.ReceiveConversationItemAddedEvent) {
|
|
1998
|
-
const
|
|
2011
|
+
const itemId = event.item.id;
|
|
2012
|
+
if (!itemId) {
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
1999
2016
|
if (!flow) {
|
|
2000
2017
|
return;
|
|
2001
2018
|
}
|
|
@@ -2005,7 +2022,11 @@ var IviRuntimeCoordinator = class {
|
|
|
2005
2022
|
return;
|
|
2006
2023
|
}
|
|
2007
2024
|
if (event instanceof iviSdkTs.ReceiveConversationItemDoneEvent) {
|
|
2008
|
-
const
|
|
2025
|
+
const itemId = event.item.id;
|
|
2026
|
+
if (!itemId) {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
2009
2030
|
if (!flow) {
|
|
2010
2031
|
return;
|
|
2011
2032
|
}
|
|
@@ -2146,14 +2167,14 @@ function IVIStageView(props) {
|
|
|
2146
2167
|
}
|
|
2147
2168
|
function buildSlotTrackMapFromState(state) {
|
|
2148
2169
|
const map = /* @__PURE__ */ new Map();
|
|
2149
|
-
state.stage?.composition
|
|
2170
|
+
state.stage?.composition?.forEach((item) => {
|
|
2150
2171
|
map.set(item.slot, item.track_id);
|
|
2151
2172
|
});
|
|
2152
2173
|
return map;
|
|
2153
2174
|
}
|
|
2154
2175
|
function buildSlotBindingsFromState(state) {
|
|
2155
2176
|
const bindings = [];
|
|
2156
|
-
state.stage?.composition
|
|
2177
|
+
state.stage?.composition?.forEach((item) => {
|
|
2157
2178
|
const track = state.tracks.get(item.track_id);
|
|
2158
2179
|
const sourceId = track?.active_source_id ?? null;
|
|
2159
2180
|
const source = sourceId ? state.sources.get(sourceId) : void 0;
|
|
@@ -2443,6 +2464,7 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
|
|
|
2443
2464
|
return () => observer.disconnect();
|
|
2444
2465
|
}, [containerRef, volume, enabled, activeSourceId]);
|
|
2445
2466
|
}
|
|
2467
|
+
var ALL_CONVERSATION_ROLES = ["user", "director", "model", "vlm"];
|
|
2446
2468
|
function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
2447
2469
|
const [visibleIds, setVisibleIds] = react.useState([]);
|
|
2448
2470
|
const timersRef = react.useRef(/* @__PURE__ */ new Map());
|
|
@@ -2518,26 +2540,82 @@ function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
|
2518
2540
|
}).filter((entry) => entry !== null);
|
|
2519
2541
|
}, [visibleIds, conversationMap]);
|
|
2520
2542
|
}
|
|
2543
|
+
function useSourceWindowSubtitleEntries(conversations, sourceKey) {
|
|
2544
|
+
const normalizedSourceKey = sourceKey ?? null;
|
|
2545
|
+
const [windowState, setWindowState] = react.useState(() => ({
|
|
2546
|
+
sourceKey: normalizedSourceKey,
|
|
2547
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2548
|
+
visibleIds: []
|
|
2549
|
+
}));
|
|
2550
|
+
react.useEffect(() => {
|
|
2551
|
+
setWindowState((prev) => {
|
|
2552
|
+
if (prev.sourceKey !== normalizedSourceKey) {
|
|
2553
|
+
return {
|
|
2554
|
+
sourceKey: normalizedSourceKey,
|
|
2555
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2556
|
+
visibleIds: []
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
if (!normalizedSourceKey) {
|
|
2560
|
+
return prev.visibleIds.length === 0 ? prev : { ...prev, visibleIds: [] };
|
|
2561
|
+
}
|
|
2562
|
+
const nextVisibleIds = conversations.filter((item) => {
|
|
2563
|
+
const displayText = item.text || item.transcript;
|
|
2564
|
+
return Boolean(displayText) && !prev.baselineIds.has(item.id);
|
|
2565
|
+
}).map((item) => item.id);
|
|
2566
|
+
if (nextVisibleIds.length === prev.visibleIds.length && nextVisibleIds.every((id, index) => id === prev.visibleIds[index])) {
|
|
2567
|
+
return prev;
|
|
2568
|
+
}
|
|
2569
|
+
return {
|
|
2570
|
+
...prev,
|
|
2571
|
+
visibleIds: nextVisibleIds
|
|
2572
|
+
};
|
|
2573
|
+
});
|
|
2574
|
+
}, [conversations, normalizedSourceKey]);
|
|
2575
|
+
const conversationMap = react.useMemo(() => {
|
|
2576
|
+
const map = /* @__PURE__ */ new Map();
|
|
2577
|
+
for (const item of conversations) {
|
|
2578
|
+
map.set(item.id, item);
|
|
2579
|
+
}
|
|
2580
|
+
return map;
|
|
2581
|
+
}, [conversations]);
|
|
2582
|
+
return react.useMemo(() => {
|
|
2583
|
+
if (windowState.sourceKey !== normalizedSourceKey) {
|
|
2584
|
+
return [];
|
|
2585
|
+
}
|
|
2586
|
+
return windowState.visibleIds.map((id) => {
|
|
2587
|
+
const item = conversationMap.get(id);
|
|
2588
|
+
if (!item) return null;
|
|
2589
|
+
const text = item.text || item.transcript;
|
|
2590
|
+
if (!text) return null;
|
|
2591
|
+
return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
|
|
2592
|
+
}).filter((entry) => entry !== null);
|
|
2593
|
+
}, [windowState.sourceKey, windowState.visibleIds, conversationMap, normalizedSourceKey]);
|
|
2594
|
+
}
|
|
2521
2595
|
var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
|
|
2522
2596
|
function IVISubtitleOverlay(props) {
|
|
2523
2597
|
const {
|
|
2524
2598
|
conversations,
|
|
2525
|
-
roles
|
|
2599
|
+
roles,
|
|
2526
2600
|
maxVisible = 2,
|
|
2527
2601
|
dismissAfterMs = 5e3,
|
|
2602
|
+
stickySourceKey,
|
|
2528
2603
|
subtitleStyle,
|
|
2529
2604
|
className,
|
|
2530
2605
|
style
|
|
2531
2606
|
} = props;
|
|
2607
|
+
const resolvedRoles = roles ?? (stickySourceKey ? ALL_CONVERSATION_ROLES : "user");
|
|
2532
2608
|
const roleSet = react.useMemo(
|
|
2533
|
-
() => new Set(Array.isArray(
|
|
2534
|
-
[
|
|
2609
|
+
() => new Set(Array.isArray(resolvedRoles) ? resolvedRoles : [resolvedRoles]),
|
|
2610
|
+
[resolvedRoles]
|
|
2535
2611
|
);
|
|
2536
2612
|
const filtered = react.useMemo(
|
|
2537
2613
|
() => conversations.filter((c) => roleSet.has(c.role)),
|
|
2538
2614
|
[conversations, roleSet]
|
|
2539
2615
|
);
|
|
2540
|
-
const
|
|
2616
|
+
const queueEntries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
|
|
2617
|
+
const sourceWindowEntries = useSourceWindowSubtitleEntries(filtered, stickySourceKey);
|
|
2618
|
+
const entries = stickySourceKey ? sourceWindowEntries : queueEntries;
|
|
2541
2619
|
if (entries.length === 0) return null;
|
|
2542
2620
|
const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
2543
2621
|
const fontSize = subtitleStyle?.fontSize ?? 14;
|
|
@@ -3082,6 +3160,14 @@ function supportsSubtitleOverlay(source) {
|
|
|
3082
3160
|
if (source.source.asset_type === "image") return false;
|
|
3083
3161
|
return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
|
|
3084
3162
|
}
|
|
3163
|
+
function supportsGeneratedClipStickySubtitles(source) {
|
|
3164
|
+
if (!source) return false;
|
|
3165
|
+
if (source.source.kind !== "generated_clip") return false;
|
|
3166
|
+
if (source.source.asset_type === "image") return false;
|
|
3167
|
+
if (source.playback.type === "trtc") return false;
|
|
3168
|
+
const playbackUrl = source.playback.url;
|
|
3169
|
+
return typeof playbackUrl === "string" && isM3u8Url(playbackUrl);
|
|
3170
|
+
}
|
|
3085
3171
|
function detectMediaVolumeType(source) {
|
|
3086
3172
|
if (!source) return null;
|
|
3087
3173
|
if (source.playback.type === "trtc") return "trtc";
|
|
@@ -3117,7 +3203,7 @@ function TrackSlotMediaContent(props) {
|
|
|
3117
3203
|
const shouldMute = !isActive;
|
|
3118
3204
|
if (renderMedia) return renderMedia(renderContext);
|
|
3119
3205
|
if (source.playback.type === "trtc") {
|
|
3120
|
-
if (!source.playback.trtc) return null;
|
|
3206
|
+
if (!isRuntimeTrtcPlayback(source.playback.trtc)) return null;
|
|
3121
3207
|
if (renderTrtc) return renderTrtc(renderContext);
|
|
3122
3208
|
const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
|
|
3123
3209
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3182,6 +3268,9 @@ function TrackSlotMediaContent(props) {
|
|
|
3182
3268
|
}
|
|
3183
3269
|
);
|
|
3184
3270
|
}
|
|
3271
|
+
function isRuntimeTrtcPlayback(trtc) {
|
|
3272
|
+
return typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
|
|
3273
|
+
}
|
|
3185
3274
|
function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
|
|
3186
3275
|
const objectFitStyle = fitStrategy === "auto" ? {} : {
|
|
3187
3276
|
objectFit: fitStrategy ?? "contain"
|
|
@@ -3278,6 +3367,7 @@ function IVITrackSlot(props) {
|
|
|
3278
3367
|
volumeControlProps,
|
|
3279
3368
|
showSubtitle,
|
|
3280
3369
|
subtitleProps,
|
|
3370
|
+
keepGeneratedClipSubtitlesUntilEnded = false,
|
|
3281
3371
|
background = "black"
|
|
3282
3372
|
} = props;
|
|
3283
3373
|
const context = useIviStageView();
|
|
@@ -3288,6 +3378,7 @@ function IVITrackSlot(props) {
|
|
|
3288
3378
|
const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
|
|
3289
3379
|
const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
|
|
3290
3380
|
const activeSource = activeEntry?.source ?? null;
|
|
3381
|
+
const stickySubtitleSourceKey = keepGeneratedClipSubtitlesUntilEnded && supportsGeneratedClipStickySubtitles(activeSource) ? activeSourceId : null;
|
|
3291
3382
|
const mediaType = detectMediaVolumeType(activeSource);
|
|
3292
3383
|
const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
|
|
3293
3384
|
useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
|
|
@@ -3350,7 +3441,8 @@ function IVITrackSlot(props) {
|
|
|
3350
3441
|
IVISubtitleOverlay,
|
|
3351
3442
|
{
|
|
3352
3443
|
conversations: context.state.conversations,
|
|
3353
|
-
...subtitleProps
|
|
3444
|
+
...subtitleProps,
|
|
3445
|
+
stickySourceKey: stickySubtitleSourceKey
|
|
3354
3446
|
}
|
|
3355
3447
|
) }),
|
|
3356
3448
|
showVolumeControl && mediaType !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx(
|