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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -198,6 +198,7 @@ declare class TrtcSourceManager {
198
198
  private readonly onTrtcEvent?;
199
199
  private readonly aiDenoiser?;
200
200
  private readonly sessions;
201
+ private readonly sourceSessionKeys;
201
202
  private readonly listeners;
202
203
  constructor(onLog?: IviRuntimeLogCallback | undefined, onTrtcEvent?: IviRuntimeTrtcEventListener | undefined, aiDenoiser?: (boolean | IviRuntimeTrtcAIDenoiserOptions) | undefined);
203
204
  /**
@@ -238,6 +239,11 @@ declare class TrtcSourceManager {
238
239
  attachView(sourceId: string, viewId: string, container: HTMLElement, muted: boolean): Promise<void>;
239
240
  detachView(sourceId: string, viewId: string): void;
240
241
  updateViewMuted(sourceId: string, viewId: string, muted: boolean): void;
242
+ private unlinkSourceFromSession;
243
+ private getSession;
244
+ private snapshotFromSession;
245
+ private emitSessionSnapshot;
246
+ private emitSessionTrtcEvent;
241
247
  private createSession;
242
248
  /**
243
249
  * 懒连接入口:同一会话只允许一次并发 connectPromise。
package/dist/index.d.ts CHANGED
@@ -198,6 +198,7 @@ declare class TrtcSourceManager {
198
198
  private readonly onTrtcEvent?;
199
199
  private readonly aiDenoiser?;
200
200
  private readonly sessions;
201
+ private readonly sourceSessionKeys;
201
202
  private readonly listeners;
202
203
  constructor(onLog?: IviRuntimeLogCallback | undefined, onTrtcEvent?: IviRuntimeTrtcEventListener | undefined, aiDenoiser?: (boolean | IviRuntimeTrtcAIDenoiserOptions) | undefined);
203
204
  /**
@@ -238,6 +239,11 @@ declare class TrtcSourceManager {
238
239
  attachView(sourceId: string, viewId: string, container: HTMLElement, muted: boolean): Promise<void>;
239
240
  detachView(sourceId: string, viewId: string): void;
240
241
  updateViewMuted(sourceId: string, viewId: string, muted: boolean): void;
242
+ private unlinkSourceFromSession;
243
+ private getSession;
244
+ private snapshotFromSession;
245
+ private emitSessionSnapshot;
246
+ private emitSessionTrtcEvent;
241
247
  private createSession;
242
248
  /**
243
249
  * 懒连接入口:同一会话只允许一次并发 connectPromise。
package/dist/index.js CHANGED
@@ -1115,6 +1115,7 @@ var TrtcSourceManager = class {
1115
1115
  this.onTrtcEvent = onTrtcEvent;
1116
1116
  this.aiDenoiser = aiDenoiser;
1117
1117
  this.sessions = /* @__PURE__ */ new Map();
1118
+ this.sourceSessionKeys = /* @__PURE__ */ new Map();
1118
1119
  this.listeners = /* @__PURE__ */ new Map();
1119
1120
  }
1120
1121
  /**
@@ -1123,15 +1124,16 @@ var TrtcSourceManager = class {
1123
1124
  * - 清理已不在 runtime 中的会话(释放资源)
1124
1125
  */
