17 · 引擎适配器全景:HLS/DASH/Vimeo/Mux/Cast

这篇讲 media/src/dom/ 下的引擎适配器全景。v10 怎么把 hls.js、dash.js、Vimeo、Google Cast 这些五花八门的播放源,统一塞进前面讲的能力契约和主机模型?答案是两种集成模式。看懂这两种模式,你就理解了 v10 的「异质媒体源」支持是怎么落地的。

两种集成模式

我读完所有适配器后发现,它们分成两种截然不同的集成方式:

模式 谁用 怎么做
Host-subclass(主机子类) hls-js, native-hls, dash, vimeo 继承 HTMLVideoElementHost,重写 src/attach/detach 驱动 JS 引擎
Component/override(组件覆盖) google-cast 是个 MediaComponent,靠 targetOverride 逐个属性接管

下面分别看。

Host-subclass 模式

hls.js 适配器

hls-js/media.ts:56

ts 复制代码
export class HlsJsMedia extends HTMLVideoElementHost implements HlsMediaProps {
  #delegate: HlsJsOnlyMedia | NativeHlsMedia | null;
  // ...
}

它继承 HTMLVideoElementHost,持有一个 #delegate------根据特性支持在 hls.js MSE 播放原生 Safari HLS 之间选择(HlsJsOnlyMediaNativeHlsMedia,L6-8)。这是最丰富的适配器,有 live/stream-type/media-tracks/metadata-tracks/text-tracks/preload/airplay-bridge/errors 一堆文件。

配置命名空间是 HlsMediaConfig extends MediaConfig(L37-41),带 preferPlayback/contentType/hlsJs 子键。

native-hls(Safari 原生)

native-hls/media.ts:24

ts 复制代码
export class NativeHlsMediaBase extends HTMLVideoElementHost
  implements Omit<NativeHlsMediaProps, 'streamType'> {
  // ...
}

它重写 src(L36-43)、preload(L46-53)、attach(L55-60)直接设 target.src------Safari 原生支持 HLS,不需要 JS 引擎。engine 返回 null(L32-34,注释说「Native HLS 没有 JS 引擎」)。

最终的 NativeHlsMedia(L63-65)通过三个 mixin 组合直播、流类型、错误处理:

ts 复制代码
NativeHlsMediaLiveMixin(NativeHlsMediaStreamTypeMixin(NativeHlsMediaErrorsMixin(base)))

这种 mixin 套娃是 v10 组合功能点的常见手法------每个 mixin 加一种能力。

dash.js 适配器

dash/media.ts:14-19

ts 复制代码
export const DashMediaBase = MediaTracksMixin(HTMLVideoElementHost);
export class DashMedia extends DashMediaBase
  implements MediaEngineHost<dashjs.MediaPlayerClass, HTMLVideoElement>, DashMediaProps {
  // ...
}

它先用 MediaTracksMixin 给 host 加轨道能力,然后构建 dashjs.MediaPlayer().create() 引擎(L25),重写 attach/detachengine.attachView(target)(L29-38),destroy(L40-44)先销毁引擎再销毁 host。

这是 MediaTracksMixin 如何通过 host 机制添加 targetOverride 轨道/rendition 的范例------dash.js 的轨道 API 和原生不同,靠 mixin 适配进契约。

Vimeo 适配器(最特殊)

vimeo/media.ts:49

ts 复制代码
export const VimeoMediaBase = MediaPlayedRangesMixin(EventTarget);

Vimeo 不继承 HTMLVideoElementHost ------它直接从 EventTarget 构建,包装 @vimeo/playerVimeoPlayer iframe API。因为 Vimeo 是通过 postMessage 驱动 iframe 的,没有原生媒体元素。

它定义自己的 VimeoMediaProps(L23-34,大得多的平面状态),完全独立的适配器。这是「异质源」的极端情况------v10 的契约设计让它也能接入,虽然不能继承 host。

Component/override 模式:Google Cast

这是最值得讲的,因为它是「部分能力实现」的典型范例。

google-cast/media.ts:27

ts 复制代码
export class GoogleCast implements GoogleCastProps, MediaComponent {
  static readonly configKey = 'googleCast';   // L28
  // ...
}

它是 MediaComponent,不是 host 子类 。回忆 15 篇------组件靠 targetOverride 逐个属性接管。Cast 把这套机制用到了极致。

配置键的模块增强

media.ts:21-25

