@video-lab/react-frame
v4.1.5
Published
Video Lab Player React iframe 组件:隔离沙箱接入,不引入播放内核
Readme
@video-lab/react-frame
Video Lab Player 的 React iframe 接入方式。播放器运行在独立 iframe 中,并通过通信契约收发命令与事件。
对外提供 <VideoPlayerFrame />、命令句柄,以及应用级 PlayerProvider / usePlayerI18n。和 @video-lab/vue-frame 行为对齐。
多个 Frame 可继承一次设置的语言、默认值和连接 origin;单实例 prop 仍可覆盖。无需安装 @video-lab/react。字段与切换规则见包内 API 参考。
什么时候用它
| 场景 | 选这个包 |
|:---|:---|
| 要把播放器和宿主隔离(CSS 冲突、第三方内容、崩溃隔离) | ✅ 本包 |
| 宿主页面样式复杂,不想被播放器影响 | ✅ 本包 |
| 追求首帧速度、能接受同进程 | ❌ 用 @video-lab/react(inline,首帧更快) |
| 只能嵌 URL、跑不了 npm 包(CMS / Markdown) | ❌ 用 @video-lab/embed-helper |
安装
pnpm add @video-lab/react-frame默认简体中文;传 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 内播放器语言。完整接法见包内 API 参考。
peer:react >=19、react-dom >=19。React 18 不支持当前 custom element 覆盖层所需的 property / CustomEvent 绑定;仍在 React 18 的项目请停在上一个 major。
用法
import { VideoPlayerFrame, type VideoPlayerFrameHandle } from '@video-lab/react-frame'
import { useRef } from 'react'
// 来自受控部署配置,不是媒体 URL,也不能照抄到生产环境。
const playerOrigin = 'https://player.example.internal'
function Player() {
const ref = useRef<VideoPlayerFrameHandle>(null)
return (
<VideoPlayerFrame
ref={ref}
source="https://media.example.com/lesson.m3u8"
origin={playerOrigin}
autoplay
muted
playsinline
onReady={({ duration }) => {
console.info('iframe 内播放器已获得 metadata,时长:', duration)
}}
onError={(error) => {
console.error(error.code, error.message)
}}
/>
)
}命令走句柄(经 iframe 通道异步下发):
try {
await ref.current?.play()
await ref.current?.seek(30)
await ref.current?.enterFullscreen()
} catch (error) {
console.error('iframe 命令执行失败', error)
}配置 iframe 地址
origin 应由内部部署配置提供;本包不声明公共播放器地址。以下值仅为占位符:
<VideoPlayerFrame
origin="https://player.example.com"
iframeVersion="v2.0.1"
source="https://media.example.com/master.m3u8"
/>边界
- 薄壳——本包不含播放逻辑,只负责创建 iframe、握手及转发命令与事件。
- props 与
@video-lab/react对齐,另有 iframe 专属的origin/iframeVersion;省略iframeVersion时加载部署根目录。 - 不内置品牌 UI;认证只支持签名 URL,不接受自定义请求头。
- 业务应用可直接使用本组件,也可在自己的业务组件中封装它。
接入完成清单
origin必须来自部署配置;它是 iframe 部署根地址,不是播放器媒体 URL。需要锁定 iframe artifact 时传iframeVersion。- 所有句柄命令跨 iframe 异步下发;宿主主动调用的每次命令都要有失败处理路径,可在业务封装层集中
await+catch。命令 rejection 与播放期error是两条独立事实。 - 宿主 UI 用
onPlayableChange决定 loading / 重试入口,onError只用于诊断;不要凭单个playing推断恢复成功。签名源若不提供resolveSource,宿主必须在action: 'replace-source'时展示业务入口并取得新源;否则用户会停在没有重试按钮的授权错误态。 - 需要会话 QoE、排序或重传去重时,监听带
delivery的onPlayerEvent并接入@video-lab/telemetry;不要将高频事件逐条发送到埋点服务。 - iframe 加载、CSP、握手和版本不兼容会走明确 fallback;生产部署要同时验收
origin、iframe artifact 和宿主包版本。
AI 接入 Skill
安装包包含宿主接入 Skill及五种接入方式的参考页。
复制整个 skills/host-integration/ 到宿主 AI 的 Skill 目录,并先读
React iframe 参考。核对 front matter 的
supportedPackages 与已安装版本;画中画、封面、Loading、QoE 和 403 可查
功能配方。具体 API 仍以本 README 与导出类型为准。
相关
MIT
完整事件与命令失败
onPlayerEvent 交付完整 PlayerEvent,其顶层 delivery 包含生产端排序、去重与迟到事件证据。
PlayerEvent 用 event 字段区分类型(如 e.event === 'ready'),数据在 payload;它没有 type 字段。
旧 iframe 的事件可缺少该字段。宿主主动调用的每次可失败命令都应处理 rejection,可在业务封装层
集中处理,不必在每个按钮里重复写 catch。命令 rejection 不保证产生 error,播放期 error
也不保证对应某个命令 rejection。void ref.current?.play() 不会处理 Promise rejection。
import { useRef, useState } from 'react'
import { VideoPlayerFrame, type VideoPlayerFrameHandle } from '@video-lab/react-frame'
function PlayerWithCommands() {
const player = useRef<VideoPlayerFrameHandle>(null)
const [commandsReady, setCommandsReady] = useState(false)
const [canRetry, setCanRetry] = useState(false)
const [commandError, setCommandError] = useState('')
async function runCommand(command: (handle: VideoPlayerFrameHandle) => Promise<unknown>) {
const handle = player.current
if (!handle || !commandsReady) {
setCommandError('播放器尚未就绪')
return
}
setCommandError('')
try {
await command(handle)
} catch {
// 宿主可在这里接入脱敏诊断;不要把命令失败重复计为播放期 error。
setCommandError('操作未完成,请稍后重试')
}
}
const play = () => runCommand((handle) => handle.play())
const retry = () => runCommand((handle) => handle.retry())
return (
<>
<VideoPlayerFrame
ref={player}
source="https://media.example.com/lesson.m3u8"
origin="https://player.example.internal"
onReady={() => setCommandsReady(true)}
onPlayableChange={({ reason, playable, recoverable, action }) => {
if (reason === 'frame_disconnected') setCommandsReady(false)
setCanRetry(!playable && !recoverable && action === 'retry')
}}
/>
<button type="button" disabled={!commandsReady} onClick={play}>播放</button>
{canRetry && <button type="button" disabled={!commandsReady} onClick={retry}>重试</button>}
{commandError && <p role="alert">{commandError}</p>}
</>
)
}当前实现中,组件 ref 已挂载但内部连接尚未建立时,play() 等多数方法会返回已成功的 Promise 并且
不执行命令;setPageFullscreen() 则会拒绝。不要把 Promise resolve 当成“已经连接”或“已经出画面”,
应等 onReady 再开放播放控制,并以 onPlayableChange 判断播放状态。SDK 因 props 变化发起的命令
由组件内部处理,宿主的统一处理器只负责宿主主动调用的命令。
iframe 基础路径
origin 是宿主提供的完整部署基础 URL,可包含 /custom/player 等目录。SDK 直接访问该根目录,不追加版本段或 /embed;旧部署的 /embed 可显式保留在 origin 中。
错误 UI 接管
通过 showErrorOverlay={false} 关闭默认错误文字、背景和动作按钮。省略时保持开启,错误事件与恢复能力不受影响;宿主也可用 ref.current?.performErrorAction() 触发当前可执行动作。支持新展示桥的 iframe,其宿主默认层可随 Provider 和实例配置原地更新;旧 iframe 仍由内部默认层处理。宿主包与 iframe 应用必须部署兼容的契约版本。
内置控件按需隐藏
初始化 prop controlVisibility(Vue 模板写 :control-visibility)支持 { cssFullscreen: false } 仅隐藏 CSS 全屏按钮。
八个可选键为 play、progress、time、volume、playbackRate、fullscreen、cssFullscreen、pip。
false 隐藏入口,true/省略保留平台默认;controls=false 整体关闭优先。隐藏不禁用既有命令,
运行时修改须由宿主显式重建,不增加 PiP RPC。
iframe 须与宿主代码同批部署并支持该功能;旧应用可能忽略新配置。
宿主网页全屏
通过 pageFullscreen 传入宿主布局适配器(setActive(active) / dispose()),句柄 setPageFullscreen(active) 等待布局确认。实际状态事件为 pagefullscreenchange;React 使用 onPageFullscreenChange,Vue 使用 @page-fullscreen-change。
网页全屏的宿主责任与接口见包内 API 参考。iframe 需要新版宿主与 iframe 协商支持;未启用时新入口不可用。浏览器原生全屏接口保持独立。
Poster 上叠宿主侧自定义 Loading
Frame 的 renderLoading 在宿主 DOM 中渲染,可以覆盖 iframe 创建、脚本加载和握手空档:
<VideoPlayerFrame
source={selectedRoom.source}
poster={{ url: selectedRoom.videoPoster, fit: 'cover', loading: 'eager' }}
renderLoading={({ phase, reason }) => (
<BusinessLoading transparent phase={phase} reason={reason} />
)}
/>phase='initial' 从宿主首次渲染保持到 firstframe,phase='runtime' 表示首帧后的可恢复等待。
传入 renderer 后会自动关闭 iframe 内默认 Loading,showDefaultLoadingOverlay 无需设为 false。
renderer 是否存在属于构造期策略;要切换实现时重新挂载组件。
统一恢复(契约 v2)
旧 reconnect({ resetCounter }) 和 reconnectstart/success/failed 已移除。命令入口是 retry(): Promise<void>;Promise 完成只表示接受或合并请求。恢复中的重复请求共用预算,强播放证据通过 recovery 的 recovered 回报。
页面级文案和终态 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 原地更新。
renderPoster={() => <MyPoster />} 与 renderPause={() => <MyPause onPlay={() => void ref.current?.play()} />} 在宿主 DOM 渲染,由 SDK 管首帧及明确暂停的显隐。内置控件或公开 pause() 对应的实际暂停会显示暂停画面,被动暂停不会显示。提供 renderer 后,同类内置图片不会传入 iframe。不传 poster / pauseImage 即不使用;null 仅用于清除 Provider 默认值。
旧恢复 API 的迁移细节在仓库 ADR-099;消费者当前接入以本节和包内 API 参考为准。
带业务流程的启动占位由宿主维护,且只能出现一次
普通 Poster + Loading 使用上面的 renderLoading 即可。带倒计时、跳过或开播门控的业务启动占位仍由宿主维护,写它时有一条必须注意:
firstframe 不是「一辈子只发一次」的事件,它是「每个会话发一次」。 换源(load() 换地址、跨内核被拒后重建)、手动 retry()、iframe 重建都会开启新会话,firstframe 随之再发一次,poster 也会重新显示。如果把占位的显隐直接绑在 firstframe 上而不记状态,换源时启动占位会再盖一次——对一个已经在看的用户来说,画面上突然又出现「加载中」。
正确写法是宿主自己维护一个只置位、不复位的标记:
// 只认第一次:此后换源、重连、卡顿都不再显示启动占位
const onFirstFrame = () => setStartupDone(true)把回调传给 onFirstFrame,占位本身按 startupDone 条件渲染即可
(<VideoPlayerFrame onFirstFrame={onFirstFrame} />,{!startupDone && <MyStartupPlaceholder />})。
要点:
- 绑
firstframe,不要绑ready或play:后两者只表示「可以播 / 开始播这个动作」,画面还没出来(实测 FLV 上ready到首帧可差 6 秒以上)。 - 不要用固定时长的倒计时来撤:起播耗时跨度很大(实测 HLS 0.4s、FLV 6.6s),计时器撤早了会露黑、撤晚了让用户白等。倒计时可以数,但撤不撤要看事件。
- 不要在换源时复位:换源期间的覆盖由 SDK 的恢复态遮罩接管,宿主再盖一层是重复。
- 只有组件真正卸载重挂(用户离开播放页又回来)才算新的一次首次进入,那时可以复位。
- 失败分支:收到不可恢复终态(
playablechange的playable:false, recoverable:false)时撤掉占位,让位给错误 UI,否则会一直盖着。
