videojs v10 源代码系列解读:14 · 谓词守卫:在运行时安全地调用能力

上一篇讲了能力契约的类型设计,但光有类型不够------TS 的类型在运行时消失。你还需要在运行时安全地探测 「这个媒体对象到底支不支持某能力」。这就是谓词(predicate)的工作。这篇讲 predicate.ts 的实现模式,特别是那几个排除空哨兵的「复合谓词」------不读源码很难想到这层。

谓词的基本模式

packages/media/src/core/predicate.ts(134 行)全是 type guard(value is X)。实现模式高度统一,我拆出来看:

ts 复制代码
// predicate.ts:25-29
export function isMediaPauseCapable(media: unknown): media is MediaPauseCapability {
  if (!isObject(media)) return false;
  const { paused, ended, pause } = media as Record<string, unknown>;
  return !isUndefined(paused) && !isUndefined(ended) && isFunction(pause);
}

三步走:

  1. isObject(media) 守卫(排除 null/undefined)。
  2. cast 成 Record<string, unknown>(拿属性)。
  3. 逐个检查:字段用 !isUndefined(x),方法用 isFunction(x)。

isObject/isFunction/isUndefined 都来自 @videojs/utils/predicate(01 篇讲过的工具包)。

这个模式贯穿所有谓词,但具体查什么有讲究。比如 isMediaSeekCapable(predicate.ts:31-35)查 currentTime/duration/seeking,但故意不查 loop ------因为 loop 在 capability 里,但谓词宽松,loop 不影响 seek 能力的判定。

复合谓词:排除空哨兵

这是最值得讲的部分。有三个谓词不只查字段存在,还额外排除空哨兵对象:

isMediaBufferCapable(predicate.ts:60-69)

ts 复制代码
export function isMediaBufferCapable(media: unknown): media is MediaBufferCapability {
  if (!isObject(media)) return false;
  const { buffered, seekable } = media as Record<string, unknown>;
  return !isUndefined(buffered)
    && media.buffered !== EMPTY_TIME_RANGES    // L66 --- 排除哨兵!
    && !isUndefined(seekable)
    && media.seekable !== EMPTY_TIME_RANGES;   // L67
}

为什么排除 EMPTY_TIME_RANGES?这要去 constants.ts:4-8 看:

ts 复制代码
export const EMPTY_TIME_RANGES = Object.freeze({
  length: 0, start: () => 0, end: () => 0,
}) as Readonly<TimeRangeLike>;

MediaBufferCapability.buffered 的类型是非空 TimeRangeLike(不是 ... | null)。所以没 buffer 数据的宿主不能给 null ,必须给一个合法但空的占位------这就是 EMPTY_TIME_RANGES。

但这带来一个问题:宿主类型上满足契约,运行时却给了个空占位。谓词必须区分「占位」和「真能用」。所以 isMediaBufferCapable 额外检查 !== EMPTY_TIME_RANGES------只有真有 buffer 数据才算「有这个能力」。

isMediaTextTrackCapable(predicate.ts:77-81)和 isMediaRemotePlaybackCapable(L101-105)

同理:

ts 复制代码
// isMediaTextTrackCapable 排除 EMPTY_TEXT_TRACKS
!isUndefined(textTracks) && media.textTracks !== EMPTY_TEXT_TRACKS

// isMediaRemotePlaybackCapable 排除 EMPTY_REMOTE
isObject(media.remote) && media.remote !== EMPTY_REMOTE

EMPTY_TEXT_TRACKS(constants.ts:11-15)是 Object.assign(new EventTarget(), { length: 0, *[Symbol.iterator](){}, getTrackById: () => null })------基于 EventTarget,因为 TextTrackListLike extends EventTargetLike 需要能监听事件。EMPTY_REMOTE(L17)最粗暴:new EventTarget() as unknown as RemotePlaybackLike。

这套哨兵的存在理由:让宿主对象类型上满足契约、但运行时表达「我没有这个东西」,同时让谓词能识别它不是真能用。这是个很务实的设计------TS 的非空类型逼着你给个占位,但占位不能骗过谓词。

谓词只覆盖「需要运行时探测」的能力

我读的时候注意到:不是每个 Capability 都有对应谓词。比如:

  • 没有 isMediaPlaybackCapable------因为 Media 本身就是它(任何媒体都能 play)。
  • 没有 isMediaFullscreenCapable/isMediaPictureInPictureCapable------这些在 Video 接口里是必选的(你 implements Video 就承诺了)。
  • 没有 isMediaPosterCapable/isMediaPlaysInlineCapable/isMediaControlsCapable/isMediaAutoplayCapable------简单的布尔标志位,不需要专门探测。

谓词只覆盖那些「宿主可能没有、需要运行时探测」的能力。这是个有意识的选择------不为一眼能看穿的能力写谓词,保持 API 表面精简。

hasMetadata:唯一的非 guard 函数