ts 复制代码
declare module '../media-host' {
  interface MediaComponentConfig {
    googleCast: GoogleCastProps;
  }
}

通过模块增强把 googleCast 配置键加进类型化的 MediaComponentConfig。这样 <mux-video config='{"googleCast": {...}}'> 的配置是类型安全的------TS 知道 googleCast 字段的存在和形状。

部分能力接管:targetOverride

Cast 的核心是 targetOverride 的动态切换。setMedia(host)(L43-52)初始化时,#override 只暴露 remote

ts 复制代码
// media.ts:78-85 简化
#createRemoteOverride() {
  return { get remote() { return this.#remotePlayback; } };
}

不投屏时 ,host 的 getMediaOwner('remote') 命中 Cast 的 override,其他属性(paused/currentTime/play...)都 fallback 到本地 <video>。所以 Cast 按钮可见,但播放还是本地的。

投屏时#onStateChange L68-76),#override整个 provider 替换:

ts 复制代码
// 投屏时
this.#override = this.#provider;   // 暴露完整播放接口

现在 host 的 paused/currentTime/play()/pause() 全部解析到远程接收端。本地 <video> 不再直接播放。

这就是「逐个属性地选择进入/退出契约」 。Cast 组件不需要继承 host,它只是在投屏时把自己的整个接口暴露成 targetOverride,host 的 getMediaOwner 自动路由过去。

RemotePlayback 的规范实现

google-cast/remote-playback.ts:30

ts 复制代码
export class RemotePlayback extends EventTarget {
  // state, watchAvailability, cancelWatchAvailability, prompt
  // 事件 connecting/connect/disconnect
}

这是 W3C RemotePlayback API 的规范一致性实现 。文件头(L15-29)明确警告不要加非规范成员------因为代码用类型断言假设它符合 W3C 规范。状态由 GoogleCastProvider 通过 provider.bindHooks({ setState, setAvailable }) 推送(L39-42)。

为什么两种模式

我读的时候琢磨了为什么 Cast 用组件模式而其他用子类模式。区别在于接管粒度

  • hls.js/dash.js/Vimeo全程接管 ------媒体源从头到尾由它们处理。继承 host、重写 src/attach 是自然的。
  • Google Cast条件接管------只有投屏时才接管,不投屏时本地元素照常工作。组件模式让它在「只暴露 remote」和「暴露全部」之间动态切换,子类模式做不到这种粒度。

这套设计让 v10 能同时支持「始终异质」(Vimeo)和「动态切换」(Cast)两类场景。

小结

引擎适配器两种模式,各有适用场景:

  1. Host-subclass(hls-js/native-hls/dash/vimeo)------全程接管,继承 host 重写生命周期。
  2. Component/override (google-cast)------条件接管,靠 targetOverride 逐个属性切换。

带走几个点:

  • mixin 套娃组合功能点(native-hls 三个 mixin 叠加)。
  • 配置键模块增强让配置类型安全。
  • Cast 的动态 targetOverride 是部分能力实现的典范。
  • Vimeo 完全独立(不继承 host),证明契约设计能容纳极端异质源。
  • RemotePlayback 规范实现,不加非规范成员。

下一篇讲媒体轨道和 rendition 列表的实现------v10 怎么在没有原生支持的浏览器里 polyfill 音视频轨道和清晰度列表。

相关推荐
计算机魔术师1 小时前
烧掉2780亿美元还不够?OpenAI的资本豪赌让人头皮发麻
前端
IT_陈寒2 小时前
Java线程池用错参数,我的服务居然悄悄崩溃了
前端·人工智能·后端
卡布鲁2 小时前
React useMemo 与 useCallback 完全指南:原理、场景与避坑
前端·react.js
音视频牛哥3 小时前
视频编码的终局不是“更省码率”:从 H.266、AV2 到 AI 编解码的下一场竞争
音视频·h.266·rtsp h.266·h.266编解码·rtp h.266·rtp vvc·ai编解码
碳基修炼3 小时前
排查记:本地 devServer 是 http,浏览器却把 302 重定向升级成了 https
前端·http
步行cgn3 小时前
Spring p 命名空间注入详解
java·前端·spring
何何____3 小时前
Vue 生命周期详解
前端·javascript
江米小枣tonylua3 小时前
TypeScript 全面の拥抱 !Prisma 8 + Electron 升级实战
前端
闪耀之光M783 小时前
npm命令解析
前端