这篇讲
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 之间选择(HlsJsOnlyMedia 和 NativeHlsMedia,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/detach 调 engine.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/player 的 VimeoPlayer 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)两类场景。
小结
引擎适配器两种模式,各有适用场景:
- Host-subclass(hls-js/native-hls/dash/vimeo)------全程接管,继承 host 重写生命周期。
- Component/override (google-cast)------条件接管,靠
targetOverride逐个属性切换。
带走几个点:
- mixin 套娃组合功能点(native-hls 三个 mixin 叠加)。
- 配置键模块增强让配置类型安全。
- Cast 的动态 targetOverride 是部分能力实现的典范。
- Vimeo 完全独立(不继承 host),证明契约设计能容纳极端异质源。
- RemotePlayback 规范实现,不加非规范成员。
下一篇讲媒体轨道和 rendition 列表的实现------v10 怎么在没有原生支持的浏览器里 polyfill 音视频轨道和清晰度列表。