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

@sentinel-lab/video-player-ui

v4.0.0

Published

Sentinel Video Player 覆盖层:Poster / Loading / Error / Pause 四个框架无关的 custom element + 覆盖层状态机

Readme

@sentinel-lab/video-player-ui

Sentinel Video Player 的覆盖层(L2)· 框架无关。四个 custom element,零框架依赖 —— inline(video-react)和 iframe 内部(apps/embed-app)用的是同一份。

依赖:只有 @sentinel-lab/video-protocol没有 peer 依赖,不依赖 React,也不依赖 Vue。

实现范围

SDK 侧 4 个覆盖层全部实现:<sentinel-poster>(封面)、<sentinel-loading>(缓冲)、 <sentinel-error>(错误 + 重试)、<sentinel-pause>(暂停时的中性广告 / 推荐插槽,可选关闭按钮)。

结束覆盖层 + 倒计时(End / Countdown)按 ADR-025 归团队层(examples/team-video-vue 用 ended 事件 自己渲染、自跑 setInterval 倒计时),刻意不在本包 —— 业务调度的覆盖层归团队层,SDK 只做 事件驱动、无需业务内容的自动覆盖层(Poster/Loading/Error/Pause)。

用法

import {
  defineSentinelOverlays,
  INITIAL_OVERLAY,
  type OverlayState,
  reduceOverlay,
} from '@sentinel-lab/video-player-ui'
import type { PlayerEvent } from '@sentinel-lab/video-protocol'

// ① 注册。**必须显式调**,不会自动注册(理由见下)
defineSentinelOverlays()

const container = document.querySelector('.player-container') as HTMLElement
const poster = document.createElement('sentinel-poster')
const error = document.createElement('sentinel-error')
container.append(poster, error)

// ② 播放器事件 → 覆盖层状态 → 元素属性。
//    reduceOverlay 是纯函数,inline / iframe 两面共用同一份,免得"什么时候盖 loading"漂移
let state: OverlayState = INITIAL_OVERLAY

export function onPlayerEvent(event: PlayerEvent): void {
  const next = reduceOverlay(state, event)
  if (next === state) return // 引用没变 = 这个事件不影响覆盖层,别白刷 DOM
  state = next

  poster.toggleAttribute('visible', state.posterVisible)
  error.toggleAttribute('visible', state.error !== null)
  if (state.error) {
    error.setAttribute('code', state.error.code)
    error.setAttribute('message', state.error.message)
    error.toggleAttribute('retryable', state.error.retryable)
    error.setAttribute('retry-label', '重试') // 不传就退回英文 'Retry'
  }
}

// ③ 重试是 CustomEvent(冒泡),不是回调 prop
error.addEventListener('sentinel-retry', () => {
  /* 重建播放器 */
})

API

元素(标签名 → 导出的构造器,后者只在做类型标注 / 手动注册时才需要)

| 标签 | 构造器 | 默认 z-index | 专有 attribute | part | |---|---|---|---|---| | <sentinel-poster> | SentinelPosterElement | 10 | src fit loading alt | image | | <sentinel-loading> | SentinelLoadingElement | 12 | text show-text | spinner text | | <sentinel-pause> | SentinelPauseElement | 13 | closable close-label image image-fit | close-button image | | <sentinel-error> | SentinelErrorElement | 15 | code message retryable retry-label | box message retry-button |

四个共有:attribute visible / z-index,property visible(boolean), 元素上会被打上 data-sentinel-overlay="<名字>"(测试选择器约定)。

<sentinel-poster><sentinel-pause> 另有 property 通道 poster / pauseImage 收契约对象(PosterConfig / PauseImageConfig)—— 实现是写进 attribute 而不是另存一份, 所以 DOM 里看到的始终是实际生效的值,没有"property 和 attribute 都给了听谁的"这种规则。

事件(全部是 CustomEvent)

| 事件 | 冒泡 | 什么时候 | |---|---|---| | sentinel-show / sentinel-hide | 否 | visible 真正翻转时。挂载时就 visible 也会派发 show | | sentinel-retry | 是(composed) | 点 <sentinel-error> 的重试按钮 | | sentinel-close | 是(composed) | 点 <sentinel-pause> 的关闭按钮 |

状态机:reduceOverlay(prev, event) / INITIAL_OVERLAY / dismissPauseImage(prev) / OverlayState

主题:OVERLAY_CSS_VARS / OVERLAY_CSS_FALLBACKS / OverlayCssVar

其他:defineSentinelOverlays() / SENTINEL_OVERLAY_ELEMENTS / OVERLAY_SHARED_CSS / SentinelOverlayElement(抽象基类,不注册)

两条红线

不做团队品牌 UI(ADR-021)。所有颜色走 --sentinel-overlay-* CSS 变量,元素里没有一个写死的色值——有测试守着这条。CSS 自定义属性能穿透 shadow 边界,所以主题照常从外面注入;要改内部结构的样式走 ::part():

.player-container {
  --sentinel-overlay-primary: #ff4d94;
  --sentinel-overlay-radius: 12px;
}
sentinel-error::part(retry-button) {
  font-weight: 600;
}

不内置任何翻译(ARCHITECTURE § 8.7.1)。元素只收已经解析好的字符串—— message / retry-label / text 传什么显示什么,本包不做 key → 文案的查表。 文案解析归组合层(video-react / embed-app),每个市场的垂直行业术语各家不同, SDK 内置一份"标准翻译"反而不准。

⚠️ 只有 retry-label 有硬编码兜底('Retry')。不传就是英文,不是漏翻译的报错。

设计上值得知道的几件事

注册刻意做成显式函数,不在模块求值期自动跑。自动注册要在模块顶层碰 customElements, 而那是浏览器全局 —— SSR / next build 在 Node 里求值这个模块会当场炸。本仓库为这类事付过一次代价 (flv.js 是 UMD、模块求值期引用 self,导致 inline 模式 next build 直接失败,而单测 / e2e / 体积检查三层全碰不到)。 defineSentinelOverlays() 幂等,重复调不抛;没有 customElements 的环境直接返回 false 空转。

visible: false 时整棵子树不渲染,而不是 display: none。覆盖层里有 spinner 动画,留在树里会白白跑着。 shadow root 清空后 <slot> 也没了,所以 light DOM 里的插槽内容同样不渲染。

reduceOverlay 事件不在表里就返回同一个引用,不是新对象。seeking / timeupdate 这种高频事件 每次都造新对象会让覆盖层跟着 60fps 重刷 —— 上面示例里那句 if (next === state) return 就是靠这条。

只有 retryable 时才画重试按钮。对着一个必然失败的错误(比如 E_MEDIA_NOT_SUPPORTED)给用户 重试按钮是在骗人,点几次都不会好。retryable 的判定在 protocol 的 ERROR_META 里,是唯一真相。

spinner 尊重 prefers-reduced-motion:前庭功能障碍用户会被旋转动画诱发眩晕。

测试

pnpm --filter @sentinel-lab/video-player-ui test

Vitest + jsdom,直接测 custom element 的注册 / attribute → DOM / 事件派发。