@video-lab/react-frame 3.1.0 → 4.0.1

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
@@ -2,7 +2,9 @@
2
2
 
3
3
  Video Lab Player 的 React iframe 接入方式。播放器运行在独立 iframe 中,并通过通信契约收发命令与事件。
4
4
 
5
- 对外只有一个组件 `<VideoPlayerFrame />` + 一个命令句柄。和 [`@video-lab/vue-frame`](https://www.npmjs.com/package/@video-lab/vue-frame) 行为对齐。
5
+ 对外提供 `<VideoPlayerFrame />`、命令句柄,以及应用级 `PlayerProvider` / `usePlayerI18n`。和 [`@video-lab/vue-frame`](https://www.npmjs.com/package/@video-lab/vue-frame) 行为对齐。
6
+
7
+ 多个 Frame 可继承一次设置的语言、默认值和连接 origin;单实例 prop 仍可覆盖。无需安装 `@video-lab/react`。字段与切换规则见[全局配置指南](../../docs/guides/PLAYER-GLOBAL-CONFIG.md)。
6
8
 
7
9
  ## 什么时候用它
8
10
 
@@ -19,6 +21,8 @@ Video Lab Player 的 React iframe 接入方式。播放器运行在独立 iframe
19
21
  pnpm add @video-lab/react-frame
20
22
  ```
21
23
 
24
+ 默认简体中文;传 `locale="en-US"` 切英文。越南语按需安装 `@video-lab/locales` 并从 `@video-lab/locales/vi-VN` 导入 `viVN`,传入 `locale={{ locale: 'vi-VN', messages: { 'vi-VN': viVN } }}`;不需安装 `@video-lab/react`。实例 `setLocale()` 会同步 iframe 内播放器语言。完整接法见[国际化指南](../../docs/guides/USER-GUIDE.md#_9-国际化)。
25
+
22
26
  peer:`react >=19`、`react-dom >=19`。React 18 不支持当前 custom element 覆盖层所需的 property / CustomEvent 绑定;仍在 React 18 的项目请停在上一个 major。
23
27
 
24
28
  ## 用法
@@ -27,17 +31,26 @@ peer:`react >=19`、`react-dom >=19`。React 18 不支持当前 custom element
27
31
  import { VideoPlayerFrame, type VideoPlayerFrameHandle } from '@video-lab/react-frame'
28
32
  import { useRef } from 'react'
29
33
 
34
+ // 来自受控部署配置,不是媒体 URL,也不能照抄到生产环境。
35
+ const playerOrigin = 'https://player.example.internal'
36
+
30
37
  function Player() {
31
38
  const ref = useRef<VideoPlayerFrameHandle>(null)
32
39
 
33
40
  return (
34
41
  <VideoPlayerFrame
35
42
  ref={ref}
36
- source="https://media.example.com/live/master.m3u8"
43
+ source="https://media.example.com/lesson.m3u8"
44
+ origin={playerOrigin}
37
45
  autoplay
38
46
  muted
39
- onReady={({ duration, quality, subtitles }) => {}}
40
- onError={(err) => {}}
47
+ playsinline
48
+ onReady={({ duration }) => {
49
+ console.info('iframe 内播放器已获得 metadata,时长:', duration)
50
+ }}
51
+ onError={(error) => {
52
+ console.error(error.code, error.message)
53
+ }}
41
54
  />
42
55
  )
43
56
  }
@@ -50,9 +63,13 @@ import type { VideoPlayerFrameHandle } from '@video-lab/react-frame'
50
63
  declare const ref: { current: VideoPlayerFrameHandle | null }
51
64
  -->
52
65
  ```tsx
53
- await ref.current?.play()
54
- ref.current?.seek(30)
55
- await ref.current?.enterFullscreen()
66
+ try {
67
+ await ref.current?.play()
68
+ await ref.current?.seek(30)
69
+ await ref.current?.enterFullscreen()
70
+ } catch (error) {
71
+ console.error('iframe 命令执行失败', error)
72
+ }
56
73
  ```
57
74
 
58
75
  ### 配置 iframe 地址
@@ -77,24 +94,91 @@ import { VideoPlayerFrame } from '@video-lab/react-frame'
77
94
  - **不内置品牌 UI;认证只支持签名 URL**,不接受自定义请求头。
78
95
  - 业务应用可直接使用本组件,也可在自己的业务组件中封装它。
79
96
 
97
+ ## 接入完成清单
98
+
99
+ 1. `origin` 必须来自部署配置;它是 iframe 部署根地址,不是播放器媒体 URL。需要锁定 iframe artifact 时传 `iframeVersion`。
100
+ 2. 所有句柄命令跨 iframe 异步下发;宿主主动调用的每次命令都要有失败处理路径,可在业务封装层集中 `await` + `catch`。命令 rejection 与播放期 `error` 是两条独立事实。
101
+ 3. 宿主 UI 用 `onPlayableChange` 决定 loading / 重试入口,`onError` 只用于诊断;不要凭单个 `playing` 推断恢复成功。
102
+ 4. 需要会话 QoE、排序或重传去重时,监听带 `delivery` 的 `onPlayerEvent` 并接入
103
+ [`@video-lab/telemetry`](https://www.npmjs.com/package/@video-lab/telemetry);不要将高频事件逐条发送到埋点服务。
104
+ 5. iframe 加载、CSP、握手和版本不兼容会走明确 fallback;生产部署要同时验收 `origin`、iframe artifact 和宿主包版本。
105
+
80
106
  ## AI 接入 Skill
81
107
 
82
108
  安装包包含 `skills/host-integration/SKILL.md`。让宿主 AI 使用前,先确认该文件 front matter
83
- 中的 `supportedPackages` 与已安装版本匹配;它提供接入面选择、错误/恢复和 delivered 事件的边界,
109
+ 中的 `supportedPackages` 与已安装版本匹配;它提供接入面选择、错误/恢复和 统一 `PlayerEvent` 的边界,
84
110
  但具体 API 仍以本 README 与类型定义为准。
85
111
 
86
112
  ## 相关
87
113
 
88
- - 完整 props / 事件 / 句柄清单请联系项目维护团队获取内部使用手册。
89
- - 契约定义:[`@video-lab/protocol`](https://www.npmjs.com/package/@video-lab/protocol)
114
+ - [五种接入方式怎么选](../../apps/docs/guide/access-modes.md)
115
+ - [按场景查 API](../../apps/docs/reference/by-scenario.md)
116
+ - [完整宿主接入、部署与错误/恢复语义](../../docs/guides/USER-GUIDE.md)
117
+ - 契约定义:[`@video-lab/protocol`](https://www.npmjs.com/package/@video-lab/protocol)
90
118
 
91
119
  MIT
92
120
 
93
121
  ## 完整事件与命令失败
94
122
 
95
- `onPlayerEvent` 保持交付 raw `PlayerEvent`;需要跨重连排序、去重或识别迟到事件时使用
96
- `onDeliveredPlayerEvent`。所有可失败命令都应 `await` 并 `catch`,并同时订阅事件流:命令
97
- rejection 不保证产生 `error`,播放期 `error` 也不保证对应某个命令 rejection。
123
+ `onPlayerEvent` 交付完整 `PlayerEvent`,其顶层 `delivery` 包含生产端排序、去重与迟到事件证据。
124
+ `PlayerEvent` 用 `event` 字段区分类型(如 `e.event === 'ready'`),数据在 `payload`;它没有 `type` 字段。
125
+ 旧 iframe 的事件可缺少该字段。宿主主动调用的每次可失败命令都应处理 rejection,可在业务封装层
126
+ 集中处理,不必在每个按钮里重复写 `catch`。命令 rejection 不保证产生 `error`,播放期 `error`
127
+ 也不保证对应某个命令 rejection。`void ref.current?.play()` 不会处理 Promise rejection。
128
+
129
+ ```tsx
130
+ import { useRef, useState } from 'react'
131
+ import { VideoPlayerFrame, type VideoPlayerFrameHandle } from '@video-lab/react-frame'
132
+
133
+ function PlayerWithCommands() {
134
+ const player = useRef<VideoPlayerFrameHandle>(null)
135
+ const [commandsReady, setCommandsReady] = useState(false)
136
+ const [canRetry, setCanRetry] = useState(false)
137
+ const [commandError, setCommandError] = useState('')
138
+
139
+ async function runCommand(command: (handle: VideoPlayerFrameHandle) => Promise<unknown>) {
140
+ const handle = player.current
141
+ if (!handle || !commandsReady) {
142
+ setCommandError('播放器尚未就绪')
143
+ return
144
+ }
145
+
146
+ setCommandError('')
147
+ try {
148
+ await command(handle)
149
+ } catch {
150
+ // 宿主可在这里接入脱敏诊断;不要把命令失败重复计为播放期 error。
151
+ setCommandError('操作未完成,请稍后重试')
152
+ }
153
+ }
154
+
155
+ const play = () => runCommand((handle) => handle.play())
156
+ const retry = () => runCommand((handle) => handle.retry())
157
+
158
+ return (
159
+ <>
160
+ <VideoPlayerFrame
161
+ ref={player}
162
+ source="https://media.example.com/lesson.m3u8"
163
+ origin="https://player.example.internal"
164
+ onReady={() => setCommandsReady(true)}
165
+ onPlayableChange={({ reason, playable, recoverable, action }) => {
166
+ if (reason === 'frame_disconnected') setCommandsReady(false)
167
+ setCanRetry(!playable && !recoverable && action === 'retry')
168
+ }}
169
+ />
170
+ <button type="button" disabled={!commandsReady} onClick={play}>播放</button>
171
+ {canRetry && <button type="button" disabled={!commandsReady} onClick={retry}>重试</button>}
172
+ {commandError && <p role="alert">{commandError}</p>}
173
+ </>
174
+ )
175
+ }
176
+ ```
177
+
178
+ 当前实现中,组件 ref 已挂载但内部连接尚未建立时,`play()` 等多数方法会返回已成功的 Promise 并且
179
+ 不执行命令;`setPageFullscreen()` 则会拒绝。不要把 Promise resolve 当成“已经连接”或“已经出画面”,
180
+ 应等 `onReady` 再开放播放控制,并以 `onPlayableChange` 判断播放状态。SDK 因 props 变化发起的命令
181
+ 由组件内部处理,宿主的统一处理器只负责宿主主动调用的命令。
98
182
 
99
183
  ### iframe 基础路径
100
184
 
@@ -102,7 +186,7 @@ rejection 不保证产生 `error`,播放期 `error` 也不保证对应某个
102
186
 
103
187
  ## 错误 UI 接管
104
188
 
105
- 通过 `showErrorOverlay={false}` 关闭默认错误文字、背景和 Retry。省略时保持开启,错误事件与恢复能力不受影响;宿主自行决定提示文案和样式。本项仅初始化读取,改变时须重新挂载。iframe 需使用 v1.1.0 或更高的兼容应用,旧应用可能仍显示默认错误 UI。纯 iframe 标签需另接 helper 才能订阅事件。
189
+ 通过 `showErrorOverlay={false}` 关闭默认错误文字、背景和动作按钮。省略时保持开启,错误事件与恢复能力不受影响;宿主也可用 `ref.current?.performErrorAction()` 触发当前可执行动作。支持新展示桥的 iframe,其宿主默认层可随 Provider 和实例配置原地更新;旧 iframe 仍由内部默认层处理。宿主包与 iframe 应用必须部署兼容的契约版本。
106
190
 
107
191
  ## 内置控件按需隐藏
108
192
 
@@ -119,10 +203,58 @@ iframe 须与宿主代码同批部署并支持该功能;旧应用可能忽略
119
203
  详见[网页全屏接入与完整参考实现](../../docs/guides/PAGE-FULLSCREEN.md)。iframe 需要新版宿主与 iframe 协商支持;未启用时新入口不可用。浏览器原生全屏接口保持独立。
120
204
 
121
205
 
206
+ ### Poster 上叠宿主侧自定义 Loading
207
+
208
+ Frame 的 `renderLoading` 在宿主 DOM 中渲染,可以覆盖 iframe 创建、脚本加载和握手空档:
209
+
210
+ <!-- doc-snippet: skip `selectedRoom` 与 `BusinessLoading` 是宿主业务对象;SDK 调用由组件单测覆盖 -->
211
+ ```tsx
212
+ <VideoPlayerFrame
213
+ source={selectedRoom.source}
214
+ poster={{ url: selectedRoom.videoPoster, fit: 'cover', loading: 'eager' }}
215
+ renderLoading={({ phase, reason }) => (
216
+ <BusinessLoading transparent phase={phase} reason={reason} />
217
+ )}
218
+ />
219
+ ```
220
+
221
+ `phase='initial'` 从宿主首次渲染保持到 `firstframe`,`phase='runtime'` 表示首帧后的可恢复等待。
222
+ 传入 renderer 后会自动关闭 iframe 内默认 Loading,`showDefaultLoadingOverlay` 无需设为 false。
223
+ renderer 是否存在属于构造期策略;要切换实现时重新挂载组件。
224
+
122
225
  ### 统一恢复(契约 v2)
123
226
 
124
227
  旧 `reconnect({ resetCounter })` 和 `reconnectstart/success/failed` 已移除。命令入口是 `retry(): Promise<void>`;Promise 完成只表示接受或合并请求。恢复中的重复请求共用预算,强播放证据通过 `recovery` 的 `recovered` 回报。
125
228
 
126
- 宿主 UI 读取 `playablechange` 的 `playable`、`recoverable`、`action`。`error` 只提供诊断;预算耗尽才给出 `action: 'retry'`,宿主无需按错误原因拼接重试状态。使用 `showErrorOverlay={false}` 接管错误样式,使用 `showLoadingOverlay={false}` 接管运行时 Loading;配置在构造时生效,事件仍保留。静态 iframe URL 没有宿主命令通道,内部按钮仍走同一调度。
229
+ 页面级文案和终态 CTA 可读取 `playablechange` 的 `playable`、`recoverable`、`action`;宿主自建按钮直接调用 `ref.current?.performErrorAction()`,无需自行拼 `play()`、`retry()`、授权换源或 Frame 重建。默认连接 Loading 从组件首次渲染就在宿主 DOM;支持展示桥的 iframe 的媒体 Loading 和默认错误层也由宿主组件管理,自动播放按钮留在 iframe。旧 iframe 不支持展示桥时保留 iframe 内媒体层。`renderLoading` 接收 `{ phase, reason, text? }`,可直接显示已本地化的 `text`。`showDefaultLoadingOverlay={false}` 只关闭默认视觉,不关闭自定义 renderer 或事件;默认层的语言、显隐和按钮色可随 Provider 原地更新。
230
+
231
+ `renderPoster={() => <MyPoster />}` 与 `renderPause={() => <MyPause onPlay={() => void ref.current?.play()} />}` 在宿主 DOM 渲染,由 SDK 管首帧及明确暂停的显隐。内置控件或公开 `pause()` 对应的实际暂停会显示暂停画面,被动暂停不会显示。提供 renderer 后,同类内置图片不会传入 iframe。不传 `poster` / `pauseImage` 即不使用;`null` 仅用于清除 Provider 默认值。
127
232
 
128
233
  更多迁移语义见 [ADR-099](../../docs/adr/ADR-099-unified-recovery-contract.md)。
234
+
235
+ ### 带业务流程的启动占位由宿主维护,且**只能出现一次**
236
+
237
+ 普通 Poster + Loading 使用上面的 `renderLoading` 即可。带倒计时、跳过或开播门控的业务启动占位仍由宿主维护,写它时有一条必须注意:
238
+
239
+ **`firstframe` 不是「一辈子只发一次」的事件,它是「每个会话发一次」。** 换源(`load()` 换地址、跨内核被拒后重建)、手动 `retry()`、iframe 重建都会开启新会话,`firstframe` 随之再发一次,`poster` 也会重新显示。如果把占位的显隐直接绑在 `firstframe` 上而不记状态,**换源时启动占位会再盖一次**——对一个已经在看的用户来说,画面上突然又出现「加载中」。
240
+
241
+ 正确写法是宿主自己维护一个只置位、不复位的标记:
242
+
243
+ <!-- doc-snippet-preamble
244
+ declare const setStartupDone: (done: boolean) => void
245
+ -->
246
+ ```ts
247
+ // 只认第一次:此后换源、重连、卡顿都不再显示启动占位
248
+ const onFirstFrame = () => setStartupDone(true)
249
+ ```
250
+
251
+ 把回调传给 `onFirstFrame`,占位本身按 `startupDone` 条件渲染即可
252
+ (`<VideoPlayerFrame onFirstFrame={onFirstFrame} />`,`{!startupDone && <MyStartupPlaceholder />}`)。
253
+
254
+ 要点:
255
+
256
+ - **绑 `firstframe`,不要绑 `ready` 或 `play`**:后两者只表示「可以播 / 开始播这个动作」,画面还没出来(实测 FLV 上 `ready` 到首帧可差 6 秒以上)。
257
+ - **不要用固定时长的倒计时来撤**:起播耗时跨度很大(实测 HLS 0.4s、FLV 6.6s),计时器撤早了会露黑、撤晚了让用户白等。倒计时可以数,但撤不撤要看事件。
258
+ - **不要在换源时复位**:换源期间的覆盖由 SDK 的恢复态遮罩接管,宿主再盖一层是重复。
259
+ - **只有组件真正卸载重挂(用户离开播放页又回来)才算新的一次首次进入**,那时可以复位。
260
+ - **失败分支**:收到不可恢复终态(`playablechange` 的 `playable:false, recoverable:false`)时撤掉占位,让位给错误 UI,否则会一直盖着。