npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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,不接受自定义请求头。
  • 业务应用可直接使用本组件,也可在自己的业务组件中封装它。

接入完成清单

  1. origin 必须来自部署配置;它是 iframe 部署根地址,不是播放器媒体 URL。需要锁定 iframe artifact 时传 iframeVersion。
  2. 所有句柄命令跨 iframe 异步下发;宿主主动调用的每次命令都要有失败处理路径,可在业务封装层集中 await + catch。命令 rejection 与播放期 error 是两条独立事实。
  3. 宿主 UI 用 onPlayableChange 决定 loading / 重试入口,onError 只用于诊断;不要凭单个 playing 推断恢复成功。签名源若不提供 resolveSource,宿主必须在 action: 'replace-source' 时展示业务入口并取得新源;否则用户会停在没有重试按钮的授权错误态。
  4. 需要会话 QoE、排序或重传去重时,监听带 delivery 的 onPlayerEvent 并接入 @video-lab/telemetry;不要将高频事件逐条发送到埋点服务。
  5. 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,否则会一直盖着。