@vivix-ai/ivi-frontend-sdk 0.3.0 → 0.3.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 CHANGED
@@ -45,12 +45,6 @@ pnpm update @vivix-ai/ivi-frontend-sdk
45
45
  yarn up @vivix-ai/ivi-frontend-sdk
46
46
  ```
47
47
 
48
- ### `dist` 与 npm 发布
49
-
50
- `package.json` 的 `main` / `module` / `types` / `exports` 均指向 `dist/`,且 npm 包通过 `"files": ["dist"]` 发布到 npm 官方仓库。因此发布前必须先完成构建,确保 npm 包包含可运行、可解析类型的产物。
51
-
52
- **协作约定**:修改 `src/` 后必须在本地执行 `npm run build`(或 `pnpm run build`),并将**与本次源码变更对应的一次完整 `dist/` 输出**一并提交:可与功能改动放在**同一提交**,或在必要时单独提交,提交信息示例:`chore: 重新构建 dist`。禁止只改源码而不更新 `dist/`,避免仓库内源码与产物不一致。
53
-
54
48
  ### Peer Dependencies
55
49
 
56
50
  React 为可选 peer dependency,仅使用 React 组件时需要:
@@ -161,12 +155,29 @@ const unlistenEvent = runtime.onEvent((event, state) => {
161
155
  console.log("event:", event.type, "runtime:", state.status);
162
156
  });
163
157
 
158
+ const unlistenTrtcEvent = runtime.onTrtcEvent((event) => {
159
+ console.log("trtc event:", event.sourceId, event.type, event.payload);
160
+ });
161
+
164
162
  // 业务结束时
165
163
  unlistenState();
166
164
  unlistenEvent();
165
+ unlistenTrtcEvent();
167
166
  runtime.stop();
168
167
  ```
169
168
 
169
+ 也可以在创建 runtime 时通过 `onTrtcEvent` 配置项统一接收 TRTC SDK 原始事件:
170
+
171
+ ```ts
172
+ const runtime = new IviRuntimeCoordinator(client, {
173
+ onTrtcEvent: (event) => {
174
+ // event.type: "remote_video_available" | "remote_video_unavailable" | "remote_audio_available" | "remote_audio_unavailable"
175
+ // event.rawType: TRTC SDK 原始事件名
176
+ // event.payload: TRTC SDK 原始事件 payload
177
+ }
178
+ });
179
+ ```
180
+
170
181
  ### 3)发送用户文本并触发模型回复
171
182
 
