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

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
@@ -380,20 +380,27 @@ function useIviSubtitles(
380
380
  ): IviSubtitleItem[];
381
381
  ```
382
382
 
383
- 基于 runtime 的 conversation / response 事件维护字幕队列,不负责渲染,也不按时间自动消失。
383
+ 基于 runtime 的 conversation / response 事件维护字幕队列,不负责渲染。字幕进入完成态后默认 3000ms 自动移除,可通过 `hideCompletedAfterMs={false}` 关闭,或通过 `onSubtitleCompleted` 自定义保留/删除策略。默认从 conversation 条目聚合字幕;当 `roles` 包含 `"model"` 且开启 `useModelStreamingTranscript` 时,model 字幕改为使用 `response.output_audio_transcript.*` 事件,`response.output_audio_transcript.done` 只更新文本,只有 `response.done` 才表示这一轮 model 字幕结束。
384
384
 
385
385
  | Option | 类型 | 默认值 | 说明 |
386
386
  |--------|------|--------|------|
387
387
  | `roles` | `IviSubtitleRole \| IviSubtitleRole[]` | `"user"` | 要收集的发言人角色 |
388
388
  | `maxItems` | `number` | `2` | 最多保留的字幕条数,超过后清理最旧条目 |
389
+ | `useModelStreamingTranscript` | `boolean` | `false` | 当 `roles` 包含 `"model"` 时,model 字幕是否改用 `response.output_audio_transcript.*` 事件 |
390
+ | `hideCompletedAfterMs` | `number \| false` | `3000` | 字幕进入完成态后多久自动移除;传 `false` 保留到被 `maxItems` 裁剪 |
391
+ | `onSubtitleCompleted` | `(item, context) => "keep" \| "remove" \| { removeAfterMs } \| void \| Promise<...>` | — | 自定义完成态清理策略,优先于 `hideCompletedAfterMs` |
389
392
 
390
- `IviSubtitleItem` 包含 `id`、`role`、`lifecycle`、`status`、`text`、`transcript`、`displayText`、`timestamp`、`updatedAt`、`content`、`item`。
393
+ `IviSubtitleItem` 包含 `id`、`role`、`lifecycle`、`status`、`text`、`transcript`、`displayText`、`timestamp`、`updatedAt`、`content`、`item`、`source`、`responseId`、`itemId`。
391
394
 
392
395
  ```tsx
