@vivix-ai/ivi-frontend-sdk 0.3.2 → 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/README.md CHANGED
@@ -170,6 +170,10 @@ runtime.stop();
170
170
 
171
171
  ```ts
172
172
  const runtime = new IviRuntimeCoordinator(client, {
173
+ trtcAIDenoiser: {
174
+ enabled: true,
175
+ mode: "normal" // 也可使用 "far_field_reduction"
176
+ },
173
177
  onTrtcEvent: (event) => {
174
178
  // event.type: "remote_video_available" | "remote_video_unavailable" | "remote_audio_available" | "remote_audio_unavailable"
175
179
  // event.rawType: TRTC SDK 原始事件名
@@ -178,6 +182,8 @@ const runtime = new IviRuntimeCoordinator(client, {
178
182
  });
179
183
  ```
180
184
 
185
+ `trtcAIDenoiser` 默认开启,传 `false` 可关闭;也可以传 `{ enabled, mode, assetsPath }` 调整 TRTC SDK 的 AIDenoiser 插件参数。
186
+
181
187
  ### 3)发送用户文本并触发模型回复
182
188
 
183
189
  ```ts
@@ -489,6 +495,7 @@ TRTC 远端流播放器,自动以 audience 身份入会并订阅远端流。
489
495
  | `loadingFallback` | `ReactNode` | 加载中兜底 |
490
496
  | `errorFallback` | `ReactNode` | 错误兜底 |
491
497
  | `muted` | `boolean` | 是否静音 |
498
+ | `trtcAIDenoiser` | `boolean \| IviRuntimeTrtcAIDenoiserOptions` | 独立使用 `IVITrtcPlayer` 时的 TRTC AIDenoiser 配置,默认开启 |
492
499
  | `className` / `style` | — | 容器样式 |
493
500
 
494
501
  ### `IVILivekitPlayer`
package/dist/index.cjs CHANGED
@@ -1107,11 +1107,17 @@ var ConversationManager = class {
1107
1107
 
1108
1108
  // src/runtime/managers/trtc-source-manager.ts
1109
1109
  var TAG = "[IVI-TRTC]";
1110
+ var DEFAULT_DENOISER_OPTIONS = {
1111
+ enabled: true,
1112
+ mode: "normal"
1113
+ };
1110
1114
  var TrtcSourceManager = class {
1111
- constructor(onLog, onTrtcEvent) {
1115
+ constructor(onLog, onTrtcEvent, aiDenoiser) {
1112
1116
  this.onLog = onLog;
1113
1117
  this.onTrtcEvent = onTrtcEvent;
1118
+ this.aiDenoiser = aiDenoiser;
1114
1119
  this.sessions = /* @__PURE__ */ new Map();
1120
+ this.sourceSessionKeys = /* @__PURE__ */ new Map();
1115
1121
  this.listeners = /* @__PURE__ */ new Map();
1116
1122
  }
1117
1123
  /**
@@ -1120,15 +1126,16 @@ var TrtcSourceManager = class {
1120
1126
  * - 清理已不在 runtime 中的会话(释放资源)
1121
1127
  */
1122
1128
  syncRuntimeSources(sources) {
1123
- const existingIds = new Set(sources.keys());
1129
+ const trtcSourceIds = /* @__PURE__ */ new Set();
1124
1130
  sources.forEach((source, sourceId) => {
1125
1131
  if (!isRuntimeTrtcSource(source)) {
1126
1132
  return;
1127
1133
  }
1134
+ trtcSourceIds.add(sourceId);
1128
1135
  this.upsertSource(sourceId, source.playback.trtc);
1129
1136
  });
1130
- this.sessions.forEach((_session, sourceId) => {
1131
- if (!existingIds.has(sourceId)) {
1137
+ Array.from(this.sourceSessionKeys.keys()).forEach((sourceId) => {
1138
+ if (!trtcSourceIds.has(sourceId)) {
1132
1139
  this.removeSource(sourceId);
1133
1140
  }
1134
1141
  });
@@ -1137,22 +1144,29 @@ var TrtcSourceManager = class {
1137
1144
  * 按 sourceId 注册/更新 TRTC 配置。
1138
1145
  * 若配置发生变化,会先销毁旧会话再按新参数重建连接。
1139
1146
  */
1140
- upsertSource(sourceId, trtc) {
1141
- const existing = this.sessions.get(sourceId);
1142
- if (!existing) {
1143
- this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1144
- const session2 = this.createSession(sourceId, trtc);
1145
- this.sessions.set(sourceId, session2);
1146
- void this.ensureConnected(session2);
1147
+ upsertSource(sourceId, trtc, aiDenoiser) {
1148
+ const existing = this.getSession(sourceId);
1149
+ if (existing && isSameTrtcConfig(existing.trtc, trtc) && isSameAIDenoiserOptions(existing.aiDenoiser, aiDenoiser)) {
1147
1150
  return;
1148
1151
  }
1149
- if (isSameTrtcConfig(existing.trtc, trtc)) {
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);
1150
1164
  return;
1151
1165
  }
1152
- this.log("info", `\u914D\u7F6E\u53D8\u66F4\uFF0C\u91CD\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")}`);
1153
- void this.disposeSession(existing);
1154
- const session = this.createSession(sourceId, trtc);
1155
- this.sessions.set(sourceId, session);
1166
+ this.log("info", `\u65B0\u5EFA\u4F1A\u8BDD source=${sourceId} room=${getTrtcString(trtc, "room_id")} user=${getTrtcString(trtc, "user_id")}`);
1167
+ const session = this.createSession(sourceId, trtc, aiDenoiser);
1168
+ this.sessions.set(session.sessionKey, session);
1169
+ this.sourceSessionKeys.set(sourceId, session.sessionKey);
1156
1170
  void this.ensureConnected(session);