172
183
  ```ts
@@ -514,7 +525,27 @@ HLS 流播放器(基于 `hls.js`),由 `IVITrackSlot` 内部用于 `.m3u8`
514
525
  >
515
526
  > 若需“保留播放/音量,仅隐藏进度条”,请在上层自行渲染自定义控制栏。
516
527
 
517
- ### `IVIVolumeControl`(内部组件)
528
+ ### `IVIFlvVideo`
529
+
530
+ FLV 流播放器(基于 `xgplayer` + `xgplayer-flv`),用于 `.flv` 地址播放。`IVITrackSlot` 会按 URL 后缀自动选用。辅助函数 `isFlvUrl(url)` 可判断 URL 是否为 flv。
531
+
532
+ 使用前需安装 peer 依赖:
533
+
534
+ ```bash
535
+ npm install xgplayer xgplayer-flv
536
+ ```
537
+
538
+ | Prop | 类型 | 说明 |
539
+ |------|------|------|
540
+ | `url` | `string` | FLV 播放地址 |
541
+ | `videoProps` | — | 透传 muted / controls / onEnded 等语义 |
542
+ | `style` | — | 容器样式 |
543
+ | `isLive` | `boolean` | 是否直播,默认 `true` |
544
+ | `paused` | `boolean` | 是否暂停(standby 预加载) |
545
+
546
+ > 直播 FLV 通常不会触发 `ended`,切换 source 时请使用定时模式而非「播放完毕」。
547
+
548
+ ### `IVIVolumeControl`
518
549
 
519
550
  音量控制浮层组件,由 `IVITrackSlot` 在 `showVolumeControl` 开启时内部使用;当前不从包根公开导出。
520
551
 
package/dist/index.cjs CHANGED
@@ -1108,8 +1108,9 @@ var ConversationManager = class {
1108
1108
  // src/runtime/managers/trtc-source-manager.ts
1109
1109
  var TAG = "[IVI-TRTC]";
1110
1110
  var TrtcSourceManager = class {
1111
- constructor(onLog) {
1111
+ constructor(onLog, onTrtcEvent) {
1112
1112
  this.onLog = onLog;
1113
+ this.onTrtcEvent = onTrtcEvent;
1113
1114
  this.sessions = /* @__PURE__ */ new Map();
1114
1115
  this.listeners = /* @__PURE__ */ new Map();
1115
1116
  }
@@ -1345,6 +1346,7 @@ var TrtcSourceManager = class {
1345
1346
  session.views.forEach((binding) => {
1346
1347
  void this.startRemoteVideoForBinding(client, event.userId, event.streamType, binding, remoteVideoKey);
1347
1348
  });
1349
+ this.emitTrtcEvent(session.sourceId, "remote_video_available", TRTC.EVENT.REMOTE_VIDEO_AVAILABLE, event);
1348
1350
  };
1349
1351
  const onRemoteVideoUnavailable = (event) => {
1350
1352
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
@@ -1358,16 +1360,19 @@ var TrtcSourceManager = class {
1358
1360
  userId: event.userId,
1359
1361
  streamType: event.streamType
1360
1362
  }).catch(() => void 0);
1363
+ this.emitTrtcEvent(session.sourceId, "remote_video_unavailable", TRTC.EVENT.REMOTE_VIDEO_UNAVAILABLE, event);
1361
1364
  };
1362
1365
  const onRemoteAudioAvailable = (event) => {
1363
1366
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1364
1367
  session.remoteAudioUsers.add(event.userId);
1365
1368
  void this.applyAudioPolicy(session);
1369
+ this.emitTrtcEvent(session.sourceId, "remote_audio_available", TRTC.EVENT.REMOTE_AUDIO_AVAILABLE, event);
1366
1370
  };
1367
1371
  const onRemoteAudioUnavailable = (event) => {
1368
1372
  this.log("info", `\u8FDC\u7AEF\u97F3\u9891\u4E0D\u53EF\u7528 source=${session.sourceId} userId=${event.userId}`);
1369
1373
  session.remoteAudioUsers.delete(event.userId);
1370
1374
  void this.applyAudioPolicy(session);
1375
+ this.emitTrtcEvent(session.sourceId, "remote_audio_unavailable", TRTC.EVENT.REMOTE_AUDIO_UNAVAILABLE, event);
1371
1376
  };
1372
1377
  session.onRemoteVideoAvailable = onRemoteVideoAvailable;
1373
1378
  session.onRemoteVideoUnavailable = onRemoteVideoUnavailable;
@@ -1540,6 +1545,17 @@ var TrtcSourceManager = class {
1540
1545
  data: extra.length > 0 ? { message, extra } : { message }
1541
1546
  });
1542
1547
  }
1548
+ emitTrtcEvent(sourceId, type, rawType, payload) {
1549
+ const event = {
1550
+ sourceId,
1551
+ type,
1552
+ rawType: String(rawType),
1553
+ payload,
1554
+ userId: payload.userId,
1555
+ streamType: payload.streamType
1556
+ };
1557
+ this.onTrtcEvent?.(event);
1558
+ }
1543
1559
  };
1544
1560
  function isRuntimeTrtcSource(source) {
1545
1561
  return source.status === "ready" && source.playback?.type === "trtc" && typeof source.playback.trtc === "object" && source.playback.trtc !== null;
@@ -2038,6 +2054,7 @@ var IviRuntimeCoordinator = class {
2038
2054
  this.conversationManager = new ConversationManager();
2039
2055
  this.stateListeners = /* @__PURE__ */ new Set();
2040
2056
  this.eventListeners = /* @__PURE__ */ new Set();
2057
+ this.trtcEventListeners = /* @__PURE__ */ new Set();
2041
2058
  this.pendingUserTextToResponseFlows = /* @__PURE__ */ new Map();
2042
2059
  this.userTextFlowCounter = 0;
2043
2060
  this.waitingTracksListValidation = false;
@@ -2057,7 +2074,10 @@ var IviRuntimeCoordinator = class {
2057
2074
  syncStageOnSessionCreated: true,
2058
2075
  ...config
2059
2076
  };
2060
- this.trtcSourceManager = new TrtcSourceManager(this.config.onLog);
2077
+ this.trtcSourceManager = new TrtcSourceManager(
2078
+ this.config.onLog,
2079
+ (event) => this.emitTrtcEvent(event)
2080
+ );
2061
2081
  this.livekitSourceManager = new LivekitSourceManager(this.config.onLog);
2062
2082
  this.sessionHandler = new SessionEventHandler(this.sessionManager, {
2063
2083
  onSessionCreated: (event) => this.onSessionCreated(event),
@@ -2130,6 +2150,12 @@ var IviRuntimeCoordinator = class {
2130
2150
  this.eventListeners.delete(listener);
2131
2151
  };
2132
2152
  }
2153
+ onTrtcEvent(listener) {
2154
+ this.trtcEventListeners.add(listener);
2155
+ return () => {
2156
+ this.trtcEventListeners.delete(listener);
2157
+ };
2158
+ }
2133
2159
  emitLog(entry) {
2134
2160
  this.config.onLog?.(entry);
2135
2161
  }
@@ -2384,6 +2410,10 @@ var IviRuntimeCoordinator = class {
2384
2410
  this.progressUserTextToResponseFlows(event);
2385
2411
  this.eventListeners.forEach((listener) => listener(event, this.state));
2386
2412
  }
2413
+ emitTrtcEvent(event) {
2414
+ this.config.onTrtcEvent?.(event);
2415
+ this.trtcEventListeners.forEach((listener) => listener(event));
2416
+ }
2387
2417
  resetStoppedState() {
2388
2418
  this.rejectPendingUserTextToResponseFlows(
2389
2419
  new Error("Runtime stopped before user text to response flow completed.")
@@ -3292,6 +3322,132 @@ function IVILivekitPlayer(props) {
3292
3322
  }
3293
3323
  );
3294
3324
  }
3325
+ var loadModulesPromise = null;
3326
+ async function loadXgplayerModules() {
3327
+ if (!loadModulesPromise) {
3328
+ loadModulesPromise = (async () => {
3329
+ try {
3330
+ await import('xgplayer/dist/index.min.css');
3331
+ } catch (err) {
3332
+ console.warn("[IVIFlvVideo] \u52A0\u8F7D xgplayer \u6837\u5F0F\u5931\u8D25\uFF0C\u64AD\u653E\u5668\u63A7\u4EF6\u53EF\u80FD\u663E\u793A\u5F02\u5E38", err);
3333
+ }
3334
+ const [playerMod, flvMod] = await Promise.all([import('xgplayer'), import('xgplayer-flv')]);
3335
+ const playerModule = playerMod;
3336
+ const flvModule = flvMod;
3337
+ const Player = playerModule.default ?? playerModule.Player;
3338
+ const FlvPlugin = flvModule.default ?? flvModule.FlvPlugin;
3339
+ if (!Player || !FlvPlugin) {
3340
+ throw new Error("xgplayer \u6216 xgplayer-flv \u6A21\u5757\u5BFC\u51FA\u65E0\u6548");
3341
+ }
3342
+ return { Player, FlvPlugin };
3343
+ })().catch((err) => {
3344
+ loadModulesPromise = null;
3345
+ throw err;
3346
+ });
3347
+ }
3348
+ return loadModulesPromise;
3349
+ }
3350
+ function IVIFlvVideo(props) {
3351
+ const { url, videoProps, style, isLive = true, paused = false } = props;
3352
+ const containerRef = react.useRef(null);
3353
+ const playerRef = react.useRef(null);
3354
+ const pausedRef = react.useRef(paused);
3355
+ const videoPropsRef = react.useRef(videoProps);
3356
+ videoPropsRef.current = videoProps;
3357
+ const reactId = react.useId();
3358
+ const containerId = `ivi-flv-${reactId.replace(/:/g, "")}`;
3359
+ react.useEffect(() => {
3360
+ pausedRef.current = paused;
3361
+ const player = playerRef.current;
3362
+ if (!player) return;
3363
+ if (paused) {
3364
+ player.pause();
3365
+ } else {
3366
+ Promise.resolve(player.play()).catch((err) => {
3367
+ console.warn(
3368
+ "[IVIFlvVideo] 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",
3369
+ err
3370
+ );
3371
+ });
3372
+ }
3373
+ }, [paused]);
3374
+ react.useEffect(() => {
3375
+ const container = containerRef.current;
3376
+ if (!container) return;
3377
+ let disposed = false;
3378
+ let endedHandler = null;
3379
+ let errorHandler = null;
3380
+ const setup = async () => {
3381
+ try {
3382
+ const { Player, FlvPlugin } = await loadXgplayerModules();
3383
+ if (disposed || !containerRef.current) return;
3384
+ playerRef.current?.destroy();
3385
+ playerRef.current = null;
3386
+ container.innerHTML = "";
3387
+ const shouldAutoplay = !pausedRef.current;
3388
+ const currentVideoProps = videoPropsRef.current;
3389
+ const muted = Boolean(currentVideoProps?.muted);
3390
+ const player = new Player({
3391
+ id: containerId,
3392
+ el: containerRef.current,
3393
+ url,
3394
+ plugins: [FlvPlugin],
3395
+ isLive,
3396
+ autoplay: shouldAutoplay,
3397
+ autoplayMuted: muted,
3398
+ muted,
3399
+ controls: currentVideoProps?.controls ?? true,
3400
+ width: "100%",
3401
+ height: "100%",
3402
+ fluid: true,
3403
+ videoFillMode: "contain",
3404
+ flv: {
3405
+ retryCount: Number.MAX_SAFE_INTEGER,
3406
+ retryDelay: 500,
3407
+ loadTimeout: 1e4
3408
+ }
3409
+ });
3410
+ playerRef.current = player;
3411
+ errorHandler = (...args) => {
3412
+ console.warn("[IVIFlvVideo] \u64AD\u653E\u5668 error \u4E8B\u4EF6", { url, args });
3413
+ };
3414
+ player.on("error", errorHandler);
3415
+ const onEnded = currentVideoProps?.onEnded;
3416
+ if (onEnded) {
3417
+ endedHandler = () => {
3418
+ videoPropsRef.current?.onEnded?.({});
3419
+ };
3420
+ player.on("ended", endedHandler);
3421
+ }
3422
+ if (!pausedRef.current) {
3423
+ Promise.resolve(player.play()).catch((err) => {
3424
+ console.warn("[IVIFlvVideo] \u521D\u59CB play() \u88AB\u6D4F\u89C8\u5668\u62D2\u7EDD", err);
3425
+ });
3426
+ }
3427
+ } catch (err) {
3428
+ console.warn("[IVIFlvVideo] \u521D\u59CB\u5316 xgplayer-flv \u5931\u8D25", err);
3429
+ }
3430
+ };
3431
+ void setup();
3432
+ return () => {
3433
+ disposed = true;
3434
+ const player = playerRef.current;
3435
+ if (player) {
3436
+ if (endedHandler) player.off("ended", endedHandler);
3437
+ if (errorHandler) player.off("error", errorHandler);
3438
+ player.destroy();
3439
+ }
3440
+ playerRef.current = null;
3441
+ if (containerRef.current) {
3442
+ containerRef.current.innerHTML = "";
3443
+ }
3444
+ };
3445
+ }, [url, isLive, containerId]);
3446
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { id: containerId, ref: containerRef, style: { width: "100%", height: "100%", ...style } });
3447
+ }
3448
+ function isFlvUrl(url) {
3449
+ return /\.flv(?:$|[?#])/i.test(url);
3450
+ }
3295
3451
  var RETRY_DELAY_MS = 500;
3296
3452
  var UNLIMITED_RETRIES = Number.MAX_SAFE_INTEGER;
3297
3453
  var HLS_LOG_TAG = "[IVI-HLS]";
@@ -3648,16 +3804,32 @@ function TrackSlotMediaContent(props) {
3648
3804
  ) : void 0
3649
3805
  };
3650
3806
  const shouldPause = !isActive;
3651
- return isM3u8Url(playbackUrl) ? /* @__PURE__ */ jsxRuntime.jsx(
3652
- IVIHlsVideo,
3653
- {
3654
- url: playbackUrl,
3655
- videoProps: mergedVideoProps,
3656
- style: videoStyle,
3657
- paused: shouldPause,
3658
- onLog
3659
- }
3660
- ) : /* @__PURE__ */ jsxRuntime.jsx(
3807
+ if (isM3u8Url(playbackUrl)) {
3808
+ return /* @__PURE__ */ jsxRuntime.jsx(
3809
+ IVIHlsVideo,
3810
+ {
3811
+ url: playbackUrl,
3812
+ videoProps: mergedVideoProps,
3813
+ style: videoStyle,
3814
+ paused: shouldPause
3815
+ }
3816
+ );
3817
+ }
3818
+ if (isFlvUrl(playbackUrl)) {
3819
+ return /* @__PURE__ */ jsxRuntime.jsx(
3820
+ IVIFlvVideo,
3821
+ {
3822
+ url: playbackUrl,
3823
+ videoProps: {
3824
+ ...mergedVideoProps,
3825
+ controls: isActive ? videoProps?.controls ?? true : false
3826
+ },
3827
+ style: videoStyle,
3828
+ paused: shouldPause
3829
+ }
3830
+ );
3831
+ }
3832
+ return /* @__PURE__ */ jsxRuntime.jsx(
3661
3833
  SlotVideo,
3662
3834
  {
3663
3835
  src: playbackUrl,