@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 +147 -15
- package/dist/index.cjs +586 -45
- package/dist/index.d.cts +25 -9
- package/dist/index.d.mts +25 -9
- package/dist/index.mjs +576 -48
- package/package.json +4 -3
- package/skills/host-integration/SKILL.md +6 -7
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Video Lab Player 的 React iframe 接入方式。播放器运行在独立 iframe 中,并通过通信契约收发命令与事件。
|
|
4
4
|
|
|
5
|
-
|
|
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/
|
|
43
|
+
source="https://media.example.com/lesson.m3u8"
|
|
44
|
+
origin={playerOrigin}
|
|
37
45
|
autoplay
|
|
38
46
|
muted
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
54
|
-
ref.current?.
|
|
55
|
-
await ref.current?.
|
|
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` 与已安装版本匹配;它提供接入面选择、错误/恢复和
|
|
109
|
+
中的 `supportedPackages` 与已安装版本匹配;它提供接入面选择、错误/恢复和 统一 `PlayerEvent` 的边界,
|
|
84
110
|
但具体 API 仍以本 README 与类型定义为准。
|
|
85
111
|
|
|
86
112
|
## 相关
|
|
87
113
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
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`
|
|
96
|
-
`
|
|
97
|
-
|
|
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}`
|
|
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
|
-
|
|
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,否则会一直盖着。
|