1157
1171
  }
1158
1172
  /**
@@ -1160,13 +1174,12 @@ var TrtcSourceManager = class {
1160
1174
  * 该操作通常由 source.deleted 或会话重置触发。
1161
1175
  */
1162
1176
  removeSource(sourceId) {
1163
- const session = this.sessions.get(sourceId);
1177
+ const session = this.getSession(sourceId);
1164
1178
  if (!session) {
1165
1179
  return;
1166
1180
  }
1167
1181
  this.log("info", `\u79FB\u9664\u4F1A\u8BDD source=${sourceId}`);
1168
- this.sessions.delete(sourceId);
1169
- void this.disposeSession(session);
1182
+ this.unlinkSourceFromSession(sourceId, session);
1170
1183
  this.emitSnapshot(sourceId, {
1171
1184
  status: "idle"
1172
1185
  });
@@ -1176,6 +1189,7 @@ var TrtcSourceManager = class {
1176
1189
  this.log("info", `\u91CD\u7F6E\u5168\u90E8\u4F1A\u8BDD count=${this.sessions.size}`);
1177
1190
  const sessions = Array.from(this.sessions.values());
1178
1191
  this.sessions.clear();
1192
+ this.sourceSessionKeys.clear();
1179
1193
  sessions.forEach((session) => {
1180
1194
  void this.disposeSession(session);
1181
1195
  });
@@ -1198,7 +1212,7 @@ var TrtcSourceManager = class {
1198
1212
  };
1199
1213
  }
1200
1214
  getSnapshot(sourceId) {
1201
- const session = this.sessions.get(sourceId);
1215
+ const session = this.getSession(sourceId);
1202
1216
  if (!session) {
1203
1217
  return {
1204
1218
  status: "idle"
@@ -1213,7 +1227,7 @@ var TrtcSourceManager = class {
1213
1227
  * 检查指定 source 的 TRTC 会话是否曾收到过 REMOTE_VIDEO_AVAILABLE 事件。
1214
1228
  */
1215
1229
  hasRemoteVideoAvailable(sourceId) {
1216
- const session = this.sessions.get(sourceId);
1230
+ const session = this.getSession(sourceId);
1217
1231
  return session?.hasEverReceivedRemoteVideo ?? false;
1218
1232
  }
1219
1233
  /**
@@ -1222,7 +1236,7 @@ var TrtcSourceManager = class {
1222
1236
  * - 若会话被销毁或超时,返回 false。
1223
1237
  */
1224
1238
  waitForRemoteVideoAvailable(sourceId, timeoutMs = 3e4) {
1225
- const session = this.sessions.get(sourceId);
1239
+ const session = this.getSession(sourceId);
1226
1240
  if (!session) return Promise.resolve(false);
1227
1241
  if (session.hasEverReceivedRemoteVideo) return Promise.resolve(true);
1228
1242
  return new Promise((resolve) => {
@@ -1250,11 +1264,12 @@ var TrtcSourceManager = class {
1250
1264
  * - 应用全局音频策略(根据所有 view 的 muted 汇总)
1251
1265
  */
1252
1266
  async attachView(sourceId, viewId, container, muted) {
1253
- const session = this.sessions.get(sourceId);
1267
+ const session = this.getSession(sourceId);
1254
1268
  if (!session) {
1255
1269
  throw new Error(`TRTC source session not found: ${sourceId}`);
1256
1270
  }
1257
1271
  session.views.set(viewId, {
1272
+ sourceId,
1258
1273
  container,
1259
1274
  muted,
1260
1275
  startedVideoKeys: /* @__PURE__ */ new Set(),
@@ -1269,15 +1284,18 @@ var TrtcSourceManager = class {
1269
1284
  await this.applyAudioPolicy(session);
1270
1285
  }
1271
1286
  detachView(sourceId, viewId) {
1272
- const session = this.sessions.get(sourceId);
1287
+ const session = this.getSession(sourceId);
1273
1288
  if (!session) {
1274
1289
  return;
1275
1290
  }
1276
- session.views.delete(viewId);
1291
+ const binding = session.views.get(viewId);
1292
+ if (binding?.sourceId === sourceId) {
1293
+ session.views.delete(viewId);
1294
+ }
1277
1295
  void this.applyAudioPolicy(session);
1278
1296
  }
1279
1297
  updateViewMuted(sourceId, viewId, muted) {
1280
- const session = this.sessions.get(sourceId);
1298
+ const session = this.getSession(sourceId);
1281
1299
  if (!session) {
1282
1300
  return;
1283
1301
  }
@@ -1288,10 +1306,45 @@ var TrtcSourceManager = class {
1288
1306
  binding.muted = muted;
1289
1307
  void this.applyAudioPolicy(session);
1290
1308
  }
1291
- createSession(sourceId, trtc) {
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
+ }
1341
+ createSession(sourceId, trtc, aiDenoiser) {
1292
1342
  return {
1343
+ sessionKey: buildTrtcSessionKey(trtc, aiDenoiser),
1293
1344
  sourceId,
1345
+ sourceIds: /* @__PURE__ */ new Set([sourceId]),
1294
1346
  trtc,
1347
+ aiDenoiser,
1295
1348
  TRTC: null,
1296
1349
  client: null,
1297
1350
  connectPromise: null,
@@ -1317,7 +1370,7 @@ var TrtcSourceManager = class {
1317
1370
  }
1318
1371
  session.status = "connecting";
1319
1372
  session.error = void 0;
1320
- this.emitSnapshot(session.sourceId, {
1373
+ this.emitSessionSnapshot(session, {
1321
1374
  status: session.status
1322
1375
  });
1323
1376
  session.connectPromise = (async () => {
@@ -1346,7 +1399,7 @@ var TrtcSourceManager = class {
1346
1399
  session.views.forEach((binding) => {
1347
1400
  void this.startRemoteVideoForBinding(client, event.userId, event.streamType, binding, remoteVideoKey);
1348
1401
  });
1349
- 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);
1350
1403
  };
1351
1404
  const onRemoteVideoUnavailable = (event) => {
1352
1405
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
@@ -1360,19 +1413,19 @@ var TrtcSourceManager = class {
1360
1413
  userId: event.userId,
1361
1414
  streamType: event.streamType
1362
1415
  }).catch(() => void 0);
1363
- 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);
1364
1417
  };
1365
1418
  const onRemoteAudioAvailable = (event) => {
1366
1419
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1367
1420
  session.remoteAudioUsers.add(event.userId);
1368
1421
  void this.applyAudioPolicy(session);
1369
- 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);
1370
1423
  };
1371
1424
  const onRemoteAudioUnavailable = (event) => {
1372
1425
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1373
1426
  session.remoteAudioUsers.delete(event.userId);
1374
1427
  void this.applyAudioPolicy(session);
1375
- 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);
1376
1429
  };
1377
1430
  session.onRemoteVideoAvailable = onRemoteVideoAvailable;
1378
1431
  session.onRemoteVideoUnavailable = onRemoteVideoUnavailable;
@@ -1397,7 +1450,8 @@ var TrtcSourceManager = class {
1397
1450
  session.status = "connected";
1398
1451
  session.error = void 0;
1399
1452
  this.log("info", `\u8FDB\u623F\u6210\u529F source=${session.sourceId} room=${roomId}`);
1400
- this.emitSnapshot(session.sourceId, {
1453
+ await this.applyAIDenoiserPolicy(session, client, sdkAppId, userId, userSig);
1454
+ this.emitSessionSnapshot(session, {
1401
1455
  status: session.status
1402
1456
  });
1403
1457
  await this.applyAudioPolicy(session);
@@ -1405,7 +1459,7 @@ var TrtcSourceManager = class {
1405
1459
  session.status = "error";
1406
1460
  session.error = error instanceof Error ? error.message : String(error);
1407
1461
  this.log("error", `\u8FDE\u63A5\u5931\u8D25 source=${session.sourceId} error=${session.error}`);
1408
- this.emitSnapshot(session.sourceId, {
1462
+ this.emitSessionSnapshot(session, {
1409
1463
  status: session.status,
1410
1464
  error: session.error
1411
1465
  });
@@ -1482,6 +1536,25 @@ var TrtcSourceManager = class {
1482
1536
  await client.muteRemoteAudio(userId, false);
1483
1537
  }
1484
1538
  }
1539
+ async applyAIDenoiserPolicy(session, client, sdkAppId, userId, userSig) {
1540
+ const options = resolveAIDenoiserOptions(session.aiDenoiser ?? this.aiDenoiser);
1541
+ if (!options.enabled) {
1542
+ this.log("info", `TRTC AIDenoiser \u5DF2\u5173\u95ED source=${session.sourceId}`);
1543
+ return;
1544
+ }
1545
+ try {
1546
+ await client.startPlugin("AIDenoiser", {
1547
+ sdkAppId,
1548
+ userId,
1549
+ userSig,
1550
+ mode: getAIDenoiserModeValue(options.mode),
1551
+ ...options.assetsPath ? { assetsPath: options.assetsPath } : {}
1552
+ });
1553
+ this.log("info", `TRTC AIDenoiser \u5DF2\u5F00\u542F source=${session.sourceId} mode=${options.mode}`);
1554
+ } catch (error) {
1555
+ this.log("warn", `TRTC AIDenoiser \u5F00\u542F\u5931\u8D25 source=${session.sourceId}`, error);
1556
+ }
1557
+ }
1485
1558
  /**
1486
1559
  * 销毁单个 source 会话:
1487
1560
  * 解绑事件、停止远端视频、退出房间并销毁客户端。
@@ -1563,6 +1636,25 @@ function isRuntimeTrtcSource(source) {
1563
1636
  function isSameTrtcConfig(a, b) {
1564
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;
1565
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
+ }
1653
+ function isSameAIDenoiserOptions(a, b) {
1654
+ const left = resolveAIDenoiserOptions(a);
1655
+ const right = resolveAIDenoiserOptions(b);
1656
+ return left.enabled === right.enabled && left.mode === right.mode && left.assetsPath === right.assetsPath;
1657
+ }
1566
1658
  function shouldUseStringRoomId(roomId) {
1567
1659
  if (!/^\d+$/.test(roomId)) {
1568
1660
  return true;
@@ -1574,6 +1666,26 @@ function getTrtcString(trtc, key) {
1574
1666
  const value = trtc[key];
1575
1667
  return typeof value === "string" ? value : "";
1576
1668
  }
1669
+ function resolveAIDenoiserOptions(options) {
1670
+ if (options === false) {
1671
+ return {
1672
+ ...DEFAULT_DENOISER_OPTIONS,
1673
+ enabled: false
1674
+ };
1675
+ }
1676
+ if (options === true || options === void 0) {
1677
+ return DEFAULT_DENOISER_OPTIONS;
1678
+ }
1679
+ return {
1680
+ ...DEFAULT_DENOISER_OPTIONS,
1681
+ ...options,
1682
+ enabled: options.enabled ?? DEFAULT_DENOISER_OPTIONS.enabled,
1683
+ mode: options.mode ?? DEFAULT_DENOISER_OPTIONS.mode
1684
+ };
1685
+ }
1686
+ function getAIDenoiserModeValue(mode) {
1687
+ return mode === "far_field_reduction" ? 1 : 0;
1688
+ }
1577
1689
  function buildRemoteVideoKey(userId, streamType) {
1578
1690
  return `${userId}::${streamType}`;
1579
1691
  }
@@ -2076,7 +2188,8 @@ var IviRuntimeCoordinator = class {
2076
2188
  };
2077
2189
  this.trtcSourceManager = new TrtcSourceManager(
2078
2190
  this.config.onLog,
2079
- (event) => this.emitTrtcEvent(event)
2191
+ (event) => this.emitTrtcEvent(event),
2192
+ this.config.trtcAIDenoiser
2080
2193
  );
2081
2194
  this.livekitSourceManager = new LivekitSourceManager(this.config.onLog);
2082
2195
  this.sessionHandler = new SessionEventHandler(this.sessionManager, {
@@ -3156,7 +3269,8 @@ function IVITrtcPlayer(props) {
3156
3269
  style,
3157
3270
  loadingFallback = null,
3158
3271
  errorFallback = null,
3159
- muted = false
3272
+ muted = false,
3273
+ trtcAIDenoiser
3160
3274
  } = props;
3161
3275
  const containerRef = react.useRef(null);
3162
3276
  const viewIdRef = react.useRef(`trtc-view-${Math.random().toString(36).slice(2, 10)}`);
@@ -3177,7 +3291,7 @@ function IVITrtcPlayer(props) {
3177
3291
  }
3178
3292
  let disposed = false;
3179
3293
  if (shouldManageSourceLifecycle) {
3180
- manager.upsertSource(resolvedSourceId, trtc);
3294
+ manager.upsertSource(resolvedSourceId, trtc, trtcAIDenoiser);
3181
3295
  }
3182
3296
  const unsubscribe = manager.subscribe(resolvedSourceId, (snapshot) => {
3183
3297
  if (disposed) {
@@ -3208,7 +3322,8 @@ function IVITrtcPlayer(props) {
3208
3322
  trtc.app_id,
3209
3323
  trtc.room_id,
3210
3324
  trtc.user_id,
3211
- trtc.user_sig
3325
+ trtc.user_sig,
3326
+ trtcAIDenoiser
3212
3327
  ]);
3213
3328
  react.useEffect(() => {
3214
3329
  manager.updateViewMuted(resolvedSourceId, viewIdRef.current, muted);