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 篇讲过的工具包)。

这个模式贯穿所有谓词,但具体查什么有讲究。比如 isMediaSeekCapablepredicate.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_TRACKSconstants.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 函数

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

相关推荐
kyriewen1 小时前
我扒了 10,221 条 JD:腾讯技术岗 75% 在要 AI
前端·人工智能·ai编程
郑州光合科技余经理2 小时前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程
赛博仓鼠2 小时前
秋叶ComfyUI 3.2整合包实测:Python 3.13+Torch 2.13全栈升级,一键跑通MiniMax H3(附完整部署)
ai·ai作画·aigc·音视频
IT_陈寒3 小时前
Vue的响应式让我熬到凌晨三点,原来漏了这个小细节
前端·人工智能·后端
Blanche15003 小时前
利用 RAG 为答疑机器人扩展知识范围
前端
天若有情6733 小时前
【纯前端小工具】公历生日转农历,批量查询每年农历生日对应的公历日期(GitHub Pages在线直接用)
前端·javascript·github pages·农历转换·lunisolar·网页小工具
SoonITer3 小时前
怎样构建一个 Agent-friendly 的网站
前端·agent
颜进强3 小时前
01 · NestJS 是什么:用途、解决什么问题、与热门框架对比
前端·后端·ai编程
wendZzoo3 小时前
前端工程师的 3D 第一课:一个模型如何进入网页
前端