393
396
  function CustomSubtitles({ runtime }: { runtime: IviRuntimeCoordinator | null }) {
394
397
  const subtitles = useIviSubtitles(runtime, {
395
398
  roles: ["user", "model"],
396
- maxItems: 5
399
+ maxItems: 5,
400
+ useModelStreamingTranscript: true,
401
+ onSubtitleCompleted: (item) => (
402
+ item.role === "model" ? { removeAfterMs: 4000 } : { removeAfterMs: 2500 }
403
+ )
397
404
  });
398
405
 
399
406
  return (
@@ -464,7 +471,7 @@ function useApplyVolumeToSlot(
464
471
  | `showVolumeControl` | `boolean` | — | 是否显示音量控制浮层 |
465
472
  | `volumeControlProps` | — | — | 音量控制自定义配置 |
466
473
  | `showSubtitle` | `boolean` | — | 是否显示字幕浮层 |
467
- | `subtitleProps` | `Omit<IVISubtitleOverlayProps, "runtime">` | — | 字幕自定义配置 |
474
+ | `subtitleProps` | `Omit<IVISubtitleOverlayProps, "runtime">` | — | 字幕自定义配置,可传 `roles`、`maxItems`、`useModelStreamingTranscript`、`hideCompletedAfterMs`、`onSubtitleCompleted` 等 |
468
475
  | `trtcPlayerProps` | — | — | TRTC 播放器自定义配置 |
469
476
  | `livekitPlayerProps` | — | — | LiveKit 播放器自定义配置 |
470
477
  | `videoProps` / `imageProps` | — | — | 透传给原生 `<video>` / `<img>` |
@@ -574,6 +581,9 @@ npm install xgplayer xgplayer-flv
574
581
  | `roles` | `IviSubtitleRole \| IviSubtitleRole[]` | `"user"` | 要展示的发言人角色 |
575
582
  | `maxItems` | `number` | `2` | 最多保留的字幕条数,超过后清理最旧条目 |
576
583
  | `maxVisible` | `number` | — | 已废弃,兼容旧字段;请使用 `maxItems` |
584
+ | `useModelStreamingTranscript` | `boolean` | `false` | 当 `roles` 包含 `"model"` 时,model 字幕是否改用 `response.output_audio_transcript.*` 事件 |
585
+ | `hideCompletedAfterMs` | `number \| false` | `3000` | 字幕进入完成态后多久自动移除;传 `false` 关闭自动移除 |
586
+ | `onSubtitleCompleted` | `(item, context) => "keep" \| "remove" \| { removeAfterMs } \| void \| Promise<...>` | — | 自定义完成态清理策略,优先于 `hideCompletedAfterMs` |
577
587
  | `subtitleStyle` | `IVISubtitleOverlayStyle` | — | 样式配置 |
578
588
  | `className` / `style` | — | — | 样式 |
579
589
 
package/dist/index.cjs CHANGED
@@ -3080,20 +3080,29 @@ function useApplyVolumeToSlot(containerRef, volume, enabled, activeSourceId) {
3080
3080
  return () => observer.disconnect();
3081
3081
  }, [containerRef, volume, enabled, activeSourceId]);
3082
3082
  }
3083
+ var DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = 3e3;
3083
3084
  function useIviSubtitles(runtime, options = {}) {
3084
3085
  const roles = options.roles ?? "user";
3085
3086
  const maxItems = normalizeMaxItems(options.maxItems);
3087
+ const hideCompletedAfterMs = normalizeHideCompletedAfterMs(options.hideCompletedAfterMs);
3088
+ const onSubtitleCompleted = options.onSubtitleCompleted;
3086
3089
  const roleKey = Array.isArray(roles) ? roles.join("\0") : roles;
3087
3090
  const roleSet = react.useMemo(
3088
3091
  () => new Set(roleKey.split("\0")),
3089
3092
  [roleKey]
3090
3093
  );
3094
+ const useModelStreamingTranscript = options.useModelStreamingTranscript === true && roleSet.has("model");
3091
3095
  const [subtitles, setSubtitles] = react.useState([]);
3092
3096
  const seenIdsRef = react.useRef(/* @__PURE__ */ new Set());
3093
3097
  const initializedRef = react.useRef(false);
3098
+ const completedSubtitleIdsRef = react.useRef(/* @__PURE__ */ new Set());
3099
+ const completionControllersRef = react.useRef(/* @__PURE__ */ new Map());
3100
+ const completionPolicyRef = react.useRef(null);
3094
3101
  react.useEffect(() => {
3095
3102
  seenIdsRef.current = /* @__PURE__ */ new Set();
3096
3103
  initializedRef.current = false;
3104
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3105
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3097
3106
  setSubtitles([]);
3098
3107
  if (!runtime) {
3099
3108
  return;
@@ -3104,7 +3113,7 @@ function useIviSubtitles(runtime, options = {}) {
3104
3113
  if (!initializedRef.current) {
3105
3114
  initializedRef.current = true;
3106
3115
  for (const item of conversations) {
3107
- if (item.lifecycle === "done" || !getDisplayText(item) || !roleSet.has(item.role)) {
3116
+ if (item.lifecycle === "done" || !getDisplayText(item) || !shouldUseConversationRole(item.role, roleSet, useModelStreamingTranscript)) {
3108
3117
  seenIds.add(item.id);
3109
3118
  }
3110
3119
  }
@@ -3113,8 +3122,12 @@ function useIviSubtitles(runtime, options = {}) {
3113
3122
  const conversationMap = new Map(conversations.map((item) => [item.id, item]));
3114
3123
  const nextById = /* @__PURE__ */ new Map();
3115
3124
  for (const previousItem of previous) {
3125
+ if (previousItem.source === "response_audio_transcript") {
3126
+ nextById.set(previousItem.id, previousItem);
3127
+ continue;
3128
+ }
3116
3129
  const conversation = conversationMap.get(previousItem.id);
3117
- if (!conversation || !roleSet.has(conversation.role) || !getDisplayText(conversation)) {
3130
+ if (!conversation || !shouldUseConversationRole(conversation.role, roleSet, useModelStreamingTranscript) || !getDisplayText(conversation)) {
3118
3131
  continue;
3119
3132
  }
3120
3133
  nextById.set(
@@ -3127,7 +3140,7 @@ function useIviSubtitles(runtime, options = {}) {
3127
3140
  );
3128
3141
  }
3129
3142
  for (const conversation of conversations) {
3130
- if (!roleSet.has(conversation.role) || !getDisplayText(conversation)) {
3143
+ if (!shouldUseConversationRole(conversation.role, roleSet, useModelStreamingTranscript) || !getDisplayText(conversation)) {
3131
3144
  continue;
3132
3145
  }
3133
3146
  if (seenIds.has(conversation.id)) {
@@ -3137,10 +3150,7 @@ function useIviSubtitles(runtime, options = {}) {
3137
3150
  nextById.set(conversation.id, buildSubtitleItem(conversation, now, now));
3138
3151
  }
3139
3152
  const next = Array.from(nextById.values());
3140
- if (maxItems === 0) {
3141
- return [];
3142
- }
3143
- return next.length > maxItems ? next.slice(next.length - maxItems) : next;
3153
+ return trimSubtitleItems(next, maxItems);
3144
3154
  });
3145
3155
  };
3146
3156
  syncConversations(runtime.getState().conversations);
@@ -3148,15 +3158,97 @@ function useIviSubtitles(runtime, options = {}) {
3148
3158
  if (event.type === "session.ended") {
3149
3159
  seenIdsRef.current = /* @__PURE__ */ new Set();
3150
3160
  initializedRef.current = false;
3161
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3162
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3151
3163
  setSubtitles([]);
3152
3164
  return;
3153
3165
  }
3166
+ if (useModelStreamingTranscript && isModelStreamingTranscriptEvent(event)) {
3167
+ setSubtitles((previous) => trimSubtitleItems(upsertModelStreamingSubtitle(previous, event), maxItems));
3168
+ return;
3169
+ }
3170
+ if (useModelStreamingTranscript && isResponseDoneEvent(event)) {
3171
+ setSubtitles((previous) => trimSubtitleItems(markModelStreamingSubtitleDone(previous, event), maxItems));
3172
+ return;
3173
+ }
3154
3174
  if (!isSubtitleRelatedEvent(event.type)) {
3155
3175
  return;
3156
3176
  }
3157
3177
  syncConversations(state.conversations);
3158
3178
  });
3159
- }, [runtime, roleSet, maxItems]);
3179
+ }, [runtime, roleSet, maxItems, useModelStreamingTranscript]);
3180
+ react.useEffect(() => {
3181
+ return () => {
3182
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3183
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3184
+ };
3185
+ }, []);
3186
+ react.useEffect(() => {
3187
+ const previousPolicy = completionPolicyRef.current;
3188
+ const policyChanged = Boolean(
3189
+ previousPolicy && (previousPolicy.hideCompletedAfterMs !== hideCompletedAfterMs || previousPolicy.onSubtitleCompleted !== onSubtitleCompleted)
3190
+ );
3191
+ completionPolicyRef.current = { hideCompletedAfterMs, onSubtitleCompleted };
3192
+ if (policyChanged) {
3193
+ completedSubtitleIdsRef.current = /* @__PURE__ */ new Set();
3194
+ abortCompletedSubtitleTasks(completionControllersRef.current);
3195
+ }
3196
+ const subtitleById = new Map(subtitles.map((item) => [item.id, item]));
3197
+ const completedIds = completedSubtitleIdsRef.current;
3198
+ const controllers = completionControllersRef.current;
3199
+ for (const [id, controller] of Array.from(controllers)) {
3200
+ const item = subtitleById.get(id);
3201
+ if (!item || !isCompletedSubtitle(item)) {
3202
+ controller.abort();
3203
+ controllers.delete(id);
3204
+ completedIds.delete(id);
3205
+ }
3206
+ }
3207
+ for (const id of Array.from(completedIds)) {
3208
+ const item = subtitleById.get(id);
3209
+ if (!item || !isCompletedSubtitle(item)) {
3210
+ completedIds.delete(id);
3211
+ }
3212
+ }
3213
+ for (const item of subtitles) {
3214
+ if (!isCompletedSubtitle(item) || completedIds.has(item.id)) {
3215
+ continue;
3216
+ }
3217
+ completedIds.add(item.id);
3218
+ const controller = new AbortController();
3219
+ controllers.set(item.id, controller);
3220
+ const context = {
3221
+ entries: subtitles,
3222
+ reason: getSubtitleCompletedReason(item),
3223
+ signal: controller.signal
3224
+ };
3225
+ let decisionResult;
3226
+ try {
3227
+ decisionResult = onSubtitleCompleted ? onSubtitleCompleted(item, context) : getDefaultCompletedSubtitleDecision(hideCompletedAfterMs);
3228
+ } catch {
3229
+ decisionResult = "keep";
3230
+ }
3231
+ void Promise.resolve(decisionResult).then((decision) => {
3232
+ applyCompletedSubtitleDecision(
3233
+ item.id,
3234
+ decision,
3235
+ controller,
3236
+ controllers,
3237
+ completedIds,
3238
+ (id) => setSubtitles((previous) => removeSubtitleById(previous, id))
3239
+ );
3240
+ }).catch(() => {
3241
+ applyCompletedSubtitleDecision(
3242
+ item.id,
3243
+ "keep",
3244
+ controller,
3245
+ controllers,
3246
+ completedIds,
3247
+ (id) => setSubtitles((previous) => removeSubtitleById(previous, id))
3248
+ );
3249
+ });
3250
+ }
3251
+ }, [subtitles, hideCompletedAfterMs, onSubtitleCompleted]);
3160
3252
  return subtitles;
3161
3253
  }
3162
3254
  function normalizeMaxItems(maxItems) {
@@ -3168,6 +3260,96 @@ function normalizeMaxItems(maxItems) {
3168
3260
  }
3169
3261
  return Math.max(0, Math.floor(maxItems));
3170
3262
  }
3263
+ function normalizeHideCompletedAfterMs(hideCompletedAfterMs) {
3264
+ if (hideCompletedAfterMs === false) {
3265
+ return false;
3266
+ }
3267
+ if (hideCompletedAfterMs === void 0 || !Number.isFinite(hideCompletedAfterMs)) {
3268
+ return DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS;
3269
+ }
3270
+ return Math.max(0, Math.floor(hideCompletedAfterMs));
3271
+ }
3272
+ function getDefaultCompletedSubtitleDecision(hideCompletedAfterMs) {
3273
+ if (hideCompletedAfterMs === false) {
3274
+ return "keep";
3275
+ }
3276
+ return hideCompletedAfterMs === 0 ? "remove" : { removeAfterMs: hideCompletedAfterMs };
3277
+ }
3278
+ function normalizeCompletedSubtitleDecision(decision) {
3279
+ if (decision === "keep" || decision === "remove") {
3280
+ return decision;
3281
+ }
3282
+ if (decision && typeof decision === "object" && "removeAfterMs" in decision) {
3283
+ const removeAfterMs = Number(decision.removeAfterMs);
3284
+ if (!Number.isFinite(removeAfterMs)) {
3285
+ return "keep";
3286
+ }
3287
+ const normalized = Math.max(0, Math.floor(removeAfterMs));
3288
+ return normalized === 0 ? "remove" : { removeAfterMs: normalized };
3289
+ }
3290
+ return "keep";
3291
+ }
3292
+ function shouldUseConversationRole(role, roleSet, useModelStreamingTranscript) {
3293
+ if (!roleSet.has(role)) {
3294
+ return false;
3295
+ }
3296
+ return !(useModelStreamingTranscript && role === "model");
3297
+ }
3298
+ function trimSubtitleItems(items, maxItems) {
3299
+ if (maxItems === 0) {
3300
+ return [];
3301
+ }
3302
+ const sorted = [...items].sort((a, b) => a.timestamp - b.timestamp);
3303
+ return sorted.length > maxItems ? sorted.slice(sorted.length - maxItems) : sorted;
3304
+ }
3305
+ function isCompletedSubtitle(item) {
3306
+ return item.lifecycle === "done" || item.status === "completed" || item.status === "incomplete";
3307
+ }
3308
+ function getSubtitleCompletedReason(item) {
3309
+ return item.source === "response_audio_transcript" || item.role === "model" ? "response_done" : "conversation_done";
3310
+ }
3311
+ function abortCompletedSubtitleTasks(controllers) {
3312
+ for (const controller of controllers.values()) {
3313
+ controller.abort();
3314
+ }
3315
+ controllers.clear();
3316
+ }
3317
+ function applyCompletedSubtitleDecision(itemId, decision, controller, controllers, completedIds, removeSubtitle) {
3318
+ if (controller.signal.aborted) {
3319
+ return;
3320
+ }
3321
+ const normalizedDecision = normalizeCompletedSubtitleDecision(decision);
3322
+ if (normalizedDecision === "keep") {
3323
+ controllers.delete(itemId);
3324
+ return;
3325
+ }
3326
+ const remove = () => {
3327
+ if (controller.signal.aborted) {
3328
+ return;
3329
+ }
3330
+ controllers.delete(itemId);
3331
+ completedIds.delete(itemId);
3332
+ removeSubtitle(itemId);
3333
+ };
3334
+ if (normalizedDecision === "remove") {
3335
+ remove();
3336
+ return;
3337
+ }
3338
+ const timeout = setTimeout(remove, normalizedDecision.removeAfterMs);
3339
+ controller.signal.addEventListener(
3340
+ "abort",
3341
+ () => {
3342
+ clearTimeout(timeout);
3343
+ },
3344
+ { once: true }
3345
+ );
3346
+ }
3347
+ function removeSubtitleById(items, itemId) {
3348
+ if (!items.some((item) => item.id === itemId)) {
3349
+ return items;
3350
+ }
3351
+ return items.filter((item) => item.id !== itemId);
3352
+ }
3171
3353
  function getDisplayText(item) {
3172
3354
  return item.text || item.transcript;
3173
3355
  }
@@ -3182,16 +3364,109 @@ function buildSubtitleItem(item, timestamp, updatedAt) {
3182
3364
  displayText: getDisplayText(item),
3183
3365
  content: item.content,
3184
3366
  item: item.item,
3367
+ source: "conversation",
3185
3368
  timestamp,
3186
3369
  updatedAt
3187
3370
  };
3188
3371
  }
3372
+ function upsertModelStreamingSubtitle(previous, event, now = Date.now()) {
3373
+ const responseId = getEventString(event, "responseId", "response_id");
3374
+ if (!responseId) {
3375
+ return previous;
3376
+ }
3377
+ const displayText = event.type === "response.output_audio_transcript.done" ? event.transcript : event.delta;
3378
+ if (typeof displayText !== "string" || displayText.length === 0) {
3379
+ return previous;
3380
+ }
3381
+ const id = buildModelStreamingSubtitleId(responseId);
3382
+ const itemId = getEventString(event, "itemId", "item_id");
3383
+ const existing = previous.find((item) => item.id === id);
3384
+ const nextItem = {
3385
+ id,
3386
+ role: "model",
3387
+ lifecycle: "added",
3388
+ status: "in_progress",
3389
+ text: "",
3390
+ transcript: displayText,
3391
+ displayText,
3392
+ content: [],
3393
+ item: {
3394
+ id: itemId ?? id,
3395
+ type: "message",
3396
+ role: "model",
3397
+ status: "in_progress",
3398
+ content: [{ type: "audio", transcript: displayText }]
3399
+ },
3400
+ source: "response_audio_transcript",
3401
+ responseId,
3402
+ itemId,
3403
+ timestamp: existing?.timestamp ?? now,
3404
+ updatedAt: hasStreamingSubtitleChanged(existing, displayText) ? now : existing?.updatedAt ?? now
3405
+ };
3406
+ return replaceSubtitleItem(previous, nextItem);
3407
+ }
3408
+ function markModelStreamingSubtitleDone(previous, event, now = Date.now()) {
3409
+ const responseId = event.response?.id;
3410
+ if (!responseId) {
3411
+ return previous;
3412
+ }
3413
+ const id = buildModelStreamingSubtitleId(responseId);
3414
+ const status = mapResponseStatusToConversationStatus(event.response?.status);
3415
+ return previous.map((item) => {
3416
+ if (item.id !== id || item.source !== "response_audio_transcript") {
3417
+ return item;
3418
+ }
3419
+ return {
3420
+ ...item,
3421
+ lifecycle: "done",
3422
+ status,
3423
+ item: {
3424
+ ...item.item,
3425
+ status
3426
+ },
3427
+ updatedAt: now
3428
+ };
3429
+ });
3430
+ }
3431
+ function replaceSubtitleItem(previous, nextItem) {
3432
+ const replaced = previous.map((item) => item.id === nextItem.id ? nextItem : item);
3433
+ if (replaced.some((item) => item.id === nextItem.id)) {
3434
+ return replaced;
3435
+ }
3436
+ return [...previous, nextItem];
3437
+ }
3438
+ function hasStreamingSubtitleChanged(previous, nextDisplayText) {
3439
+ return !previous || previous.displayText !== nextDisplayText || previous.lifecycle !== "added" || previous.status !== "in_progress";
3440
+ }
3441
+ function buildModelStreamingSubtitleId(responseId) {
3442
+ return `model-response:${responseId}`;
3443
+ }
3444
+ function getEventString(event, camelKey, snakeKey) {
3445
+ const record = event;
3446
+ const value = record[camelKey] ?? record[snakeKey];
3447
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3448
+ }
3449
+ function mapResponseStatusToConversationStatus(status) {
3450
+ if (status === "completed") {
3451
+ return "completed";
3452
+ }
3453
+ if (status === "in_progress") {
3454
+ return "in_progress";
3455
+ }
3456
+ return "incomplete";
3457
+ }
3189
3458
  function hasSubtitleChanged(previous, next) {
3190
3459
  return previous.text !== next.text || previous.transcript !== next.transcript || previous.lifecycle !== next.lifecycle || previous.status !== next.status || previous.role !== next.role;
3191
3460
  }
3192
3461
  function isSubtitleRelatedEvent(type) {
3193
3462
  return type.startsWith("conversation.") || type.startsWith("response.");
3194
3463
  }
3464
+ function isModelStreamingTranscriptEvent(event) {
3465
+ return event.type === "response.output_audio_transcript.delta" || event.type === "response.output_audio_transcript.done";
3466
+ }
3467
+ function isResponseDoneEvent(event) {
3468
+ return event.type === "response.done";
3469
+ }
3195
3470
  var BREATHE_KEYFRAMES = `@keyframes ivi-subtitle-breathe{0%,100%{opacity:1}50%{opacity:.55}}`;
3196
3471
  function IVISubtitleOverlay(props) {
3197
3472
  const {
@@ -3199,13 +3474,19 @@ function IVISubtitleOverlay(props) {
3199
3474
  roles = "user",
3200
3475
  maxItems,
3201
3476
  maxVisible,
3477
+ useModelStreamingTranscript,
3478
+ hideCompletedAfterMs,
3479
+ onSubtitleCompleted,
3202
3480
  subtitleStyle,
3203
3481
  className,
3204
3482
  style
3205
3483
  } = props;
3206
3484
  const entries = useIviSubtitles(runtime, {
3207
3485
  roles,
3208
- maxItems: maxItems ?? maxVisible
3486
+ maxItems: maxItems ?? maxVisible,
3487
+ useModelStreamingTranscript,
3488
+ hideCompletedAfterMs,
3489
+ onSubtitleCompleted
3209
3490
  });
3210
3491
  if (entries.length === 0) return null;
3211
3492
  const fontFamily = subtitleStyle?.fontFamily ?? "system-ui, -apple-system, sans-serif";
@@ -4296,6 +4577,7 @@ function getClientLogTag(category) {
4296
4577
  return "[IVI-CLIENT]";
4297
4578
  }
4298
4579
 
4580
+ exports.DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS = DEFAULT_HIDE_COMPLETED_SUBTITLE_AFTER_MS;
4299
4581
  exports.EMPTY_RUNTIME_STATE = EMPTY_RUNTIME_STATE;
4300
4582
  exports.IVILivekitPlayer = IVILivekitPlayer;
4301
4583
  exports.IVIStageView = IVIStageView;