卷三收尾篇。这篇讲
media/src/core/media-tracks/目录——v10 怎么在浏览器原生不支持的地方,polyfill 出音频轨道、视频轨道、音频 rendition、视频 rendition(清晰度)列表。这套实现有几个让我印象深刻的细节:用 WeakRef 反向引用宿主、用 queueMicrotask 做事件防抖、用 generator 实现[Symbol.iterator]。
为什么需要 polyfill
浏览器对媒体轨道的支持参差不齐:
- Safari 有原生
audioTracks/videoTracks(带 rendition)。 - Chrome/Firefox 长期没有(或部分)。
- hls.js/dash.js 自己管理轨道,不走原生 API。
v10 需要一套统一的轨道抽象(13 篇讲的 AudioTrackLike/VideoTrackLike/*RenditionListLike),在原生不支持时用 polyfill 填充。media-tracks/ 就是这套 polyfill 的具体实现类。
Track 类:AudioTrack 和 VideoTrack
AudioTrack(audio-track.ts)
// audio-track.ts:14-45 简化
export class AudioTrack {
id; kind; label = ''; language = ''; sourceBuffer;
#enabled = false;
addRendition(src, codec?, bitrate?) {
const rendition = new AudioRendition();
// 赋值 src/codec/bitrate
addRendition(this, rendition); // 挂到 list
return rendition;
}
get enabled() { return this.#enabled; }
set enabled(value) {
if (this.#enabled !== value) {
this.#enabled = value;
enabledChanged(this); // 触发 list 事件
}
}
}
关键字段:id(可写)、kind(只读,alternative/descriptions/main/'main-desc'/translation/commentary,L5-12)、enabled(可写,选轨入口)。
enabled 的 setter 在值真改变时才调 enabledChanged(this)——触发 list 的 change 事件。这个「值变了才通知」的模式在 v10 里到处都是(element 的 Object.is 判变、store 的 patch 判变)。
VideoTrack(video-track.ts)
结构类似 AudioTrack,但有两个关键差异:
- 用
#selected而非#enabled(video-track.ts:20)。 - setter(L42-49)只在变 true 时才触发
selectedChanged(L46 的if (value !== true) return)——隐式实现了 video track 的「同时只能选一个」互斥语义。
addRendition 签名更宽(L22):(src, width?, height?, codec?, bitrate?, frameRate?)——视频 rendition 有尺寸和帧率。
kind 集合不同
我注意到 AudioTrackKind 和 VideoTrackKind 的值集不一样:
- Audio:
alternative, descriptions, main, 'main-desc', translation, commentary - Video:
alternative, captions, main, sign, subtitles, commentary
这忠实反映了 HTML 规范——音视频轨道的 kind 语义本就不同。
Rendition 类:清晰度/编码变体
AudioRendition(audio-rendition.ts)
export class AudioRendition {
src; id; bitrate; codec; // 都 | undefined
#selected;
// selected setter 值变时调 selectedChanged(this)
}
字段简单——音频 rendition 主要是 bitrate 和 codec。
VideoRendition(video-rendition.ts)
比 audio 多 width/height/frameRate(L10-13),以及双私有状态#selected 和 #active(L15-16)。
这俩的区别很重要:
selected——用户/上层指定「想播这个」。active——引擎实际正在播这个(由 ABR 算法决定)。
对应 types.ts 的 VideoRenditionLike.active?。ADR 算法可能因为带宽选了比 selected 更低的 rendition,active 反映实际选择。UI 的「当前清晰度」应该读 active 而非 selected。
List 类:基于 EventTarget 的类数组容器
AudioRenditionList(audio-rendition-list.ts,143 行)
这是最有料的。它 extends EventTarget,但内部逻辑大量是模块级自由函数(不在 class 里)。
addRendition(track, rendition)(L7-30):
// 简化
const list = getPrivate(track).media?.deref()?.audioRenditions; // L7 — WeakRef 反查 list
// 设 rendition 的 media/track 私有指针
renditionSet.add(rendition);
// 必要时在原型上 Object.defineProperty 添加数字索引 getter(L17-23)
queueMicrotask(() => {
if (track.enabled) list.dispatchEvent(new TrackEvent('addrendition', rendition));
});
几个精妙点:
getPrivate(track).media?.deref()——通过 WeakRef 从 track 反查到 media 元素的 list。track 不强引用 media,避免循环引用。Object.defineProperty添加数字索引(L17-23)——动态给 list 加[0]/[1]… 的 getter,让它像数组。因为 rendition 是动态加的,不能预先定义。queueMicrotask派发事件(仅当 track.enabled)——批量操作时合并事件。
selectedChanged(rendition)(L45-60)——事件防抖:
// 简化
if (!getPrivate(list).changeRequested) {
getPrivate(list).changeRequested = true;
queueMicrotask(() => {
getPrivate(list).changeRequested = false;
list.dispatchEvent(new Event('change'));
});
}
一 tick 内多次 selected 变化只派发一次 change。这和 store 的微任务批处理(11 篇)是同一思想——高频变化合并成一次通知。
getCurrentRenditions(list)(L62-68):从 media 取 audioTracks,filter enabled,flatMap 出每个 enabled track 的 renditionSet——即 rendition list 是「所有启用轨道的 rendition 并集」。
class 本身(L70-142)提供:数字索引签名、[Symbol.iterator](返回 getCurrentRenditions(this).values())、get length、getRenditionById、get/set selectedIndex(set 时遍历设 rendition.selected = i === index)、onaddrendition/onremoverendition/onchange 的 get/set。
on* 回调的 setter(L98-141)每次先 removeEventListener 旧 callback 再 addEventListener 新的——防止重复注册。
VideoRenditionList(video-rendition-list.ts,159 行)
结构几乎和 Audio 版相同,三个差异:
- 用
track.selected判定(非enabled)。 - 多一个
activeChanged(rendition)函数(L62-76),防抖派发activechange事件——对应 ABR 算法的实际选择变化。 getCurrentRenditionsfiltertrack.selected。
activechange 是视频 rendition list 独有的事件——音频 rendition 没有「active」概念(音频 ABR 不暴露给 UI)。
WeakRef + 私有存储:避免循环引用
这套实现有个贯穿性的设计:用 WeakRef 让 track/rendition 反向引用 media,不形成强引用环。
utils.ts 的 getPrivate 用 WeakMap 给对象挂 media/track/renditionSet 等私有字段。media 字段是 WeakRef<HTMLMediaElement>——track 不阻止 media 被 GC。media 被回收时,deref() 返回 undefined,操作安全降级。
这解决了「track 属于 media,但 track 要触发 media 上的 list 事件」的循环引用难题。经典前端工程问题,v10 用 WeakRef 干净解决。
queueMicrotask 事件防抖
另一个贯穿性设计:用 queueMicrotask 合并高频事件。
addRendition批量添加时,事件延迟到微任务统一派发。selectedChanged一 tick 内多次变化只派发一次change。activeChanged同理。
这和 store 的跨容器批处理(11 篇)、element 的更新去重(09 篇)是同一套思想——高频变化合并通知。v10 在三个不同的包里用了三种不同的批处理机制,但核心理念一致:减少不必要的通知/渲染。
global.ts:polyfill 注册
index.ts 第一行 import './global'——有副作用。global.ts(803 字节)往全局注册 polyfill,让没有原生 audioTracks/videoTracks 的浏览器也能用。mixin.ts(8677 字节,目录里最大的)大概率是 mixinMediaTracks(Media),把轨道能力注入到 media 对象。
小结:卷三全貌
至此卷三·媒体契约层讲完。回顾 6 篇:
- 13 能力契约——正交分解,每个 Capability 只管一件事。
- 14 谓词守卫——运行时安全探测,复合谓词排除空哨兵。
- 15 主机-组件模型——host 不持有状态,
getMediaOwner路由组件覆盖。 - 16 custom-media-element——两层属性转发,包装成 Web Component。
- 17 引擎适配器——Host-subclass(全程接管)vs Component/override(条件接管)。
- 18 媒体轨道 polyfill——WeakRef 反引用 + queueMicrotask 防抖。
这套设计的核心成就:用一套类型系统,描述了从最全的原生 <video> 到最异质的 Vimeo iframe、再到动态切换的 Google Cast。Feature(卷四)靠谓词安全降级,引擎靠两种模式接入,轨道靠 polyfill 统一。这就是 v10 相比 v8 「tech 注册表」的根本进步。
1126



被折叠的 条评论
为什么被折叠?



