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 怎么用「主机-组件」模型把能力契约落到一个可委托、可覆盖的对象上。

相关推荐
FRing035 分钟前
JavaScript入门到精通三十三(异常处理、call方法、apply方法、bind方法、防抖、节流)
开发语言·前端·javascript
计算机魔术师1 小时前
OpenAI 砍掉 100 亿融资目标,AI 资本市场的温度计变了
前端
jktantan1 小时前
(二)Vue 3 的 attrs 不是响应式:el-form 扩展属性同步的三种方案
前端·javascript·vue.js·element-plus
辰辰观影2 小时前
无广告播放器推荐来啦
音视频
TrueDei2 小时前
GStreamer 使用 valve 分别控制音视频流的暂停与恢复
音视频
前端 贾公子2 小时前
第 5 章:Codex 从可运行程序到可交付工具
java·前端·数据库
用户360055579002 小时前
Kestrel_VLLM - 26 · 发射空档三步曲:事件计时 → nsys → 判别实验
前端
echome8882 小时前
JavaScript async/await 错误处理:别让 Promise 静默失败
开发语言·前端·javascript·promise
PedroQue992 小时前
v0.2.0 架构升级:class + reactive 重塑状态管理
前端·uni-app
鶴哥只手遮天2 小时前
Angular 22 源码级深度解析-第 02 章 编译链路总览:从模板字符串到 `ɵɵ` 指令
前端·开源·angular.js