1125
1126
  syncRuntimeSources(sources) {
1126
- const existingIds = new Set(sources.keys());
1127
+ const trtcSourceIds = /* @__PURE__ */ new Set();
1127
1128
  sources.forEach((source, sourceId) => {
1128
1129
  if (!isRuntimeTrtcSource(source)) {
1129
1130
  return;
1130
1131
  }
1132
+ trtcSourceIds.add(sourceId);
1131
1133
  this.upsertSource(sourceId, source.playback.trtc);
1132
1134
  });
1133
- this.sessions.forEach((_session, sourceId) => {
1134
- if (!existingIds.has(sourceId)) {
1135
+ Array.from(this.sourceSessionKeys.keys()).forEach((sourceId) => {
1136
+ if (!trtcSourceIds.has(sourceId)) {
1135
1137
  this.removeSource(sourceId);
1136
1138
  }
1137
1139
  });
@@ -1141,21 +1143,28 @@ var TrtcSourceManager = class {
1141
1143
  * 若配置发生变化,会先销毁旧会话再按新参数重建连接。
1142
1144
  */
1143
1145
  upsertSource(sourceId, trtc, aiDenoiser) {
1144
- const existing = this.sessions.get(sourceId);
1145
- if (!existing) {
1146
- this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1147
- const session2 = this.createSession(sourceId, trtc, aiDenoiser);
1148
- this.sessions.set(sourceId, session2);
1149
- void this.ensureConnected(session2);
1146
+ const existing = this.getSession(sourceId);
1147
+ if (existing && isSameTrtcConfig(existing.trtc, trtc) && isSameAIDenoiserOptions(existing.aiDenoiser, aiDenoiser)) {
1150
1148
  return;
1151
1149
  }
1152
- if (isSameTrtcConfig(existing.trtc, trtc) && isSameAIDenoiserOptions(existing.aiDenoiser, aiDenoiser)) {
1150
+ if (existing) {
1151
+ this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")}`);
1152
+ this.unlinkSourceFromSession(sourceId, existing);
1153
+ }
1154
+ const sessionKey = buildTrtcSessionKey(trtc, aiDenoiser);
1155
+ const reusable = this.sessions.get(sessionKey);
1156
+ if (reusable) {
1157
+ this.log("info", `\u590D\u7528\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1158
+ reusable.sourceIds.add(sourceId);
1159
+ this.sourceSessionKeys.set(sourceId, sessionKey);
1160
+ this.emitSnapshot(sourceId, this.snapshotFromSession(reusable));
1161
+ void this.ensureConnected(reusable);
1153
1162
  return;
1154
1163
  }
1155
- this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")}`);
1156
- void this.disposeSession(existing);
1164
+ this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1157
1165
  const session = this.createSession(sourceId, trtc, aiDenoiser);
1158
- this.sessions.set(sourceId, session);
1166
+ this.sessions.set(session.sessionKey, session);
1167
+ this.sourceSessionKeys.set(sourceId, session.sessionKey);
1159
1168
  void this.ensureConnected(session);
1160
1169
  }
1161
1170
  /**
@@ -1163,13 +1172,12 @@ var TrtcSourceManager = class {
1163
1172
  * 该操作通常由 source.deleted 或会话重置触发。
1164
1173
  */
1165
1174
  removeSource(sourceId) {
1166
- const session = this.sessions.get(sourceId);
1175
+ const session = this.getSession(sourceId);
1167
1176
  if (!session) {
1168
1177
  return;
1169
1178
  }
1170
1179
  this.log("info", `\u79FB\u9664\u4F1A\u8BDD source=${sourceId}`);
1171
- this.sessions.delete(sourceId);
1172
- void this.disposeSession(session);
1180
+ this.unlinkSourceFromSession(sourceId, session);
1173
1181
  this.emitSnapshot(sourceId, {
1174
1182
  status: "idle"
1175
1183
  });
@@ -1179,6 +1187,7 @@ var TrtcSourceManager = class {
1179
1187
  this.log("info", `\u91CD\u7F6E\u5168\u90E8\u4F1A\u8BDD count=${this.sessions.size}`);
1180
1188
  const sessions = Array.from(this.sessions.values());
1181
1189
  this.sessions.clear();
1190
+ this.sourceSessionKeys.clear();
1182
1191
  sessions.forEach((session) => {
1183
1192
  void this.disposeSession(session);
1184
1193
  });
@@ -1201,7 +1210,7 @@ var TrtcSourceManager = class {
1201
1210
  };
1202
1211
  }
1203
1212
  getSnapshot(sourceId) {
1204
- const session = this.sessions.get(sourceId);
1213
+ const session = this.getSession(sourceId);
1205
1214
  if (!session) {
1206
1215
  return {
1207
1216
  status: "idle"
@@ -1216,7 +1225,7 @@ var TrtcSourceManager = class {
1216
1225
  * 检查指定 source 的 TRTC 会话是否曾收到过 REMOTE_VIDEO_AVAILABLE 事件。
1217
1226
  */
1218
1227
  hasRemoteVideoAvailable(sourceId) {
1219
- const session = this.sessions.get(sourceId);
1228
+ const session = this.getSession(sourceId);
1220
1229
  return session?.hasEverReceivedRemoteVideo ?? false;
1221
1230
  }
1222
1231
  /**
@@ -1225,7 +1234,7 @@ var TrtcSourceManager = class {
1225
1234
  * - 若会话被销毁或超时,返回 false。
1226
1235
  */
1227
1236
  waitForRemoteVideoAvailable(sourceId, timeoutMs = 3e4) {
1228
- const session = this.sessions.get(sourceId);
1237
+ const session = this.getSession(sourceId);
1229
1238
  if (!session) return Promise.resolve(false);
1230
1239
  if (session.hasEverReceivedRemoteVideo) return Promise.resolve(true);
1231
1240
  return new Promise((resolve) => {
@@ -1253,11 +1262,12 @@ var TrtcSourceManager = class {
1253
1262
  * - 应用全局音频策略(根据所有 view 的 muted 汇总)
1254
1263
  */
1255
1264
  async attachView(sourceId, viewId, container, muted) {
1256
- const session = this.sessions.get(sourceId);
1265
+ const session = this.getSession(sourceId);
1257
1266
  if (!session) {
1258
1267
  throw new Error(`TRTC source session not found: ${sourceId}`);
1259
1268
  }
1260
1269
  session.views.set(viewId, {
1270
+ sourceId,
1261
1271
  container,
1262
1272
  muted,
1263
1273
  startedVideoKeys: /* @__PURE__ */ new Set(),
@@ -1272,15 +1282,18 @@ var TrtcSourceManager = class {
1272
1282
  await this.applyAudioPolicy(session);
1273
1283
  }
1274
1284
  detachView(sourceId, viewId) {
1275
- const session = this.sessions.get(sourceId);
1285
+ const session = this.getSession(sourceId);
1276
1286
  if (!session) {
1277
1287
  return;
1278
1288
  }
1279
- session.views.delete(viewId);
1289
+ const binding = session.views.get(viewId);
1290
+ if (binding?.sourceId === sourceId) {
1291
+ session.views.delete(viewId);
1292
+ }
1280
1293
  void this.applyAudioPolicy(session);
1281
1294
  }
1282
1295
  updateViewMuted(sourceId, viewId, muted) {
1283
- const session = this.sessions.get(sourceId);
1296
+ const session = this.getSession(sourceId);
1284
1297
  if (!session) {
1285
1298
  return;
1286
1299
  }
@@ -1291,9 +1304,43 @@ var TrtcSourceManager = class {
1291
1304
  binding.muted = muted;
1292
1305
  void this.applyAudioPolicy(session);
1293
1306
  }
1307
+ unlinkSourceFromSession(sourceId, session) {
1308
+ session.sourceIds.delete(sourceId);
1309
+ this.sourceSessionKeys.delete(sourceId);
1310
+ session.views.forEach((binding, viewId) => {
1311
+ if (binding.sourceId === sourceId) {
1312
+ session.views.delete(viewId);
1313
+ }
1314
+ });
1315
+ if (session.sourceIds.size > 0) {
1316
+ session.sourceId = session.sourceIds.values().next().value ?? session.sourceId;
1317
+ void this.applyAudioPolicy(session);
1318
+ return;
1319
+ }
1320
+ this.sessions.delete(session.sessionKey);
1321
+ void this.disposeSession(session);
1322
+ }
1323
+ getSession(sourceId) {
1324
+ const sessionKey = this.sourceSessionKeys.get(sourceId);
1325
+ return sessionKey ? this.sessions.get(sessionKey) : void 0;
1326
+ }
1327
+ snapshotFromSession(session) {
1328
+ return {
1329
+ status: session.status,
1330
+ error: session.error
1331
+ };
1332
+ }
1333
+ emitSessionSnapshot(session, snapshot) {
1334
+ session.sourceIds.forEach((sourceId) => this.emitSnapshot(sourceId, snapshot));
1335
+ }
1336
+ emitSessionTrtcEvent(session, type, rawType, payload) {
1337
+ session.sourceIds.forEach((sourceId) => this.emitTrtcEvent(sourceId, type, rawType, payload));
1338
+ }
1294
1339
  createSession(sourceId, trtc, aiDenoiser) {
1295
1340
  return {
1341
+ sessionKey: buildTrtcSessionKey(trtc, aiDenoiser),
1296
1342
  sourceId,
1343
+ sourceIds: /* @__PURE__ */ new Set([sourceId]),
1297
1344
  trtc,
1298
1345
  aiDenoiser,
1299
1346
  TRTC: null,
@@ -1321,7 +1368,7 @@ var TrtcSourceManager = class {
1321
1368
  }
1322
1369
  session.status = "connecting";
1323
1370
  session.error = void 0;
1324
- this.emitSnapshot(session.sourceId, {
1371
+ this.emitSessionSnapshot(session, {
1325
1372
  status: session.status
1326
1373
  });
1327
1374
  session.connectPromise = (async () => {
@@ -1350,7 +1397,7 @@ var TrtcSourceManager = class {
1350
1397
  session.views.forEach((binding) => {
1351
1398
  void this.startRemoteVideoForBinding(client, event.userId, event.streamType, binding, remoteVideoKey);
1352
1399
  });
1353
- this.emitTrtcEvent(session.sourceId, "remote_video_available", TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, event);
1400
+ this.emitSessionTrtcEvent(session, "remote_video_available", TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, event);
1354
1401
  };
1355
1402
  const onRemoteVideoUnavailable = (event) => {
1356
1403
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
@@ -1364,19 +1411,19 @@ var TrtcSourceManager = class {
1364
1411
  userId: event.userId,
1365
1412
  streamType: event.streamType
1366
1413
  }).catch(() => void 0);
1367
- this.emitTrtcEvent(session.sourceId, "remote_video_unavailable", TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, event);
1414
+ this.emitSessionTrtcEvent(session, "remote_video_unavailable", TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, event);
1368
1415
  };
1369
1416
  const onRemoteAudioAvailable = (event) => {
1370
1417
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1371
1418
  session.remoteAudioUsers.add(event.userId);
1372
1419
  void this.applyAudioPolicy(session);
1373
- this.emitTrtcEvent(session.sourceId, "remote_audio_available", TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, event);
1420
+ this.emitSessionTrtcEvent(session, "remote_audio_available", TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, event);
1374
1421
  };
1375
1422
  const onRemoteAudioUnavailable = (event) => {
1376
1423
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1377
1424
  session.remoteAudioUsers.delete(event.userId);
1378
1425
  void this.applyAudioPolicy(session);
1379
- this.emitTrtcEvent(session.sourceId, "remote_audio_unavailable", TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, event);
1426
+ this.emitSessionTrtcEvent(session, "remote_audio_unavailable", TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, event);
1380
1427
  };
1381
1428
  session.onRemoteVideoAvailable = onRemoteVideoAvailable;
1382
1429
  session.onRemoteVideoUnavailable = onRemoteVideoUnavailable;
@@ -1402,7 +1449,7 @@ var TrtcSourceManager = class {
1402
1449
  session.error = void 0;
1403
1450
  this.log("info", `\u8FDB\u623F\u6210\u529F source=${session.sourceId} room=${roomId}`);
1404
1451
  await this.applyAIDenoiserPolicy(session, client, sdkAppId, userId, userSig);
1405
- this.emitSnapshot(session.sourceId, {
1452
+ this.emitSessionSnapshot(session, {
1406
1453
  status: session.status
1407
1454
  });
1408
1455
  await this.applyAudioPolicy(session);
@@ -1410,7 +1457,7 @@ var TrtcSourceManager = class {
1410
1457
  session.status = "error";
1411
1458
  session.error = error instanceof Error ? error.message : String(error);
1412
1459
  this.log("error", `\u8FDE\u63A5\u5931\u8D25 source=${session.sourceId} error=${session.error}`);
1413
- this.emitSnapshot(session.sourceId, {
1460
+ this.emitSessionSnapshot(session, {
1414
1461
  status: session.status,
1415
1462
  error: session.error
1416
1463
  });
@@ -1587,6 +1634,20 @@ function isRuntimeTrtcSource(source) {
1587
1634
  function isSameTrtcConfig(a, b) {
1588
1635
  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;
1589
1636
  }
1637
+ function buildTrtcSessionKey(trtc, aiDenoiser) {
1638
+ const denoiser = resolveAIDenoiserOptions(aiDenoiser);
1639
+ return JSON.stringify({
1640
+ appId: trtc.app_id,
1641
+ roomId: trtc.room_id,
1642
+ userId: trtc.user_id,
1643
+ userSig: trtc.user_sig,
1644
+ aiDenoiser: {
1645
+ enabled: denoiser.enabled,
1646
+ mode: denoiser.mode,
1647
+ assetsPath: denoiser.assetsPath
1648
+ }
1649
+ });
1650
+ }
1590
1651
  function isSameAIDenoiserOptions(a, b) {
1591
1652
  const left = resolveAIDenoiserOptions(a);
1592
1653
  const right = resolveAIDenoiserOptions(b);