@vivix-ai/ivi-frontend-sdk 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1117,6 +1117,7 @@ var TrtcSourceManager = class {
1117
1117
  this.onTrtcEvent = onTrtcEvent;
1118
1118
  this.aiDenoiser = aiDenoiser;
1119
1119
  this.sessions = /* @__PURE__ */ new Map();
1120
+ this.sourceSessionKeys = /* @__PURE__ */ new Map();
1120
1121
  this.listeners = /* @__PURE__ */ new Map();
1121
1122
  }
1122
1123
  /**
@@ -1125,15 +1126,16 @@ var TrtcSourceManager = class {
1125
1126
  * - 清理已不在 runtime 中的会话(释放资源)
1126
1127
  */
1127
1128
  syncRuntimeSources(sources) {
1128
- const existingIds = new Set(sources.keys());
1129
+ const trtcSourceIds = /* @__PURE__ */ new Set();
1129
1130
  sources.forEach((source, sourceId) => {
1130
1131
  if (!isRuntimeTrtcSource(source)) {
1131
1132
  return;
1132
1133
  }
1134
+ trtcSourceIds.add(sourceId);
1133
1135
  this.upsertSource(sourceId, source.playback.trtc);
1134
1136
  });
1135
- this.sessions.forEach((_session, sourceId) => {
1136
- if (!existingIds.has(sourceId)) {
1137
+ Array.from(this.sourceSessionKeys.keys()).forEach((sourceId) => {
1138
+ if (!trtcSourceIds.has(sourceId)) {
1137
1139
  this.removeSource(sourceId);
1138
1140
  }
1139
1141
  });
@@ -1143,21 +1145,28 @@ var TrtcSourceManager = class {
1143
1145
  * 若配置发生变化,会先销毁旧会话再按新参数重建连接。
1144
1146
  */
1145
1147
  upsertSource(sourceId, trtc, aiDenoiser) {
1146
- const existing = this.sessions.get(sourceId);
1147
- if (!existing) {
1148
- this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1149
- const session2 = this.createSession(sourceId, trtc, aiDenoiser);
1150
- this.sessions.set(sourceId, session2);
1151
- void this.ensureConnected(session2);
1148
+ const existing = this.getSession(sourceId);
1149
+ if (existing && isSameTrtcConfig(existing.trtc, trtc) && isSameAIDenoiserOptions(existing.aiDenoiser, aiDenoiser)) {
1152
1150
  return;
1153
1151
  }
1154
- if (isSameTrtcConfig(existing.trtc, trtc) && isSameAIDenoiserOptions(existing.aiDenoiser, aiDenoiser)) {
1152
+ if (existing) {
1153
+ this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")}`);
1154
+ this.unlinkSourceFromSession(sourceId, existing);
1155
+ }
1156
+ const sessionKey = buildTrtcSessionKey(trtc, aiDenoiser);
1157
+ const reusable = this.sessions.get(sessionKey);
1158
+ if (reusable) {
1159
+ this.log("info", `\u590D\u7528\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1160
+ reusable.sourceIds.add(sourceId);
1161
+ this.sourceSessionKeys.set(sourceId, sessionKey);
1162
+ this.emitSnapshot(sourceId, this.snapshotFromSession(reusable));
1163
+ void this.ensureConnected(reusable);
1155
1164
  return;
1156
1165
  }
1157
- this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")}`);
1158
- void this.disposeSession(existing);
1166
+ this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1159
1167
  const session = this.createSession(sourceId, trtc, aiDenoiser);
1160
- this.sessions.set(sourceId, session);
1168
+ this.sessions.set(session.sessionKey, session);
1169
+ this.sourceSessionKeys.set(sourceId, session.sessionKey);
1161
1170
  void this.ensureConnected(session);
1162
1171
  }
1163
1172
  /**
@@ -1165,13 +1174,12 @@ var TrtcSourceManager = class {
1165
1174
  * 该操作通常由 source.deleted 或会话重置触发。
1166
1175
  */
1167
1176
  removeSource(sourceId) {
1168
- const session = this.sessions.get(sourceId);
1177
+ const session = this.getSession(sourceId);
1169
1178
  if (!session) {
1170
1179
  return;
1171
1180
  }
1172
1181
  this.log("info", `\u79FB\u9664\u4F1A\u8BDD source=${sourceId}`);
1173
- this.sessions.delete(sourceId);
1174
- void this.disposeSession(session);
1182
+ this.unlinkSourceFromSession(sourceId, session);
1175
1183
  this.emitSnapshot(sourceId, {
1176
1184
  status: "idle"
1177
1185
  });
@@ -1181,6 +1189,7 @@ var TrtcSourceManager = class {
1181
1189
  this.log("info", `\u91CD\u7F6E\u5168\u90E8\u4F1A\u8BDD count=${this.sessions.size}`);
1182
1190
  const sessions = Array.from(this.sessions.values());
1183
1191
  this.sessions.clear();
1192
+ this.sourceSessionKeys.clear();
1184
1193
  sessions.forEach((session) => {
1185
1194
  void this.disposeSession(session);
1186
1195
  });
@@ -1203,7 +1212,7 @@ var TrtcSourceManager = class {
1203
1212
  };
1204
1213
  }
1205
1214
  getSnapshot(sourceId) {
1206
- const session = this.sessions.get(sourceId);
1215
+ const session = this.getSession(sourceId);
1207
1216
  if (!session) {
1208
1217
  return {
1209
1218
  status: "idle"
@@ -1218,7 +1227,7 @@ var TrtcSourceManager = class {
1218
1227
  * 检查指定 source 的 TRTC 会话是否曾收到过 REMOTE_VIDEO_AVAILABLE 事件。
1219
1228
  */
1220
1229
  hasRemoteVideoAvailable(sourceId) {
1221
- const session = this.sessions.get(sourceId);
1230
+ const session = this.getSession(sourceId);
1222
1231
  return session?.hasEverReceivedRemoteVideo ?? false;
1223
1232
  }
1224
1233
  /**
@@ -1227,7 +1236,7 @@ var TrtcSourceManager = class {
1227
1236
  * - 若会话被销毁或超时,返回 false。
1228
1237
  */
1229
1238
  waitForRemoteVideoAvailable(sourceId, timeoutMs = 3e4) {
1230
- const session = this.sessions.get(sourceId);
1239
+ const session = this.getSession(sourceId);
1231
1240
  if (!session) return Promise.resolve(false);
1232
1241
  if (session.hasEverReceivedRemoteVideo) return Promise.resolve(true);
1233
1242
  return new Promise((resolve) => {
@@ -1255,11 +1264,12 @@ var TrtcSourceManager = class {
1255
1264
  * - 应用全局音频策略(根据所有 view 的 muted 汇总)
1256
1265
  */
1257
1266
  async attachView(sourceId, viewId, container, muted) {
1258
- const session = this.sessions.get(sourceId);
1267
+ const session = this.getSession(sourceId);
1259
1268
  if (!session) {
1260
1269
  throw new Error(`TRTC source session not found: ${sourceId}`);
1261
1270
  }
1262
1271
  session.views.set(viewId, {
1272
+ sourceId,
1263
1273
  container,
1264
1274
  muted,
1265
1275
  startedVideoKeys: /* @__PURE__ */ new Set(),
@@ -1274,15 +1284,18 @@ var TrtcSourceManager = class {
1274
1284
  await this.applyAudioPolicy(session);
1275
1285
  }
1276
1286
  detachView(sourceId, viewId) {
1277
- const session = this.sessions.get(sourceId);
1287
+ const session = this.getSession(sourceId);
1278
1288
  if (!session) {
1279
1289
  return;
1280
1290
  }
1281
- session.views.delete(viewId);
1291
+ const binding = session.views.get(viewId);
1292
+ if (binding?.sourceId === sourceId) {
1293
+ session.views.delete(viewId);
1294
+ }
1282
1295
  void this.applyAudioPolicy(session);
1283
1296
  }
1284
1297
  updateViewMuted(sourceId, viewId, muted) {
1285
- const session = this.sessions.get(sourceId);
1298
+ const session = this.getSession(sourceId);
1286
1299
  if (!session) {
1287
1300
  return;
1288
1301
  }
@@ -1293,9 +1306,43 @@ var TrtcSourceManager = class {
1293
1306
  binding.muted = muted;
1294
1307
  void this.applyAudioPolicy(session);
1295
1308
  }
1309
+ unlinkSourceFromSession(sourceId, session) {
1310
+ session.sourceIds.delete(sourceId);
1311
+ this.sourceSessionKeys.delete(sourceId);
1312
+ session.views.forEach((binding, viewId) => {
1313
+ if (binding.sourceId === sourceId) {
1314
+ session.views.delete(viewId);
1315
+ }
1316
+ });
1317
+ if (session.sourceIds.size > 0) {
1318
+ session.sourceId = session.sourceIds.values().next().value ?? session.sourceId;
1319
+ void this.applyAudioPolicy(session);
1320
+ return;
1321
+ }
1322
+ this.sessions.delete(session.sessionKey);
1323
+ void this.disposeSession(session);
1324
+ }
1325
+ getSession(sourceId) {
1326
+ const sessionKey = this.sourceSessionKeys.get(sourceId);
1327
+ return sessionKey ? this.sessions.get(sessionKey) : void 0;
1328
+ }
1329
+ snapshotFromSession(session) {
1330
+ return {
1331
+ status: session.status,
1332
+ error: session.error
1333
+ };
1334
+ }
1335
+ emitSessionSnapshot(session, snapshot) {
1336
+ session.sourceIds.forEach((sourceId) => this.emitSnapshot(sourceId, snapshot));
1337
+ }
1338
+ emitSessionTrtcEvent(session, type, rawType, payload) {
1339
+ session.sourceIds.forEach((sourceId) => this.emitTrtcEvent(sourceId, type, rawType, payload));
1340
+ }
1296
1341
  createSession(sourceId, trtc, aiDenoiser) {
1297
1342
  return {
1343
+ sessionKey: buildTrtcSessionKey(trtc, aiDenoiser),
1298
1344
  sourceId,
1345
+ sourceIds: /* @__PURE__ */ new Set([sourceId]),
1299
1346
  trtc,
1300
1347
  aiDenoiser,
1301
1348
  TRTC: null,
@@ -1323,7 +1370,7 @@ var TrtcSourceManager = class {
1323
1370
  }
1324
1371
  session.status = "connecting";
1325
1372
  session.error = void 0;
1326
- this.emitSnapshot(session.sourceId, {
1373
+ this.emitSessionSnapshot(session, {
1327
1374
  status: session.status
1328
1375
  });
1329
1376
  session.connectPromise = (async () => {
@@ -1352,7 +1399,7 @@ var TrtcSourceManager = class {
1352
1399
  session.views.forEach((binding) => {
1353
1400
  void this.startRemoteVideoForBinding(client, event.userId, event.streamType, binding, remoteVideoKey);
1354
1401
  });
1355
- this.emitTrtcEvent(session.sourceId, "remote_video_available", TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, event);
1402
+ this.emitSessionTrtcEvent(session, "remote_video_available", TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, event);
1356
1403
  };
1357
1404
  const onRemoteVideoUnavailable = (event) => {
1358
1405
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
@@ -1366,19 +1413,19 @@ var TrtcSourceManager = class {
1366
1413
  userId: event.userId,
1367
1414
  streamType: event.streamType
1368
1415
  }).catch(() => void 0);
1369
- this.emitTrtcEvent(session.sourceId, "remote_video_unavailable", TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, event);
1416
+ this.emitSessionTrtcEvent(session, "remote_video_unavailable", TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, event);
1370
1417
  };
1371
1418
  const onRemoteAudioAvailable = (event) => {
1372
1419
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1373
1420
  session.remoteAudioUsers.add(event.userId);
1374
1421
  void this.applyAudioPolicy(session);
1375
- this.emitTrtcEvent(session.sourceId, "remote_audio_available", TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, event);
1422
+ this.emitSessionTrtcEvent(session, "remote_audio_available", TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, event);
1376
1423
  };
1377
1424
  const onRemoteAudioUnavailable = (event) => {
1378
1425
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1379
1426
  session.remoteAudioUsers.delete(event.userId);
1380
1427
  void this.applyAudioPolicy(session);
1381
- this.emitTrtcEvent(session.sourceId, "remote_audio_unavailable", TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, event);
1428
+ this.emitSessionTrtcEvent(session, "remote_audio_unavailable", TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, event);
1382
1429
  };
1383
1430
  session.onRemoteVideoAvailable = onRemoteVideoAvailable;
1384
1431
  session.onRemoteVideoUnavailable = onRemoteVideoUnavailable;
@@ -1404,7 +1451,7 @@ var TrtcSourceManager = class {
1404
1451
  session.error = void 0;
1405
1452
  this.log("info", `\u8FDB\u623F\u6210\u529F source=${session.sourceId} room=${roomId}`);
1406
1453
  await this.applyAIDenoiserPolicy(session, client, sdkAppId, userId, userSig);
1407
- this.emitSnapshot(session.sourceId, {
1454
+ this.emitSessionSnapshot(session, {
1408
1455
  status: session.status
1409
1456
  });
1410
1457
  await this.applyAudioPolicy(session);
@@ -1412,7 +1459,7 @@ var TrtcSourceManager = class {
1412
1459
  session.status = "error";
1413
1460
  session.error = error instanceof Error ? error.message : String(error);
1414
1461
  this.log("error", `\u8FDE\u63A5\u5931\u8D25 source=${session.sourceId} error=${session.error}`);
1415
- this.emitSnapshot(session.sourceId, {
1462
+ this.emitSessionSnapshot(session, {
1416
1463
  status: session.status,
1417
1464
  error: session.error
1418
1465
  });
@@ -1589,6 +1636,20 @@ function isRuntimeTrtcSource(source) {
1589
1636
  function isSameTrtcConfig(a, b) {
1590
1637
  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;
1591
1638
  }
1639
+ function buildTrtcSessionKey(trtc, aiDenoiser) {
1640
+ const denoiser = resolveAIDenoiserOptions(aiDenoiser);
1641
+ return JSON.stringify({
1642
+ appId: trtc.app_id,
1643
+ roomId: trtc.room_id,
1644
+ userId: trtc.user_id,
1645
+ userSig: trtc.user_sig,
1646
+ aiDenoiser: {
1647
+ enabled: denoiser.enabled,
1648
+ mode: denoiser.mode,
1649
+ assetsPath: denoiser.assetsPath
1650
+ }
1651
+ });
1652
+ }
1592
1653
  function isSameAIDenoiserOptions(a, b) {
1593
1654
  const left = resolveAIDenoiserOptions(a);
1594
1655
  const right = resolveAIDenoiserOptions(b);
@@ -3767,6 +3828,19 @@ function detectMediaVolumeType(source) {
3767
3828
  if (!url) return null;
3768
3829
  return isM3u8Url(url) ? "hls" : "video";
3769
3830
  }
3831
+ function getSourceRenderKey(sourceId, source) {
3832
+ const trtc = getTrtcPlayback(source.playback);
3833
+ if (!trtc) {
3834
+ return sourceId;
3835
+ }
3836
+ return [
3837
+ "trtc",
3838
+ trtc.app_id ?? "",
3839
+ trtc.room_id ?? "",
3840
+ trtc.user_id ?? "",
3841
+ trtc.user_sig ?? ""
3842
+ ].join(":");
3843
+ }
3770
3844
  function TrackSlotMediaContent(props) {
3771
3845
  const {
3772
3846
  slot,
@@ -3804,7 +3878,7 @@ function TrackSlotMediaContent(props) {
3804
3878
  IVITrtcPlayer,
3805
3879
  {
3806
3880
  trtc,
3807
- sourceId: source.source.source_id,
3881
+ sourceId: slotSourceId,
3808
3882
  runtime,
3809
3883
  ...trtcPlayerProps,
3810
3884
  muted: trtcMuted,
@@ -3959,26 +4033,35 @@ function SlotVideo(props) {
3959
4033
  // src/react/internal/use-multi-preload-sources.ts
3960
4034
  function useMultiPreloadSources(sources, activeSourceId) {
3961
4035
  return react.useMemo(() => {
3962
- const entries = [];
3963
- for (const [id, runtimeSource] of sources) {
3964
- const isActive = id === activeSourceId;
3965
- const shouldMount = Boolean(runtimeSource.preload) || isActive;
3966
- if (!shouldMount) {
3967
- continue;
3968
- }
3969
- const ready = toReadyRuntimeSource(runtimeSource);
3970
- if (!ready) {
3971
- continue;
3972
- }
3973
- entries.push({
3974
- sourceId: id,
3975
- source: ready,
3976
- isActive
3977
- });
3978
- }
3979
- return entries;
4036
+ return buildPreloadSourceEntries(sources, activeSourceId);
3980
4037
  }, [sources, activeSourceId]);
3981
4038
  }
4039
+ function buildPreloadSourceEntries(sources, activeSourceId) {
4040
+ const entriesByRenderKey = /* @__PURE__ */ new Map();
4041
+ for (const [id, runtimeSource] of sources) {
4042
+ const isActive = id === activeSourceId;
4043
+ const shouldMount = Boolean(runtimeSource.preload) || isActive;
4044
+ if (!shouldMount) {
4045
+ continue;
4046
+ }
4047
+ const ready = toReadyRuntimeSource(runtimeSource);
4048
+ if (!ready) {
4049
+ continue;
4050
+ }
4051
+ const renderKey = getSourceRenderKey(id, ready);
4052
+ const entry = {
4053
+ sourceId: id,
4054
+ renderKey,
4055
+ source: ready,
4056
+ isActive
4057
+ };
4058
+ const previous = entriesByRenderKey.get(renderKey);
4059
+ if (!previous || !previous.isActive && isActive) {
4060
+ entriesByRenderKey.set(renderKey, entry);
4061
+ }
4062
+ }
4063
+ return Array.from(entriesByRenderKey.values());
4064
+ }
3982
4065
  function IVITrackSlot(props) {
3983
4066
  const {
3984
4067
  slot,
@@ -4068,7 +4151,7 @@ function IVITrackSlot(props) {
4068
4151
  }
4069
4152
  ) })
4070
4153
  },
4071
- entry.sourceId
4154
+ entry.renderKey
4072
4155
  );
4073
4156
  }),
4074
4157
  showSubtitle && activeSource && supportsSubtitleOverlay(activeSource) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: SUBTITLE_OVERLAY_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx(