@vivix-ai/ivi-frontend-sdk 0.2.2 → 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/dist/index.d.cts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IviRealtimeSessionConfig, IviStageComposition, IviTrack, IviSourceAsset, IviReceiveSessionSourceReadyPayload, IviReceiveSessionSourceFailedPayload, IviStream, IviConversationItem, IviConversationItemContent, ReceiveSessionEndedEvent, ParsedIviEvent, IviRealtimeResponseCreateParams, ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient, IviClientConfig } from '@vivix/ivi-sdk-ts';
|
|
2
2
|
import { ReactNode, CSSProperties, ReactElement, VideoHTMLAttributes, ImgHTMLAttributes } from 'react';
|
|
3
3
|
import { IviClientLogEntry } from '@vivix/ivi-sdk-ts/client';
|
|
4
4
|
|
|
5
5
|
type IviRuntimeStatus = "idle" | "connecting" | "syncing" | "running" | "stopped";
|
|
6
|
+
type IviRuntimeConversationRole = NonNullable<IviConversationItem["role"]>;
|
|
7
|
+
type IviRuntimeSourcePlayback = IviReceiveSessionSourceReadyPayload["playback"];
|
|
8
|
+
type IviRuntimeTrtcPlayback = NonNullable<NonNullable<IviRuntimeSourcePlayback>["trtc"]> & {
|
|
9
|
+
app_id: string;
|
|
10
|
+
user_id: string;
|
|
11
|
+
user_sig: string;
|
|
12
|
+
room_id: string;
|
|
13
|
+
};
|
|
14
|
+
type IviRuntimeStreamStatus = IviStream["status"];
|
|
6
15
|
interface IviRuntimeState {
|
|
7
16
|
status: IviRuntimeStatus;
|
|
8
|
-
session:
|
|
9
|
-
stage:
|
|
17
|
+
session: IviRealtimeSessionConfig | null;
|
|
18
|
+
stage: IviStageComposition | null;
|
|
10
19
|
tracks: Map<string, IviTrack>;
|
|
11
20
|
sources: Map<string, IviRuntimeSource>;
|
|
12
21
|
streams: Map<string, IviRuntimeStream>;
|
|
@@ -18,7 +27,7 @@ interface IviRuntimeState {
|
|
|
18
27
|
*/
|
|
19
28
|
interface IviRuntimeStream {
|
|
20
29
|
stream: IviStream;
|
|
21
|
-
status:
|
|
30
|
+
status: IviRuntimeStreamStatus;
|
|
22
31
|
/** stream.started 事件携带的关联 track ID。 */
|
|
23
32
|
trackId?: string;
|
|
24
33
|
error?: unknown;
|
|
@@ -32,14 +41,14 @@ interface IviRuntimeSourcePreloadState {
|
|
|
32
41
|
autoclearAfterPlay: boolean;
|
|
33
42
|
}
|
|
34
43
|
interface IviRuntimeSource {
|
|
35
|
-
source:
|
|
44
|
+
source: IviSourceAsset;
|
|
36
45
|
status: "created" | "ready" | "failed";
|
|
37
|
-
playback?:
|
|
46
|
+
playback?: IviRuntimeSourcePlayback;
|
|
38
47
|
width?: number;
|
|
39
48
|
height?: number;
|
|
40
49
|
durationMs?: number;
|
|
41
50
|
hasAudio?: boolean;
|
|
42
|
-
error?:
|
|
51
|
+
error?: IviReceiveSessionSourceFailedPayload["error"];
|
|
43
52
|
/**
|
|
44
53
|
* 预加载状态:
|
|
45
54
|
* - 存在:渲染层应对该 source 进行预加载;
|
|
@@ -60,7 +69,7 @@ type IviRuntimeConversationLifecycle = "added" | "done";
|
|
|
60
69
|
type IviRuntimeConversationStatus = "in_progress" | "completed" | "incomplete";
|
|
61
70
|
interface IviRuntimeConversationItem {
|
|
62
71
|
id: string;
|
|
63
|
-
role:
|
|
72
|
+
role: IviRuntimeConversationRole;
|
|
64
73
|
lifecycle: IviRuntimeConversationLifecycle;
|
|
65
74
|
status: IviRuntimeConversationStatus;
|
|
66
75
|
item: IviConversationItem;
|
|
@@ -162,7 +171,7 @@ declare class TrtcSourceManager {
|
|
|
162
171
|
* 按 sourceId 注册/更新 TRTC 配置。
|
|
163
172
|
* 若配置发生变化,会先销毁旧会话再按新参数重建连接。
|
|
164
173
|
*/
|
|
165
|
-
upsertSource(sourceId: string, trtc:
|
|
174
|
+
upsertSource(sourceId: string, trtc: IviRuntimeTrtcPlayback): void;
|
|
166
175
|
/**
|
|
167
176
|
* 删除指定 source 的 TRTC 会话并释放连接资源。
|
|
168
177
|
* 该操作通常由 source.deleted 或会话重置触发。
|
|
@@ -402,7 +411,7 @@ declare const EMPTY_RUNTIME_STATE: IviRuntimeState;
|
|
|
402
411
|
declare function useRuntimeState(runtime: IviRuntimeCoordinator | null): IviRuntimeState;
|
|
403
412
|
|
|
404
413
|
interface IVITrtcPlayerProps {
|
|
405
|
-
trtc:
|
|
414
|
+
trtc: IviRuntimeTrtcPlayback;
|
|
406
415
|
sourceId?: string;
|
|
407
416
|
runtime?: IviRuntimeCoordinator | null;
|
|
408
417
|
className?: string;
|
|
@@ -451,11 +460,18 @@ interface IVISubtitleOverlayProps {
|
|
|
451
460
|
/** 会话条目列表(由 runtime state 提供) */
|
|
452
461
|
conversations: IviRuntimeConversationItem[];
|
|
453
462
|
/** 要显示的发言人角色,默认 ["user"]。传单个字符串或数组均可。 */
|
|
454
|
-
roles?:
|
|
463
|
+
roles?: IviRuntimeConversationRole | IviRuntimeConversationRole[];
|
|
455
464
|
/** 同时可见的最大条目数,默认 2 */
|
|
456
465
|
maxVisible?: number;
|
|
457
466
|
/** lifecycle 变为 done 后自动消失的毫秒数,默认 5000 */
|
|
458
467
|
dismissAfterMs?: number;
|
|
468
|
+
/**
|
|
469
|
+
* 按 source 生命周期展示字幕:
|
|
470
|
+
* - sourceKey 变化时建立一个新窗口;
|
|
471
|
+
* - 只展示窗口建立后新增到 conversations 的条目;
|
|
472
|
+
* - 不按 dismissAfterMs 自动消失,直到下一次 sourceKey 变化。
|
|
473
|
+
*/
|
|
474
|
+
stickySourceKey?: string | null;
|
|
459
475
|
/** 样式配置 */
|
|
460
476
|
subtitleStyle?: IVISubtitleOverlayStyle;
|
|
461
477
|
/** 自定义类名 */
|
|
@@ -505,6 +521,12 @@ interface IVITrackSlotProps {
|
|
|
505
521
|
showSubtitle?: boolean;
|
|
506
522
|
/** 字幕组件的自定义配置(字体、颜色、消失延时等) */
|
|
507
523
|
subtitleProps?: Omit<IVISubtitleOverlayProps, "conversations">;
|
|
524
|
+
/**
|
|
525
|
+
* 对连续 generated_clip m3u8 启用按切片生命周期保留字幕。
|
|
526
|
+
* 启用后会展示 active source 切换后新增到 conversations 的全部条目,
|
|
527
|
+
* 并保留到下一次 source 切换(即当前切片播放完成)。
|
|
528
|
+
*/
|
|
529
|
+
keepGeneratedClipSubtitlesUntilEnded?: boolean;
|
|
508
530
|
/** 媒体空白区域(letterbox/pillarbox)的背景模式,默认 "black" */
|
|
509
531
|
background?: IviTrackSlotBackground;
|
|
510
532
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IviRealtimeSessionConfig, IviStageComposition, IviTrack, IviSourceAsset, IviReceiveSessionSourceReadyPayload, IviReceiveSessionSourceFailedPayload, IviStream, IviConversationItem, IviConversationItemContent, ReceiveSessionEndedEvent, ParsedIviEvent, IviRealtimeResponseCreateParams, ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient, IviClientConfig } from '@vivix/ivi-sdk-ts';
|
|
2
2
|
import { ReactNode, CSSProperties, ReactElement, VideoHTMLAttributes, ImgHTMLAttributes } from 'react';
|
|
3
3
|
import { IviClientLogEntry } from '@vivix/ivi-sdk-ts/client';
|
|
4
4
|
|
|
5
5
|
type IviRuntimeStatus = "idle" | "connecting" | "syncing" | "running" | "stopped";
|
|
6
|
+
type IviRuntimeConversationRole = NonNullable<IviConversationItem["role"]>;
|
|
7
|
+
type IviRuntimeSourcePlayback = IviReceiveSessionSourceReadyPayload["playback"];
|
|
8
|
+
type IviRuntimeTrtcPlayback = NonNullable<NonNullable<IviRuntimeSourcePlayback>["trtc"]> & {
|
|
9
|
+
app_id: string;
|
|
10
|
+
user_id: string;
|
|
11
|
+
user_sig: string;
|
|
12
|
+
room_id: string;
|
|
13
|
+
};
|
|
14
|
+
type IviRuntimeStreamStatus = IviStream["status"];
|
|
6
15
|
interface IviRuntimeState {
|
|
7
16
|
status: IviRuntimeStatus;
|
|
8
|
-
session:
|
|
9
|
-
stage:
|
|
17
|
+
session: IviRealtimeSessionConfig | null;
|
|
18
|
+
stage: IviStageComposition | null;
|
|
10
19
|
tracks: Map<string, IviTrack>;
|
|
11
20
|
sources: Map<string, IviRuntimeSource>;
|
|
12
21
|
streams: Map<string, IviRuntimeStream>;
|
|
@@ -18,7 +27,7 @@ interface IviRuntimeState {
|
|
|
18
27
|
*/
|
|
19
28
|
interface IviRuntimeStream {
|
|
20
29
|
stream: IviStream;
|
|
21
|
-
status:
|
|
30
|
+
status: IviRuntimeStreamStatus;
|
|
22
31
|
/** stream.started 事件携带的关联 track ID。 */
|
|
23
32
|
trackId?: string;
|
|
24
33
|
error?: unknown;
|
|
@@ -32,14 +41,14 @@ interface IviRuntimeSourcePreloadState {
|
|
|
32
41
|
autoclearAfterPlay: boolean;
|
|
33
42
|
}
|
|
34
43
|
interface IviRuntimeSource {
|
|
35
|
-
source:
|
|
44
|
+
source: IviSourceAsset;
|
|
36
45
|
status: "created" | "ready" | "failed";
|
|
37
|
-
playback?:
|
|
46
|
+
playback?: IviRuntimeSourcePlayback;
|
|
38
47
|
width?: number;
|
|
39
48
|
height?: number;
|
|
40
49
|
durationMs?: number;
|
|
41
50
|
hasAudio?: boolean;
|
|
42
|
-
error?:
|
|
51
|
+
error?: IviReceiveSessionSourceFailedPayload["error"];
|
|
43
52
|
/**
|
|
44
53
|
* 预加载状态:
|
|
45
54
|
* - 存在:渲染层应对该 source 进行预加载;
|
|
@@ -60,7 +69,7 @@ type IviRuntimeConversationLifecycle = "added" | "done";
|
|
|
60
69
|
type IviRuntimeConversationStatus = "in_progress" | "completed" | "incomplete";
|
|
61
70
|
interface IviRuntimeConversationItem {
|
|
62
71
|
id: string;
|
|
63
|
-
role:
|
|
72
|
+
role: IviRuntimeConversationRole;
|
|
64
73
|
lifecycle: IviRuntimeConversationLifecycle;
|
|
65
74
|
status: IviRuntimeConversationStatus;
|
|
66
75
|
item: IviConversationItem;
|
|
@@ -162,7 +171,7 @@ declare class TrtcSourceManager {
|
|
|
162
171
|
* 按 sourceId 注册/更新 TRTC 配置。
|
|
163
172
|
* 若配置发生变化,会先销毁旧会话再按新参数重建连接。
|
|
164
173
|
*/
|
|
165
|
-
upsertSource(sourceId: string, trtc:
|
|
174
|
+
upsertSource(sourceId: string, trtc: IviRuntimeTrtcPlayback): void;
|
|
166
175
|
/**
|
|
167
176
|
* 删除指定 source 的 TRTC 会话并释放连接资源。
|
|
168
177
|
* 该操作通常由 source.deleted 或会话重置触发。
|
|
@@ -402,7 +411,7 @@ declare const EMPTY_RUNTIME_STATE: IviRuntimeState;
|
|
|
402
411
|
declare function useRuntimeState(runtime: IviRuntimeCoordinator | null): IviRuntimeState;
|
|
403
412
|
|
|
404
413
|
interface IVITrtcPlayerProps {
|
|
405
|
-
trtc:
|
|
414
|
+
trtc: IviRuntimeTrtcPlayback;
|
|
406
415
|
sourceId?: string;
|
|
407
416
|
runtime?: IviRuntimeCoordinator | null;
|
|
408
417
|
className?: string;
|
|
@@ -451,11 +460,18 @@ interface IVISubtitleOverlayProps {
|
|
|
451
460
|
/** 会话条目列表(由 runtime state 提供) */
|
|
452
461
|
conversations: IviRuntimeConversationItem[];
|
|
453
462
|
/** 要显示的发言人角色,默认 ["user"]。传单个字符串或数组均可。 */
|
|
454
|
-
roles?:
|
|
463
|
+
roles?: IviRuntimeConversationRole | IviRuntimeConversationRole[];
|
|
455
464
|
/** 同时可见的最大条目数,默认 2 */
|
|
456
465
|
maxVisible?: number;
|
|
457
466
|
/** lifecycle 变为 done 后自动消失的毫秒数,默认 5000 */
|
|
458
467
|
dismissAfterMs?: number;
|
|
468
|
+
/**
|
|
469
|
+
* 按 source 生命周期展示字幕:
|
|
470
|
+
* - sourceKey 变化时建立一个新窗口;
|
|
471
|
+
* - 只展示窗口建立后新增到 conversations 的条目;
|
|
472
|
+
* - 不按 dismissAfterMs 自动消失,直到下一次 sourceKey 变化。
|
|
473
|
+
*/
|
|
474
|
+
stickySourceKey?: string | null;
|
|
459
475
|
/** 样式配置 */
|
|
460
476
|
subtitleStyle?: IVISubtitleOverlayStyle;
|
|
461
477
|
/** 自定义类名 */
|
|
@@ -505,6 +521,12 @@ interface IVITrackSlotProps {
|
|
|
505
521
|
showSubtitle?: boolean;
|
|
506
522
|
/** 字幕组件的自定义配置(字体、颜色、消失延时等) */
|
|
507
523
|
subtitleProps?: Omit<IVISubtitleOverlayProps, "conversations">;
|
|
524
|
+
/**
|
|
525
|
+
* 对连续 generated_clip m3u8 启用按切片生命周期保留字幕。
|
|
526
|
+
* 启用后会展示 active source 切换后新增到 conversations 的全部条目,
|
|
527
|
+
* 并保留到下一次 source 切换(即当前切片播放完成)。
|
|
528
|
+
*/
|
|
529
|
+
keepGeneratedClipSubtitlesUntilEnded?: boolean;
|
|
508
530
|
/** 媒体空白区域(letterbox/pillarbox)的背景模式,默认 "black" */
|
|
509
531
|
background?: IviTrackSlotBackground;
|
|
510
532
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient,
|
|
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/ivi-sdk-ts';
|
|
2
2
|
import { createContext, useState, useEffect, useMemo, useContext, useRef, useCallback } from 'react';
|
|
3
3
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
4
|
|
|
@@ -108,7 +108,7 @@ var SessionEventHandler = class {
|
|
|
108
108
|
this.callbacks = callbacks;
|
|
109
109
|
}
|
|
110
110
|
handle(event) {
|
|
111
|
-
if (event instanceof
|
|
111
|
+
if (event instanceof ReceiveSessionCreatedEvent) {
|
|
112
112
|
const before = this.sessionManager.getSession();
|
|
113
113
|
this.sessionManager.setSession(event.session);
|
|
114
114
|
logIviStateChange("session", null, event.type, before, this.sessionManager.getSession());
|
|
@@ -257,7 +257,7 @@ var SourceEventHandler = class {
|
|
|
257
257
|
const before = this.sourceManager.get(sourceId);
|
|
258
258
|
this.sourceManager.upsertCreated(source);
|
|
259
259
|
this.sourceManager.applyPreload(sourceId, {
|
|
260
|
-
autoclearAfterPlay: event.autoclearAfterPlay
|
|
260
|
+
autoclearAfterPlay: event.autoclearAfterPlay ?? true
|
|
261
261
|
});
|
|
262
262
|
logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
|
|
263
263
|
}
|
|
@@ -299,7 +299,7 @@ var StreamEventHandler = class {
|
|
|
299
299
|
this.callbacks = callbacks;
|
|
300
300
|
}
|
|
301
301
|
handle(event) {
|
|
302
|
-
if (event instanceof
|
|
302
|
+
if (event instanceof ReceiveSessionStreamCreatedEvent) {
|
|
303
303
|
const streamId = event.stream.stream_id;
|
|
304
304
|
const before = this.streamManager.getAll().get(streamId);
|
|
305
305
|
this.streamManager.upsertCreated(event.stream);
|
|
@@ -372,32 +372,37 @@ var ConversationEventHandler = class {
|
|
|
372
372
|
return { handled: true };
|
|
373
373
|
}
|
|
374
374
|
if (event instanceof ReceiveConversationItemAddedEvent) {
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
if (!event.item.id) return { handled: true };
|
|
376
|
+
const item = { ...event.item, id: event.item.id };
|
|
377
|
+
const before = this.conversationManager.getAllMap().get(item.id);
|
|
378
|
+
this.conversationManager.upsertAdded(item);
|
|
377
379
|
logIviStateChange(
|
|
378
380
|
"conversationItem",
|
|
379
|
-
|
|
381
|
+
item.id,
|
|
380
382
|
event.type,
|
|
381
383
|
before,
|
|
382
|
-
this.conversationManager.getAllMap().get(
|
|
384
|
+
this.conversationManager.getAllMap().get(item.id)
|
|
383
385
|
);
|
|
384
386
|
this.callbacks.onConversationsChanged();
|
|
385
387
|
return { handled: true };
|
|
386
388
|
}
|
|
387
389
|
if (event instanceof ReceiveConversationItemDoneEvent) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
+
if (!event.item.id) return { handled: true };
|
|
391
|
+
const item = { ...event.item, id: event.item.id };
|
|
392
|
+
const before = this.conversationManager.getAllMap().get(item.id);
|
|
393
|
+
this.conversationManager.markDone(item);
|
|
390
394
|
logIviStateChange(
|
|
391
395
|
"conversationItem",
|
|
392
|
-
|
|
396
|
+
item.id,
|
|
393
397
|
event.type,
|
|
394
398
|
before,
|
|
395
|
-
this.conversationManager.getAllMap().get(
|
|
399
|
+
this.conversationManager.getAllMap().get(item.id)
|
|
396
400
|
);
|
|
397
401
|
this.callbacks.onConversationsChanged();
|
|
398
402
|
return { handled: true };
|
|
399
403
|
}
|
|
400
404
|
if (event instanceof ReceiveResponseOutputTextDeltaEvent) {
|
|
405
|
+
if (!event.itemId) return { handled: true };
|
|
401
406
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
402
407
|
this.conversationManager.applyTextDelta(event.itemId, event.delta);
|
|
403
408
|
logIviStateChange(
|
|
@@ -411,6 +416,7 @@ var ConversationEventHandler = class {
|
|
|
411
416
|
return { handled: true };
|
|
412
417
|
}
|
|
413
418
|
if (event instanceof ReceiveResponseOutputTextDoneEvent) {
|
|
419
|
+
if (!event.itemId) return { handled: true };
|
|
414
420
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
415
421
|
this.conversationManager.applyTextDone(event.itemId, event.text);
|
|
416
422
|
logIviStateChange(
|
|
@@ -424,6 +430,7 @@ var ConversationEventHandler = class {
|
|
|
424
430
|
return { handled: true };
|
|
425
431
|
}
|
|
426
432
|
if (event instanceof ReceiveResponseOutputAudioTranscriptDeltaEvent) {
|
|
433
|
+
if (!event.itemId) return { handled: true };
|
|
427
434
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
428
435
|
this.conversationManager.applyTranscriptDelta(event.itemId, event.delta);
|
|
429
436
|
logIviStateChange(
|
|
@@ -437,6 +444,7 @@ var ConversationEventHandler = class {
|
|
|
437
444
|
return { handled: true };
|
|
438
445
|
}
|
|
439
446
|
if (event instanceof ReceiveResponseOutputAudioTranscriptDoneEvent) {
|
|
447
|
+
if (!event.itemId) return { handled: true };
|
|
440
448
|
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
441
449
|
this.conversationManager.applyTranscriptDone(event.itemId, event.transcript);
|
|
442
450
|
logIviStateChange(
|
|
@@ -548,13 +556,13 @@ var TrackManager = class {
|
|
|
548
556
|
}
|
|
549
557
|
this.patchTrack(trackId, {
|
|
550
558
|
active_source_id: resolvedSourceId,
|
|
551
|
-
next_source_id:
|
|
559
|
+
next_source_id: void 0
|
|
552
560
|
});
|
|
553
561
|
}
|
|
554
562
|
applyTrackCued(trackId, sourceId) {
|
|
555
563
|
this.patchTrack(trackId, {
|
|
556
564
|
active_source_id: sourceId,
|
|
557
|
-
next_source_id:
|
|
565
|
+
next_source_id: void 0
|
|
558
566
|
});
|
|
559
567
|
}
|
|
560
568
|
applyTrackNextSet(trackId, sourceId) {
|
|
@@ -851,6 +859,7 @@ var ConversationManager = class {
|
|
|
851
859
|
const nextItems = /* @__PURE__ */ new Map();
|
|
852
860
|
const nextOrder = [];
|
|
853
861
|
items.forEach((item) => {
|
|
862
|
+
if (!hasItemId(item)) return;
|
|
854
863
|
const runtimeItem = this.buildRuntimeItem(item, item.status === "in_progress" ? "added" : "done");
|
|
855
864
|
nextItems.set(runtimeItem.id, runtimeItem);
|
|
856
865
|
nextOrder.push(runtimeItem.id);
|
|
@@ -1083,6 +1092,9 @@ var ConversationManager = class {
|
|
|
1083
1092
|
];
|
|
1084
1093
|
}
|
|
1085
1094
|
};
|
|
1095
|
+
function hasItemId(item) {
|
|
1096
|
+
return typeof item.id === "string" && item.id.length > 0;
|
|
1097
|
+
}
|
|
1086
1098
|
|
|
1087
1099
|
// src/runtime/managers/trtc-source-manager.ts
|
|
1088
1100
|
var TAG = "[IVI-TRTC]";
|
|
@@ -1527,7 +1539,8 @@ var TrtcSourceManager = class {
|
|
|
1527
1539
|
}
|
|
1528
1540
|
};
|
|
1529
1541
|
function isRuntimeTrtcSource(source) {
|
|
1530
|
-
|
|
1542
|
+
const trtc = source.playback?.trtc;
|
|
1543
|
+
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";
|
|
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;
|
|
@@ -1958,7 +1971,7 @@ var IviRuntimeCoordinator = class {
|
|
|
1958
1971
|
return [];
|
|
1959
1972
|
}
|
|
1960
1973
|
const missing = /* @__PURE__ */ new Set();
|
|
1961
|
-
stage.composition
|
|
1974
|
+
stage.composition?.forEach((item) => {
|
|
1962
1975
|
if (!hasTrack(item.track_id)) {
|
|
1963
1976
|
missing.add(item.track_id);
|
|
1964
1977
|
}
|
|
@@ -1993,7 +2006,11 @@ var IviRuntimeCoordinator = class {
|
|
|
1993
2006
|
}
|
|
1994
2007
|
progressUserTextToResponseFlows(event) {
|
|
1995
2008
|
if (event instanceof ReceiveConversationItemAddedEvent) {
|
|
1996
|
-
const
|
|
2009
|
+
const itemId = event.item.id;
|
|
2010
|
+
if (!itemId) {
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
1997
2014
|
if (!flow) {
|
|
1998
2015
|
return;
|
|
1999
2016
|
}
|
|
@@ -2003,7 +2020,11 @@ var IviRuntimeCoordinator = class {
|
|
|
2003
2020
|
return;
|
|
2004
2021
|
}
|
|
2005
2022
|
if (event instanceof ReceiveConversationItemDoneEvent) {
|
|
2006
|
-
const
|
|
2023
|
+
const itemId = event.item.id;
|
|
2024
|
+
if (!itemId) {
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
2007
2028
|
if (!flow) {
|
|
2008
2029
|
return;
|
|
2009
2030
|
}
|
|
@@ -2144,14 +2165,14 @@ function IVIStageView(props) {
|
|
|
2144
2165
|
}
|
|
2145
2166
|
function buildSlotTrackMapFromState(state) {
|
|
2146
2167
|
const map = /* @__PURE__ */ new Map();
|
|
2147
|
-
state.stage?.composition
|
|
2168
|
+
state.stage?.composition?.forEach((item) => {
|
|
2148
2169
|
map.set(item.slot, item.track_id);
|
|
2149
2170
|
});
|
|
2150
2171
|
return map;
|
|
2151
2172
|
}
|
|
2152
2173
|
function buildSlotBindingsFromState(state) {
|
|
2153
2174
|
const bindings = [];
|
|
2154
|
-
state.stage?.composition
|
|
2175
|
+
state.stage?.composition?.forEach((item) => {
|
|
2155
2176
|
const track = state.tracks.get(item.track_id);
|
|
2156
2177
|
const sourceId = track?.active_source_id ?? null;
|
|
2157
2178
|
const source = sourceId ? state.sources.get(sourceId) : void 0;
|
|
@@ -2441,6 +2462,7 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
|
|
|
2441
2462
|
return () => observer.disconnect();
|
|
2442
2463
|
}, [containerRef, volume, enabled, activeSourceId]);
|
|
2443
2464
|
}
|
|
2465
|
+
var ALL_CONVERSATION_ROLES = ["user", "director", "model", "vlm"];
|
|
2444
2466
|
function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
2445
2467
|
const [visibleIds, setVisibleIds] = useState([]);
|
|
2446
2468
|
const timersRef = useRef(/* @__PURE__ */ new Map());
|
|
@@ -2516,26 +2538,82 @@ function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
|
2516
2538
|
}).filter((entry) => entry !== null);
|
|
2517
2539
|
}, [visibleIds, conversationMap]);
|
|
2518
2540
|
}
|
|
2541
|
+
function useSourceWindowSubtitleEntries(conversations, sourceKey) {
|
|
2542
|
+
const normalizedSourceKey = sourceKey ?? null;
|
|
2543
|
+
const [windowState, setWindowState] = useState(() => ({
|
|
2544
|
+
sourceKey: normalizedSourceKey,
|
|
2545
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2546
|
+
visibleIds: []
|
|
2547
|
+
}));
|
|
2548
|
+
useEffect(() => {
|
|
2549
|
+
setWindowState((prev) => {
|
|
2550
|
+
if (prev.sourceKey !== normalizedSourceKey) {
|
|
2551
|
+
return {
|
|
2552
|
+
sourceKey: normalizedSourceKey,
|
|
2553
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2554
|
+
visibleIds: []
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
if (!normalizedSourceKey) {
|
|
2558
|
+
return prev.visibleIds.length === 0 ? prev : { ...prev, visibleIds: [] };
|
|
2559
|
+
}
|
|
2560
|
+
const nextVisibleIds = conversations.filter((item) => {
|
|
2561
|
+
const displayText = item.text || item.transcript;
|
|
2562
|
+
return Boolean(displayText) && !prev.baselineIds.has(item.id);
|
|
2563
|
+
}).map((item) => item.id);
|
|
2564
|
+
if (nextVisibleIds.length === prev.visibleIds.length && nextVisibleIds.every((id, index) => id === prev.visibleIds[index])) {
|
|
2565
|
+
return prev;
|
|
2566
|
+
}
|
|
2567
|
+
return {
|
|
2568
|
+
...prev,
|
|
2569
|
+
visibleIds: nextVisibleIds
|
|
2570
|
+
};
|
|
2571
|
+
});
|
|
2572
|
+
}, [conversations, normalizedSourceKey]);
|
|
2573
|
+
const conversationMap = useMemo(() => {
|
|
2574
|
+
const map = /* @__PURE__ */ new Map();
|
|
2575
|
+
for (const item of conversations) {
|
|
2576
|
+
map.set(item.id, item);
|
|
2577
|
+
}
|
|
2578
|
+
return map;
|
|
2579
|
+
}, [conversations]);
|
|
2580
|
+
return useMemo(() => {
|
|
2581
|
+
if (windowState.sourceKey !== normalizedSourceKey) {
|
|
2582
|
+
return [];
|
|
2583
|
+
}
|
|
2584
|
+
return windowState.visibleIds.map((id) => {
|
|
2585
|
+
const item = conversationMap.get(id);
|
|
2586
|
+
if (!item) return null;
|
|
2587
|
+
const text = item.text || item.transcript;
|
|
2588
|
+
if (!text) return null;
|
|
2589
|
+
return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
|
|
2590
|
+
}).filter((entry) => entry !== null);
|
|
2591
|
+
}, [windowState.sourceKey, windowState.visibleIds, conversationMap, normalizedSourceKey]);
|
|
2592
|
+
}
|
|
2519
2593
|
var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
|
|
2520
2594
|
function IVISubtitleOverlay(props) {
|
|
2521
2595
|
const {
|
|
2522
2596
|
conversations,
|
|
2523
|
-
roles
|
|
2597
|
+
roles,
|
|
2524
2598
|
maxVisible = 2,
|
|
2525
2599
|
dismissAfterMs = 5e3,
|
|
2600
|
+
stickySourceKey,
|
|
2526
2601
|
subtitleStyle,
|
|
2527
2602
|
className,
|
|
2528
2603
|
style
|
|
2529
2604
|
} = props;
|
|
2605
|
+
const resolvedRoles = roles ?? (stickySourceKey ? ALL_CONVERSATION_ROLES : "user");
|
|
2530
2606
|
const roleSet = useMemo(
|
|
2531
|
-
() => new Set(Array.isArray(
|
|
2532
|
-
[
|
|
2607
|
+
() => new Set(Array.isArray(resolvedRoles) ? resolvedRoles : [resolvedRoles]),
|
|
2608
|
+
[resolvedRoles]
|
|
2533
2609
|
);
|
|
2534
2610
|
const filtered = useMemo(
|
|
2535
2611
|
() => conversations.filter((c) => roleSet.has(c.role)),
|
|
2536
2612
|
[conversations, roleSet]
|
|
2537
2613
|
);
|
|
2538
|
-
const
|
|
2614
|
+
const queueEntries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
|
|
2615
|
+
const sourceWindowEntries = useSourceWindowSubtitleEntries(filtered, stickySourceKey);
|
|
2616
|
+
const entries = stickySourceKey ? sourceWindowEntries : queueEntries;
|
|
2539
2617
|
if (entries.length === 0) return null;
|
|
2540
2618
|
const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
2541
2619
|
const fontSize = subtitleStyle?.fontSize ?? 14;
|
|
@@ -3080,6 +3158,14 @@ function supportsSubtitleOverlay(source) {
|
|
|
3080
3158
|
if (source.source.asset_type === "image") return false;
|
|
3081
3159
|
return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
|
|
3082
3160
|
}
|
|
3161
|
+
function supportsGeneratedClipStickySubtitles(source) {
|
|
3162
|
+
if (!source) return false;
|
|
3163
|
+
if (source.source.kind !== "generated_clip") return false;
|
|
3164
|
+
if (source.source.asset_type === "image") return false;
|
|
3165
|
+
if (source.playback.type === "trtc") return false;
|
|
3166
|
+
const playbackUrl = source.playback.url;
|
|
3167
|
+
return typeof playbackUrl === "string" && isM3u8Url(playbackUrl);
|
|
3168
|
+
}
|
|
3083
3169
|
function detectMediaVolumeType(source) {
|
|
3084
3170
|
if (!source) return null;
|
|
3085
3171
|
if (source.playback.type === "trtc") return "trtc";
|
|
@@ -3115,7 +3201,7 @@ function TrackSlotMediaContent(props) {
|
|
|
3115
3201
|
const shouldMute = !isActive;
|
|
3116
3202
|
if (renderMedia) return renderMedia(renderContext);
|
|
3117
3203
|
if (source.playback.type === "trtc") {
|
|
3118
|
-
if (!source.playback.trtc) return null;
|
|
3204
|
+
if (!isRuntimeTrtcPlayback(source.playback.trtc)) return null;
|
|
3119
3205
|
if (renderTrtc) return renderTrtc(renderContext);
|
|
3120
3206
|
const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
|
|
3121
3207
|
return /* @__PURE__ */ jsx(
|
|
@@ -3180,6 +3266,9 @@ function TrackSlotMediaContent(props) {
|
|
|
3180
3266
|
}
|
|
3181
3267
|
);
|
|
3182
3268
|
}
|
|
3269
|
+
function isRuntimeTrtcPlayback(trtc) {
|
|
3270
|
+
return typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
|
|
3271
|
+
}
|
|
3183
3272
|
function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
|
|
3184
3273
|
const objectFitStyle = fitStrategy === "auto" ? {} : {
|
|
3185
3274
|
objectFit: fitStrategy ?? "contain"
|
|
@@ -3276,6 +3365,7 @@ function IVITrackSlot(props) {
|
|
|
3276
3365
|
volumeControlProps,
|
|
3277
3366
|
showSubtitle,
|
|
3278
3367
|
subtitleProps,
|
|
3368
|
+
keepGeneratedClipSubtitlesUntilEnded = false,
|
|
3279
3369
|
background = "black"
|
|
3280
3370
|
} = props;
|
|
3281
3371
|
const context = useIviStageView();
|
|
@@ -3286,6 +3376,7 @@ function IVITrackSlot(props) {
|
|
|
3286
3376
|
const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
|
|
3287
3377
|
const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
|
|
3288
3378
|
const activeSource = activeEntry?.source ?? null;
|
|
3379
|
+
const stickySubtitleSourceKey = keepGeneratedClipSubtitlesUntilEnded && supportsGeneratedClipStickySubtitles(activeSource) ? activeSourceId : null;
|
|
3289
3380
|
const mediaType = detectMediaVolumeType(activeSource);
|
|
3290
3381
|
const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
|
|
3291
3382
|
useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
|
|
@@ -3348,7 +3439,8 @@ function IVITrackSlot(props) {
|
|
|
3348
3439
|
IVISubtitleOverlay,
|
|
3349
3440
|
{
|
|
3350
3441
|
conversations: context.state.conversations,
|
|
3351
|
-
...subtitleProps
|
|
3442
|
+
...subtitleProps,
|
|
3443
|
+
stickySourceKey: stickySubtitleSourceKey
|
|
3352
3444
|
}
|
|
3353
3445
|
) }),
|
|
3354
3446
|
showVolumeControl && mediaType !== null && /* @__PURE__ */ jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsx(
|