@vivix-ai/ivi-frontend-sdk 0.2.2
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 +561 -0
- package/dist/index.cjs +3515 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +549 -0
- package/dist/index.d.ts +549 -0
- package/dist/index.js +3505 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3505 @@
|
|
|
1
|
+
import { ReceiveConversationItemAddedEvent, ReceiveConversationItemDoneEvent, ReceiveResponseCreatedEvent, IviClient, SessionCreatedEvent, ReceiveSessionEndedEvent, ReceiveSessionStageGetResponseEvent, ReceiveSessionStageUpdatedEvent, ReceiveSessionTrackCreatedEvent, ReceiveSessionTrackDeletedEvent, ReceiveSessionTrackTookEvent, ReceiveSessionTrackCuedEvent, ReceiveSessionTrackNextSetEvent, ReceiveSessionTracksListResponseEvent, ReceiveSessionSourceCreatedEvent, ReceiveSessionSourceReadyEvent, ReceiveSessionSourceFailedEvent, ReceiveSessionSourceDeletedEvent, ReceiveSessionSourcePreloadEvent, ReceiveSessionSourceClearPreloadEvent, ReceiveSessionSourcesListResponseEvent, ReceiveSessionSourcePlaybackCompletedEvent, SessionStreamCreatedEvent, ReceiveSessionStreamStartedEvent, ReceiveSessionStreamEndedEvent, ReceiveSessionStreamFailedEvent, ReceiveSessionStreamDeletedEvent, ReceiveSessionStreamsListResponseEvent, ReceiveConversationListResponseEvent, ReceiveResponseOutputTextDeltaEvent, ReceiveResponseOutputTextDoneEvent, ReceiveResponseOutputAudioTranscriptDeltaEvent, ReceiveResponseOutputAudioTranscriptDoneEvent, ReceiveResponseDoneEvent } from '@vivix/ivi-sdk-ts';
|
|
2
|
+
import { createContext, useState, useEffect, useMemo, useContext, useRef, useCallback } from 'react';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/runtime/runtime-coordinator.ts
|
|
6
|
+
|
|
7
|
+
// src/runtime/logger.ts
|
|
8
|
+
var EVENT_LOG_PREFIX = "[IVI-EVT]";
|
|
9
|
+
var STATE_LOG_PREFIX = "[IVI-STATE]";
|
|
10
|
+
var activeLogCallback;
|
|
11
|
+
function runWithIviRuntimeLogCallback(onLog, fn) {
|
|
12
|
+
const previous = activeLogCallback;
|
|
13
|
+
activeLogCallback = onLog;
|
|
14
|
+
try {
|
|
15
|
+
return fn();
|
|
16
|
+
} finally {
|
|
17
|
+
activeLogCallback = previous;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function logIviEventReceived(event) {
|
|
21
|
+
const message = `${EVENT_LOG_PREFIX} ${event.type}`;
|
|
22
|
+
activeLogCallback?.({
|
|
23
|
+
level: "info",
|
|
24
|
+
tag: EVENT_LOG_PREFIX,
|
|
25
|
+
message,
|
|
26
|
+
args: [message, event.raw],
|
|
27
|
+
data: event.raw
|
|
28
|
+
});
|
|
29
|
+
console.log(message, event.raw);
|
|
30
|
+
}
|
|
31
|
+
function logIviStateChange(entity, key, eventType, before, after) {
|
|
32
|
+
if (!didChange(before, after)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const label = key === null ? entity : `${entity}[${key}]`;
|
|
36
|
+
const message = `${STATE_LOG_PREFIX} ${label} <- ${eventType}`;
|
|
37
|
+
const data = {
|
|
38
|
+
before,
|
|
39
|
+
after
|
|
40
|
+
};
|
|
41
|
+
activeLogCallback?.({
|
|
42
|
+
level: "info",
|
|
43
|
+
tag: STATE_LOG_PREFIX,
|
|
44
|
+
message,
|
|
45
|
+
args: [message, data],
|
|
46
|
+
data
|
|
47
|
+
});
|
|
48
|
+
console.log(message, data);
|
|
49
|
+
}
|
|
50
|
+
function didChange(before, after) {
|
|
51
|
+
if (before === after) return false;
|
|
52
|
+
if (before === void 0 || after === void 0) return true;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(before) !== JSON.stringify(after);
|
|
55
|
+
} catch {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/runtime/runtime-dispatcher.ts
|
|
61
|
+
var IviRuntimeDispatcher = class {
|
|
62
|
+
constructor(client, callbacks) {
|
|
63
|
+
this.unsubscribeEvent = null;
|
|
64
|
+
this.unsubscribeConnection = null;
|
|
65
|
+
this.client = client;
|
|
66
|
+
this.callbacks = callbacks;
|
|
67
|
+
}
|
|
68
|
+
start() {
|
|
69
|
+
if (this.unsubscribeEvent || this.unsubscribeConnection) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.unsubscribeEvent = this.client.onEvent((event) => {
|
|
73
|
+
this.dispatchEvent(event);
|
|
74
|
+
});
|
|
75
|
+
this.unsubscribeConnection = this.client.onConnectionChange((connected) => {
|
|
76
|
+
this.callbacks.onConnectionChange(connected);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
stop() {
|
|
80
|
+
this.unsubscribeEvent?.();
|
|
81
|
+
this.unsubscribeEvent = null;
|
|
82
|
+
this.unsubscribeConnection?.();
|
|
83
|
+
this.unsubscribeConnection = null;
|
|
84
|
+
}
|
|
85
|
+
dispatchEvent(event) {
|
|
86
|
+
runWithIviRuntimeLogCallback(this.callbacks.onLog, () => {
|
|
87
|
+
logIviEventReceived(event);
|
|
88
|
+
if (event.type === "session.created" || event.type === "session.ended") {
|
|
89
|
+
this.callbacks.sessionHandler.handle(event);
|
|
90
|
+
} else if (event.type.startsWith("session.stage.")) {
|
|
91
|
+
this.callbacks.stageHandler.handle(event);
|
|
92
|
+
} else if (event.type.startsWith("session.track.") || event.type.startsWith("session.tracks.")) {
|
|
93
|
+
this.callbacks.trackHandler.handle(event);
|
|
94
|
+
} else if (event.type.startsWith("session.source.") || event.type.startsWith("session.sources.")) {
|
|
95
|
+
this.callbacks.sourceHandler.handle(event);
|
|
96
|
+
} else if (event.type.startsWith("session.stream.") || event.type.startsWith("session.streams.")) {
|
|
97
|
+
this.callbacks.streamHandler.handle(event);
|
|
98
|
+
} else if (event.type.startsWith("conversation.") || event.type.startsWith("response.")) {
|
|
99
|
+
this.callbacks.conversationHandler.handle(event);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
this.callbacks.onEvent?.(event);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var SessionEventHandler = class {
|
|
106
|
+
constructor(sessionManager, callbacks) {
|
|
107
|
+
this.sessionManager = sessionManager;
|
|
108
|
+
this.callbacks = callbacks;
|
|
109
|
+
}
|
|
110
|
+
handle(event) {
|
|
111
|
+
if (event instanceof SessionCreatedEvent) {
|
|
112
|
+
const before = this.sessionManager.getSession();
|
|
113
|
+
this.sessionManager.setSession(event.session);
|
|
114
|
+
logIviStateChange("session", null, event.type, before, this.sessionManager.getSession());
|
|
115
|
+
this.callbacks.onSessionCreated(event);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (event instanceof ReceiveSessionEndedEvent) {
|
|
119
|
+
this.callbacks.onSessionEnded(event);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var StageEventHandler = class {
|
|
124
|
+
constructor(stageManager, callbacks) {
|
|
125
|
+
this.stageManager = stageManager;
|
|
126
|
+
this.callbacks = callbacks;
|
|
127
|
+
}
|
|
128
|
+
handle(event) {
|
|
129
|
+
if (!(event instanceof ReceiveSessionStageGetResponseEvent) && !(event instanceof ReceiveSessionStageUpdatedEvent)) {
|
|
130
|
+
return {
|
|
131
|
+
handled: false,
|
|
132
|
+
fromStageGetResponse: false
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const before = this.stageManager.getStage();
|
|
136
|
+
this.stageManager.setStage(event.stage);
|
|
137
|
+
logIviStateChange("stage", null, event.type, before, this.stageManager.getStage());
|
|
138
|
+
const result = {
|
|
139
|
+
handled: true,
|
|
140
|
+
fromStageGetResponse: event instanceof ReceiveSessionStageGetResponseEvent
|
|
141
|
+
};
|
|
142
|
+
this.callbacks.onStageChanged({
|
|
143
|
+
fromStageGetResponse: result.fromStageGetResponse
|
|
144
|
+
});
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var TrackEventHandler = class {
|
|
149
|
+
constructor(trackManager, callbacks) {
|
|
150
|
+
this.trackManager = trackManager;
|
|
151
|
+
this.callbacks = callbacks;
|
|
152
|
+
}
|
|
153
|
+
handle(event) {
|
|
154
|
+
if (event instanceof ReceiveSessionTrackCreatedEvent) {
|
|
155
|
+
const trackId = event.track.track_id;
|
|
156
|
+
const before = this.trackManager.getAll().get(trackId);
|
|
157
|
+
this.trackManager.upsert(event.track);
|
|
158
|
+
logIviStateChange("track", trackId, event.type, before, this.trackManager.getAll().get(trackId));
|
|
159
|
+
this.callbacks.onTracksChanged({ listRefreshed: false });
|
|
160
|
+
return { handled: true, listRefreshed: false };
|
|
161
|
+
}
|
|
162
|
+
if (event instanceof ReceiveSessionTrackDeletedEvent) {
|
|
163
|
+
const before = this.trackManager.getAll().get(event.trackId);
|
|
164
|
+
this.trackManager.remove(event.trackId);
|
|
165
|
+
logIviStateChange("track", event.trackId, event.type, before, void 0);
|
|
166
|
+
this.callbacks.onTracksChanged({ listRefreshed: false });
|
|
167
|
+
return { handled: true, listRefreshed: false };
|
|
168
|
+
}
|
|
169
|
+
if (event instanceof ReceiveSessionTrackTookEvent) {
|
|
170
|
+
const before = this.trackManager.getAll().get(event.trackId);
|
|
171
|
+
if (before?.active_source_id === event.sourceId) {
|
|
172
|
+
return { handled: true, listRefreshed: false };
|
|
173
|
+
}
|
|
174
|
+
this.trackManager.applyTrackTook(event.trackId, event.sourceId);
|
|
175
|
+
logIviStateChange("track", event.trackId, event.type, before, this.trackManager.getAll().get(event.trackId));
|
|
176
|
+
this.callbacks.onTracksChanged({ listRefreshed: false });
|
|
177
|
+
return { handled: true, listRefreshed: false };
|
|
178
|
+
}
|
|
179
|
+
if (event instanceof ReceiveSessionTrackCuedEvent) {
|
|
180
|
+
const before = this.trackManager.getAll().get(event.trackId);
|
|
181
|
+
this.trackManager.applyTrackCued(event.trackId, event.sourceId);
|
|
182
|
+
logIviStateChange("track", event.trackId, event.type, before, this.trackManager.getAll().get(event.trackId));
|
|
183
|
+
this.callbacks.onTracksChanged({ listRefreshed: false });
|
|
184
|
+
return { handled: true, listRefreshed: false };
|
|
185
|
+
}
|
|
186
|
+
if (event instanceof ReceiveSessionTrackNextSetEvent) {
|
|
187
|
+
const before = this.trackManager.getAll().get(event.trackId);
|
|
188
|
+
this.trackManager.applyTrackNextSet(event.trackId, event.sourceId);
|
|
189
|
+
logIviStateChange("track", event.trackId, event.type, before, this.trackManager.getAll().get(event.trackId));
|
|
190
|
+
this.callbacks.onTracksChanged({ listRefreshed: false });
|
|
191
|
+
return { handled: true, listRefreshed: false };
|
|
192
|
+
}
|
|
193
|
+
if (event instanceof ReceiveSessionTracksListResponseEvent) {
|
|
194
|
+
const beforeSnapshot = snapshotMap(this.trackManager.getAll());
|
|
195
|
+
this.trackManager.replaceAll(event.tracks);
|
|
196
|
+
logIviStateChange("tracks(list)", null, event.type, beforeSnapshot, snapshotMap(this.trackManager.getAll()));
|
|
197
|
+
this.callbacks.onTracksChanged({ listRefreshed: true });
|
|
198
|
+
return { handled: true, listRefreshed: true };
|
|
199
|
+
}
|
|
200
|
+
return { handled: false, listRefreshed: false };
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function snapshotMap(map) {
|
|
204
|
+
const snap = {};
|
|
205
|
+
map.forEach((value, key) => {
|
|
206
|
+
snap[key] = value;
|
|
207
|
+
});
|
|
208
|
+
return snap;
|
|
209
|
+
}
|
|
210
|
+
var SourceEventHandler = class {
|
|
211
|
+
constructor(sourceManager, callbacks) {
|
|
212
|
+
this.sourceManager = sourceManager;
|
|
213
|
+
this.callbacks = callbacks;
|
|
214
|
+
}
|
|
215
|
+
handle(event) {
|
|
216
|
+
if (event instanceof ReceiveSessionSourceCreatedEvent) {
|
|
217
|
+
const sourceId = event.source.source_id;
|
|
218
|
+
const before = this.sourceManager.get(sourceId);
|
|
219
|
+
this.sourceManager.upsertCreated(event.source);
|
|
220
|
+
logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
|
|
221
|
+
this.callbacks.onSourcesChanged({ listRefreshed: false });
|
|
222
|
+
return { handled: true, listRefreshed: false };
|
|
223
|
+
}
|
|
224
|
+
if (event instanceof ReceiveSessionSourceReadyEvent) {
|
|
225
|
+
const before = this.sourceManager.get(event.sourceId);
|
|
226
|
+
this.sourceManager.markReady({
|
|
227
|
+
sourceId: event.sourceId,
|
|
228
|
+
kind: event.kind,
|
|
229
|
+
assetType: event.assetType,
|
|
230
|
+
playback: event.playback,
|
|
231
|
+
width: event.width,
|
|
232
|
+
height: event.height,
|
|
233
|
+
durationMs: event.durationMs,
|
|
234
|
+
hasAudio: event.hasAudio
|
|
235
|
+
});
|
|
236
|
+
logIviStateChange("source", event.sourceId, event.type, before, this.sourceManager.get(event.sourceId));
|
|
237
|
+
this.callbacks.onSourcesChanged({ listRefreshed: false });
|
|
238
|
+
return { handled: true, listRefreshed: false };
|
|
239
|
+
}
|
|
240
|
+
if (event instanceof ReceiveSessionSourceFailedEvent) {
|
|
241
|
+
const before = this.sourceManager.get(event.sourceId);
|
|
242
|
+
this.sourceManager.markFailed(event.sourceId, event.error);
|
|
243
|
+
logIviStateChange("source", event.sourceId, event.type, before, this.sourceManager.get(event.sourceId));
|
|
244
|
+
this.callbacks.onSourcesChanged({ listRefreshed: false });
|
|
245
|
+
return { handled: true, listRefreshed: false };
|
|
246
|
+
}
|
|
247
|
+
if (event instanceof ReceiveSessionSourceDeletedEvent) {
|
|
248
|
+
const before = this.sourceManager.get(event.sourceId);
|
|
249
|
+
this.sourceManager.remove(event.sourceId);
|
|
250
|
+
logIviStateChange("source", event.sourceId, event.type, before, void 0);
|
|
251
|
+
this.callbacks.onSourcesChanged({ listRefreshed: false });
|
|
252
|
+
return { handled: true, listRefreshed: false };
|
|
253
|
+
}
|
|
254
|
+
if (event instanceof ReceiveSessionSourcePreloadEvent) {
|
|
255
|
+
for (const source of event.sources) {
|
|
256
|
+
const sourceId = source.source_id;
|
|
257
|
+
const before = this.sourceManager.get(sourceId);
|
|
258
|
+
this.sourceManager.upsertCreated(source);
|
|
259
|
+
this.sourceManager.applyPreload(sourceId, {
|
|
260
|
+
autoclearAfterPlay: event.autoclearAfterPlay
|
|
261
|
+
});
|
|
262
|
+
logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
|
|
263
|
+
}
|
|
264
|
+
this.callbacks.onSourcesChanged({ listRefreshed: false });
|
|
265
|
+
return { handled: true, listRefreshed: false };
|
|
266
|
+
}
|
|
267
|
+
if (event instanceof ReceiveSessionSourceClearPreloadEvent) {
|
|
268
|
+
for (const sourceId of event.sourceIds ?? []) {
|
|
269
|
+
const before = this.sourceManager.get(sourceId);
|
|
270
|
+
this.sourceManager.clearPreload(sourceId);
|
|
271
|
+
logIviStateChange("source", sourceId, event.type, before, this.sourceManager.get(sourceId));
|
|
272
|
+
}
|
|
273
|
+
this.callbacks.onSourcesChanged({ listRefreshed: false });
|
|
274
|
+
return { handled: true, listRefreshed: false };
|
|
275
|
+
}
|
|
276
|
+
if (event instanceof ReceiveSessionSourcesListResponseEvent) {
|
|
277
|
+
const beforeSnapshot = snapshotMap2(this.sourceManager.getAll());
|
|
278
|
+
this.sourceManager.replaceAll(event.sources);
|
|
279
|
+
logIviStateChange("sources(list)", null, event.type, beforeSnapshot, snapshotMap2(this.sourceManager.getAll()));
|
|
280
|
+
this.callbacks.onSourcesChanged({ listRefreshed: true });
|
|
281
|
+
return { handled: true, listRefreshed: true };
|
|
282
|
+
}
|
|
283
|
+
if (event instanceof ReceiveSessionSourcePlaybackCompletedEvent) {
|
|
284
|
+
return { handled: true, listRefreshed: false };
|
|
285
|
+
}
|
|
286
|
+
return { handled: false, listRefreshed: false };
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function snapshotMap2(map) {
|
|
290
|
+
const snap = {};
|
|
291
|
+
map.forEach((value, key) => {
|
|
292
|
+
snap[key] = value;
|
|
293
|
+
});
|
|
294
|
+
return snap;
|
|
295
|
+
}
|
|
296
|
+
var StreamEventHandler = class {
|
|
297
|
+
constructor(streamManager, callbacks) {
|
|
298
|
+
this.streamManager = streamManager;
|
|
299
|
+
this.callbacks = callbacks;
|
|
300
|
+
}
|
|
301
|
+
handle(event) {
|
|
302
|
+
if (event instanceof SessionStreamCreatedEvent) {
|
|
303
|
+
const streamId = event.stream.stream_id;
|
|
304
|
+
const before = this.streamManager.getAll().get(streamId);
|
|
305
|
+
this.streamManager.upsertCreated(event.stream);
|
|
306
|
+
logIviStateChange("stream", streamId, event.type, before, this.streamManager.getAll().get(streamId));
|
|
307
|
+
this.callbacks.onStreamsChanged({ listRefreshed: false });
|
|
308
|
+
return { handled: true, listRefreshed: false };
|
|
309
|
+
}
|
|
310
|
+
if (event instanceof ReceiveSessionStreamStartedEvent) {
|
|
311
|
+
const before = this.streamManager.getAll().get(event.streamId);
|
|
312
|
+
this.streamManager.markStarted(event.streamId, event.trackId);
|
|
313
|
+
logIviStateChange("stream", event.streamId, event.type, before, this.streamManager.getAll().get(event.streamId));
|
|
314
|
+
this.callbacks.onStreamsChanged({ listRefreshed: false });
|
|
315
|
+
return { handled: true, listRefreshed: false };
|
|
316
|
+
}
|
|
317
|
+
if (event instanceof ReceiveSessionStreamEndedEvent) {
|
|
318
|
+
const before = this.streamManager.getAll().get(event.streamId);
|
|
319
|
+
this.streamManager.markEnded(event.streamId);
|
|
320
|
+
logIviStateChange("stream", event.streamId, event.type, before, this.streamManager.getAll().get(event.streamId));
|
|
321
|
+
this.callbacks.onStreamsChanged({ listRefreshed: false });
|
|
322
|
+
return { handled: true, listRefreshed: false };
|
|
323
|
+
}
|
|
324
|
+
if (event instanceof ReceiveSessionStreamFailedEvent) {
|
|
325
|
+
const before = this.streamManager.getAll().get(event.streamId);
|
|
326
|
+
this.streamManager.markFailed(event.streamId, event.error);
|
|
327
|
+
logIviStateChange("stream", event.streamId, event.type, before, this.streamManager.getAll().get(event.streamId));
|
|
328
|
+
this.callbacks.onStreamsChanged({ listRefreshed: false });
|
|
329
|
+
return { handled: true, listRefreshed: false };
|
|
330
|
+
}
|
|
331
|
+
if (event instanceof ReceiveSessionStreamDeletedEvent) {
|
|
332
|
+
const before = this.streamManager.getAll().get(event.streamId);
|
|
333
|
+
this.streamManager.remove(event.streamId);
|
|
334
|
+
logIviStateChange("stream", event.streamId, event.type, before, void 0);
|
|
335
|
+
this.callbacks.onStreamsChanged({ listRefreshed: false });
|
|
336
|
+
return { handled: true, listRefreshed: false };
|
|
337
|
+
}
|
|
338
|
+
if (event instanceof ReceiveSessionStreamsListResponseEvent) {
|
|
339
|
+
const beforeSnapshot = snapshotMap3(this.streamManager.getAll());
|
|
340
|
+
this.streamManager.replaceAll(event.streams);
|
|
341
|
+
logIviStateChange("streams(list)", null, event.type, beforeSnapshot, snapshotMap3(this.streamManager.getAll()));
|
|
342
|
+
this.callbacks.onStreamsChanged({ listRefreshed: true });
|
|
343
|
+
return { handled: true, listRefreshed: true };
|
|
344
|
+
}
|
|
345
|
+
return { handled: false, listRefreshed: false };
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
function snapshotMap3(map) {
|
|
349
|
+
const snap = {};
|
|
350
|
+
map.forEach((value, key) => {
|
|
351
|
+
snap[key] = value;
|
|
352
|
+
});
|
|
353
|
+
return snap;
|
|
354
|
+
}
|
|
355
|
+
var ConversationEventHandler = class {
|
|
356
|
+
constructor(conversationManager, callbacks) {
|
|
357
|
+
this.conversationManager = conversationManager;
|
|
358
|
+
this.callbacks = callbacks;
|
|
359
|
+
}
|
|
360
|
+
handle(event) {
|
|
361
|
+
if (event instanceof ReceiveConversationListResponseEvent) {
|
|
362
|
+
const before = snapshotMap4(this.conversationManager.getAllMap());
|
|
363
|
+
this.conversationManager.replaceAll(event.items);
|
|
364
|
+
logIviStateChange(
|
|
365
|
+
"conversations(list)",
|
|
366
|
+
null,
|
|
367
|
+
event.type,
|
|
368
|
+
before,
|
|
369
|
+
snapshotMap4(this.conversationManager.getAllMap())
|
|
370
|
+
);
|
|
371
|
+
this.callbacks.onConversationsChanged();
|
|
372
|
+
return { handled: true };
|
|
373
|
+
}
|
|
374
|
+
if (event instanceof ReceiveConversationItemAddedEvent) {
|
|
375
|
+
const before = this.conversationManager.getAllMap().get(event.item.id);
|
|
376
|
+
this.conversationManager.upsertAdded(event.item);
|
|
377
|
+
logIviStateChange(
|
|
378
|
+
"conversationItem",
|
|
379
|
+
event.item.id,
|
|
380
|
+
event.type,
|
|
381
|
+
before,
|
|
382
|
+
this.conversationManager.getAllMap().get(event.item.id)
|
|
383
|
+
);
|
|
384
|
+
this.callbacks.onConversationsChanged();
|
|
385
|
+
return { handled: true };
|
|
386
|
+
}
|
|
387
|
+
if (event instanceof ReceiveConversationItemDoneEvent) {
|
|
388
|
+
const before = this.conversationManager.getAllMap().get(event.item.id);
|
|
389
|
+
this.conversationManager.markDone(event.item);
|
|
390
|
+
logIviStateChange(
|
|
391
|
+
"conversationItem",
|
|
392
|
+
event.item.id,
|
|
393
|
+
event.type,
|
|
394
|
+
before,
|
|
395
|
+
this.conversationManager.getAllMap().get(event.item.id)
|
|
396
|
+
);
|
|
397
|
+
this.callbacks.onConversationsChanged();
|
|
398
|
+
return { handled: true };
|
|
399
|
+
}
|
|
400
|
+
if (event instanceof ReceiveResponseOutputTextDeltaEvent) {
|
|
401
|
+
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
402
|
+
this.conversationManager.applyTextDelta(event.itemId, event.delta);
|
|
403
|
+
logIviStateChange(
|
|
404
|
+
"conversationItem",
|
|
405
|
+
event.itemId,
|
|
406
|
+
event.type,
|
|
407
|
+
before,
|
|
408
|
+
this.conversationManager.getAllMap().get(event.itemId)
|
|
409
|
+
);
|
|
410
|
+
this.callbacks.onConversationsChanged();
|
|
411
|
+
return { handled: true };
|
|
412
|
+
}
|
|
413
|
+
if (event instanceof ReceiveResponseOutputTextDoneEvent) {
|
|
414
|
+
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
415
|
+
this.conversationManager.applyTextDone(event.itemId, event.text);
|
|
416
|
+
logIviStateChange(
|
|
417
|
+
"conversationItem",
|
|
418
|
+
event.itemId,
|
|
419
|
+
event.type,
|
|
420
|
+
before,
|
|
421
|
+
this.conversationManager.getAllMap().get(event.itemId)
|
|
422
|
+
);
|
|
423
|
+
this.callbacks.onConversationsChanged();
|
|
424
|
+
return { handled: true };
|
|
425
|
+
}
|
|
426
|
+
if (event instanceof ReceiveResponseOutputAudioTranscriptDeltaEvent) {
|
|
427
|
+
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
428
|
+
this.conversationManager.applyTranscriptDelta(event.itemId, event.delta);
|
|
429
|
+
logIviStateChange(
|
|
430
|
+
"conversationItem",
|
|
431
|
+
event.itemId,
|
|
432
|
+
event.type,
|
|
433
|
+
before,
|
|
434
|
+
this.conversationManager.getAllMap().get(event.itemId)
|
|
435
|
+
);
|
|
436
|
+
this.callbacks.onConversationsChanged();
|
|
437
|
+
return { handled: true };
|
|
438
|
+
}
|
|
439
|
+
if (event instanceof ReceiveResponseOutputAudioTranscriptDoneEvent) {
|
|
440
|
+
const before = this.conversationManager.getAllMap().get(event.itemId);
|
|
441
|
+
this.conversationManager.applyTranscriptDone(event.itemId, event.transcript);
|
|
442
|
+
logIviStateChange(
|
|
443
|
+
"conversationItem",
|
|
444
|
+
event.itemId,
|
|
445
|
+
event.type,
|
|
446
|
+
before,
|
|
447
|
+
this.conversationManager.getAllMap().get(event.itemId)
|
|
448
|
+
);
|
|
449
|
+
this.callbacks.onConversationsChanged();
|
|
450
|
+
return { handled: true };
|
|
451
|
+
}
|
|
452
|
+
if (event instanceof ReceiveResponseDoneEvent) {
|
|
453
|
+
if (event.response.id) {
|
|
454
|
+
const itemId = `item_${event.response.id}`;
|
|
455
|
+
const before = this.conversationManager.getAllMap().get(itemId);
|
|
456
|
+
this.conversationManager.markModelItemStatusFromResponse(event.response.id, event.response.status);
|
|
457
|
+
logIviStateChange(
|
|
458
|
+
"conversationItem",
|
|
459
|
+
itemId,
|
|
460
|
+
event.type,
|
|
461
|
+
before,
|
|
462
|
+
this.conversationManager.getAllMap().get(itemId)
|
|
463
|
+
);
|
|
464
|
+
this.callbacks.onConversationsChanged();
|
|
465
|
+
}
|
|
466
|
+
return { handled: true };
|
|
467
|
+
}
|
|
468
|
+
if (event instanceof ReceiveResponseCreatedEvent) {
|
|
469
|
+
return { handled: true };
|
|
470
|
+
}
|
|
471
|
+
return { handled: false };
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
function snapshotMap4(map) {
|
|
475
|
+
const snap = {};
|
|
476
|
+
map.forEach((value, key) => {
|
|
477
|
+
snap[key] = value;
|
|
478
|
+
});
|
|
479
|
+
return snap;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/runtime/managers/session-manager.ts
|
|
483
|
+
var SessionManager = class {
|
|
484
|
+
constructor() {
|
|
485
|
+
this.session = null;
|
|
486
|
+
}
|
|
487
|
+
setSession(session) {
|
|
488
|
+
this.session = session;
|
|
489
|
+
}
|
|
490
|
+
getSession() {
|
|
491
|
+
return this.session;
|
|
492
|
+
}
|
|
493
|
+
reset() {
|
|
494
|
+
this.session = null;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// src/runtime/managers/stage-manager.ts
|
|
499
|
+
var StageManager = class {
|
|
500
|
+
constructor() {
|
|
501
|
+
this.stage = null;
|
|
502
|
+
}
|
|
503
|
+
setStage(stage) {
|
|
504
|
+
this.stage = stage;
|
|
505
|
+
}
|
|
506
|
+
getStage() {
|
|
507
|
+
return this.stage;
|
|
508
|
+
}
|
|
509
|
+
reset() {
|
|
510
|
+
this.stage = null;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/runtime/managers/track-manager.ts
|
|
515
|
+
var TrackManager = class {
|
|
516
|
+
constructor() {
|
|
517
|
+
this.tracks = /* @__PURE__ */ new Map();
|
|
518
|
+
}
|
|
519
|
+
patchTrack(trackId, patch) {
|
|
520
|
+
const next = new Map(this.tracks);
|
|
521
|
+
const current = next.get(trackId) ?? { track_id: trackId };
|
|
522
|
+
next.set(trackId, {
|
|
523
|
+
...current,
|
|
524
|
+
...patch
|
|
525
|
+
});
|
|
526
|
+
this.tracks = next;
|
|
527
|
+
}
|
|
528
|
+
upsert(track) {
|
|
529
|
+
const next = new Map(this.tracks);
|
|
530
|
+
next.set(track.track_id, track);
|
|
531
|
+
this.tracks = next;
|
|
532
|
+
}
|
|
533
|
+
remove(trackId) {
|
|
534
|
+
const next = new Map(this.tracks);
|
|
535
|
+
next.delete(trackId);
|
|
536
|
+
this.tracks = next;
|
|
537
|
+
}
|
|
538
|
+
replaceAll(tracks) {
|
|
539
|
+
const next = /* @__PURE__ */ new Map();
|
|
540
|
+
tracks.forEach((track) => next.set(track.track_id, track));
|
|
541
|
+
this.tracks = next;
|
|
542
|
+
}
|
|
543
|
+
applyTrackTook(trackId, sourceId) {
|
|
544
|
+
const current = this.tracks.get(trackId);
|
|
545
|
+
const resolvedSourceId = sourceId ?? current?.next_source_id ?? null;
|
|
546
|
+
if (!resolvedSourceId) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
this.patchTrack(trackId, {
|
|
550
|
+
active_source_id: resolvedSourceId,
|
|
551
|
+
next_source_id: null
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
applyTrackCued(trackId, sourceId) {
|
|
555
|
+
this.patchTrack(trackId, {
|
|
556
|
+
active_source_id: sourceId,
|
|
557
|
+
next_source_id: null
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
applyTrackNextSet(trackId, sourceId) {
|
|
561
|
+
this.patchTrack(trackId, {
|
|
562
|
+
next_source_id: sourceId
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
findTrackIdByActiveSource(sourceId) {
|
|
566
|
+
for (const [trackId, track] of this.tracks) {
|
|
567
|
+
if (track.active_source_id === sourceId) return trackId;
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
has(trackId) {
|
|
572
|
+
return this.tracks.has(trackId);
|
|
573
|
+
}
|
|
574
|
+
getAll() {
|
|
575
|
+
return this.tracks;
|
|
576
|
+
}
|
|
577
|
+
reset() {
|
|
578
|
+
this.tracks = /* @__PURE__ */ new Map();
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/runtime/managers/source-manager.ts
|
|
583
|
+
var SourceManager = class {
|
|
584
|
+
constructor() {
|
|
585
|
+
this.sources = /* @__PURE__ */ new Map();
|
|
586
|
+
/**
|
|
587
|
+
* 记录本次运行期间曾经被某个 track 的 active/next 槽引用过的 source id 集合。
|
|
588
|
+
* 用于 autoclearAfterPlay 判定:只有曾经"上过 track"的 source,在其离开 track 时才需要清理预加载。
|
|
589
|
+
*/
|
|
590
|
+
this.sourcesEverReferencedByTrack = /* @__PURE__ */ new Set();
|
|
591
|
+
}
|
|
592
|
+
upsertCreated(source) {
|
|
593
|
+
const previous = this.sources.get(source.source_id);
|
|
594
|
+
const next = new Map(this.sources);
|
|
595
|
+
next.set(source.source_id, {
|
|
596
|
+
source,
|
|
597
|
+
status: previous?.status === "ready" ? "ready" : "created",
|
|
598
|
+
playback: previous?.playback,
|
|
599
|
+
width: previous?.width,
|
|
600
|
+
height: previous?.height,
|
|
601
|
+
durationMs: previous?.durationMs,
|
|
602
|
+
hasAudio: previous?.hasAudio,
|
|
603
|
+
error: previous?.status === "failed" ? previous.error : void 0,
|
|
604
|
+
preload: previous?.preload
|
|
605
|
+
});
|
|
606
|
+
this.sources = next;
|
|
607
|
+
}
|
|
608
|
+
markReady(payload) {
|
|
609
|
+
const previous = this.sources.get(payload.sourceId);
|
|
610
|
+
const next = new Map(this.sources);
|
|
611
|
+
next.set(payload.sourceId, {
|
|
612
|
+
source: previous?.source ?? this.buildReadyFallbackSource(payload.sourceId, payload.kind, payload.assetType),
|
|
613
|
+
status: "ready",
|
|
614
|
+
playback: payload.playback,
|
|
615
|
+
width: payload.width,
|
|
616
|
+
height: payload.height,
|
|
617
|
+
durationMs: payload.durationMs,
|
|
618
|
+
hasAudio: payload.hasAudio,
|
|
619
|
+
preload: previous?.preload
|
|
620
|
+
});
|
|
621
|
+
this.sources = next;
|
|
622
|
+
}
|
|
623
|
+
markFailed(sourceId, error) {
|
|
624
|
+
const previous = this.sources.get(sourceId);
|
|
625
|
+
const next = new Map(this.sources);
|
|
626
|
+
next.set(sourceId, {
|
|
627
|
+
source: previous?.source ?? {
|
|
628
|
+
source_id: sourceId,
|
|
629
|
+
kind: "stream",
|
|
630
|
+
url: ""
|
|
631
|
+
},
|
|
632
|
+
status: "failed",
|
|
633
|
+
playback: previous?.playback,
|
|
634
|
+
width: previous?.width,
|
|
635
|
+
height: previous?.height,
|
|
636
|
+
durationMs: previous?.durationMs,
|
|
637
|
+
hasAudio: previous?.hasAudio,
|
|
638
|
+
error,
|
|
639
|
+
preload: previous?.preload
|
|
640
|
+
});
|
|
641
|
+
this.sources = next;
|
|
642
|
+
}
|
|
643
|
+
remove(sourceId) {
|
|
644
|
+
const next = new Map(this.sources);
|
|
645
|
+
next.delete(sourceId);
|
|
646
|
+
this.sources = next;
|
|
647
|
+
this.sourcesEverReferencedByTrack.delete(sourceId);
|
|
648
|
+
}
|
|
649
|
+
replaceAll(sources) {
|
|
650
|
+
const next = /* @__PURE__ */ new Map();
|
|
651
|
+
sources.forEach((source) => {
|
|
652
|
+
const previous = this.sources.get(source.source_id);
|
|
653
|
+
next.set(source.source_id, {
|
|
654
|
+
source,
|
|
655
|
+
status: source.playback ? "ready" : "created",
|
|
656
|
+
playback: source.playback,
|
|
657
|
+
preload: previous?.preload
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
this.sources = next;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* 应用一次显式预加载。若 source 目前尚未注册,会建立一个占位实例,
|
|
664
|
+
* 待后续 `session.source.created` / `ready` 事件补齐字段。
|
|
665
|
+
*/
|
|
666
|
+
applyPreload(sourceId, state) {
|
|
667
|
+
const previous = this.sources.get(sourceId);
|
|
668
|
+
const next = new Map(this.sources);
|
|
669
|
+
if (previous) {
|
|
670
|
+
next.set(sourceId, {
|
|
671
|
+
...previous,
|
|
672
|
+
preload: state
|
|
673
|
+
});
|
|
674
|
+
} else {
|
|
675
|
+
next.set(sourceId, {
|
|
676
|
+
source: {
|
|
677
|
+
source_id: sourceId,
|
|
678
|
+
kind: "stream",
|
|
679
|
+
url: ""
|
|
680
|
+
},
|
|
681
|
+
status: "created",
|
|
682
|
+
preload: state
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
this.sources = next;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* 清除指定 source 的预加载标记。
|
|
689
|
+
*/
|
|
690
|
+
clearPreload(sourceId) {
|
|
691
|
+
const previous = this.sources.get(sourceId);
|
|
692
|
+
if (!previous || !previous.preload) return;
|
|
693
|
+
const next = new Map(this.sources);
|
|
694
|
+
next.set(sourceId, {
|
|
695
|
+
...previous,
|
|
696
|
+
preload: void 0
|
|
697
|
+
});
|
|
698
|
+
this.sources = next;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* 根据当前 tracks 状态同步 source 运行态:
|
|
702
|
+
* 1) 将 track 的 active/next 引用的 source 由 `created` 提升为 `ready`(保留既有行为)。
|
|
703
|
+
* 2) 对当前不再被任何 track 引用、且 `preload.autoclearAfterPlay === true`、
|
|
704
|
+
* 且历史上曾经被 track 引用过的 source,自动清除其预加载标记。
|
|
705
|
+
*/
|
|
706
|
+
syncWithTracks(tracks) {
|
|
707
|
+
const currentlyReferencedIds = /* @__PURE__ */ new Set();
|
|
708
|
+
tracks.forEach((track) => {
|
|
709
|
+
if (track.active_source_id) {
|
|
710
|
+
currentlyReferencedIds.add(track.active_source_id);
|
|
711
|
+
}
|
|
712
|
+
if (track.next_source_id) {
|
|
713
|
+
currentlyReferencedIds.add(track.next_source_id);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
const next = new Map(this.sources);
|
|
717
|
+
let changed = false;
|
|
718
|
+
currentlyReferencedIds.forEach((sourceId) => {
|
|
719
|
+
if (!this.sourcesEverReferencedByTrack.has(sourceId)) {
|
|
720
|
+
this.sourcesEverReferencedByTrack.add(sourceId);
|
|
721
|
+
}
|
|
722
|
+
const existing = next.get(sourceId);
|
|
723
|
+
if (!existing) return;
|
|
724
|
+
if (existing.status === "created") {
|
|
725
|
+
next.set(sourceId, {
|
|
726
|
+
...existing,
|
|
727
|
+
status: "ready"
|
|
728
|
+
});
|
|
729
|
+
changed = true;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
next.forEach((source, sourceId) => {
|
|
733
|
+
if (!source.preload || !source.preload.autoclearAfterPlay) return;
|
|
734
|
+
if (currentlyReferencedIds.has(sourceId)) return;
|
|
735
|
+
if (!this.sourcesEverReferencedByTrack.has(sourceId)) return;
|
|
736
|
+
next.set(sourceId, {
|
|
737
|
+
...source,
|
|
738
|
+
preload: void 0
|
|
739
|
+
});
|
|
740
|
+
changed = true;
|
|
741
|
+
});
|
|
742
|
+
if (changed) {
|
|
743
|
+
this.sources = next;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
has(sourceId) {
|
|
747
|
+
return this.sources.has(sourceId);
|
|
748
|
+
}
|
|
749
|
+
get(sourceId) {
|
|
750
|
+
return this.sources.get(sourceId);
|
|
751
|
+
}
|
|
752
|
+
getAll() {
|
|
753
|
+
return this.sources;
|
|
754
|
+
}
|
|
755
|
+
reset() {
|
|
756
|
+
this.sources = /* @__PURE__ */ new Map();
|
|
757
|
+
this.sourcesEverReferencedByTrack = /* @__PURE__ */ new Set();
|
|
758
|
+
}
|
|
759
|
+
buildReadyFallbackSource(sourceId, kind, assetType) {
|
|
760
|
+
return {
|
|
761
|
+
source_id: sourceId,
|
|
762
|
+
kind,
|
|
763
|
+
asset_type: assetType
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// src/runtime/managers/stream-manager.ts
|
|
769
|
+
var StreamManager = class {
|
|
770
|
+
constructor() {
|
|
771
|
+
this.streams = /* @__PURE__ */ new Map();
|
|
772
|
+
}
|
|
773
|
+
upsertCreated(stream) {
|
|
774
|
+
const next = new Map(this.streams);
|
|
775
|
+
next.set(stream.stream_id, {
|
|
776
|
+
stream,
|
|
777
|
+
status: stream.status
|
|
778
|
+
});
|
|
779
|
+
this.streams = next;
|
|
780
|
+
}
|
|
781
|
+
markStarted(streamId, trackId) {
|
|
782
|
+
const previous = this.streams.get(streamId);
|
|
783
|
+
if (!previous) return;
|
|
784
|
+
const next = new Map(this.streams);
|
|
785
|
+
next.set(streamId, {
|
|
786
|
+
...previous,
|
|
787
|
+
status: "active",
|
|
788
|
+
stream: { ...previous.stream, status: "active" },
|
|
789
|
+
trackId
|
|
790
|
+
});
|
|
791
|
+
this.streams = next;
|
|
792
|
+
}
|
|
793
|
+
markEnded(streamId) {
|
|
794
|
+
const previous = this.streams.get(streamId);
|
|
795
|
+
if (!previous) return;
|
|
796
|
+
const next = new Map(this.streams);
|
|
797
|
+
next.set(streamId, {
|
|
798
|
+
...previous,
|
|
799
|
+
status: "ended",
|
|
800
|
+
stream: { ...previous.stream, status: "ended" }
|
|
801
|
+
});
|
|
802
|
+
this.streams = next;
|
|
803
|
+
}
|
|
804
|
+
markFailed(streamId, error) {
|
|
805
|
+
const previous = this.streams.get(streamId);
|
|
806
|
+
if (!previous) return;
|
|
807
|
+
const next = new Map(this.streams);
|
|
808
|
+
next.set(streamId, {
|
|
809
|
+
...previous,
|
|
810
|
+
status: "failed",
|
|
811
|
+
stream: { ...previous.stream, status: "failed" },
|
|
812
|
+
error
|
|
813
|
+
});
|
|
814
|
+
this.streams = next;
|
|
815
|
+
}
|
|
816
|
+
remove(streamId) {
|
|
817
|
+
const next = new Map(this.streams);
|
|
818
|
+
next.delete(streamId);
|
|
819
|
+
this.streams = next;
|
|
820
|
+
}
|
|
821
|
+
replaceAll(streams) {
|
|
822
|
+
const next = /* @__PURE__ */ new Map();
|
|
823
|
+
streams.forEach((stream) => {
|
|
824
|
+
const previous = this.streams.get(stream.stream_id);
|
|
825
|
+
next.set(stream.stream_id, {
|
|
826
|
+
stream,
|
|
827
|
+
status: stream.status,
|
|
828
|
+
trackId: previous?.trackId
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
this.streams = next;
|
|
832
|
+
}
|
|
833
|
+
has(streamId) {
|
|
834
|
+
return this.streams.has(streamId);
|
|
835
|
+
}
|
|
836
|
+
getAll() {
|
|
837
|
+
return this.streams;
|
|
838
|
+
}
|
|
839
|
+
reset() {
|
|
840
|
+
this.streams = /* @__PURE__ */ new Map();
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
// src/runtime/managers/conversation-manager.ts
|
|
845
|
+
var ConversationManager = class {
|
|
846
|
+
constructor() {
|
|
847
|
+
this.items = /* @__PURE__ */ new Map();
|
|
848
|
+
this.order = [];
|
|
849
|
+
}
|
|
850
|
+
replaceAll(items) {
|
|
851
|
+
const nextItems = /* @__PURE__ */ new Map();
|
|
852
|
+
const nextOrder = [];
|
|
853
|
+
items.forEach((item) => {
|
|
854
|
+
const runtimeItem = this.buildRuntimeItem(item, item.status === "in_progress" ? "added" : "done");
|
|
855
|
+
nextItems.set(runtimeItem.id, runtimeItem);
|
|
856
|
+
nextOrder.push(runtimeItem.id);
|
|
857
|
+
});
|
|
858
|
+
this.items = nextItems;
|
|
859
|
+
this.order = nextOrder;
|
|
860
|
+
}
|
|
861
|
+
upsertAdded(item) {
|
|
862
|
+
const previous = this.items.get(item.id);
|
|
863
|
+
const next = new Map(this.items);
|
|
864
|
+
const mergedItem = this.mergeConversationItem(previous?.item, item);
|
|
865
|
+
const mergedText = this.extractText(mergedItem.content);
|
|
866
|
+
const mergedTranscript = this.extractTranscript(mergedItem.content);
|
|
867
|
+
const runtimeItem = this.buildRuntimeItem(
|
|
868
|
+
mergedItem,
|
|
869
|
+
"added",
|
|
870
|
+
mergedText || previous?.text || "",
|
|
871
|
+
mergedTranscript || previous?.transcript || ""
|
|
872
|
+
);
|
|
873
|
+
next.set(item.id, runtimeItem);
|
|
874
|
+
this.items = next;
|
|
875
|
+
this.ensureOrder(item.id);
|
|
876
|
+
}
|
|
877
|
+
markDone(item) {
|
|
878
|
+
const previous = this.items.get(item.id);
|
|
879
|
+
const next = new Map(this.items);
|
|
880
|
+
const mergedItem = this.mergeConversationItem(previous?.item, item);
|
|
881
|
+
const mergedText = this.extractText(mergedItem.content);
|
|
882
|
+
const mergedTranscript = this.extractTranscript(mergedItem.content);
|
|
883
|
+
const runtimeItem = this.buildRuntimeItem(
|
|
884
|
+
mergedItem,
|
|
885
|
+
"done",
|
|
886
|
+
mergedText || previous?.text || "",
|
|
887
|
+
mergedTranscript || previous?.transcript || ""
|
|
888
|
+
);
|
|
889
|
+
if (runtimeItem.role === "model" && previous && previous.status !== "in_progress") {
|
|
890
|
+
runtimeItem.status = previous.status;
|
|
891
|
+
runtimeItem.item = {
|
|
892
|
+
...runtimeItem.item,
|
|
893
|
+
status: previous.status
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
next.set(item.id, runtimeItem);
|
|
897
|
+
this.items = next;
|
|
898
|
+
this.ensureOrder(item.id);
|
|
899
|
+
}
|
|
900
|
+
applyTextDelta(itemId, delta) {
|
|
901
|
+
const existing = this.items.get(itemId);
|
|
902
|
+
if (!existing) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.applyContentUpdate(itemId, {
|
|
906
|
+
text: `${existing.text}${delta}`,
|
|
907
|
+
transcript: existing.transcript,
|
|
908
|
+
content: this.upsertContentText(existing.content, `${existing.text}${delta}`)
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
applyTextDone(itemId, text) {
|
|
912
|
+
const existing = this.items.get(itemId);
|
|
913
|
+
if (!existing) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
this.applyContentUpdate(itemId, {
|
|
917
|
+
text,
|
|
918
|
+
transcript: existing.transcript,
|
|
919
|
+
content: this.upsertContentText(existing.content, text)
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
applyTranscriptDelta(itemId, delta) {
|
|
923
|
+
const existing = this.items.get(itemId);
|
|
924
|
+
if (!existing) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
this.applyContentUpdate(itemId, {
|
|
928
|
+
text: existing.text,
|
|
929
|
+
transcript: `${existing.transcript}${delta}`,
|
|
930
|
+
content: this.upsertContentTranscript(existing.content, `${existing.transcript}${delta}`)
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
applyTranscriptDone(itemId, transcript) {
|
|
934
|
+
const existing = this.items.get(itemId);
|
|
935
|
+
if (!existing) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
this.applyContentUpdate(itemId, {
|
|
939
|
+
text: existing.text,
|
|
940
|
+
transcript,
|
|
941
|
+
content: this.upsertContentTranscript(existing.content, transcript)
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
markModelItemStatusFromResponse(responseId, status) {
|
|
945
|
+
const itemId = `item_${responseId}`;
|
|
946
|
+
const existing = this.items.get(itemId);
|
|
947
|
+
if (!existing || existing.role !== "model") {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const nextStatus = this.mapResponseStatusToConversationStatus(status);
|
|
951
|
+
const next = new Map(this.items);
|
|
952
|
+
next.set(itemId, {
|
|
953
|
+
...existing,
|
|
954
|
+
lifecycle: "done",
|
|
955
|
+
status: nextStatus,
|
|
956
|
+
item: {
|
|
957
|
+
...existing.item,
|
|
958
|
+
status: nextStatus
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
this.items = next;
|
|
962
|
+
}
|
|
963
|
+
getAllMap() {
|
|
964
|
+
return this.items;
|
|
965
|
+
}
|
|
966
|
+
getOrderedList() {
|
|
967
|
+
return this.order.map((id) => this.items.get(id)).filter((item) => !!item);
|
|
968
|
+
}
|
|
969
|
+
reset() {
|
|
970
|
+
this.items = /* @__PURE__ */ new Map();
|
|
971
|
+
this.order = [];
|
|
972
|
+
}
|
|
973
|
+
applyContentUpdate(itemId, update) {
|
|
974
|
+
const existing = this.items.get(itemId);
|
|
975
|
+
if (!existing) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const next = new Map(this.items);
|
|
979
|
+
next.set(itemId, {
|
|
980
|
+
...existing,
|
|
981
|
+
text: update.text,
|
|
982
|
+
transcript: update.transcript,
|
|
983
|
+
content: update.content,
|
|
984
|
+
item: {
|
|
985
|
+
...existing.item,
|
|
986
|
+
content: update.content
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
this.items = next;
|
|
990
|
+
}
|
|
991
|
+
ensureOrder(itemId) {
|
|
992
|
+
if (this.order.includes(itemId)) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
this.order = [...this.order, itemId];
|
|
996
|
+
}
|
|
997
|
+
buildRuntimeItem(item, lifecycle, text = this.extractText(item.content), transcript = this.extractTranscript(item.content)) {
|
|
998
|
+
const role = item.role ?? "user";
|
|
999
|
+
return {
|
|
1000
|
+
id: item.id,
|
|
1001
|
+
role,
|
|
1002
|
+
lifecycle,
|
|
1003
|
+
status: this.normalizeConversationStatus(role, item.status),
|
|
1004
|
+
item,
|
|
1005
|
+
text,
|
|
1006
|
+
transcript,
|
|
1007
|
+
content: item.content ?? []
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
normalizeConversationStatus(role, status) {
|
|
1011
|
+
if (role === "model") {
|
|
1012
|
+
if (status === "completed") {
|
|
1013
|
+
return "completed";
|
|
1014
|
+
}
|
|
1015
|
+
if (status === "incomplete") {
|
|
1016
|
+
return "incomplete";
|
|
1017
|
+
}
|
|
1018
|
+
return "in_progress";
|
|
1019
|
+
}
|
|
1020
|
+
if (status === void 0) {
|
|
1021
|
+
return "completed";
|
|
1022
|
+
}
|
|
1023
|
+
return status === "in_progress" ? "in_progress" : "completed";
|
|
1024
|
+
}
|
|
1025
|
+
mapResponseStatusToConversationStatus(status) {
|
|
1026
|
+
if (status === "completed") {
|
|
1027
|
+
return "completed";
|
|
1028
|
+
}
|
|
1029
|
+
if (status === "in_progress") {
|
|
1030
|
+
return "in_progress";
|
|
1031
|
+
}
|
|
1032
|
+
return "incomplete";
|
|
1033
|
+
}
|
|
1034
|
+
mergeConversationItem(previous, next) {
|
|
1035
|
+
return {
|
|
1036
|
+
...previous,
|
|
1037
|
+
...next,
|
|
1038
|
+
content: next.content ?? previous?.content ?? []
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
extractText(content) {
|
|
1042
|
+
return content?.find((item) => item.type === "text")?.text ?? "";
|
|
1043
|
+
}
|
|
1044
|
+
extractTranscript(content) {
|
|
1045
|
+
return content?.find((item) => item.type === "audio")?.transcript ?? "";
|
|
1046
|
+
}
|
|
1047
|
+
upsertContentText(content, text) {
|
|
1048
|
+
const next = [...content];
|
|
1049
|
+
const textIndex = next.findIndex((item) => item.type === "text");
|
|
1050
|
+
if (textIndex >= 0) {
|
|
1051
|
+
next[textIndex] = {
|
|
1052
|
+
...next[textIndex],
|
|
1053
|
+
type: "text",
|
|
1054
|
+
text
|
|
1055
|
+
};
|
|
1056
|
+
return next;
|
|
1057
|
+
}
|
|
1058
|
+
return [
|
|
1059
|
+
...next,
|
|
1060
|
+
{
|
|
1061
|
+
type: "text",
|
|
1062
|
+
text
|
|
1063
|
+
}
|
|
1064
|
+
];
|
|
1065
|
+
}
|
|
1066
|
+
upsertContentTranscript(content, transcript) {
|
|
1067
|
+
const next = [...content];
|
|
1068
|
+
const audioIndex = next.findIndex((item) => item.type === "audio");
|
|
1069
|
+
if (audioIndex >= 0) {
|
|
1070
|
+
next[audioIndex] = {
|
|
1071
|
+
...next[audioIndex],
|
|
1072
|
+
type: "audio",
|
|
1073
|
+
transcript
|
|
1074
|
+
};
|
|
1075
|
+
return next;
|
|
1076
|
+
}
|
|
1077
|
+
return [
|
|
1078
|
+
...next,
|
|
1079
|
+
{
|
|
1080
|
+
type: "audio",
|
|
1081
|
+
transcript
|
|
1082
|
+
}
|
|
1083
|
+
];
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// src/runtime/managers/trtc-source-manager.ts
|
|
1088
|
+
var TAG = "[IVI-TRTC]";
|
|
1089
|
+
var TrtcSourceManager = class {
|
|
1090
|
+
constructor(onLog) {
|
|
1091
|
+
this.onLog = onLog;
|
|
1092
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
1093
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* 将 runtime 当前 sources 与 TRTC 会话池对齐:
|
|
1097
|
+
* - 对 ready + trtc 的 source 进行 upsert(必要时建立连接)
|
|
1098
|
+
* - 清理已不在 runtime 中的会话(释放资源)
|
|
1099
|
+
*/
|
|
1100
|
+
syncRuntimeSources(sources) {
|
|
1101
|
+
const existingIds = new Set(sources.keys());
|
|
1102
|
+
sources.forEach((source, sourceId) => {
|
|
1103
|
+
if (!isRuntimeTrtcSource(source)) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
this.upsertSource(sourceId, source.playback.trtc);
|
|
1107
|
+
});
|
|
1108
|
+
this.sessions.forEach((_session, sourceId) => {
|
|
1109
|
+
if (!existingIds.has(sourceId)) {
|
|
1110
|
+
this.removeSource(sourceId);
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* 按 sourceId 注册/更新 TRTC 配置。
|
|
1116
|
+
* 若配置发生变化,会先销毁旧会话再按新参数重建连接。
|
|
1117
|
+
*/
|
|
1118
|
+
upsertSource(sourceId, trtc) {
|
|
1119
|
+
const existing = this.sessions.get(sourceId);
|
|
1120
|
+
if (!existing) {
|
|
1121
|
+
this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${trtc.room_id} user=${trtc.user_id}`);
|
|
1122
|
+
const session2 = this.createSession(sourceId, trtc);
|
|
1123
|
+
this.sessions.set(sourceId, session2);
|
|
1124
|
+
void this.ensureConnected(session2);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (isSameTrtcConfig(existing.trtc, trtc)) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${trtc.room_id}`);
|
|
1131
|
+
void this.disposeSession(existing);
|
|
1132
|
+
const session = this.createSession(sourceId, trtc);
|
|
1133
|
+
this.sessions.set(sourceId, session);
|
|
1134
|
+
void this.ensureConnected(session);
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* 删除指定 source 的 TRTC 会话并释放连接资源。
|
|
1138
|
+
* 该操作通常由 source.deleted 或会话重置触发。
|
|
1139
|
+
*/
|
|
1140
|
+
removeSource(sourceId) {
|
|
1141
|
+
const session = this.sessions.get(sourceId);
|
|
1142
|
+
if (!session) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
this.log("info", `\u79FB\u9664\u4F1A\u8BDD source=${sourceId}`);
|
|
1146
|
+
this.sessions.delete(sourceId);
|
|
1147
|
+
void this.disposeSession(session);
|
|
1148
|
+
this.emitSnapshot(sourceId, {
|
|
1149
|
+
status: "idle"
|
|
1150
|
+
});
|
|
1151
|
+
this.listeners.delete(sourceId);
|
|
1152
|
+
}
|
|
1153
|
+
reset() {
|
|
1154
|
+
this.log("info", `\u91CD\u7F6E\u5168\u90E8\u4F1A\u8BDD count=${this.sessions.size}`);
|
|
1155
|
+
const sessions = Array.from(this.sessions.values());
|
|
1156
|
+
this.sessions.clear();
|
|
1157
|
+
sessions.forEach((session) => {
|
|
1158
|
+
void this.disposeSession(session);
|
|
1159
|
+
});
|
|
1160
|
+
this.listeners.clear();
|
|
1161
|
+
}
|
|
1162
|
+
subscribe(sourceId, listener) {
|
|
1163
|
+
const set = this.listeners.get(sourceId) ?? /* @__PURE__ */ new Set();
|
|
1164
|
+
set.add(listener);
|
|
1165
|
+
this.listeners.set(sourceId, set);
|
|
1166
|
+
listener(this.getSnapshot(sourceId));
|
|
1167
|
+
return () => {
|
|
1168
|
+
const target = this.listeners.get(sourceId);
|
|
1169
|
+
if (!target) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
target.delete(listener);
|
|
1173
|
+
if (target.size === 0) {
|
|
1174
|
+
this.listeners.delete(sourceId);
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
getSnapshot(sourceId) {
|
|
1179
|
+
const session = this.sessions.get(sourceId);
|
|
1180
|
+
if (!session) {
|
|
1181
|
+
return {
|
|
1182
|
+
status: "idle"
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
status: session.status,
|
|
1187
|
+
error: session.error
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* 检查指定 source 的 TRTC 会话是否曾收到过 REMOTE_VIDEO_AVAILABLE 事件。
|
|
1192
|
+
*/
|
|
1193
|
+
hasRemoteVideoAvailable(sourceId) {
|
|
1194
|
+
const session = this.sessions.get(sourceId);
|
|
1195
|
+
return session?.hasEverReceivedRemoteVideo ?? false;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* 等待指定 source 的 TRTC 会话首次收到 REMOTE_VIDEO_AVAILABLE。
|
|
1199
|
+
* - 若已收到过,立即返回 true。
|
|
1200
|
+
* - 若会话被销毁或超时,返回 false。
|
|
1201
|
+
*/
|
|
1202
|
+
waitForRemoteVideoAvailable(sourceId, timeoutMs = 3e4) {
|
|
1203
|
+
const session = this.sessions.get(sourceId);
|
|
1204
|
+
if (!session) return Promise.resolve(false);
|
|
1205
|
+
if (session.hasEverReceivedRemoteVideo) return Promise.resolve(true);
|
|
1206
|
+
return new Promise((resolve) => {
|
|
1207
|
+
let settled = false;
|
|
1208
|
+
const timer = setTimeout(() => {
|
|
1209
|
+
if (settled) return;
|
|
1210
|
+
settled = true;
|
|
1211
|
+
const idx = session.remoteVideoWaiters.indexOf(waiter);
|
|
1212
|
+
if (idx >= 0) session.remoteVideoWaiters.splice(idx, 1);
|
|
1213
|
+
resolve(false);
|
|
1214
|
+
}, timeoutMs);
|
|
1215
|
+
const waiter = (available) => {
|
|
1216
|
+
if (settled) return;
|
|
1217
|
+
settled = true;
|
|
1218
|
+
clearTimeout(timer);
|
|
1219
|
+
resolve(available);
|
|
1220
|
+
};
|
|
1221
|
+
session.remoteVideoWaiters.push(waiter);
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* 将一个渲染容器绑定到 source 会话:
|
|
1226
|
+
* - 确保连接已建立
|
|
1227
|
+
* - 回放历史已知远端视频轨道到该容器
|
|
1228
|
+
* - 应用全局音频策略(根据所有 view 的 muted 汇总)
|
|
1229
|
+
*/
|
|
1230
|
+
async attachView(sourceId, viewId, container, muted) {
|
|
1231
|
+
const session = this.sessions.get(sourceId);
|
|
1232
|
+
if (!session) {
|
|
1233
|
+
throw new Error(`TRTC source session not found: ${sourceId}`);
|
|
1234
|
+
}
|
|
1235
|
+
session.views.set(viewId, {
|
|
1236
|
+
container,
|
|
1237
|
+
muted,
|
|
1238
|
+
startedVideoKeys: /* @__PURE__ */ new Set(),
|
|
1239
|
+
startingVideoKeys: /* @__PURE__ */ new Set()
|
|
1240
|
+
});
|
|
1241
|
+
await this.ensureConnected(session);
|
|
1242
|
+
const binding = session.views.get(viewId);
|
|
1243
|
+
if (!binding) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
await this.renderKnownRemoteVideosToView(session, binding);
|
|
1247
|
+
await this.applyAudioPolicy(session);
|
|
1248
|
+
}
|
|
1249
|
+
detachView(sourceId, viewId) {
|
|
1250
|
+
const session = this.sessions.get(sourceId);
|
|
1251
|
+
if (!session) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
session.views.delete(viewId);
|
|
1255
|
+
void this.applyAudioPolicy(session);
|
|
1256
|
+
}
|
|
1257
|
+
updateViewMuted(sourceId, viewId, muted) {
|
|
1258
|
+
const session = this.sessions.get(sourceId);
|
|
1259
|
+
if (!session) {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const binding = session.views.get(viewId);
|
|
1263
|
+
if (!binding) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
binding.muted = muted;
|
|
1267
|
+
void this.applyAudioPolicy(session);
|
|
1268
|
+
}
|
|
1269
|
+
createSession(sourceId, trtc) {
|
|
1270
|
+
return {
|
|
1271
|
+
sourceId,
|
|
1272
|
+
trtc,
|
|
1273
|
+
TRTC: null,
|
|
1274
|
+
client: null,
|
|
1275
|
+
connectPromise: null,
|
|
1276
|
+
views: /* @__PURE__ */ new Map(),
|
|
1277
|
+
remoteVideoStreams: /* @__PURE__ */ new Set(),
|
|
1278
|
+
remoteAudioUsers: /* @__PURE__ */ new Set(),
|
|
1279
|
+
status: "idle",
|
|
1280
|
+
hasEverReceivedRemoteVideo: false,
|
|
1281
|
+
remoteVideoWaiters: []
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* 懒连接入口:同一会话只允许一次并发 connectPromise。
|
|
1286
|
+
* 建连后统一注册远端音视频事件,并持续向所有已绑定 view 分发渲染。
|
|
1287
|
+
*/
|
|
1288
|
+
async ensureConnected(session) {
|
|
1289
|
+
if (session.client && session.TRTC) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (session.connectPromise) {
|
|
1293
|
+
await session.connectPromise;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
session.status = "connecting";
|
|
1297
|
+
session.error = void 0;
|
|
1298
|
+
this.emitSnapshot(session.sourceId, {
|
|
1299
|
+
status: session.status
|
|
1300
|
+
});
|
|
1301
|
+
session.connectPromise = (async () => {
|
|
1302
|
+
const m = await import('trtc-sdk-v5');
|
|
1303
|
+
const TRTC = m.default ?? m;
|
|
1304
|
+
const sdkAppId = Number(session.trtc.app_id);
|
|
1305
|
+
if (!Number.isFinite(sdkAppId)) {
|
|
1306
|
+
throw new Error("TRTC app_id \u5FC5\u987B\u662F\u6570\u5B57\u5B57\u7B26\u4E32\u3002");
|
|
1307
|
+
}
|
|
1308
|
+
const isStringRoomId = shouldUseStringRoomId(session.trtc.room_id);
|
|
1309
|
+
const client = TRTC.create();
|
|
1310
|
+
const onRemoteVideoAvailable = (event) => {
|
|
1311
|
+
this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
|
|
1312
|
+
const remoteVideoKey = buildRemoteVideoKey(event.userId, String(event.streamType));
|
|
1313
|
+
session.remoteVideoStreams.add(remoteVideoKey);
|
|
1314
|
+
if (!session.hasEverReceivedRemoteVideo) {
|
|
1315
|
+
session.hasEverReceivedRemoteVideo = true;
|
|
1316
|
+
for (const waiter of session.remoteVideoWaiters) {
|
|
1317
|
+
waiter(true);
|
|
1318
|
+
}
|
|
1319
|
+
session.remoteVideoWaiters.length = 0;
|
|
1320
|
+
}
|
|
1321
|
+
session.views.forEach((binding) => {
|
|
1322
|
+
void this.startRemoteVideoForBinding(client, event.userId, event.streamType, binding, remoteVideoKey);
|
|
1323
|
+
});
|
|
1324
|
+
};
|
|
1325
|
+
const onRemoteVideoUnavailable = (event) => {
|
|
1326
|
+
this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
|
|
1327
|
+
const remoteVideoKey = buildRemoteVideoKey(event.userId, String(event.streamType));
|
|
1328
|
+
session.remoteVideoStreams.delete(remoteVideoKey);
|
|
1329
|
+
session.views.forEach((binding) => {
|
|
1330
|
+
binding.startedVideoKeys.delete(remoteVideoKey);
|
|
1331
|
+
binding.startingVideoKeys.delete(remoteVideoKey);
|
|
1332
|
+
});
|
|
1333
|
+
void client.stopRemoteVideo({
|
|
1334
|
+
userId: event.userId,
|
|
1335
|
+
streamType: event.streamType
|
|
1336
|
+
}).catch(() => void 0);
|
|
1337
|
+
};
|
|
1338
|
+
const onRemoteAudioAvailable = (event) => {
|
|
1339
|
+
this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
|
|
1340
|
+
session.remoteAudioUsers.add(event.userId);
|
|
1341
|
+
void this.applyAudioPolicy(session);
|
|
1342
|
+
};
|
|
1343
|
+
const onRemoteAudioUnavailable = (event) => {
|
|
1344
|
+
this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
|
|
1345
|
+
session.remoteAudioUsers.delete(event.userId);
|
|
1346
|
+
void this.applyAudioPolicy(session);
|
|
1347
|
+
};
|
|
1348
|
+
session.onRemoteVideoAvailable = onRemoteVideoAvailable;
|
|
1349
|
+
session.onRemoteVideoUnavailable = onRemoteVideoUnavailable;
|
|
1350
|
+
session.onRemoteAudioAvailable = onRemoteAudioAvailable;
|
|
1351
|
+
session.onRemoteAudioUnavailable = onRemoteAudioUnavailable;
|
|
1352
|
+
client.on(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, onRemoteVideoAvailable);
|
|
1353
|
+
client.on(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, onRemoteVideoUnavailable);
|
|
1354
|
+
client.on(TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, onRemoteAudioAvailable);
|
|
1355
|
+
client.on(TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, onRemoteAudioUnavailable);
|
|
1356
|
+
this.log("info", `\u6B63\u5728\u8FDB\u623F source=${session.sourceId} room=${session.trtc.room_id} sdkAppId=${sdkAppId} userId=${session.trtc.user_id}`);
|
|
1357
|
+
await client.enterRoom({
|
|
1358
|
+
sdkAppId,
|
|
1359
|
+
userId: session.trtc.user_id,
|
|
1360
|
+
userSig: session.trtc.user_sig,
|
|
1361
|
+
scene: TRTC.TYPE.SCENE_LIVE,
|
|
1362
|
+
role: TRTC.TYPE.ROLE_AUDIENCE,
|
|
1363
|
+
autoReceiveAudio: true,
|
|
1364
|
+
...isStringRoomId ? { strRoomId: session.trtc.room_id } : { roomId: Number(session.trtc.room_id) }
|
|
1365
|
+
});
|
|
1366
|
+
session.TRTC = TRTC;
|
|
1367
|
+
session.client = client;
|
|
1368
|
+
session.status = "connected";
|
|
1369
|
+
session.error = void 0;
|
|
1370
|
+
this.log("info", `\u8FDB\u623F\u6210\u529F source=${session.sourceId} room=${session.trtc.room_id}`);
|
|
1371
|
+
this.emitSnapshot(session.sourceId, {
|
|
1372
|
+
status: session.status
|
|
1373
|
+
});
|
|
1374
|
+
await this.applyAudioPolicy(session);
|
|
1375
|
+
})().catch((error) => {
|
|
1376
|
+
session.status = "error";
|
|
1377
|
+
session.error = error instanceof Error ? error.message : String(error);
|
|
1378
|
+
this.log("error", `\u8FDE\u63A5\u5931\u8D25 source=${session.sourceId} error=${session.error}`);
|
|
1379
|
+
this.emitSnapshot(session.sourceId, {
|
|
1380
|
+
status: session.status,
|
|
1381
|
+
error: session.error
|
|
1382
|
+
});
|
|
1383
|
+
throw error;
|
|
1384
|
+
}).finally(() => {
|
|
1385
|
+
session.connectPromise = null;
|
|
1386
|
+
});
|
|
1387
|
+
await session.connectPromise;
|
|
1388
|
+
}
|
|
1389
|
+
async renderKnownRemoteVideosToView(session, binding) {
|
|
1390
|
+
const client = session.client;
|
|
1391
|
+
if (!client) {
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
for (const key of session.remoteVideoStreams) {
|
|
1395
|
+
const parsed = parseRemoteVideoKey(key);
|
|
1396
|
+
if (!parsed) {
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
await this.startRemoteVideoForBinding(
|
|
1400
|
+
client,
|
|
1401
|
+
parsed.userId,
|
|
1402
|
+
parsed.streamType,
|
|
1403
|
+
binding,
|
|
1404
|
+
key
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
async startRemoteVideoForBinding(client, userId, streamType, binding, remoteVideoKey) {
|
|
1409
|
+
if (binding.startedVideoKeys.has(remoteVideoKey) || binding.startingVideoKeys.has(remoteVideoKey)) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
binding.startingVideoKeys.add(remoteVideoKey);
|
|
1413
|
+
try {
|
|
1414
|
+
await client.startRemoteVideo({
|
|
1415
|
+
userId,
|
|
1416
|
+
streamType,
|
|
1417
|
+
view: binding.container,
|
|
1418
|
+
option: { fillMode: "contain" }
|
|
1419
|
+
});
|
|
1420
|
+
binding.startedVideoKeys.add(remoteVideoKey);
|
|
1421
|
+
enforceContainMedia(binding.container);
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
if (error instanceof Error && /already started|OPERATION_ABORT/i.test(error.message)) {
|
|
1424
|
+
binding.startedVideoKeys.add(remoteVideoKey);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
this.log("warn", `\u8FDC\u7AEF\u89C6\u9891\u6E32\u67D3\u5931\u8D25 userId=${userId} streamType=${streamType}`, error);
|
|
1428
|
+
} finally {
|
|
1429
|
+
binding.startingVideoKeys.delete(remoteVideoKey);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* 统一音频策略:
|
|
1434
|
+
* - 所有 view 都 muted -> 全局静音
|
|
1435
|
+
* - 至少一个 view 需出声 -> 对可用远端用户解除静音
|
|
1436
|
+
*/
|
|
1437
|
+
async applyAudioPolicy(session) {
|
|
1438
|
+
const client = session.client;
|
|
1439
|
+
if (!client) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const hasUnmutedView = Array.from(session.views.values()).some((view) => !view.muted);
|
|
1443
|
+
if (!hasUnmutedView) {
|
|
1444
|
+
await client.muteRemoteAudio("*", true);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const activeUsers = Array.from(session.remoteAudioUsers);
|
|
1448
|
+
if (activeUsers.length === 0) {
|
|
1449
|
+
await client.muteRemoteAudio("*", false);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
for (const userId of activeUsers) {
|
|
1453
|
+
await client.muteRemoteAudio(userId, false);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* 销毁单个 source 会话:
|
|
1458
|
+
* 解绑事件、停止远端视频、退出房间并销毁客户端。
|
|
1459
|
+
*/
|
|
1460
|
+
async disposeSession(session) {
|
|
1461
|
+
const TRTC = session.TRTC;
|
|
1462
|
+
const client = session.client;
|
|
1463
|
+
session.connectPromise = null;
|
|
1464
|
+
session.views.clear();
|
|
1465
|
+
session.remoteAudioUsers.clear();
|
|
1466
|
+
session.remoteVideoStreams.clear();
|
|
1467
|
+
session.status = "idle";
|
|
1468
|
+
session.error = void 0;
|
|
1469
|
+
for (const waiter of session.remoteVideoWaiters) {
|
|
1470
|
+
waiter(false);
|
|
1471
|
+
}
|
|
1472
|
+
session.remoteVideoWaiters.length = 0;
|
|
1473
|
+
session.hasEverReceivedRemoteVideo = false;
|
|
1474
|
+
if (!client) {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
this.log("info", `\u9500\u6BC1\u4F1A\u8BDD source=${session.sourceId}`);
|
|
1478
|
+
try {
|
|
1479
|
+
if (TRTC && session.onRemoteVideoAvailable && session.onRemoteVideoUnavailable) {
|
|
1480
|
+
client.off(TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, session.onRemoteVideoAvailable);
|
|
1481
|
+
client.off(TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, session.onRemoteVideoUnavailable);
|
|
1482
|
+
}
|
|
1483
|
+
if (TRTC && session.onRemoteAudioAvailable && session.onRemoteAudioUnavailable) {
|
|
1484
|
+
client.off(TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, session.onRemoteAudioAvailable);
|
|
1485
|
+
client.off(TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, session.onRemoteAudioUnavailable);
|
|
1486
|
+
}
|
|
1487
|
+
await client.stopRemoteVideo({ userId: "*" });
|
|
1488
|
+
await client.exitRoom();
|
|
1489
|
+
this.log("info", `\u9000\u623F\u5B8C\u6210 source=${session.sourceId}`);
|
|
1490
|
+
client.destroy();
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
this.log("warn", `\u9000\u623F/\u9500\u6BC1\u5F02\u5E38 source=${session.sourceId}`, err);
|
|
1493
|
+
try {
|
|
1494
|
+
client.destroy();
|
|
1495
|
+
} catch {
|
|
1496
|
+
}
|
|
1497
|
+
} finally {
|
|
1498
|
+
session.TRTC = null;
|
|
1499
|
+
session.client = null;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
emitSnapshot(sourceId, snapshot) {
|
|
1503
|
+
const targetListeners = this.listeners.get(sourceId);
|
|
1504
|
+
if (!targetListeners) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
targetListeners.forEach((listener) => listener(snapshot));
|
|
1508
|
+
}
|
|
1509
|
+
log(level, message, ...extra) {
|
|
1510
|
+
const args = [TAG, message, ...extra];
|
|
1511
|
+
this.onLog?.({
|
|
1512
|
+
level,
|
|
1513
|
+
tag: TAG,
|
|
1514
|
+
message: `${TAG} ${message}`,
|
|
1515
|
+
args,
|
|
1516
|
+
data: extra.length > 0 ? { message, extra } : { message }
|
|
1517
|
+
});
|
|
1518
|
+
if (level === "error") {
|
|
1519
|
+
console.error(...args);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (level === "warn") {
|
|
1523
|
+
console.warn(...args);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
console.log(...args);
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
function isRuntimeTrtcSource(source) {
|
|
1530
|
+
return source.status === "ready" && source.playback?.type === "trtc" && Boolean(source.playback.trtc);
|
|
1531
|
+
}
|
|
1532
|
+
function isSameTrtcConfig(a, b) {
|
|
1533
|
+
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;
|
|
1534
|
+
}
|
|
1535
|
+
function shouldUseStringRoomId(roomId) {
|
|
1536
|
+
if (!/^\d+$/.test(roomId)) {
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
const roomNumber = Number(roomId);
|
|
1540
|
+
return !Number.isInteger(roomNumber) || roomNumber < 1 || roomNumber > 4294967294;
|
|
1541
|
+
}
|
|
1542
|
+
function buildRemoteVideoKey(userId, streamType) {
|
|
1543
|
+
return `${userId}::${streamType}`;
|
|
1544
|
+
}
|
|
1545
|
+
function parseRemoteVideoKey(key) {
|
|
1546
|
+
const separatorIndex = key.indexOf("::");
|
|
1547
|
+
if (separatorIndex < 0) {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
userId: key.slice(0, separatorIndex),
|
|
1552
|
+
streamType: key.slice(separatorIndex + 2)
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function enforceContainMedia(container) {
|
|
1556
|
+
const playerNodes = container.querySelectorAll("[id^='player_'], [id^='video_'], [id^='audio_']");
|
|
1557
|
+
playerNodes.forEach((node) => {
|
|
1558
|
+
if (!(node instanceof HTMLElement)) {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
node.style.setProperty("width", "100%", "important");
|
|
1562
|
+
node.style.setProperty("height", "100%", "important");
|
|
1563
|
+
node.style.setProperty("max-width", "100%", "important");
|
|
1564
|
+
node.style.setProperty("max-height", "100%", "important");
|
|
1565
|
+
});
|
|
1566
|
+
const mediaElements = container.querySelectorAll("video, canvas");
|
|
1567
|
+
mediaElements.forEach((element) => {
|
|
1568
|
+
if (!(element instanceof HTMLElement)) {
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
element.style.setProperty("width", "100%", "important");
|
|
1572
|
+
element.style.setProperty("height", "100%", "important");
|
|
1573
|
+
element.style.setProperty("max-width", "100%", "important");
|
|
1574
|
+
element.style.setProperty("max-height", "100%", "important");
|
|
1575
|
+
element.style.setProperty("object-fit", "contain", "important");
|
|
1576
|
+
element.style.setProperty("display", "block", "important");
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/runtime/runtime-coordinator.ts
|
|
1581
|
+
var IviRuntimeCoordinator = class {
|
|
1582
|
+
/**
|
|
1583
|
+
* @param client 底层实时客户端,负责实际事件收发。
|
|
1584
|
+
* @param config 协调行为配置(例如会话建立后的同步策略)。
|
|
1585
|
+
*/
|
|
1586
|
+
constructor(client, config) {
|
|
1587
|
+
this.sessionManager = new SessionManager();
|
|
1588
|
+
this.stageManager = new StageManager();
|
|
1589
|
+
this.trackManager = new TrackManager();
|
|
1590
|
+
this.sourceManager = new SourceManager();
|
|
1591
|
+
this.streamManager = new StreamManager();
|
|
1592
|
+
this.conversationManager = new ConversationManager();
|
|
1593
|
+
this.stateListeners = /* @__PURE__ */ new Set();
|
|
1594
|
+
this.eventListeners = /* @__PURE__ */ new Set();
|
|
1595
|
+
this.pendingUserTextToResponseFlows = /* @__PURE__ */ new Map();
|
|
1596
|
+
this.userTextFlowCounter = 0;
|
|
1597
|
+
this.waitingTracksListValidation = false;
|
|
1598
|
+
this.waitingSourcesListValidation = false;
|
|
1599
|
+
this.state = {
|
|
1600
|
+
status: "idle",
|
|
1601
|
+
session: null,
|
|
1602
|
+
stage: null,
|
|
1603
|
+
tracks: /* @__PURE__ */ new Map(),
|
|
1604
|
+
sources: /* @__PURE__ */ new Map(),
|
|
1605
|
+
streams: /* @__PURE__ */ new Map(),
|
|
1606
|
+
conversationItems: /* @__PURE__ */ new Map(),
|
|
1607
|
+
conversations: []
|
|
1608
|
+
};
|
|
1609
|
+
this.client = client;
|
|
1610
|
+
this.config = {
|
|
1611
|
+
syncStageOnSessionCreated: true,
|
|
1612
|
+
...config
|
|
1613
|
+
};
|
|
1614
|
+
this.trtcSourceManager = new TrtcSourceManager(this.config.onLog);
|
|
1615
|
+
this.sessionHandler = new SessionEventHandler(this.sessionManager, {
|
|
1616
|
+
onSessionCreated: (event) => this.onSessionCreated(event),
|
|
1617
|
+
onSessionEnded: (event) => this.onSessionEnded(event)
|
|
1618
|
+
});
|
|
1619
|
+
this.stageHandler = new StageEventHandler(this.stageManager, {
|
|
1620
|
+
onStageChanged: ({ fromStageGetResponse }) => this.onStageChanged(fromStageGetResponse)
|
|
1621
|
+
});
|
|
1622
|
+
this.trackHandler = new TrackEventHandler(this.trackManager, {
|
|
1623
|
+
onTracksChanged: ({ listRefreshed }) => this.onTracksChanged(listRefreshed)
|
|
1624
|
+
});
|
|
1625
|
+
this.sourceHandler = new SourceEventHandler(this.sourceManager, {
|
|
1626
|
+
onSourcesChanged: ({ listRefreshed }) => this.onSourcesChanged(listRefreshed)
|
|
1627
|
+
});
|
|
1628
|
+
this.streamHandler = new StreamEventHandler(this.streamManager, {
|
|
1629
|
+
onStreamsChanged: () => this.onStreamsChanged()
|
|
1630
|
+
});
|
|
1631
|
+
this.conversationHandler = new ConversationEventHandler(this.conversationManager, {
|
|
1632
|
+
onConversationsChanged: () => this.onConversationsChanged()
|
|
1633
|
+
});
|
|
1634
|
+
this.dispatcher = new IviRuntimeDispatcher(client, {
|
|
1635
|
+
sessionHandler: this.sessionHandler,
|
|
1636
|
+
stageHandler: this.stageHandler,
|
|
1637
|
+
trackHandler: this.trackHandler,
|
|
1638
|
+
sourceHandler: this.sourceHandler,
|
|
1639
|
+
streamHandler: this.streamHandler,
|
|
1640
|
+
conversationHandler: this.conversationHandler,
|
|
1641
|
+
onConnectionChange: (connected) => this.onConnectionChange(connected),
|
|
1642
|
+
onEvent: (event) => this.emitEvent(event),
|
|
1643
|
+
onLog: this.config.onLog
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* 启动协调器并建立实时连接。
|
|
1648
|
+
* 启动后状态会从 idle/stopped 进入 connecting,连接成功后由事件驱动进入后续状态。
|
|
1649
|
+
*/
|
|
1650
|
+
async start() {
|
|
1651
|
+
this.setState({
|
|
1652
|
+
...this.state,
|
|
1653
|
+
status: "connecting"
|
|
1654
|
+
});
|
|
1655
|
+
this.dispatcher.start();
|
|
1656
|
+
try {
|
|
1657
|
+
await this.client.connect();
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
this.stop();
|
|
1660
|
+
throw error;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* 停止协调器并断开实时连接,同时重置运行态数据为 stopped。
|
|
1665
|
+
*/
|
|
1666
|
+
stop() {
|
|
1667
|
+
this.dispatcher.stop();
|
|
1668
|
+
this.client.disconnect();
|
|
1669
|
+
this.resetStoppedState();
|
|
1670
|
+
}
|
|
1671
|
+
getState() {
|
|
1672
|
+
return this.state;
|
|
1673
|
+
}
|
|
1674
|
+
onStateChange(listener) {
|
|
1675
|
+
this.stateListeners.add(listener);
|
|
1676
|
+
return () => {
|
|
1677
|
+
this.stateListeners.delete(listener);
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
onEvent(listener) {
|
|
1681
|
+
this.eventListeners.add(listener);
|
|
1682
|
+
return () => {
|
|
1683
|
+
this.eventListeners.delete(listener);
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* 编排一次"用户文本输入 -> 触发模型回复"链路。
|
|
1688
|
+
*
|
|
1689
|
+
* 流程:
|
|
1690
|
+
* 1) 发送 conversation.item.create(可指定 itemId);
|
|
1691
|
+
* 2) 等待 conversation.item.added + conversation.item.done;
|
|
1692
|
+
* 3) 发送 response.create(携带 item_reference,可选绑定 stream);
|
|
1693
|
+
* 4) 等待 response.created 并结束。
|
|
1694
|
+
*/
|
|
1695
|
+
sendUserTextAndTriggerResponse(options) {
|
|
1696
|
+
const normalizedStreamId = options.streamId?.trim() || void 0;
|
|
1697
|
+
const normalizedText = options.text.trim();
|
|
1698
|
+
if (!normalizedText) {
|
|
1699
|
+
throw new Error("text must be a non-empty string.");
|
|
1700
|
+
}
|
|
1701
|
+
const normalizedItemId = typeof options.itemId === "string" && options.itemId.trim().length > 0 ? options.itemId.trim() : void 0;
|
|
1702
|
+
if (normalizedItemId && this.pendingUserTextToResponseFlows.has(normalizedItemId)) {
|
|
1703
|
+
throw new Error(`Conversation item flow already exists for itemId: ${normalizedItemId}`);
|
|
1704
|
+
}
|
|
1705
|
+
if (normalizedStreamId && this.hasPendingResponseFlow(normalizedStreamId)) {
|
|
1706
|
+
throw new Error(`Response flow already pending for streamId: ${normalizedStreamId}`);
|
|
1707
|
+
}
|
|
1708
|
+
const itemId = normalizedItemId ?? this.buildUserTextItemId();
|
|
1709
|
+
return new Promise((resolve, reject) => {
|
|
1710
|
+
this.pendingUserTextToResponseFlows.set(itemId, {
|
|
1711
|
+
itemId,
|
|
1712
|
+
streamId: normalizedStreamId,
|
|
1713
|
+
response: options.response,
|
|
1714
|
+
callbacks: options.callbacks,
|
|
1715
|
+
responseRequested: false,
|
|
1716
|
+
resolve,
|
|
1717
|
+
reject
|
|
1718
|
+
});
|
|
1719
|
+
this.client.sendConversationUserText(normalizedText, itemId).catch((error) => {
|
|
1720
|
+
this.pendingUserTextToResponseFlows.delete(itemId);
|
|
1721
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1722
|
+
});
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* 上报某个 source 在指定 track 上播放完成。
|
|
1727
|
+
*
|
|
1728
|
+
* - 若 next 槽位为空,只发送 complete。
|
|
1729
|
+
* - 若 next 槽位已有 source,立即将 next 提升为 active,并发送 complete 与 take。
|
|
1730
|
+
* - 当前为普通视频/m3u8 且 next 为 TRTC source 时,等待 TRTC 远端视频可用后再切换并发送 complete/take。
|
|
1731
|
+
*/
|
|
1732
|
+
sendSessionSourcePlaybackCompleted(sourceId, trackId) {
|
|
1733
|
+
const resolvedTrackId = trackId ?? this.trackManager.findTrackIdByActiveSource(sourceId);
|
|
1734
|
+
if (!resolvedTrackId) {
|
|
1735
|
+
this.client.sendSessionSourcePlaybackCompleted(sourceId, trackId);
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const track = this.trackManager.getAll().get(resolvedTrackId);
|
|
1739
|
+
if (!track) {
|
|
1740
|
+
this.client.sendSessionSourcePlaybackCompleted(sourceId, trackId);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (track.next_source_id) {
|
|
1744
|
+
const currentSource = this.sourceManager.get(sourceId);
|
|
1745
|
+
const nextSource = this.sourceManager.get(track.next_source_id);
|
|
1746
|
+
if (isVideoPlaybackSource(currentSource) && isTrtcPlaybackSource(nextSource)) {
|
|
1747
|
+
void this.deferredTrtcTakeCompleteAndTake(resolvedTrackId, track.next_source_id, sourceId, trackId);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
this.applyLocalTrackTake(resolvedTrackId);
|
|
1751
|
+
this.client.sendSessionSourcePlaybackCompleted(sourceId, trackId);
|
|
1752
|
+
this.sendSessionTrackTake(resolvedTrackId);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
this.client.sendSessionSourcePlaybackCompleted(sourceId, trackId);
|
|
1756
|
+
}
|
|
1757
|
+
getTrtcSourceManager() {
|
|
1758
|
+
return this.trtcSourceManager;
|
|
1759
|
+
}
|
|
1760
|
+
onConnectionChange(connected) {
|
|
1761
|
+
if (connected || this.state.status === "idle") {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (this.state.status === "stopped") {
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
this.resetStoppedState();
|
|
1768
|
+
}
|
|
1769
|
+
validateStageTracks(stage, hasTrack) {
|
|
1770
|
+
const missingTrackIds = this.getMissingTrackIds(stage, hasTrack);
|
|
1771
|
+
if (missingTrackIds.length === 0) {
|
|
1772
|
+
return false;
|
|
1773
|
+
}
|
|
1774
|
+
this.waitingTracksListValidation = true;
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1777
|
+
validateTracksListRefreshed(stage, hasTrack) {
|
|
1778
|
+
if (!this.waitingTracksListValidation) {
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
this.waitingTracksListValidation = false;
|
|
1782
|
+
const missingTrackIds = this.getMissingTrackIds(stage, hasTrack);
|
|
1783
|
+
if (missingTrackIds.length > 0) {
|
|
1784
|
+
throw new Error(
|
|
1785
|
+
`Runtime track sync failed: missing tracks after session.tracks.list.response: ${missingTrackIds.join(", ")}`
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
reset() {
|
|
1790
|
+
this.waitingTracksListValidation = false;
|
|
1791
|
+
this.waitingSourcesListValidation = false;
|
|
1792
|
+
}
|
|
1793
|
+
onSessionCreated(event) {
|
|
1794
|
+
this.stageManager.reset();
|
|
1795
|
+
this.trackManager.reset();
|
|
1796
|
+
this.sourceManager.reset();
|
|
1797
|
+
this.streamManager.reset();
|
|
1798
|
+
this.trtcSourceManager.reset();
|
|
1799
|
+
this.conversationManager.reset();
|
|
1800
|
+
this.reset();
|
|
1801
|
+
const nextStatus = this.config.syncStageOnSessionCreated !== false ? "syncing" : "running";
|
|
1802
|
+
this.setState({
|
|
1803
|
+
status: nextStatus,
|
|
1804
|
+
session: event.session,
|
|
1805
|
+
stage: this.stageManager.getStage(),
|
|
1806
|
+
tracks: this.trackManager.getAll(),
|
|
1807
|
+
sources: this.sourceManager.getAll(),
|
|
1808
|
+
streams: this.streamManager.getAll(),
|
|
1809
|
+
conversationItems: this.conversationManager.getAllMap(),
|
|
1810
|
+
conversations: this.conversationManager.getOrderedList()
|
|
1811
|
+
});
|
|
1812
|
+
if (this.config.syncStageOnSessionCreated !== false) {
|
|
1813
|
+
this.client.sendSessionStageGet();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
onSessionEnded(event) {
|
|
1817
|
+
this.resetStoppedState();
|
|
1818
|
+
this.config.onSessionEnded?.(event);
|
|
1819
|
+
}
|
|
1820
|
+
onStageChanged(fromStageGetResponse) {
|
|
1821
|
+
const nextStatus = fromStageGetResponse && this.state.status === "syncing" ? "running" : this.state.status;
|
|
1822
|
+
const nextStage = this.stageManager.getStage();
|
|
1823
|
+
this.setState({
|
|
1824
|
+
...this.state,
|
|
1825
|
+
status: nextStatus,
|
|
1826
|
+
stage: nextStage
|
|
1827
|
+
});
|
|
1828
|
+
const shouldFetchTracks = this.validateStageTracks(nextStage, (trackId) => this.trackManager.has(trackId));
|
|
1829
|
+
if (shouldFetchTracks) {
|
|
1830
|
+
this.client.sendSessionTracksList();
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
this.ensureSourcesSyncedForTracks();
|
|
1834
|
+
}
|
|
1835
|
+
onTracksChanged(listRefreshed) {
|
|
1836
|
+
const nextStage = this.stageManager.getStage();
|
|
1837
|
+
this.sourceManager.syncWithTracks(this.trackManager.getAll());
|
|
1838
|
+
this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
|
|
1839
|
+
this.setState({
|
|
1840
|
+
...this.state,
|
|
1841
|
+
tracks: this.trackManager.getAll(),
|
|
1842
|
+
sources: this.sourceManager.getAll()
|
|
1843
|
+
});
|
|
1844
|
+
if (listRefreshed) {
|
|
1845
|
+
this.validateTracksListRefreshed(nextStage, (trackId) => this.trackManager.has(trackId));
|
|
1846
|
+
}
|
|
1847
|
+
this.ensureSourcesSyncedForTracks();
|
|
1848
|
+
}
|
|
1849
|
+
applyLocalTrackTake(trackId) {
|
|
1850
|
+
this.trackManager.applyTrackTook(trackId);
|
|
1851
|
+
this.sourceManager.syncWithTracks(this.trackManager.getAll());
|
|
1852
|
+
this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
|
|
1853
|
+
this.setState({
|
|
1854
|
+
...this.state,
|
|
1855
|
+
tracks: this.trackManager.getAll(),
|
|
1856
|
+
sources: this.sourceManager.getAll()
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
async deferredTrtcTakeCompleteAndTake(trackId, nextSourceId, completedSourceId, completedTrackIdArg) {
|
|
1860
|
+
const remoteVideoAvailable = await this.trtcSourceManager.waitForRemoteVideoAvailable(nextSourceId);
|
|
1861
|
+
if (!remoteVideoAvailable) return;
|
|
1862
|
+
if (this.state.status !== "running") return;
|
|
1863
|
+
const currentTrack = this.trackManager.getAll().get(trackId);
|
|
1864
|
+
if (!currentTrack || currentTrack.next_source_id !== nextSourceId) {
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
this.applyLocalTrackTake(trackId);
|
|
1868
|
+
this.client.sendSessionSourcePlaybackCompleted(completedSourceId, completedTrackIdArg);
|
|
1869
|
+
this.sendSessionTrackTake(trackId);
|
|
1870
|
+
}
|
|
1871
|
+
sendSessionTrackTake(trackId) {
|
|
1872
|
+
const message = {
|
|
1873
|
+
type: "session.track.take",
|
|
1874
|
+
track_id: trackId
|
|
1875
|
+
};
|
|
1876
|
+
void this.client.send(message);
|
|
1877
|
+
}
|
|
1878
|
+
onSourcesChanged(listRefreshed) {
|
|
1879
|
+
this.sourceManager.syncWithTracks(this.trackManager.getAll());
|
|
1880
|
+
this.trtcSourceManager.syncRuntimeSources(this.sourceManager.getAll());
|
|
1881
|
+
this.setState({
|
|
1882
|
+
...this.state,
|
|
1883
|
+
sources: this.sourceManager.getAll()
|
|
1884
|
+
});
|
|
1885
|
+
if (listRefreshed) {
|
|
1886
|
+
this.validateSourcesListRefreshed((sourceId) => this.sourceManager.has(sourceId));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
onStreamsChanged() {
|
|
1890
|
+
this.setState({
|
|
1891
|
+
...this.state,
|
|
1892
|
+
streams: this.streamManager.getAll()
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
onConversationsChanged() {
|
|
1896
|
+
this.setState({
|
|
1897
|
+
...this.state,
|
|
1898
|
+
conversationItems: this.conversationManager.getAllMap(),
|
|
1899
|
+
conversations: this.conversationManager.getOrderedList()
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
setState(nextState) {
|
|
1903
|
+
if (this.state.status === nextState.status && this.state.session === nextState.session && this.state.stage === nextState.stage && this.state.tracks === nextState.tracks && this.state.sources === nextState.sources && this.state.streams === nextState.streams && this.state.conversationItems === nextState.conversationItems && this.state.conversations === nextState.conversations) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
this.state = nextState;
|
|
1907
|
+
this.stateListeners.forEach((listener) => listener(this.state));
|
|
1908
|
+
}
|
|
1909
|
+
emitEvent(event) {
|
|
1910
|
+
this.progressUserTextToResponseFlows(event);
|
|
1911
|
+
this.eventListeners.forEach((listener) => listener(event, this.state));
|
|
1912
|
+
}
|
|
1913
|
+
resetStoppedState() {
|
|
1914
|
+
this.rejectPendingUserTextToResponseFlows(
|
|
1915
|
+
new Error("Runtime stopped before user text to response flow completed.")
|
|
1916
|
+
);
|
|
1917
|
+
this.sessionManager.reset();
|
|
1918
|
+
this.stageManager.reset();
|
|
1919
|
+
this.trackManager.reset();
|
|
1920
|
+
this.sourceManager.reset();
|
|
1921
|
+
this.streamManager.reset();
|
|
1922
|
+
this.trtcSourceManager.reset();
|
|
1923
|
+
this.conversationManager.reset();
|
|
1924
|
+
this.reset();
|
|
1925
|
+
this.setState({
|
|
1926
|
+
status: "stopped",
|
|
1927
|
+
session: null,
|
|
1928
|
+
stage: null,
|
|
1929
|
+
tracks: /* @__PURE__ */ new Map(),
|
|
1930
|
+
sources: /* @__PURE__ */ new Map(),
|
|
1931
|
+
streams: /* @__PURE__ */ new Map(),
|
|
1932
|
+
conversationItems: /* @__PURE__ */ new Map(),
|
|
1933
|
+
conversations: []
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
ensureSourcesSyncedForTracks() {
|
|
1937
|
+
const missingSourceIds = this.getMissingSourceIds((sourceId) => this.sourceManager.has(sourceId));
|
|
1938
|
+
if (missingSourceIds.length === 0) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
this.waitingSourcesListValidation = true;
|
|
1942
|
+
this.client.sendSessionSourcesList();
|
|
1943
|
+
}
|
|
1944
|
+
validateSourcesListRefreshed(hasSource) {
|
|
1945
|
+
if (!this.waitingSourcesListValidation) {
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
this.waitingSourcesListValidation = false;
|
|
1949
|
+
const missingSourceIds = this.getMissingSourceIds(hasSource);
|
|
1950
|
+
if (missingSourceIds.length > 0) {
|
|
1951
|
+
throw new Error(
|
|
1952
|
+
`Runtime source sync failed: missing sources after session.sources.list.response: ${missingSourceIds.join(", ")}`
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
getMissingTrackIds(stage, hasTrack) {
|
|
1957
|
+
if (!stage) {
|
|
1958
|
+
return [];
|
|
1959
|
+
}
|
|
1960
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1961
|
+
stage.composition.forEach((item) => {
|
|
1962
|
+
if (!hasTrack(item.track_id)) {
|
|
1963
|
+
missing.add(item.track_id);
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
return Array.from(missing);
|
|
1967
|
+
}
|
|
1968
|
+
getMissingSourceIds(hasSource) {
|
|
1969
|
+
const missing = /* @__PURE__ */ new Set();
|
|
1970
|
+
this.trackManager.getAll().forEach((track) => {
|
|
1971
|
+
if (track.active_source_id && !hasSource(track.active_source_id)) {
|
|
1972
|
+
missing.add(track.active_source_id);
|
|
1973
|
+
}
|
|
1974
|
+
if (track.next_source_id && !hasSource(track.next_source_id)) {
|
|
1975
|
+
missing.add(track.next_source_id);
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
return Array.from(missing);
|
|
1979
|
+
}
|
|
1980
|
+
hasPendingResponseFlow(streamId) {
|
|
1981
|
+
for (const flow of this.pendingUserTextToResponseFlows.values()) {
|
|
1982
|
+
if (flow.streamId === streamId) {
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return false;
|
|
1987
|
+
}
|
|
1988
|
+
rejectPendingUserTextToResponseFlows(error) {
|
|
1989
|
+
this.pendingUserTextToResponseFlows.forEach((flow) => {
|
|
1990
|
+
flow.reject(error);
|
|
1991
|
+
});
|
|
1992
|
+
this.pendingUserTextToResponseFlows.clear();
|
|
1993
|
+
}
|
|
1994
|
+
progressUserTextToResponseFlows(event) {
|
|
1995
|
+
if (event instanceof ReceiveConversationItemAddedEvent) {
|
|
1996
|
+
const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
|
|
1997
|
+
if (!flow) {
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
flow.addedEvent = event;
|
|
2001
|
+
flow.callbacks?.onConversationItemAdded?.(event);
|
|
2002
|
+
this.tryTriggerResponseCreate(flow);
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
if (event instanceof ReceiveConversationItemDoneEvent) {
|
|
2006
|
+
const flow = this.pendingUserTextToResponseFlows.get(event.item.id);
|
|
2007
|
+
if (!flow) {
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
flow.doneEvent = event;
|
|
2011
|
+
flow.callbacks?.onConversationItemDone?.(event);
|
|
2012
|
+
this.tryTriggerResponseCreate(flow);
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (!(event instanceof ReceiveResponseCreatedEvent)) {
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const matchedFlow = Array.from(this.pendingUserTextToResponseFlows.values()).find(
|
|
2019
|
+
(flow) => flow.responseRequested && flow.streamId === event.streamId
|
|
2020
|
+
);
|
|
2021
|
+
if (!matchedFlow) {
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
matchedFlow.callbacks?.onResponseCreated?.(event);
|
|
2025
|
+
matchedFlow.resolve({
|
|
2026
|
+
itemId: matchedFlow.itemId,
|
|
2027
|
+
streamId: matchedFlow.streamId,
|
|
2028
|
+
responseCreatedEvent: event
|
|
2029
|
+
});
|
|
2030
|
+
this.pendingUserTextToResponseFlows.delete(matchedFlow.itemId);
|
|
2031
|
+
}
|
|
2032
|
+
tryTriggerResponseCreate(flow) {
|
|
2033
|
+
if (flow.responseRequested || !flow.addedEvent || !flow.doneEvent) {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
flow.callbacks?.onConversationItemReady?.({
|
|
2037
|
+
itemId: flow.itemId,
|
|
2038
|
+
addedEvent: flow.addedEvent,
|
|
2039
|
+
doneEvent: flow.doneEvent
|
|
2040
|
+
});
|
|
2041
|
+
this.client.sendResponseCreateByItemId(flow.itemId, flow.streamId, flow.response);
|
|
2042
|
+
flow.responseRequested = true;
|
|
2043
|
+
}
|
|
2044
|
+
buildUserTextItemId() {
|
|
2045
|
+
this.userTextFlowCounter += 1;
|
|
2046
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "");
|
|
2047
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
2048
|
+
return `client.conversation.item.user.text.${timestamp}.${this.userTextFlowCounter}.${randomSuffix}`;
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
function isVideoPlaybackSource(source) {
|
|
2052
|
+
if (!source || source.status !== "ready" || !source.playback) return false;
|
|
2053
|
+
if (source.playback.type === "trtc") return false;
|
|
2054
|
+
if (source.source.asset_type === "image") return false;
|
|
2055
|
+
return Boolean(source.playback.url);
|
|
2056
|
+
}
|
|
2057
|
+
function isTrtcPlaybackSource(source) {
|
|
2058
|
+
if (!source || !source.playback) return false;
|
|
2059
|
+
return source.playback.type === "trtc" && Boolean(source.playback.trtc);
|
|
2060
|
+
}
|
|
2061
|
+
var IviFrontendSdk = class {
|
|
2062
|
+
createClient(config) {
|
|
2063
|
+
return new IviClient(config);
|
|
2064
|
+
}
|
|
2065
|
+
createRuntimeCoordinator(clientConfig, runtimeConfig) {
|
|
2066
|
+
const client = this.createClient(clientConfig);
|
|
2067
|
+
return new IviRuntimeCoordinator(client, runtimeConfig);
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
var IviStageViewContext = createContext(null);
|
|
2071
|
+
var EMPTY_RUNTIME_STATE = {
|
|
2072
|
+
status: "idle",
|
|
2073
|
+
session: null,
|
|
2074
|
+
stage: null,
|
|
2075
|
+
tracks: /* @__PURE__ */ new Map(),
|
|
2076
|
+
sources: /* @__PURE__ */ new Map(),
|
|
2077
|
+
streams: /* @__PURE__ */ new Map(),
|
|
2078
|
+
conversationItems: /* @__PURE__ */ new Map(),
|
|
2079
|
+
conversations: []
|
|
2080
|
+
};
|
|
2081
|
+
function useRuntimeState(runtime) {
|
|
2082
|
+
const [state, setState] = useState(() => runtime?.getState() ?? EMPTY_RUNTIME_STATE);
|
|
2083
|
+
useEffect(() => {
|
|
2084
|
+
if (!runtime) {
|
|
2085
|
+
setState(EMPTY_RUNTIME_STATE);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
setState(runtime.getState());
|
|
2089
|
+
return runtime.onStateChange((next) => {
|
|
2090
|
+
setState(next);
|
|
2091
|
+
});
|
|
2092
|
+
}, [runtime]);
|
|
2093
|
+
return state;
|
|
2094
|
+
}
|
|
2095
|
+
function IVIStageView(props) {
|
|
2096
|
+
const {
|
|
2097
|
+
children,
|
|
2098
|
+
className,
|
|
2099
|
+
style,
|
|
2100
|
+
runtime,
|
|
2101
|
+
onBindingsChange,
|
|
2102
|
+
onRuntimeEvent
|
|
2103
|
+
} = props;
|
|
2104
|
+
const state = useRuntimeState(runtime);
|
|
2105
|
+
const slotTrackMap = useMemo(() => {
|
|
2106
|
+
return buildSlotTrackMapFromState(state);
|
|
2107
|
+
}, [state.stage]);
|
|
2108
|
+
const slotBindings = useMemo(() => {
|
|
2109
|
+
return buildSlotBindingsFromState(state);
|
|
2110
|
+
}, [state.stage, state.tracks, state.sources]);
|
|
2111
|
+
useEffect(() => {
|
|
2112
|
+
onBindingsChange?.(slotBindings);
|
|
2113
|
+
}, [slotBindings, onBindingsChange]);
|
|
2114
|
+
useEffect(() => {
|
|
2115
|
+
if (!runtime || !onRuntimeEvent) {
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
return runtime.onEvent((event) => {
|
|
2119
|
+
onRuntimeEvent(event);
|
|
2120
|
+
});
|
|
2121
|
+
}, [runtime, onRuntimeEvent]);
|
|
2122
|
+
const contextValue = useMemo(
|
|
2123
|
+
() => ({
|
|
2124
|
+
runtime,
|
|
2125
|
+
state,
|
|
2126
|
+
slotTrackMap,
|
|
2127
|
+
slotBindings
|
|
2128
|
+
}),
|
|
2129
|
+
[runtime, state, slotTrackMap, slotBindings]
|
|
2130
|
+
);
|
|
2131
|
+
const resolvedChildren = typeof children === "function" ? children({
|
|
2132
|
+
runtime,
|
|
2133
|
+
slotTrackMap,
|
|
2134
|
+
slotBindings
|
|
2135
|
+
}) : children;
|
|
2136
|
+
const rootStyle = {
|
|
2137
|
+
width: "100%",
|
|
2138
|
+
height: "100%",
|
|
2139
|
+
minWidth: 0,
|
|
2140
|
+
minHeight: 0,
|
|
2141
|
+
...style ?? {}
|
|
2142
|
+
};
|
|
2143
|
+
return /* @__PURE__ */ jsx("div", { className, style: rootStyle, children: /* @__PURE__ */ jsx(IviStageViewContext.Provider, { value: contextValue, children: resolvedChildren }) });
|
|
2144
|
+
}
|
|
2145
|
+
function buildSlotTrackMapFromState(state) {
|
|
2146
|
+
const map = /* @__PURE__ */ new Map();
|
|
2147
|
+
state.stage?.composition.forEach((item) => {
|
|
2148
|
+
map.set(item.slot, item.track_id);
|
|
2149
|
+
});
|
|
2150
|
+
return map;
|
|
2151
|
+
}
|
|
2152
|
+
function buildSlotBindingsFromState(state) {
|
|
2153
|
+
const bindings = [];
|
|
2154
|
+
state.stage?.composition.forEach((item) => {
|
|
2155
|
+
const track = state.tracks.get(item.track_id);
|
|
2156
|
+
const sourceId = track?.active_source_id ?? null;
|
|
2157
|
+
const source = sourceId ? state.sources.get(sourceId) : void 0;
|
|
2158
|
+
bindings.push({
|
|
2159
|
+
slot: item.slot,
|
|
2160
|
+
trackId: item.track_id,
|
|
2161
|
+
track,
|
|
2162
|
+
sourceId,
|
|
2163
|
+
source
|
|
2164
|
+
});
|
|
2165
|
+
});
|
|
2166
|
+
return bindings;
|
|
2167
|
+
}
|
|
2168
|
+
var VOLUME_STORAGE_KEY = "ivi-volume-preferences";
|
|
2169
|
+
var DEFAULT_VOLUME = 80;
|
|
2170
|
+
function clampVolume(v) {
|
|
2171
|
+
return Math.max(0, Math.min(100, Math.round(v)));
|
|
2172
|
+
}
|
|
2173
|
+
function loadVolumePreferences() {
|
|
2174
|
+
try {
|
|
2175
|
+
const raw = localStorage.getItem(VOLUME_STORAGE_KEY);
|
|
2176
|
+
if (raw) {
|
|
2177
|
+
const parsed = JSON.parse(raw);
|
|
2178
|
+
return {
|
|
2179
|
+
trtc: clampVolume(parsed.trtc ?? DEFAULT_VOLUME),
|
|
2180
|
+
video: clampVolume(parsed.video ?? DEFAULT_VOLUME),
|
|
2181
|
+
hls: clampVolume(parsed.hls ?? DEFAULT_VOLUME)
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
}
|
|
2186
|
+
return { trtc: DEFAULT_VOLUME, video: DEFAULT_VOLUME, hls: DEFAULT_VOLUME };
|
|
2187
|
+
}
|
|
2188
|
+
function saveVolumePreferences(prefs) {
|
|
2189
|
+
try {
|
|
2190
|
+
localStorage.setItem(VOLUME_STORAGE_KEY, JSON.stringify(prefs));
|
|
2191
|
+
} catch {
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
function useVolumeMemory(mediaType) {
|
|
2195
|
+
const [prefs, setPrefs] = useState(loadVolumePreferences);
|
|
2196
|
+
const setVolume = useCallback(
|
|
2197
|
+
(v) => {
|
|
2198
|
+
if (!mediaType) return;
|
|
2199
|
+
setPrefs((prev) => {
|
|
2200
|
+
const next = { ...prev, [mediaType]: clampVolume(v) };
|
|
2201
|
+
saveVolumePreferences(next);
|
|
2202
|
+
return next;
|
|
2203
|
+
});
|
|
2204
|
+
},
|
|
2205
|
+
[mediaType]
|
|
2206
|
+
);
|
|
2207
|
+
return [mediaType ? prefs[mediaType] : DEFAULT_VOLUME, setVolume];
|
|
2208
|
+
}
|
|
2209
|
+
function DefaultSpeakerIcon({ volume, color }) {
|
|
2210
|
+
if (volume === 0) {
|
|
2211
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
2212
|
+
/* @__PURE__ */ jsx(
|
|
2213
|
+
"path",
|
|
2214
|
+
{
|
|
2215
|
+
d: "M11 5L6 9H2v6h4l5 4V5z",
|
|
2216
|
+
fill: color,
|
|
2217
|
+
stroke: color,
|
|
2218
|
+
strokeWidth: "1.5",
|
|
2219
|
+
strokeLinejoin: "round"
|
|
2220
|
+
}
|
|
2221
|
+
),
|
|
2222
|
+
/* @__PURE__ */ jsx("path", { d: "M22 9l-6 6M16 9l6 6", stroke: color, strokeWidth: "2", strokeLinecap: "round" })
|
|
2223
|
+
] });
|
|
2224
|
+
}
|
|
2225
|
+
if (volume < 50) {
|
|
2226
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
2227
|
+
/* @__PURE__ */ jsx(
|
|
2228
|
+
"path",
|
|
2229
|
+
{
|
|
2230
|
+
d: "M11 5L6 9H2v6h4l5 4V5z",
|
|
2231
|
+
fill: color,
|
|
2232
|
+
stroke: color,
|
|
2233
|
+
strokeWidth: "1.5",
|
|
2234
|
+
strokeLinejoin: "round"
|
|
2235
|
+
}
|
|
2236
|
+
),
|
|
2237
|
+
/* @__PURE__ */ jsx("path", { d: "M15.54 8.46a5 5 0 010 7.07", stroke: color, strokeWidth: "2", strokeLinecap: "round" })
|
|
2238
|
+
] });
|
|
2239
|
+
}
|
|
2240
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
2241
|
+
/* @__PURE__ */ jsx(
|
|
2242
|
+
"path",
|
|
2243
|
+
{
|
|
2244
|
+
d: "M11 5L6 9H2v6h4l5 4V5z",
|
|
2245
|
+
fill: color,
|
|
2246
|
+
stroke: color,
|
|
2247
|
+
strokeWidth: "1.5",
|
|
2248
|
+
strokeLinejoin: "round"
|
|
2249
|
+
}
|
|
2250
|
+
),
|
|
2251
|
+
/* @__PURE__ */ jsx("path", { d: "M15.54 8.46a5 5 0 010 7.07", stroke: color, strokeWidth: "2", strokeLinecap: "round" }),
|
|
2252
|
+
/* @__PURE__ */ jsx(
|
|
2253
|
+
"path",
|
|
2254
|
+
{
|
|
2255
|
+
d: "M19.07 4.93a10 10 0 010 14.14",
|
|
2256
|
+
stroke: color,
|
|
2257
|
+
strokeWidth: "2",
|
|
2258
|
+
strokeLinecap: "round"
|
|
2259
|
+
}
|
|
2260
|
+
)
|
|
2261
|
+
] });
|
|
2262
|
+
}
|
|
2263
|
+
function VolumeSlider({
|
|
2264
|
+
value,
|
|
2265
|
+
onChange,
|
|
2266
|
+
trackBg,
|
|
2267
|
+
fillColor,
|
|
2268
|
+
thumbColor
|
|
2269
|
+
}) {
|
|
2270
|
+
const trackRef = useRef(null);
|
|
2271
|
+
const calcValue = useCallback(
|
|
2272
|
+
(clientX) => {
|
|
2273
|
+
const el = trackRef.current;
|
|
2274
|
+
if (!el) return;
|
|
2275
|
+
const rect = el.getBoundingClientRect();
|
|
2276
|
+
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
2277
|
+
onChange(Math.round(ratio * 100));
|
|
2278
|
+
},
|
|
2279
|
+
[onChange]
|
|
2280
|
+
);
|
|
2281
|
+
const onPointerDown = useCallback(
|
|
2282
|
+
(e) => {
|
|
2283
|
+
e.preventDefault();
|
|
2284
|
+
e.target.setPointerCapture(e.pointerId);
|
|
2285
|
+
calcValue(e.clientX);
|
|
2286
|
+
},
|
|
2287
|
+
[calcValue]
|
|
2288
|
+
);
|
|
2289
|
+
const onPointerMove = useCallback(
|
|
2290
|
+
(e) => {
|
|
2291
|
+
if (e.buttons === 0) return;
|
|
2292
|
+
calcValue(e.clientX);
|
|
2293
|
+
},
|
|
2294
|
+
[calcValue]
|
|
2295
|
+
);
|
|
2296
|
+
const pct = `${value}%`;
|
|
2297
|
+
return /* @__PURE__ */ jsxs(
|
|
2298
|
+
"div",
|
|
2299
|
+
{
|
|
2300
|
+
ref: trackRef,
|
|
2301
|
+
onPointerDown,
|
|
2302
|
+
onPointerMove,
|
|
2303
|
+
style: {
|
|
2304
|
+
position: "relative",
|
|
2305
|
+
flex: 1,
|
|
2306
|
+
height: 4,
|
|
2307
|
+
minWidth: 60,
|
|
2308
|
+
borderRadius: 2,
|
|
2309
|
+
backgroundColor: trackBg,
|
|
2310
|
+
cursor: "pointer",
|
|
2311
|
+
touchAction: "none"
|
|
2312
|
+
},
|
|
2313
|
+
children: [
|
|
2314
|
+
/* @__PURE__ */ jsx(
|
|
2315
|
+
"div",
|
|
2316
|
+
{
|
|
2317
|
+
style: {
|
|
2318
|
+
position: "absolute",
|
|
2319
|
+
left: 0,
|
|
2320
|
+
top: 0,
|
|
2321
|
+
height: "100%",
|
|
2322
|
+
width: pct,
|
|
2323
|
+
borderRadius: 2,
|
|
2324
|
+
backgroundColor: fillColor,
|
|
2325
|
+
pointerEvents: "none"
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
),
|
|
2329
|
+
/* @__PURE__ */ jsx(
|
|
2330
|
+
"div",
|
|
2331
|
+
{
|
|
2332
|
+
style: {
|
|
2333
|
+
position: "absolute",
|
|
2334
|
+
left: pct,
|
|
2335
|
+
top: "50%",
|
|
2336
|
+
width: 10,
|
|
2337
|
+
height: 10,
|
|
2338
|
+
borderRadius: "50%",
|
|
2339
|
+
backgroundColor: thumbColor,
|
|
2340
|
+
transform: "translate(-50%, -50%)",
|
|
2341
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.3)",
|
|
2342
|
+
pointerEvents: "none"
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
)
|
|
2346
|
+
]
|
|
2347
|
+
}
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
function IVIVolumeControl(props) {
|
|
2351
|
+
const { volume, onVolumeChange, renderSpeakerIcon, colors, className, style } = props;
|
|
2352
|
+
const bg = colors?.background ?? "rgba(0, 0, 0, 0.55)";
|
|
2353
|
+
const fg = colors?.foreground ?? "#fff";
|
|
2354
|
+
const trackBg = colors?.trackBackground ?? "rgba(255, 255, 255, 0.25)";
|
|
2355
|
+
const trackFill = colors?.trackFill ?? fg;
|
|
2356
|
+
const prevVolumeRef = useRef(volume > 0 ? volume : DEFAULT_VOLUME);
|
|
2357
|
+
if (volume > 0) {
|
|
2358
|
+
prevVolumeRef.current = volume;
|
|
2359
|
+
}
|
|
2360
|
+
const toggleMute = useCallback(() => {
|
|
2361
|
+
onVolumeChange(volume > 0 ? 0 : prevVolumeRef.current);
|
|
2362
|
+
}, [volume, onVolumeChange]);
|
|
2363
|
+
return /* @__PURE__ */ jsxs(
|
|
2364
|
+
"div",
|
|
2365
|
+
{
|
|
2366
|
+
className,
|
|
2367
|
+
style: {
|
|
2368
|
+
display: "inline-flex",
|
|
2369
|
+
alignItems: "center",
|
|
2370
|
+
gap: 6,
|
|
2371
|
+
padding: "5px 10px",
|
|
2372
|
+
borderRadius: 999,
|
|
2373
|
+
backgroundColor: bg,
|
|
2374
|
+
color: fg,
|
|
2375
|
+
userSelect: "none",
|
|
2376
|
+
fontSize: 12,
|
|
2377
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
2378
|
+
lineHeight: 1,
|
|
2379
|
+
...style
|
|
2380
|
+
},
|
|
2381
|
+
children: [
|
|
2382
|
+
/* @__PURE__ */ jsx(
|
|
2383
|
+
"div",
|
|
2384
|
+
{
|
|
2385
|
+
onClick: toggleMute,
|
|
2386
|
+
role: "button",
|
|
2387
|
+
tabIndex: 0,
|
|
2388
|
+
onKeyDown: (e) => {
|
|
2389
|
+
if (e.key === "Enter" || e.key === " ") toggleMute();
|
|
2390
|
+
},
|
|
2391
|
+
style: { cursor: "pointer", display: "flex", alignItems: "center", flexShrink: 0 },
|
|
2392
|
+
children: renderSpeakerIcon ? renderSpeakerIcon(volume) : /* @__PURE__ */ jsx(DefaultSpeakerIcon, { volume, color: fg })
|
|
2393
|
+
}
|
|
2394
|
+
),
|
|
2395
|
+
/* @__PURE__ */ jsx(
|
|
2396
|
+
VolumeSlider,
|
|
2397
|
+
{
|
|
2398
|
+
value: volume,
|
|
2399
|
+
onChange: onVolumeChange,
|
|
2400
|
+
trackBg,
|
|
2401
|
+
fillColor: trackFill,
|
|
2402
|
+
thumbColor: fg
|
|
2403
|
+
}
|
|
2404
|
+
),
|
|
2405
|
+
/* @__PURE__ */ jsxs(
|
|
2406
|
+
"span",
|
|
2407
|
+
{
|
|
2408
|
+
style: {
|
|
2409
|
+
minWidth: 28,
|
|
2410
|
+
textAlign: "right",
|
|
2411
|
+
fontSize: 11,
|
|
2412
|
+
opacity: 0.9,
|
|
2413
|
+
flexShrink: 0,
|
|
2414
|
+
fontVariantNumeric: "tabular-nums"
|
|
2415
|
+
},
|
|
2416
|
+
children: [
|
|
2417
|
+
volume,
|
|
2418
|
+
"%"
|
|
2419
|
+
]
|
|
2420
|
+
}
|
|
2421
|
+
)
|
|
2422
|
+
]
|
|
2423
|
+
}
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
|
|
2427
|
+
useEffect(() => {
|
|
2428
|
+
const container = containerRef.current;
|
|
2429
|
+
if (!container || !enabled) return;
|
|
2430
|
+
const normalizedVolume = clampVolume(volume) / 100;
|
|
2431
|
+
const apply = () => {
|
|
2432
|
+
const activeSlot = container.querySelector('[data-ivi-slot-role="active"]');
|
|
2433
|
+
if (!activeSlot) return;
|
|
2434
|
+
activeSlot.querySelectorAll("video, audio").forEach((el) => {
|
|
2435
|
+
el.volume = normalizedVolume;
|
|
2436
|
+
});
|
|
2437
|
+
};
|
|
2438
|
+
apply();
|
|
2439
|
+
const observer = new MutationObserver(apply);
|
|
2440
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
2441
|
+
return () => observer.disconnect();
|
|
2442
|
+
}, [containerRef, volume, enabled, activeSourceId]);
|
|
2443
|
+
}
|
|
2444
|
+
function useSubtitleEntries(conversations, maxVisible, dismissAfterMs) {
|
|
2445
|
+
const [visibleIds, setVisibleIds] = useState([]);
|
|
2446
|
+
const timersRef = useRef(/* @__PURE__ */ new Map());
|
|
2447
|
+
const seenRef = useRef(/* @__PURE__ */ new Set());
|
|
2448
|
+
const dismissedRef = useRef(/* @__PURE__ */ new Set());
|
|
2449
|
+
const initializedRef = useRef(false);
|
|
2450
|
+
useEffect(() => {
|
|
2451
|
+
const seen = seenRef.current;
|
|
2452
|
+
const dismissed = dismissedRef.current;
|
|
2453
|
+
const timers = timersRef.current;
|
|
2454
|
+
if (!initializedRef.current) {
|
|
2455
|
+
initializedRef.current = true;
|
|
2456
|
+
for (const item of conversations) {
|
|
2457
|
+
if (item.lifecycle === "done" || !(item.text || item.transcript)) {
|
|
2458
|
+
seen.add(item.id);
|
|
2459
|
+
dismissed.add(item.id);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const newIds = [];
|
|
2464
|
+
for (const item of conversations) {
|
|
2465
|
+
const displayText = item.text || item.transcript;
|
|
2466
|
+
if (!displayText) continue;
|
|
2467
|
+
if (!seen.has(item.id)) {
|
|
2468
|
+
seen.add(item.id);
|
|
2469
|
+
newIds.push(item.id);
|
|
2470
|
+
}
|
|
2471
|
+
if (item.lifecycle === "done" && !dismissed.has(item.id)) {
|
|
2472
|
+
dismissed.add(item.id);
|
|
2473
|
+
const timer = setTimeout(() => {
|
|
2474
|
+
timers.delete(item.id);
|
|
2475
|
+
setVisibleIds((prev) => prev.filter((id) => id !== item.id));
|
|
2476
|
+
}, dismissAfterMs);
|
|
2477
|
+
timers.set(item.id, timer);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
if (newIds.length > 0) {
|
|
2481
|
+
setVisibleIds((prev) => {
|
|
2482
|
+
const next = [...prev, ...newIds];
|
|
2483
|
+
while (next.length > maxVisible) {
|
|
2484
|
+
const removedId = next.shift();
|
|
2485
|
+
if (timers.has(removedId)) {
|
|
2486
|
+
clearTimeout(timers.get(removedId));
|
|
2487
|
+
timers.delete(removedId);
|
|
2488
|
+
}
|
|
2489
|
+
dismissed.add(removedId);
|
|
2490
|
+
}
|
|
2491
|
+
return next;
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
}, [conversations, maxVisible, dismissAfterMs]);
|
|
2495
|
+
useEffect(() => {
|
|
2496
|
+
const timers = timersRef.current;
|
|
2497
|
+
return () => {
|
|
2498
|
+
timers.forEach((t) => clearTimeout(t));
|
|
2499
|
+
timers.clear();
|
|
2500
|
+
};
|
|
2501
|
+
}, []);
|
|
2502
|
+
const conversationMap = useMemo(() => {
|
|
2503
|
+
const map = /* @__PURE__ */ new Map();
|
|
2504
|
+
for (const item of conversations) {
|
|
2505
|
+
map.set(item.id, item);
|
|
2506
|
+
}
|
|
2507
|
+
return map;
|
|
2508
|
+
}, [conversations]);
|
|
2509
|
+
return useMemo(() => {
|
|
2510
|
+
return visibleIds.map((id) => {
|
|
2511
|
+
const item = conversationMap.get(id);
|
|
2512
|
+
if (!item) return null;
|
|
2513
|
+
const text = item.text || item.transcript;
|
|
2514
|
+
if (!text) return null;
|
|
2515
|
+
return { id: item.id, role: item.role, text, lifecycle: item.lifecycle };
|
|
2516
|
+
}).filter((entry) => entry !== null);
|
|
2517
|
+
}, [visibleIds, conversationMap]);
|
|
2518
|
+
}
|
|
2519
|
+
var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
|
|
2520
|
+
function IVISubtitleOverlay(props) {
|
|
2521
|
+
const {
|
|
2522
|
+
conversations,
|
|
2523
|
+
roles = "user",
|
|
2524
|
+
maxVisible = 2,
|
|
2525
|
+
dismissAfterMs = 5e3,
|
|
2526
|
+
subtitleStyle,
|
|
2527
|
+
className,
|
|
2528
|
+
style
|
|
2529
|
+
} = props;
|
|
2530
|
+
const roleSet = useMemo(
|
|
2531
|
+
() => new Set(Array.isArray(roles) ? roles : [roles]),
|
|
2532
|
+
[roles]
|
|
2533
|
+
);
|
|
2534
|
+
const filtered = useMemo(
|
|
2535
|
+
() => conversations.filter((c) => roleSet.has(c.role)),
|
|
2536
|
+
[conversations, roleSet]
|
|
2537
|
+
);
|
|
2538
|
+
const entries = useSubtitleEntries(filtered, maxVisible, dismissAfterMs);
|
|
2539
|
+
if (entries.length === 0) return null;
|
|
2540
|
+
const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
2541
|
+
const fontSize = subtitleStyle?.fontSize ?? 14;
|
|
2542
|
+
const color = subtitleStyle?.color ?? "#fff";
|
|
2543
|
+
const bg = subtitleStyle?.background ?? "rgba(0, 0, 0, 0.6)";
|
|
2544
|
+
return /* @__PURE__ */ jsxs(
|
|
2545
|
+
"div",
|
|
2546
|
+
{
|
|
2547
|
+
className,
|
|
2548
|
+
style: {
|
|
2549
|
+
display: "flex",
|
|
2550
|
+
flexDirection: "column",
|
|
2551
|
+
alignItems: "center",
|
|
2552
|
+
gap: 6,
|
|
2553
|
+
pointerEvents: "none",
|
|
2554
|
+
...style
|
|
2555
|
+
},
|
|
2556
|
+
children: [
|
|
2557
|
+
/* @__PURE__ */ jsx("style", { children: BREATHE_KEYFRAMES }),
|
|
2558
|
+
entries.map((entry) => /* @__PURE__ */ jsx(
|
|
2559
|
+
"div",
|
|
2560
|
+
{
|
|
2561
|
+
style: {
|
|
2562
|
+
display: "inline-block",
|
|
2563
|
+
padding: "5px 16px",
|
|
2564
|
+
borderRadius: 999,
|
|
2565
|
+
backgroundColor: bg,
|
|
2566
|
+
color,
|
|
2567
|
+
fontSize,
|
|
2568
|
+
fontFamily,
|
|
2569
|
+
lineHeight: 1.5,
|
|
2570
|
+
maxWidth: "100%",
|
|
2571
|
+
overflow: "hidden",
|
|
2572
|
+
textOverflow: "ellipsis",
|
|
2573
|
+
whiteSpace: "nowrap",
|
|
2574
|
+
animation: entry.lifecycle === "added" ? "ivi-subtitle-breathe 1.5s ease-in-out infinite" : void 0
|
|
2575
|
+
},
|
|
2576
|
+
children: entry.text
|
|
2577
|
+
},
|
|
2578
|
+
entry.id
|
|
2579
|
+
))
|
|
2580
|
+
]
|
|
2581
|
+
}
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
function useIviStageView() {
|
|
2585
|
+
const context = useContext(IviStageViewContext);
|
|
2586
|
+
if (!context) {
|
|
2587
|
+
throw new Error("useIviStageView must be used inside IVIStageView.");
|
|
2588
|
+
}
|
|
2589
|
+
return context;
|
|
2590
|
+
}
|
|
2591
|
+
var standaloneTrtcSourceManager = new TrtcSourceManager();
|
|
2592
|
+
function IVITrtcPlayer(props) {
|
|
2593
|
+
const {
|
|
2594
|
+
trtc,
|
|
2595
|
+
sourceId,
|
|
2596
|
+
runtime,
|
|
2597
|
+
className,
|
|
2598
|
+
style,
|
|
2599
|
+
loadingFallback = null,
|
|
2600
|
+
errorFallback = null,
|
|
2601
|
+
muted = false
|
|
2602
|
+
} = props;
|
|
2603
|
+
const containerRef = useRef(null);
|
|
2604
|
+
const viewIdRef = useRef(`trtc-view-${Math.random().toString(36).slice(2, 10)}`);
|
|
2605
|
+
const manager = runtime?.getTrtcSourceManager() ?? standaloneTrtcSourceManager;
|
|
2606
|
+
const resolvedSourceId = useMemo(
|
|
2607
|
+
() => sourceId ?? `adhoc:${trtc.app_id}:${trtc.user_id}:${trtc.room_id}`,
|
|
2608
|
+
[sourceId, trtc.app_id, trtc.room_id, trtc.user_id]
|
|
2609
|
+
);
|
|
2610
|
+
const shouldManageSourceLifecycle = !runtime || !sourceId;
|
|
2611
|
+
const [loading, setLoading] = useState(true);
|
|
2612
|
+
const [error, setError] = useState(null);
|
|
2613
|
+
const mutedRef = useRef(muted);
|
|
2614
|
+
mutedRef.current = muted;
|
|
2615
|
+
useEffect(() => {
|
|
2616
|
+
const container = containerRef.current;
|
|
2617
|
+
if (!container) {
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
let disposed = false;
|
|
2621
|
+
if (shouldManageSourceLifecycle) {
|
|
2622
|
+
manager.upsertSource(resolvedSourceId, trtc);
|
|
2623
|
+
}
|
|
2624
|
+
const unsubscribe = manager.subscribe(resolvedSourceId, (snapshot) => {
|
|
2625
|
+
if (disposed) {
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
setLoading(snapshot.status === "idle" || snapshot.status === "connecting");
|
|
2629
|
+
setError(snapshot.status === "error" ? snapshot.error ?? "TRTC \u62C9\u6D41\u5931\u8D25" : null);
|
|
2630
|
+
});
|
|
2631
|
+
void manager.attachView(resolvedSourceId, viewIdRef.current, container, mutedRef.current).catch((caughtError) => {
|
|
2632
|
+
if (disposed) {
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
setError(caughtError instanceof Error ? caughtError.message : "TRTC \u62C9\u6D41\u5931\u8D25");
|
|
2636
|
+
setLoading(false);
|
|
2637
|
+
});
|
|
2638
|
+
return () => {
|
|
2639
|
+
disposed = true;
|
|
2640
|
+
unsubscribe();
|
|
2641
|
+
manager.detachView(resolvedSourceId, viewIdRef.current);
|
|
2642
|
+
if (shouldManageSourceLifecycle) {
|
|
2643
|
+
manager.removeSource(resolvedSourceId);
|
|
2644
|
+
}
|
|
2645
|
+
};
|
|
2646
|
+
}, [
|
|
2647
|
+
manager,
|
|
2648
|
+
resolvedSourceId,
|
|
2649
|
+
shouldManageSourceLifecycle,
|
|
2650
|
+
trtc.app_id,
|
|
2651
|
+
trtc.room_id,
|
|
2652
|
+
trtc.user_id,
|
|
2653
|
+
trtc.user_sig
|
|
2654
|
+
]);
|
|
2655
|
+
useEffect(() => {
|
|
2656
|
+
manager.updateViewMuted(resolvedSourceId, viewIdRef.current, muted);
|
|
2657
|
+
}, [manager, muted, resolvedSourceId]);
|
|
2658
|
+
return /* @__PURE__ */ jsxs(
|
|
2659
|
+
"div",
|
|
2660
|
+
{
|
|
2661
|
+
className,
|
|
2662
|
+
style: {
|
|
2663
|
+
width: "100%",
|
|
2664
|
+
height: "100%",
|
|
2665
|
+
minWidth: 0,
|
|
2666
|
+
minHeight: 0,
|
|
2667
|
+
backgroundColor: "#000",
|
|
2668
|
+
position: "relative",
|
|
2669
|
+
...style
|
|
2670
|
+
},
|
|
2671
|
+
children: [
|
|
2672
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
|
|
2673
|
+
loading ? loadingFallback : null,
|
|
2674
|
+
error ? errorFallback ?? /* @__PURE__ */ jsx("div", { children: error }) : null
|
|
2675
|
+
]
|
|
2676
|
+
}
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
var RETRY_DELAY_MS = 500;
|
|
2680
|
+
var UNLIMITED_RETRIES = Number.MAX_SAFE_INTEGER;
|
|
2681
|
+
function makeRetryConfig(label, kind) {
|
|
2682
|
+
return {
|
|
2683
|
+
maxNumRetry: UNLIMITED_RETRIES,
|
|
2684
|
+
retryDelayMs: RETRY_DELAY_MS,
|
|
2685
|
+
maxRetryDelayMs: RETRY_DELAY_MS,
|
|
2686
|
+
backoff: "linear",
|
|
2687
|
+
shouldRetry: (_config, retryCount, isTimeout) => {
|
|
2688
|
+
const reason = kind === "timeout" || isTimeout ? "\u52A0\u8F7D\u8D85\u65F6" : "\u52A0\u8F7D\u5931\u8D25";
|
|
2689
|
+
console.warn(
|
|
2690
|
+
`[IVIHlsVideo] ${label} ${reason}\uFF0C${RETRY_DELAY_MS}ms \u540E\u8FDB\u884C\u7B2C ${retryCount + 1} \u6B21\u91CD\u8BD5\uFF08\u65E0\u4E0A\u9650\uFF09`
|
|
2691
|
+
);
|
|
2692
|
+
return true;
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
function makeLoadPolicy(label) {
|
|
2697
|
+
return {
|
|
2698
|
+
default: {
|
|
2699
|
+
maxTimeToFirstByteMs: 1e4,
|
|
2700
|
+
maxLoadTimeMs: 2e4,
|
|
2701
|
+
timeoutRetry: makeRetryConfig(label, "timeout"),
|
|
2702
|
+
errorRetry: makeRetryConfig(label, "error")
|
|
2703
|
+
}
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function IVIHlsVideo(props) {
|
|
2707
|
+
const { url, videoProps, style, aggressivePreload = false, paused = false } = props;
|
|
2708
|
+
const videoRef = useRef(null);
|
|
2709
|
+
const pausedRef = useRef(paused);
|
|
2710
|
+
useEffect(() => {
|
|
2711
|
+
pausedRef.current = paused;
|
|
2712
|
+
const video = videoRef.current;
|
|
2713
|
+
if (!video) return;
|
|
2714
|
+
if (paused) {
|
|
2715
|
+
video.pause();
|
|
2716
|
+
} else {
|
|
2717
|
+
video.play().catch((err) => {
|
|
2718
|
+
console.warn(
|
|
2719
|
+
"[IVIHlsVideo] paused\u2192active \u5207\u6362\u65F6 play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD\uFF08\u591A\u534A\u53D7\u81EA\u52A8\u64AD\u653E\u7B56\u7565\u9650\u5236\uFF09",
|
|
2720
|
+
err
|
|
2721
|
+
);
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
}, [paused]);
|
|
2725
|
+
useEffect(() => {
|
|
2726
|
+
const video = videoRef.current;
|
|
2727
|
+
if (!video) {
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
let disposed = false;
|
|
2731
|
+
let hlsInstance = null;
|
|
2732
|
+
let fullReloadTimer = null;
|
|
2733
|
+
const clearFullReloadTimer = () => {
|
|
2734
|
+
if (fullReloadTimer) {
|
|
2735
|
+
clearTimeout(fullReloadTimer);
|
|
2736
|
+
fullReloadTimer = null;
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
const onVideoError = () => {
|
|
2740
|
+
const el = videoRef.current;
|
|
2741
|
+
const mediaErr = el?.error;
|
|
2742
|
+
console.warn("[IVIHlsVideo] <video> \u5143\u7D20\u62A5\u9519", {
|
|
2743
|
+
code: mediaErr?.code,
|
|
2744
|
+
message: mediaErr?.message,
|
|
2745
|
+
currentSrc: el?.currentSrc,
|
|
2746
|
+
networkState: el?.networkState,
|
|
2747
|
+
readyState: el?.readyState
|
|
2748
|
+
});
|
|
2749
|
+
};
|
|
2750
|
+
const onVideoStalled = () => {
|
|
2751
|
+
console.warn("[IVIHlsVideo] <video> stalled\uFF08\u7F13\u51B2\u505C\u6EDE\uFF09", {
|
|
2752
|
+
currentTime: videoRef.current?.currentTime,
|
|
2753
|
+
readyState: videoRef.current?.readyState
|
|
2754
|
+
});
|
|
2755
|
+
};
|
|
2756
|
+
video.addEventListener("error", onVideoError);
|
|
2757
|
+
video.addEventListener("stalled", onVideoStalled);
|
|
2758
|
+
const resumePlayback = () => {
|
|
2759
|
+
if (disposed) return;
|
|
2760
|
+
if (pausedRef.current) return;
|
|
2761
|
+
const el = videoRef.current;
|
|
2762
|
+
if (!el) return;
|
|
2763
|
+
el.play().catch((err) => {
|
|
2764
|
+
console.warn(
|
|
2765
|
+
"[IVIHlsVideo] \u6062\u590D\u540E play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD\uFF08\u53EF\u80FD\u53D7\u81EA\u52A8\u64AD\u653E\u7B56\u7565\u9650\u5236\uFF09",
|
|
2766
|
+
err
|
|
2767
|
+
);
|
|
2768
|
+
});
|
|
2769
|
+
};
|
|
2770
|
+
const scheduleFullReload = (reason) => {
|
|
2771
|
+
if (disposed || fullReloadTimer) return;
|
|
2772
|
+
console.warn(
|
|
2773
|
+
`[IVIHlsVideo] ${reason}\uFF0C${RETRY_DELAY_MS}ms \u540E\u91CD\u65B0\u62C9\u53D6\u6E90\uFF08\u65E0\u9650\u91CD\u8BD5\uFF09`
|
|
2774
|
+
);
|
|
2775
|
+
fullReloadTimer = setTimeout(() => {
|
|
2776
|
+
fullReloadTimer = null;
|
|
2777
|
+
if (disposed || !hlsInstance || !videoRef.current) return;
|
|
2778
|
+
try {
|
|
2779
|
+
hlsInstance.loadSource(url);
|
|
2780
|
+
hlsInstance.attachMedia(videoRef.current);
|
|
2781
|
+
hlsInstance.startLoad();
|
|
2782
|
+
resumePlayback();
|
|
2783
|
+
} catch (err) {
|
|
2784
|
+
console.warn("[IVIHlsVideo] \u91CD\u65B0\u62C9\u53D6\u6E90\u629B\u51FA\u5F02\u5E38\uFF0C\u7EE7\u7EED\u91CD\u8BD5", err);
|
|
2785
|
+
scheduleFullReload("\u91CD\u65B0\u62C9\u53D6\u6E90\u5F02\u5E38");
|
|
2786
|
+
}
|
|
2787
|
+
}, RETRY_DELAY_MS);
|
|
2788
|
+
};
|
|
2789
|
+
const setup = async () => {
|
|
2790
|
+
try {
|
|
2791
|
+
const module = await import('hls.js');
|
|
2792
|
+
if (disposed || !videoRef.current) {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const hlsModule = module;
|
|
2796
|
+
const Hls = hlsModule.default ?? module;
|
|
2797
|
+
const events = hlsModule.Events ?? Hls.Events ?? {};
|
|
2798
|
+
const errorTypes = hlsModule.ErrorTypes ?? Hls.ErrorTypes ?? {};
|
|
2799
|
+
if (!Hls.isSupported()) {
|
|
2800
|
+
console.warn(
|
|
2801
|
+
"[IVIHlsVideo] \u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 hls.js\uFF0C\u964D\u7EA7\u5230\u539F\u751F HLS\uFF08\u5931\u8D25\u5C06\u7531 <video> error \u4E8B\u4EF6\u6253\u5370\uFF09",
|
|
2802
|
+
{ url }
|
|
2803
|
+
);
|
|
2804
|
+
videoRef.current.src = url;
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
const bufferPreset = aggressivePreload ? {
|
|
2808
|
+
maxBufferLength: 180,
|
|
2809
|
+
maxMaxBufferLength: 240,
|
|
2810
|
+
backBufferLength: 120
|
|
2811
|
+
} : {
|
|
2812
|
+
maxBufferLength: 30,
|
|
2813
|
+
maxMaxBufferLength: 60,
|
|
2814
|
+
backBufferLength: 30
|
|
2815
|
+
};
|
|
2816
|
+
const instance = new Hls({
|
|
2817
|
+
enableWorker: true,
|
|
2818
|
+
lowLatencyMode: false,
|
|
2819
|
+
...bufferPreset,
|
|
2820
|
+
capLevelToPlayerSize: true,
|
|
2821
|
+
// 以下三项是核心:列表与分片的拉取全部使用 hls.js 内置重试,
|
|
2822
|
+
// 无上限次数、间隔上限 0.5s、每次重试通过 shouldRetry 打印警告。
|
|
2823
|
+
manifestLoadPolicy: makeLoadPolicy("manifest \u4E3B\u5217\u8868"),
|
|
2824
|
+
playlistLoadPolicy: makeLoadPolicy("level \u5B50\u7801\u7387\u5217\u8868"),
|
|
2825
|
+
fragLoadPolicy: makeLoadPolicy("fragment \u5A92\u4F53\u5206\u7247")
|
|
2826
|
+
});
|
|
2827
|
+
hlsInstance = instance;
|
|
2828
|
+
const errorEvent = events.ERROR ?? "hlsError";
|
|
2829
|
+
const networkErrorType = errorTypes.NETWORK_ERROR ?? "networkError";
|
|
2830
|
+
const mediaErrorType = errorTypes.MEDIA_ERROR ?? "mediaError";
|
|
2831
|
+
instance.on(errorEvent, (_eventName, data) => {
|
|
2832
|
+
if (!data) {
|
|
2833
|
+
console.warn("[IVIHlsVideo] HLS ERROR \u4E8B\u4EF6 data \u4E3A\u7A7A");
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
if (!data.fatal) {
|
|
2837
|
+
if (data.type !== networkErrorType) {
|
|
2838
|
+
console.warn(
|
|
2839
|
+
"[IVIHlsVideo] HLS \u975E\u81F4\u547D\u9519\u8BEF\uFF08\u4E0D\u8D70\u5185\u7F6E\u91CD\u8BD5\uFF09",
|
|
2840
|
+
data.type,
|
|
2841
|
+
data.details
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
switch (data.type) {
|
|
2847
|
+
case networkErrorType:
|
|
2848
|
+
console.warn(
|
|
2849
|
+
"[IVIHlsVideo] HLS \u81F4\u547D\u7F51\u7EDC\u9519\u8BEF\uFF0C\u8C03\u7528 startLoad() \u91CD\u542F\u62C9\u6D41",
|
|
2850
|
+
data.details
|
|
2851
|
+
);
|
|
2852
|
+
try {
|
|
2853
|
+
hlsInstance?.startLoad();
|
|
2854
|
+
resumePlayback();
|
|
2855
|
+
} catch (err) {
|
|
2856
|
+
console.warn("[IVIHlsVideo] startLoad() \u5F02\u5E38\uFF0C\u8D70\u515C\u5E95\u91CD\u8F7D", err);
|
|
2857
|
+
scheduleFullReload("startLoad \u5F02\u5E38");
|
|
2858
|
+
}
|
|
2859
|
+
break;
|
|
2860
|
+
case mediaErrorType:
|
|
2861
|
+
console.warn(
|
|
2862
|
+
"[IVIHlsVideo] HLS \u81F4\u547D\u5A92\u4F53\u9519\u8BEF\uFF0C\u8C03\u7528 recoverMediaError() \u6062\u590D",
|
|
2863
|
+
data.details
|
|
2864
|
+
);
|
|
2865
|
+
try {
|
|
2866
|
+
hlsInstance?.recoverMediaError();
|
|
2867
|
+
resumePlayback();
|
|
2868
|
+
} catch (err) {
|
|
2869
|
+
console.warn("[IVIHlsVideo] recoverMediaError() \u5F02\u5E38\uFF0C\u8D70\u515C\u5E95\u91CD\u8F7D", err);
|
|
2870
|
+
scheduleFullReload("recoverMediaError \u5F02\u5E38");
|
|
2871
|
+
}
|
|
2872
|
+
break;
|
|
2873
|
+
default:
|
|
2874
|
+
console.warn(
|
|
2875
|
+
"[IVIHlsVideo] HLS \u5176\u4ED6\u81F4\u547D\u9519\u8BEF\uFF0C\u51C6\u5907\u91CD\u65B0\u62C9\u53D6\u6E90",
|
|
2876
|
+
data.type,
|
|
2877
|
+
data.details
|
|
2878
|
+
);
|
|
2879
|
+
scheduleFullReload("\u5176\u4ED6\u81F4\u547D\u9519\u8BEF");
|
|
2880
|
+
break;
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
instance.loadSource(url);
|
|
2884
|
+
instance.attachMedia(videoRef.current);
|
|
2885
|
+
} catch (err) {
|
|
2886
|
+
console.warn(
|
|
2887
|
+
"[IVIHlsVideo] \u52A8\u6001\u52A0\u8F7D hls.js \u5931\u8D25\uFF0C\u964D\u7EA7\u5230\u539F\u751F HLS\uFF08\u5931\u8D25\u5C06\u7531 <video> error \u4E8B\u4EF6\u6253\u5370\uFF09",
|
|
2888
|
+
err
|
|
2889
|
+
);
|
|
2890
|
+
if (disposed || !videoRef.current) return;
|
|
2891
|
+
videoRef.current.src = url;
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
void setup();
|
|
2895
|
+
return () => {
|
|
2896
|
+
disposed = true;
|
|
2897
|
+
clearFullReloadTimer();
|
|
2898
|
+
video.removeEventListener("error", onVideoError);
|
|
2899
|
+
video.removeEventListener("stalled", onVideoStalled);
|
|
2900
|
+
hlsInstance?.destroy();
|
|
2901
|
+
hlsInstance = null;
|
|
2902
|
+
};
|
|
2903
|
+
}, [url, aggressivePreload]);
|
|
2904
|
+
return /* @__PURE__ */ jsx(
|
|
2905
|
+
"video",
|
|
2906
|
+
{
|
|
2907
|
+
ref: videoRef,
|
|
2908
|
+
...videoProps,
|
|
2909
|
+
autoPlay: paused ? false : videoProps?.autoPlay ?? true,
|
|
2910
|
+
playsInline: videoProps?.playsInline ?? true,
|
|
2911
|
+
controls: videoProps?.controls ?? true,
|
|
2912
|
+
style
|
|
2913
|
+
}
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
function isM3u8Url(url) {
|
|
2917
|
+
return /\.m3u8(?:$|[?#])/i.test(url);
|
|
2918
|
+
}
|
|
2919
|
+
function resolveBlurMode(bg) {
|
|
2920
|
+
if (bg === "blur") return "live";
|
|
2921
|
+
if (typeof bg === "object" && "blur" in bg) return bg.blur;
|
|
2922
|
+
return false;
|
|
2923
|
+
}
|
|
2924
|
+
function TrackSlotBlurLayer(props) {
|
|
2925
|
+
const { source, mode, children } = props;
|
|
2926
|
+
return /* @__PURE__ */ jsxs("div", { style: CONTAINER_STYLE, children: [
|
|
2927
|
+
/* @__PURE__ */ jsx("div", { style: BG_LAYER_STYLE, children: /* @__PURE__ */ jsx(BlurBackgroundLayer, { source, mode }) }),
|
|
2928
|
+
/* @__PURE__ */ jsx("div", { style: MAIN_LAYER_STYLE, children })
|
|
2929
|
+
] });
|
|
2930
|
+
}
|
|
2931
|
+
function BlurBackgroundLayer({
|
|
2932
|
+
source,
|
|
2933
|
+
mode
|
|
2934
|
+
}) {
|
|
2935
|
+
const { playback } = source;
|
|
2936
|
+
if (source.source.asset_type === "image" && playback.url) {
|
|
2937
|
+
return /* @__PURE__ */ jsx("img", { src: playback.url, alt: "", style: BLUR_MEDIA_STYLE });
|
|
2938
|
+
}
|
|
2939
|
+
if (playback.type === "trtc") {
|
|
2940
|
+
return /* @__PURE__ */ jsx(SlotVideoBlurCanvas, { staticOnly: mode === "static" });
|
|
2941
|
+
}
|
|
2942
|
+
const url = playback.url;
|
|
2943
|
+
if (!url) return null;
|
|
2944
|
+
if (mode === "static") {
|
|
2945
|
+
return isM3u8Url(url) ? /* @__PURE__ */ jsx(HlsStaticBlurFrame, { url }) : /* @__PURE__ */ jsx(StaticBlurFrame, { url });
|
|
2946
|
+
}
|
|
2947
|
+
if (isM3u8Url(url)) {
|
|
2948
|
+
return /* @__PURE__ */ jsx(
|
|
2949
|
+
IVIHlsVideo,
|
|
2950
|
+
{
|
|
2951
|
+
url,
|
|
2952
|
+
videoProps: { muted: true, autoPlay: true, playsInline: true },
|
|
2953
|
+
style: BLUR_MEDIA_STYLE,
|
|
2954
|
+
paused: false
|
|
2955
|
+
}
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
return /* @__PURE__ */ jsx(
|
|
2959
|
+
"video",
|
|
2960
|
+
{
|
|
2961
|
+
src: url,
|
|
2962
|
+
muted: true,
|
|
2963
|
+
autoPlay: true,
|
|
2964
|
+
playsInline: true,
|
|
2965
|
+
style: BLUR_MEDIA_STYLE
|
|
2966
|
+
}
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
function SlotVideoBlurCanvas({ staticOnly }) {
|
|
2970
|
+
const canvasRef = useRef(null);
|
|
2971
|
+
useEffect(() => {
|
|
2972
|
+
const canvas = canvasRef.current;
|
|
2973
|
+
if (!canvas) return;
|
|
2974
|
+
const container = canvas.closest("[data-ivi-source-id]");
|
|
2975
|
+
if (!container) return;
|
|
2976
|
+
let animId;
|
|
2977
|
+
let lastDrawTime = 0;
|
|
2978
|
+
let captured = false;
|
|
2979
|
+
const intervalMs = 1e3 / 5;
|
|
2980
|
+
const draw = (time) => {
|
|
2981
|
+
if (staticOnly && captured) return;
|
|
2982
|
+
animId = requestAnimationFrame(draw);
|
|
2983
|
+
if (time - lastDrawTime < intervalMs) return;
|
|
2984
|
+
lastDrawTime = time;
|
|
2985
|
+
const video = container.querySelector("video");
|
|
2986
|
+
if (!video || video.readyState < 2) return;
|
|
2987
|
+
const ctx = canvas.getContext("2d");
|
|
2988
|
+
if (!ctx) return;
|
|
2989
|
+
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
|
|
2990
|
+
canvas.width = video.videoWidth || 640;
|
|
2991
|
+
canvas.height = video.videoHeight || 360;
|
|
2992
|
+
}
|
|
2993
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
2994
|
+
captured = true;
|
|
2995
|
+
};
|
|
2996
|
+
animId = requestAnimationFrame(draw);
|
|
2997
|
+
return () => cancelAnimationFrame(animId);
|
|
2998
|
+
}, [staticOnly]);
|
|
2999
|
+
return /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: BLUR_MEDIA_STYLE });
|
|
3000
|
+
}
|
|
3001
|
+
function StaticBlurFrame({ url }) {
|
|
3002
|
+
const canvasRef = useRef(null);
|
|
3003
|
+
useEffect(() => {
|
|
3004
|
+
const canvas = canvasRef.current;
|
|
3005
|
+
if (!canvas) return;
|
|
3006
|
+
const video = document.createElement("video");
|
|
3007
|
+
video.muted = true;
|
|
3008
|
+
video.playsInline = true;
|
|
3009
|
+
video.preload = "auto";
|
|
3010
|
+
video.crossOrigin = "anonymous";
|
|
3011
|
+
video.src = url;
|
|
3012
|
+
const onReady = () => {
|
|
3013
|
+
const ctx = canvas.getContext("2d");
|
|
3014
|
+
if (!ctx) return;
|
|
3015
|
+
canvas.width = video.videoWidth || 640;
|
|
3016
|
+
canvas.height = video.videoHeight || 360;
|
|
3017
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
3018
|
+
cleanup();
|
|
3019
|
+
};
|
|
3020
|
+
const cleanup = () => {
|
|
3021
|
+
video.removeEventListener("loadeddata", onReady);
|
|
3022
|
+
video.pause();
|
|
3023
|
+
video.removeAttribute("src");
|
|
3024
|
+
video.load();
|
|
3025
|
+
};
|
|
3026
|
+
video.addEventListener("loadeddata", onReady);
|
|
3027
|
+
video.load();
|
|
3028
|
+
return cleanup;
|
|
3029
|
+
}, [url]);
|
|
3030
|
+
return /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: BLUR_MEDIA_STYLE });
|
|
3031
|
+
}
|
|
3032
|
+
function HlsStaticBlurFrame({ url }) {
|
|
3033
|
+
return /* @__PURE__ */ jsx(
|
|
3034
|
+
IVIHlsVideo,
|
|
3035
|
+
{
|
|
3036
|
+
url,
|
|
3037
|
+
videoProps: { muted: true, autoPlay: true, playsInline: true },
|
|
3038
|
+
style: BLUR_MEDIA_STYLE,
|
|
3039
|
+
paused: true
|
|
3040
|
+
}
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
var CONTAINER_STYLE = {
|
|
3044
|
+
width: "100%",
|
|
3045
|
+
height: "100%",
|
|
3046
|
+
position: "relative",
|
|
3047
|
+
overflow: "hidden"
|
|
3048
|
+
};
|
|
3049
|
+
var BG_LAYER_STYLE = {
|
|
3050
|
+
position: "absolute",
|
|
3051
|
+
top: 0,
|
|
3052
|
+
left: 0,
|
|
3053
|
+
width: "100%",
|
|
3054
|
+
height: "100%",
|
|
3055
|
+
zIndex: 0,
|
|
3056
|
+
overflow: "hidden"
|
|
3057
|
+
};
|
|
3058
|
+
var MAIN_LAYER_STYLE = {
|
|
3059
|
+
position: "relative",
|
|
3060
|
+
zIndex: 1,
|
|
3061
|
+
width: "100%",
|
|
3062
|
+
height: "100%"
|
|
3063
|
+
};
|
|
3064
|
+
var BLUR_MEDIA_STYLE = {
|
|
3065
|
+
width: "100%",
|
|
3066
|
+
height: "100%",
|
|
3067
|
+
objectFit: "cover",
|
|
3068
|
+
filter: "blur(20px)",
|
|
3069
|
+
transform: "scale(1.15)",
|
|
3070
|
+
display: "block"
|
|
3071
|
+
};
|
|
3072
|
+
function toReadyRuntimeSource(source) {
|
|
3073
|
+
if (!source || source.status !== "ready" || !source.playback) {
|
|
3074
|
+
return null;
|
|
3075
|
+
}
|
|
3076
|
+
return source;
|
|
3077
|
+
}
|
|
3078
|
+
function supportsSubtitleOverlay(source) {
|
|
3079
|
+
if (!source) return false;
|
|
3080
|
+
if (source.source.asset_type === "image") return false;
|
|
3081
|
+
return source.source.kind === "stream" || source.source.kind === "generation_stream" || source.source.kind === "generated_clip" || source.source.kind === "static";
|
|
3082
|
+
}
|
|
3083
|
+
function detectMediaVolumeType(source) {
|
|
3084
|
+
if (!source) return null;
|
|
3085
|
+
if (source.playback.type === "trtc") return "trtc";
|
|
3086
|
+
if (source.source.asset_type === "image") return null;
|
|
3087
|
+
const url = source.playback.url;
|
|
3088
|
+
if (!url) return null;
|
|
3089
|
+
return isM3u8Url(url) ? "hls" : "video";
|
|
3090
|
+
}
|
|
3091
|
+
function TrackSlotMediaContent(props) {
|
|
3092
|
+
const {
|
|
3093
|
+
slot,
|
|
3094
|
+
track,
|
|
3095
|
+
source,
|
|
3096
|
+
slotSourceId,
|
|
3097
|
+
isActive,
|
|
3098
|
+
runtime,
|
|
3099
|
+
renderTrtc,
|
|
3100
|
+
renderMedia,
|
|
3101
|
+
imageProps,
|
|
3102
|
+
videoProps,
|
|
3103
|
+
trtcPlayerProps,
|
|
3104
|
+
adaptToSourceSize,
|
|
3105
|
+
fitStrategy,
|
|
3106
|
+
background
|
|
3107
|
+
} = props;
|
|
3108
|
+
const renderContext = {
|
|
3109
|
+
slot: slot ?? "",
|
|
3110
|
+
track,
|
|
3111
|
+
source,
|
|
3112
|
+
isPreloading: !isActive
|
|
3113
|
+
};
|
|
3114
|
+
const mediaStyle = buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background);
|
|
3115
|
+
const shouldMute = !isActive;
|
|
3116
|
+
if (renderMedia) return renderMedia(renderContext);
|
|
3117
|
+
if (source.playback.type === "trtc") {
|
|
3118
|
+
if (!source.playback.trtc) return null;
|
|
3119
|
+
if (renderTrtc) return renderTrtc(renderContext);
|
|
3120
|
+
const trtcMuted = shouldMute || Boolean(trtcPlayerProps?.muted);
|
|
3121
|
+
return /* @__PURE__ */ jsx(
|
|
3122
|
+
IVITrtcPlayer,
|
|
3123
|
+
{
|
|
3124
|
+
trtc: source.playback.trtc,
|
|
3125
|
+
sourceId: source.source.source_id,
|
|
3126
|
+
runtime,
|
|
3127
|
+
...trtcPlayerProps,
|
|
3128
|
+
muted: trtcMuted,
|
|
3129
|
+
loadingFallback: isActive ? trtcPlayerProps?.loadingFallback : null,
|
|
3130
|
+
errorFallback: isActive ? trtcPlayerProps?.errorFallback : null,
|
|
3131
|
+
style: { ...mediaStyle, ...trtcPlayerProps?.style ?? {} }
|
|
3132
|
+
}
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
if (source.source.asset_type === "image") {
|
|
3136
|
+
return /* @__PURE__ */ jsx(
|
|
3137
|
+
"img",
|
|
3138
|
+
{
|
|
3139
|
+
src: source.playback.url,
|
|
3140
|
+
alt: "",
|
|
3141
|
+
...imageProps,
|
|
3142
|
+
style: { ...mediaStyle, ...imageProps?.style ?? {} }
|
|
3143
|
+
}
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
const playbackUrl = source.playback.url;
|
|
3147
|
+
if (!playbackUrl) return null;
|
|
3148
|
+
const videoStyle = { ...mediaStyle, ...videoProps?.style ?? {} };
|
|
3149
|
+
const mergedVideoProps = {
|
|
3150
|
+
...videoProps,
|
|
3151
|
+
muted: shouldMute || Boolean(videoProps?.muted),
|
|
3152
|
+
onEnded: isActive ? createAutoTakeOnEndedHandler(
|
|
3153
|
+
runtime,
|
|
3154
|
+
slotSourceId,
|
|
3155
|
+
track.track_id,
|
|
3156
|
+
videoProps?.onEnded
|
|
3157
|
+
) : void 0
|
|
3158
|
+
};
|
|
3159
|
+
const shouldPause = !isActive;
|
|
3160
|
+
return isM3u8Url(playbackUrl) ? /* @__PURE__ */ jsx(
|
|
3161
|
+
IVIHlsVideo,
|
|
3162
|
+
{
|
|
3163
|
+
url: playbackUrl,
|
|
3164
|
+
videoProps: mergedVideoProps,
|
|
3165
|
+
style: videoStyle,
|
|
3166
|
+
paused: shouldPause
|
|
3167
|
+
}
|
|
3168
|
+
) : /* @__PURE__ */ jsx(
|
|
3169
|
+
SlotVideo,
|
|
3170
|
+
{
|
|
3171
|
+
src: playbackUrl,
|
|
3172
|
+
paused: shouldPause,
|
|
3173
|
+
videoProps: {
|
|
3174
|
+
...mergedVideoProps,
|
|
3175
|
+
autoPlay: videoProps?.autoPlay ?? true,
|
|
3176
|
+
playsInline: videoProps?.playsInline ?? true,
|
|
3177
|
+
controls: isActive ? videoProps?.controls ?? true : false
|
|
3178
|
+
},
|
|
3179
|
+
style: videoStyle
|
|
3180
|
+
}
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
function buildAdaptiveMediaStyle(source, adaptToSourceSize, fitStrategy, background) {
|
|
3184
|
+
const objectFitStyle = fitStrategy === "auto" ? {} : {
|
|
3185
|
+
objectFit: fitStrategy ?? "contain"
|
|
3186
|
+
};
|
|
3187
|
+
if (!adaptToSourceSize) {
|
|
3188
|
+
return objectFitStyle;
|
|
3189
|
+
}
|
|
3190
|
+
return {
|
|
3191
|
+
width: "100%",
|
|
3192
|
+
height: "100%",
|
|
3193
|
+
display: "block",
|
|
3194
|
+
...objectFitStyle,
|
|
3195
|
+
backgroundColor: resolveBackgroundColor(background)
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
function resolveBackgroundColor(bg) {
|
|
3199
|
+
if (!bg || bg === "black") return "#000";
|
|
3200
|
+
if (bg === "white") return "#fff";
|
|
3201
|
+
if (bg === "transparent") return "transparent";
|
|
3202
|
+
if (bg === "blur") return "transparent";
|
|
3203
|
+
if (typeof bg === "object") {
|
|
3204
|
+
if ("color" in bg) return bg.color;
|
|
3205
|
+
if ("blur" in bg) return "transparent";
|
|
3206
|
+
}
|
|
3207
|
+
return "#000";
|
|
3208
|
+
}
|
|
3209
|
+
function createAutoTakeOnEndedHandler(runtime, sourceId, trackId, userOnEnded) {
|
|
3210
|
+
return (event) => {
|
|
3211
|
+
runtime?.sendSessionSourcePlaybackCompleted(sourceId, trackId);
|
|
3212
|
+
userOnEnded?.(event);
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
function SlotVideo(props) {
|
|
3216
|
+
const videoRef = useRef(null);
|
|
3217
|
+
useEffect(() => {
|
|
3218
|
+
const video = videoRef.current;
|
|
3219
|
+
if (!video) return;
|
|
3220
|
+
if (props.paused) {
|
|
3221
|
+
video.pause();
|
|
3222
|
+
} else {
|
|
3223
|
+
video.play().catch(() => void 0);
|
|
3224
|
+
}
|
|
3225
|
+
}, [props.paused]);
|
|
3226
|
+
return /* @__PURE__ */ jsx(
|
|
3227
|
+
"video",
|
|
3228
|
+
{
|
|
3229
|
+
ref: videoRef,
|
|
3230
|
+
src: props.src,
|
|
3231
|
+
...props.videoProps,
|
|
3232
|
+
autoPlay: props.paused ? false : props.videoProps.autoPlay ?? true,
|
|
3233
|
+
style: props.style
|
|
3234
|
+
}
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
// src/react/internal/use-multi-preload-sources.ts
|
|
3239
|
+
function useMultiPreloadSources(sources, activeSourceId) {
|
|
3240
|
+
return useMemo(() => {
|
|
3241
|
+
const entries = [];
|
|
3242
|
+
for (const [id, runtimeSource] of sources) {
|
|
3243
|
+
const isActive = id === activeSourceId;
|
|
3244
|
+
const shouldMount = Boolean(runtimeSource.preload) || isActive;
|
|
3245
|
+
if (!shouldMount) {
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
const ready = toReadyRuntimeSource(runtimeSource);
|
|
3249
|
+
if (!ready) {
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
entries.push({
|
|
3253
|
+
sourceId: id,
|
|
3254
|
+
source: ready,
|
|
3255
|
+
isActive
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
return entries;
|
|
3259
|
+
}, [sources, activeSourceId]);
|
|
3260
|
+
}
|
|
3261
|
+
function IVITrackSlot(props) {
|
|
3262
|
+
const {
|
|
3263
|
+
slot,
|
|
3264
|
+
trackId,
|
|
3265
|
+
className,
|
|
3266
|
+
style,
|
|
3267
|
+
emptyFallback = null,
|
|
3268
|
+
renderTrtc,
|
|
3269
|
+
renderMedia,
|
|
3270
|
+
videoProps,
|
|
3271
|
+
imageProps,
|
|
3272
|
+
adaptToSourceSize = true,
|
|
3273
|
+
fitStrategy = "contain",
|
|
3274
|
+
trtcPlayerProps,
|
|
3275
|
+
showVolumeControl,
|
|
3276
|
+
volumeControlProps,
|
|
3277
|
+
showSubtitle,
|
|
3278
|
+
subtitleProps,
|
|
3279
|
+
background = "black"
|
|
3280
|
+
} = props;
|
|
3281
|
+
const context = useIviStageView();
|
|
3282
|
+
const containerRef = useRef(null);
|
|
3283
|
+
const resolvedTrackId = trackId ?? (slot ? context.slotTrackMap.get(slot) : void 0);
|
|
3284
|
+
const track = resolvedTrackId ? context.state.tracks.get(resolvedTrackId) : void 0;
|
|
3285
|
+
const activeSourceId = track?.active_source_id ?? null;
|
|
3286
|
+
const preloadEntries = useMultiPreloadSources(context.state.sources, activeSourceId);
|
|
3287
|
+
const activeEntry = preloadEntries.find((e) => e.isActive) ?? null;
|
|
3288
|
+
const activeSource = activeEntry?.source ?? null;
|
|
3289
|
+
const mediaType = detectMediaVolumeType(activeSource);
|
|
3290
|
+
const [volume, setVolume] = useVolumeMemory(showVolumeControl ? mediaType : null);
|
|
3291
|
+
useApplyVolumeToSlot(containerRef, volume, !!showVolumeControl && mediaType !== null, activeSourceId);
|
|
3292
|
+
if (!resolvedTrackId || !track) {
|
|
3293
|
+
return /* @__PURE__ */ jsx("div", { className, style, children: emptyFallback });
|
|
3294
|
+
}
|
|
3295
|
+
const slotContainerStyle = {
|
|
3296
|
+
position: "relative",
|
|
3297
|
+
width: "100%",
|
|
3298
|
+
height: "100%",
|
|
3299
|
+
minWidth: 0,
|
|
3300
|
+
minHeight: 0,
|
|
3301
|
+
...style ?? {}
|
|
3302
|
+
};
|
|
3303
|
+
const blurMode = resolveBlurMode(background);
|
|
3304
|
+
return /* @__PURE__ */ jsxs(
|
|
3305
|
+
"div",
|
|
3306
|
+
{
|
|
3307
|
+
ref: containerRef,
|
|
3308
|
+
className,
|
|
3309
|
+
style: slotContainerStyle,
|
|
3310
|
+
"data-ivi-slot": slot,
|
|
3311
|
+
"data-ivi-track-id": track.track_id,
|
|
3312
|
+
children: [
|
|
3313
|
+
!activeSource && emptyFallback,
|
|
3314
|
+
preloadEntries.map((entry) => {
|
|
3315
|
+
const isActive = entry.isActive;
|
|
3316
|
+
const showBlur = blurMode !== false && isActive;
|
|
3317
|
+
const content = /* @__PURE__ */ jsx(
|
|
3318
|
+
TrackSlotMediaContent,
|
|
3319
|
+
{
|
|
3320
|
+
slot,
|
|
3321
|
+
track,
|
|
3322
|
+
source: entry.source,
|
|
3323
|
+
slotSourceId: entry.sourceId,
|
|
3324
|
+
isActive,
|
|
3325
|
+
runtime: context.runtime,
|
|
3326
|
+
renderTrtc,
|
|
3327
|
+
renderMedia,
|
|
3328
|
+
imageProps,
|
|
3329
|
+
videoProps,
|
|
3330
|
+
trtcPlayerProps,
|
|
3331
|
+
adaptToSourceSize,
|
|
3332
|
+
fitStrategy,
|
|
3333
|
+
background
|
|
3334
|
+
}
|
|
3335
|
+
);
|
|
3336
|
+
return /* @__PURE__ */ jsx(
|
|
3337
|
+
"div",
|
|
3338
|
+
{
|
|
3339
|
+
style: isActive ? ACTIVE_SLOT_STYLE : STANDBY_SLOT_STYLE,
|
|
3340
|
+
"data-ivi-source-id": entry.sourceId,
|
|
3341
|
+
"data-ivi-slot-role": isActive ? "active" : "standby",
|
|
3342
|
+
children: /* @__PURE__ */ jsx("div", { style: SLOT_CONTENT_STYLE, children: showBlur ? /* @__PURE__ */ jsx(TrackSlotBlurLayer, { source: entry.source, mode: blurMode, children: content }) : content })
|
|
3343
|
+
},
|
|
3344
|
+
entry.sourceId
|
|
3345
|
+
);
|
|
3346
|
+
}),
|
|
3347
|
+
showSubtitle && activeSource && supportsSubtitleOverlay(activeSource) && /* @__PURE__ */ jsx("div", { style: SUBTITLE_OVERLAY_STYLE, children: /* @__PURE__ */ jsx(
|
|
3348
|
+
IVISubtitleOverlay,
|
|
3349
|
+
{
|
|
3350
|
+
conversations: context.state.conversations,
|
|
3351
|
+
...subtitleProps
|
|
3352
|
+
}
|
|
3353
|
+
) }),
|
|
3354
|
+
showVolumeControl && mediaType !== null && /* @__PURE__ */ jsx("div", { style: VOLUME_OVERLAY_STYLE, children: /* @__PURE__ */ jsx(
|
|
3355
|
+
IVIVolumeControl,
|
|
3356
|
+
{
|
|
3357
|
+
volume,
|
|
3358
|
+
onVolumeChange: setVolume,
|
|
3359
|
+
...volumeControlProps
|
|
3360
|
+
}
|
|
3361
|
+
) })
|
|
3362
|
+
]
|
|
3363
|
+
}
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
var PHYSICAL_SLOT_BASE = {
|
|
3367
|
+
position: "absolute",
|
|
3368
|
+
top: 0,
|
|
3369
|
+
left: 0,
|
|
3370
|
+
width: "100%",
|
|
3371
|
+
height: "100%"
|
|
3372
|
+
};
|
|
3373
|
+
var ACTIVE_SLOT_STYLE = {
|
|
3374
|
+
...PHYSICAL_SLOT_BASE,
|
|
3375
|
+
visibility: "visible",
|
|
3376
|
+
zIndex: 1
|
|
3377
|
+
};
|
|
3378
|
+
var STANDBY_SLOT_STYLE = {
|
|
3379
|
+
...PHYSICAL_SLOT_BASE,
|
|
3380
|
+
visibility: "hidden",
|
|
3381
|
+
zIndex: 0,
|
|
3382
|
+
pointerEvents: "none"
|
|
3383
|
+
};
|
|
3384
|
+
var SLOT_CONTENT_STYLE = {
|
|
3385
|
+
width: "100%",
|
|
3386
|
+
height: "100%"
|
|
3387
|
+
};
|
|
3388
|
+
var SUBTITLE_OVERLAY_STYLE = {
|
|
3389
|
+
position: "absolute",
|
|
3390
|
+
bottom: 48,
|
|
3391
|
+
left: "50%",
|
|
3392
|
+
transform: "translateX(-50%)",
|
|
3393
|
+
zIndex: 10,
|
|
3394
|
+
maxWidth: "90%",
|
|
3395
|
+
pointerEvents: "none"
|
|
3396
|
+
};
|
|
3397
|
+
var VOLUME_OVERLAY_STYLE = {
|
|
3398
|
+
position: "absolute",
|
|
3399
|
+
bottom: 12,
|
|
3400
|
+
right: 12,
|
|
3401
|
+
zIndex: 10,
|
|
3402
|
+
pointerEvents: "auto"
|
|
3403
|
+
};
|
|
3404
|
+
function useManagedIviRuntime(config) {
|
|
3405
|
+
const {
|
|
3406
|
+
clientConfig,
|
|
3407
|
+
autoStart = true,
|
|
3408
|
+
runtimeConfig,
|
|
3409
|
+
onRuntimeInitError,
|
|
3410
|
+
onLog
|
|
3411
|
+
} = config;
|
|
3412
|
+
const { url, sessionId } = clientConfig;
|
|
3413
|
+
const runtime = useMemo(() => {
|
|
3414
|
+
if (typeof window === "undefined" || !sessionId) {
|
|
3415
|
+
return null;
|
|
3416
|
+
}
|
|
3417
|
+
if (!url) {
|
|
3418
|
+
throw new Error("useManagedIviRuntime: `url` is required when `sessionId` is provided.");
|
|
3419
|
+
}
|
|
3420
|
+
const mergedClientConfig = {
|
|
3421
|
+
...clientConfig,
|
|
3422
|
+
url: normalizeUrlToWsUrl(url),
|
|
3423
|
+
onLog: (entry) => {
|
|
3424
|
+
onLog?.(normalizeClientLogEntry(entry));
|
|
3425
|
+
clientConfig.onLog?.(entry);
|
|
3426
|
+
}
|
|
3427
|
+
};
|
|
3428
|
+
const client = new IviClient(mergedClientConfig);
|
|
3429
|
+
const mergedRuntimeConfig = {
|
|
3430
|
+
...runtimeConfig,
|
|
3431
|
+
onLog: (entry) => {
|
|
3432
|
+
onLog?.(normalizeRuntimeLogEntry(entry));
|
|
3433
|
+
runtimeConfig?.onLog?.(entry);
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
return new IviRuntimeCoordinator(client, mergedRuntimeConfig);
|
|
3437
|
+
}, [url, sessionId, clientConfig, runtimeConfig, onLog]);
|
|
3438
|
+
useEffect(() => {
|
|
3439
|
+
if (!autoStart || !runtime) {
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
let disposed = false;
|
|
3443
|
+
runtime.start().catch((error) => {
|
|
3444
|
+
if (disposed) {
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
if (onRuntimeInitError) {
|
|
3448
|
+
onRuntimeInitError(error);
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
throw error;
|
|
3452
|
+
});
|
|
3453
|
+
return () => {
|
|
3454
|
+
disposed = true;
|
|
3455
|
+
runtime.stop();
|
|
3456
|
+
};
|
|
3457
|
+
}, [autoStart, runtime, onRuntimeInitError]);
|
|
3458
|
+
return runtime;
|
|
3459
|
+
}
|
|
3460
|
+
function normalizeUrlToWsUrl(url) {
|
|
3461
|
+
if (/^wss?:\/\//.test(url)) {
|
|
3462
|
+
return url;
|
|
3463
|
+
}
|
|
3464
|
+
if (/^https?:\/\//.test(url)) {
|
|
3465
|
+
const normalized = new URL(url);
|
|
3466
|
+
normalized.protocol = normalized.protocol === "https:" ? "wss:" : "ws:";
|
|
3467
|
+
return normalized.toString();
|
|
3468
|
+
}
|
|
3469
|
+
throw new Error(
|
|
3470
|
+
`useManagedIviRuntime: invalid url "${url}". Expected a ws://, wss://, http://, or https:// URL.`
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3473
|
+
function normalizeClientLogEntry(entry) {
|
|
3474
|
+
const tag = getClientLogTag(entry.category);
|
|
3475
|
+
return {
|
|
3476
|
+
level: entry.level === "warn" || entry.level === "error" ? entry.level : "info",
|
|
3477
|
+
source: "client",
|
|
3478
|
+
tag,
|
|
3479
|
+
message: `${tag} ${entry.message}`,
|
|
3480
|
+
args: entry.data === void 0 ? [`${tag} ${entry.message}`] : [`${tag} ${entry.message}`, entry.data],
|
|
3481
|
+
data: entry.data,
|
|
3482
|
+
clientLog: entry
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
function normalizeRuntimeLogEntry(entry) {
|
|
3486
|
+
return {
|
|
3487
|
+
level: entry.level,
|
|
3488
|
+
source: "runtime",
|
|
3489
|
+
tag: entry.tag,
|
|
3490
|
+
message: entry.message,
|
|
3491
|
+
args: entry.args,
|
|
3492
|
+
data: entry.data,
|
|
3493
|
+
runtimeLog: entry
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
function getClientLogTag(category) {
|
|
3497
|
+
if (category === "send") return "[IVI-SEND]";
|
|
3498
|
+
if (category === "ws") return "[IVI-WS]";
|
|
3499
|
+
if (category === "reconnect") return "[IVI-RECONNECT]";
|
|
3500
|
+
return "[IVI-CLIENT]";
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
export { EMPTY_RUNTIME_STATE, IVIStageView, IVITrackSlot, IviFrontendSdk, IviRuntimeCoordinator, IviRuntimeDispatcher, useIviStageView, useManagedIviRuntime, useRuntimeState };
|
|
3504
|
+
//# sourceMappingURL=index.js.map
|
|
3505
|
+
//# sourceMappingURL=index.js.map
|