有个例外:hasMetadata(predicate.ts:21-23)不是 type guard:

ts 复制代码
export function hasMetadata(media: MediaSourceCapability): boolean {
  return media.readyState >= 1;
}

它不是 value is X,参数已是 MediaSourceCapability。返回 media.readyState >= 1(对应 HAVE_METADATA)。这查的不是「有没有能力」,是「能力处于什么状态」------所以不是 guard。

完整谓词表

方便查阅,列全:

谓词 行号 检查什么
isMediaPauseCapable 25-29 paused + ended + pause()
isMediaSeekCapable 31-35 currentTime + duration + seeking(不查 loop)
isMediaSourceCapable 37-46 src + currentSrc + readyState + load()
isMediaVolumeCapable 48-52 volume + muted(不查 defaultMuted)
isMediaPlaybackRateCapable 54-58 只查 playbackRate
isMediaBufferCapable 60-69 buffered + seekable,排除 EMPTY_TIME_RANGES
isMediaErrorCapable 71-75 只查 error
isMediaTextTrackCapable 77-81 textTracks,排除 EMPTY_TEXT_TRACKS
isMediaVideoRenditionCapable 83-87 只查 videoRenditions
isMediaAudioTrackCapable 89-93 只查 audioTracks
isMediaVideoDimensionsCapable 95-99 videoWidth + videoHeight
isMediaRemotePlaybackCapable 101-105 remote,排除 EMPTY_REMOTE
isMediaStreamTypeCapable 107-111 只查 streamType
isMediaLiveCapable 113-117 liveEdgeStart + targetLiveWindow

注意复合谓词(加粗的三个)的特殊性------它们排除了哨兵。写文章时要特别强调这层,因为不读源码根本想不到。

谓词怎么被使用

04 篇追生命周期时讲过,feature 的 attach() 用谓词做前置检查。再看 playbackFeature:

ts 复制代码
// core/src/dom/store/features/playback.ts:32-35
attach({ target, signal, set }) {
  const { media } = target;
  if (!isMediaPauseCapable(media) || !isMediaSeekCapable(media) || !isMediaSourceCapable(media)) {
    return;  // 不支持就跳过
  }
  // ... 绑定事件
}

能力不支持时静默跳过,不报错。这是谓词的核心价值:让 Feature 具备渐进降级能力。Vimeo 这种部分能力实现,不支持的 Feature 自动失效,播放器其他部分照常工作。

这个模式在 v10 的 feature 里到处都是。卷四讲播放器核心时会看到每个 feature 都这么干。

State 层的 *Availability:三态可用性

谓词是布尔(有/没有),但 UI 需要更细的三态。13 篇提过 MediaFeatureAvailability = 'available' | 'unavailable' | 'unsupported'。这个三态在 state.ts 里被 volume/fullscreen/pip/remotePlayback 复用。

它和谓词的关系:谓词判「unsupported」(能力不存在),三态额外判「unavailable」(能力存在但这一刻用不了,如 iOS 上调音量)。UI 用它决定按钮显示/置灰/隐藏。这是谓词之上的 UX 细化层。

小结

谓词看似简单(就是类型守卫),但有几个值得带走的洞察:

  1. 统一模式 ------isObject 守卫 + !isUndefined/isFunction 逐个查。
  2. 复合谓词排除哨兵 ------EMPTY_TIME_RANGES/EMPTY_TEXT_TRACKS/EMPTY_REMOTE,这是 TS 非空类型逼出的务实设计。
  3. 只覆盖需要探测的能力------简单布尔标志位不写谓词。
  4. Feature 用它做渐进降级------不支持就 return,不报错。
  5. 三态可用性是谓词的 UX 细化------布尔判 unsupported,三态额外判 unavailable。

下一篇我们看 HTMLMediaElementHost------v10 怎么用「主机-组件」模型把能力契约落到一个可委托、可覆盖的对象上。

相关推荐
EasyGBS2 小时前
摄像头还在线,画面已经不能用了?国标GB28181视频平台EasyGBS视频质量诊断怎么用
服务器·安全·音视频
2601_955662462 小时前
情感解说视频如何提升感染力?配音细节很关键
人工智能·音视频·语音识别·视频
paopaokaka_luck2 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_926787053 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
IMPYLH3 小时前
HTML 的 <video> 元素
前端·html
溪语流沙3 小时前
【每天一个CSS | Day19】流光文字与一笔写成的手写签名
前端·javascript·css
逐光者9333 小时前
STM32——SPI 屏 · Flash · 高速
java·前端·stm32
摸鱼 特供版4 小时前
一款边录边标注的轻量截图录屏工具
windows·学习·电脑·音视频·软件需求
To_OC4 小时前
同样是让 AI 写全栈,为什么别人一句话就够了
前端·前端框架·next.js
CDwenhuohuo4 小时前
electron pc项目打包成桌面端
前端·javascript·electron