@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 +39 -44
- package/dist/index.cjs +118 -27
- 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 +119 -28
- 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]";
|
|
@@ -1358,8 +1370,7 @@ var TrtcSourceManager = class {
|
|
|
1358
1370
|
sdkAppId,
|
|
1359
1371
|
userId: session.trtc.user_id,
|
|
1360
1372
|
userSig: session.trtc.user_sig,
|
|
1361
|
-
scene: TRTC.TYPE.
|
|
1362
|
-
role: TRTC.TYPE.ROLE_AUDIENCE,
|
|
1373
|
+
scene: TRTC.TYPE.SCENE_RTC,
|
|
1363
1374
|
autoReceiveAudio: true,
|
|
1364
1375
|
...isStringRoomId ? { strRoomId: session.trtc.room_id } : { roomId: Number(session.trtc.room_id) }
|
|
1365
1376
|
});
|
|
@@ -1527,7 +1538,8 @@ var TrtcSourceManager = class {
|
|
|
1527
1538
|
}
|
|
1528
1539
|
};
|
|
1529
1540
|
function isRuntimeTrtcSource(source) {
|
|
1530
|
-
|
|
1541
|
+
const trtc = source.playback?.trtc;
|
|
1542
|
+
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
1543
|
}
|
|
1532
1544
|
function isSameTrtcConfig(a, b) {
|
|
1533
1545
|
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 +1970,7 @@ var IviRuntimeCoordinator = class {
|
|
|
1958
1970
|
return [];
|
|
1959
1971
|
}
|
|
1960
1972
|
const missing = /* @__PURE__ */ new Set();
|
|
1961
|
-
stage.composition
|
|
1973
|
+
stage.composition?.forEach((item) => {
|
|
1962
1974
|
if (!hasTrack(item.track_id)) {
|
|
1963
1975
|
missing.add(item.track_id);
|
|
1964
1976
|
}
|
|
@@ -1993,7 +2005,11 @@ var IviRuntimeCoordinator = class {
|
|
|
1993
2005
|
}
|
|
1994
2006
|
progressUserTextToResponseFlows(event) {
|
|
1995
2007
|
if (event instanceof ReceiveConversationItemAddedEvent) {
|
|
1996
|
-
const
|
|
2008
|
+
const itemId = event.item.id;
|
|
2009
|
+
if (!itemId) {
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
1997
2013
|
if (!flow) {
|
|
1998
2014
|
return;
|
|
1999
2015
|
}
|
|
@@ -2003,7 +2019,11 @@ var IviRuntimeCoordinator = class {
|
|
|
2003
2019
|
return;
|
|
2004
2020
|
}
|
|
2005
2021
|
if (event instanceof ReceiveConversationItemDoneEvent) {
|
|
2006
|
-
const
|
|
2022
|
+
const itemId = event.item.id;
|
|
2023
|
+
if (!itemId) {
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const flow = this.pendingUserTextToResponseFlows.get(itemId);
|
|
2007
2027
|
if (!flow) {
|
|
2008
2028
|
return;
|
|
2009
2029
|
}
|
|
@@ -2144,14 +2164,14 @@ function IVIStageView(props) {
|
|
|
2144
2164
|
}
|
|
2145
2165
|
function buildSlotTrackMapFromState(state) {
|
|
2146
2166
|
const map = /* @__PURE__ */ new Map();
|
|
2147
|
-
state.stage?.composition
|
|
2167
|
+
state.stage?.composition?.forEach((item) => {
|
|
2148
2168
|
map.set(item.slot, item.track_id);
|
|
2149
2169
|
});
|
|
2150
2170
|
return map;
|
|
2151
2171
|
}
|
|
2152
2172
|
function buildSlotBindingsFromState(state) {
|
|
2153
2173
|
const bindings = [];
|
|
2154
|
-
state.stage?.composition
|
|
2174
|
+
state.stage?.composition?.forEach((item) => {
|
|
2155
2175
|
const track = state.tracks.get(item.track_id);
|
|
2156
2176
|
const sourceId = track?.active_source_id ?? null;
|
|
2157
2177
|
const source = sourceId ? state.sources.get(sourceId) : void 0;
|
|
@@ -2441,6 +2461,7 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
|
|
|
2441
2461
|
return () => observer.disconnect();
|
|
2442
2462
|
}, [containerRef, volume, enabled, activeSourceId]);
|
|
2443
2463
|
}
|
|
2464
|
+
var ALL_CONVERSATION_ROLES = ["user", "director", "model", "vlm"];
|
|
2444
2465
|
function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
2445
2466
|
const [visibleIds, setVisibleIds] = useState([]);
|
|
2446
2467
|
const timersRef = useRef(/* @__PURE__ */ new Map());
|
|
@@ -2516,26 +2537,82 @@ function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
|
2516
2537
|
}).filter((entry) => entry !== null);
|
|
2517
2538
|
}, [visibleIds, conversationMap]);
|
|
2518
2539
|
}
|
|
2540
|
+
function useSourceWindowSubtitleEntries(conversations, sourceKey) {
|
|
2541
|
+
const normalizedSourceKey = sourceKey ?? null;
|
|
2542
|
+
const [windowState, setWindowState] = useState(() => ({
|
|
2543
|
+
sourceKey: normalizedSourceKey,
|
|
2544
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2545
|
+
visibleIds: []
|
|
2546
|
+
}));
|
|
2547
|
+
useEffect(() => {
|
|
2548
|
+
setWindowState((prev) => {
|
|
2549
|
+
if (prev.sourceKey !== normalizedSourceKey) {
|
|
2550
|
+
return {
|
|
2551
|
+
sourceKey: normalizedSourceKey,
|
|
2552
|
+
baselineIds: new Set(conversations.map((item) => item.id)),
|
|
2553
|
+
visibleIds: []
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
if (!normalizedSourceKey) {
|
|
2557
|
+
return prev.visibleIds.length === 0 ? prev : { ...prev, visibleIds: [] };
|
|
2558
|
+
}
|
|
2559
|
+
const nextVisibleIds = conversations.filter((item) => {
|
|
2560
|
+
const displayText = item.text || item.transcript;
|
|
2561
|
+
return Boolean(displayText) && !prev.baselineIds.has(item.id);
|
|
2562
|
+
}).map((item) => item.id);
|
|
2563
|
+
if (nextVisibleIds.length === prev.visibleIds.length && nextVisibleIds.every((id, index) => id === prev.visibleIds[index])) {
|
|
2564
|
+
return prev;
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
...prev,
|
|
2568
|
+
visibleIds: nextVisibleIds
|
|
2569
|
+
};
|
|
2570
|
+
});
|
|
2571
|
+
}, [conversations, normalizedSourceKey]);
|
|
2572
|
+
const conversationMap = useMemo(() => {
|
|
2573
|
+
const map = /* @__PURE__ */ new Map();
|
|
2574
|
+
for (const item of conversations) {
|
|
2575
|
+
map.set(item.id, item);
|
|
2576
|
+
}
|
|
2577
|
+
return map;
|
|
2578
|
+
}, [conversations]);
|
|
2579
|
+
return useMemo(() => {
|
|
2580
|
+
if (windowState.sourceKey !== normalizedSourceKey) {
|
|
2581
|
+
return [];
|
|
2582
|
+
}
|
|
2583
|
+
return windowState.visibleIds.map((id) => {
|
|
2584
|
+
const item = conversationMap.get(id);
|
|
2585
|
+
if (!item) return null;
|
|
2586
|
+
const text = item.text || item.transcript;
|
|
2587
|
+
if (!text) return null;
|
|
2588
|
+
return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
|
|
2589
|
+
}).filter((entry) => entry !== null);
|
|
2590
|
+
}, [windowState.sourceKey, windowState.visibleIds, conversationMap, normalizedSourceKey]);
|
|
2591
|
+
}
|
|
2519
2592
|
var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
|
|
2520
2593
|
function IVISubtitleOverlay(props) {
|
|
2521
2594
|
const {
|
|
2522
2595
|
conversations,
|
|
2523
|
-
roles
|
|
2596
|
+
roles,
|
|
2524
2597
|
maxVisible = 2,
|
|
2525
2598
|
dismissAfterMs = 5e3,
|
|
2599
|
+
stickySourceKey,
|
|
2526
2600
|
subtitleStyle,
|
|
2527
2601
|
className,
|
|
2528
2602
|
style
|
|
2529
2603
|
} = props;
|
|
2604
|
+
const resolvedRoles = roles ?? (stickySourceKey ? ALL_CONVERSATION_ROLES : "user");
|
|
2530
2605
|
const roleSet = useMemo(
|
|
2531
|
-
() => new Set(Array.isArray(
|
|
2532
|
-
[
|
|
2606
|
+
() => new Set(Array.isArray(resolvedRoles) ? resolvedRoles : [resolvedRoles]),
|
|
2607
|
+
[resolvedRoles]
|
|
2533
2608
|
);
|
|
2534
2609
|
const filtered = useMemo(
|
|
2535
2610
|
() => conversations.filter((c) => roleSet.has(c.role)),
|
|
2536
2611
|
[conversations, roleSet]
|
|
2537
2612
|
);
|
|
2538
|
-
const
|
|
2613
|
+
const queueEntries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
|
|
2614
|
+
const sourceWindowEntries = useSourceWindowSubtitleEntries(filtered, stickySourceKey);
|
|
2615
|
+
const entries = stickySourceKey ? sourceWindowEntries : queueEntries;
|
|
2539
2616
|
if (entries.length === 0) return null;
|
|
2540
2617
|
const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
2541
2618
|
const fontSize = subtitleStyle?.fontSize ?? 14;
|
|
@@ -3080,6 +3157,14 @@ function supportsSubtitleOverlay(source) {
|
|
|
3080
3157
|
if (source.source.asset_type === "image") return false;
|
|
3081
3158
|
return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
|
|
3082
3159
|
}
|
|
3160
|
+
function supportsGeneratedClipStickySubtitles(source) {
|
|
3161
|
+
if (!source) return false;
|
|
3162
|
+
if (source.source.kind !== "generated_clip") return false;
|
|
3163
|
+
if (source.source.asset_type === "image") return false;
|
|
3164
|
+
if (source.playback.type === "trtc") return false;
|
|
3165
|
+
const playbackUrl = source.playback.url;
|
|
3166
|
+
return typeof playbackUrl === "string" && isM3u8Url(playbackUrl);
|
|
3167
|
+
}
|
|
3083
3168
|
function detectMediaVolumeType(source) {
|
|
3084
3169
|
if (!source) return null;
|
|
3085
3170
|
if (source.playback.type === "trtc") return "trtc";
|
|
@@ -3115,7 +3200,7 @@ function TrackSlotMediaContent(props) {
|
|
|
3115
3200
|
const shouldMute = !isActive;
|
|
3116
3201
|
if (renderMedia) return renderMedia(renderContext);
|
|
3117
3202
|
if (source.playback.type === "trtc") {
|
|
3118
|
-
if (!source.playback.trtc) return null;
|
|
3203
|
+
if (!isRuntimeTrtcPlayback(source.playback.trtc)) return null;
|
|
3119
3204
|
if (renderTrtc) return renderTrtc(renderContext);
|
|
3120
3205
|
const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
|
|
3121
3206
|
return /* @__PURE__ */ jsx(
|
|
@@ -3180,6 +3265,9 @@ function TrackSlotMediaContent(props) {
|
|
|
3180
3265
|
}
|
|
3181
3266
|
);
|
|
3182
3267
|
}
|
|
3268
|
+
function isRuntimeTrtcPlayback(trtc) {
|
|
3269
|
+
return typeof trtc?.app_id === "string" && typeof trtc.user_id === "string" && typeof trtc.user_sig === "string" && typeof trtc.room_id === "string";
|
|
3270
|
+
}
|
|
3183
3271
|
function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
|
|
3184
3272
|
const objectFitStyle = fitStrategy === "auto" ? {} : {
|
|
3185
3273
|
objectFit: fitStrategy ?? "contain"
|
|
@@ -3276,6 +3364,7 @@ function IVITrackSlot(props) {
|
|
|
3276
3364
|
volumeControlProps,
|
|
3277
3365
|
showSubtitle,
|
|
3278
3366
|
subtitleProps,
|
|
3367
|
+
keepGeneratedClipSubtitlesUntilEnded = false,
|
|
3279
3368
|
background = "black"
|
|
3280
3369
|
} = props;
|
|
3281
3370
|
const context = useIviStageView();
|
|
@@ -3286,6 +3375,7 @@ function IVITrackSlot(props) {
|
|
|
3286
3375
|
const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
|
|
3287
3376
|
const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
|
|
3288
3377
|
const activeSource = activeEntry?.source ?? null;
|
|
3378
|
+
const stickySubtitleSourceKey = keepGeneratedClipSubtitlesUntilEnded && supportsGeneratedClipStickySubtitles(activeSource) ? activeSourceId : null;
|
|
3289
3379
|
const mediaType = detectMediaVolumeType(activeSource);
|
|
3290
3380
|
const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
|
|
3291
3381
|
useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
|
|
@@ -3348,7 +3438,8 @@ function IVITrackSlot(props) {
|
|
|
3348
3438
|
IVISubtitleOverlay,
|
|
3349
3439
|
{
|
|
3350
3440
|
conversations: context.state.conversations,
|
|
3351
|
-
...subtitleProps
|
|
3441
|
+
...subtitleProps,
|
|
3442
|
+
stickySourceKey: stickySubtitleSourceKey
|
|
3352
3443
|
}
|
|
3353
3444
|
) }),
|
|
3354
3445
|
showVolumeControl && mediaType !== null && /* @__PURE__ */ jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsx(
|