videojs v10 源代码系列解读:35 · HLS 解析器:multivariant 与 media playlist

进入卷五的领域部分。这篇拆 spf/src/media/hls/ 的三个纯函数解析器------multivariant(master playlist)、media playlist、属性字符串。它们是「文本 → 类型化 Presentation/Track/Segment」的转换层,零依赖、可直接测。最有意思的是 multivariant 解析里那个跨音频组的去重算法------HLS 的笛卡尔积坑被它漂亮地解掉了。

解析器的定位

SPF 的解析器在 media/hls/(不是 playback/)------它们是纯领域逻辑,不碰 DOM、不碰网络。输入文本字符串,输出类型化结构:

复制代码
multivariant 文本 ──parseMultivariantPlaylist──→ Presentation(无 duration/segments)
media playlist 文本 ──parseMediaPlaylist──→ 补上 segments/duration/initialization

两级解析的分工(parse-media-playlist.ts:181-188 的 spread 合并):multivariant 产出 PartiallyResolved*Track(知道 URL、带宽、分辨率,不知道分段),各 track 的 URL 再 fetch 后交给 media playlist 解析器补全。

parse-attributes.ts:属性字符串解析(187 行)

HLS 标签的属性长这样:BANDWIDTH=412000,RESOLUTION=1920x1080,CODECS="avc1.64001f,mp4a.40.2"。核心正则(L9):

ts 复制代码
/([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g

值优先匹配双引号内整段 ------因为 CODECS="avc1.64001f,mp4a.40.2" 里的逗号是值的一部分,不是分隔符。不先处理引号,CODECS 会被逗号劈开。

几个类型化的解析函数:

  • parseResolution(L25-33):/^(\d+)x(\d+)$/。
  • parseFrameRate(L38-60):容差匹配 NTSC 分数帧率 ------23.976±0.01 → 24000/1001、29.97 → 30000/1001、59.94 → 60000/1001。NTSC 帧率是分数,直接 parseFloat 会丢精度。
  • parseCodecs(L73-87):按逗号拆分后按前缀分类。视频前缀 avc1./hvc1./hev1.,音频前缀 mp4a./ac-3/ec-3/ac-4/opus/flac/...(大小写不敏感,fLaC 靠 lower 匹配)。L62-67 注释说明了动机:识别不了的 codec 解析为空,能力探测将无法裁剪它------宁可保守。
  • parseExtInfDuration(L92-96):split(',')[0] 剥掉可选标题(#EXTINF:6.006,标题)。
  • parseByteRange(L103-121):/^(\d+)(?:@(\d+))?$/------有显式 offset 用之,无 offset 用 previousEnd 续接,返回闭区间 {start, end: start + length - 1}。
  • getBool(L160-162)严格 === 'YES'------HLS 布尔值只有 YES 算真。

matchTag(L182-186)做 #TAG: 前缀匹配,命中就对剩余部分建 AttributeList。

parse-multivariant.ts:master playlist(395 行)

行扫描(L72-151)

按行切分(L33 的 split(/\r?\n/)),跳过空行和 #EXTM3U/#EXT-X-VERSION 等杂项标签。

EXT-X-MEDIA (L89-128)处理音频/字幕 rendition 声明:TYPE/GROUP-ID/NAME 三者必须存在(L92-94)。有个细节我很欣赏------CHANNELS 的解析(L108):HLS 里它是带引号字符串(如 "6" 或 "16/JOC" 空间音频),getInt 只取前导整数(L105-107 注释)。

EXT-X-STREAM-INF (L130-151)用两行式结构 :STREAM-INF 标签在上一行,URI 在下一行。所以 L133-139 先把属性存进 pendingStreamInfo(不立即产出),L144-150 遇到非 # 行且有待定信息时才 resolveUrl 合成完整 StreamInfo。这是 HLS spec 的quirk,解析器必须处理。

跨音频组的去重(L183-236)------本文件最关键的算法

我读这段时先被背景注释(L184-189)教育了:HLS 中同一视频 rendition 会按它可配对的每个音频组重复出现在多个 STREAM-INF 里(笛卡尔积),URI 相同。比如 3 个画质 × 2 个音频组 = 6 行 STREAM-INF,但实际只有 3 个视频 rendition。

解法(L190-236):

ts 复制代码
const tracksByUrl = new Map<string, PartiallyResolvedVideoTrack>();  // L190 --- key=URI 折叠
// 同 URI 重复出现时,把新的 audioGroupId 追加进 existing.audioGroupIds(L193-195)

但有个陷阱 :STREAM-INF 的 BANDWIDTH 是视频+音频之和 ,跨组重复项只在配对音频上不同。L197-202 的处理:保留最小值作为「最接近纯视频带宽」的代理,ABR 按它排序。而冗余流(不同 CDN 的不同 URI)不会被误合并------key 是 URI,URI 不同就是不同 track。

其余产出

  • 纯音频流(L238-256):mimeType 'audio/mp4',sampleRate 48000 / channels 2 占位默认(L251-252,等 media playlist 修正)。
  • EXT-X-MEDIA 音频 rendition → track(L258-300):从引用该 groupId 的 stream 的 CODECS 里「偷」出音频 codec(L261-270)------multivariant 不直接给 demuxed 音频的 codec,这是 HLS 规范的空隙,只能这么补。
  • 文本 track(L305-334):对齐 hls.js 的行为------仅当 DEFAULT=YES 且 AUTOSELECT=YES 同时成立才置 default: true(L322-325)。
  • 最终 Presentation(L387-394):duration 有意缺省(L392 注释:至少 fetch+parse 一个 media playlist 之前不可知)。

parse-media-playlist.ts:分段列表(189 行)

状态机式行扫描(L78-158)

状态变量(L78-86):initSegmentUrl/initSegmentByteRange(EXT-X-MAP)、currentDuration(EXTINF 暂存)、currentByteRange、currentTime(时间轴累加器 )、previousByteRangeEnd(BYTERANGE 无偏移时续接)。

关键标签处理:

  • EXT-X-MAP(L106-117):init segment 的 URI + 可选 BYTERANGE(偏移从 0 续接,L111-114)。
  • EXTINF(L120-123):暂存时长。
  • EXT-X-BYTERANGE (L126-129):parseByteRange(trimmed.slice(17), previousByteRangeEnd)------无显式 offset 时从上一段结束处续接。
  • Segment URI (L136-157):非 # 且 currentDuration > 0 时构建 Segment(id: segment-${index}、url、duration、startTime: currentTime),然后 currentTime += currentDuration 推进时间轴。

三个值得注意的「不处理」:

  1. #EXT-X-TARGETDURATION 被显式忽略 (L98)------总时长由 EXTINF 累加得出(L160 的 totalDuration = currentTime),不用 targetDuration × n。
  2. #EXT-X-ENDLIST 只是 continue(L131-133)------解析器对 VOD/live 不做区分,live 判定在别处(streamType feature,21 篇)。
  3. 容器嗅探(L170-177):fMP4 必有 EXT-X-MAP,所以「无 map + 识别到的扩展名」即非 fMP4------.ts → 'video/mp2t'、.aac → 'audio/aac'(L23-26)。这些 MIME 标记为当前不可播放,交给能力探测裁剪。

设计观察:解析器的「无知」

读完三个解析器,我最深的印象是它们的克制 ------不知道的就不填(duration 留空)、不该管的就不管(ENDLIST、live 判定)、识别不了的保守处理(codec 解析为空)。解析器只做「忠实转译文本 → 类型」,一切策略判断(能不能播、是 live 吗)都在下游。

这让它们成为理想的纯函数:给文本出结构,Vitest 直接表驱动测试(media/ 目录下 27 个测试文件,大部分是解析器测试)。

小结

  • 引号优先的属性正则------CODECS 的逗号是值的一部分。
  • 两行式 STREAM-INF------pending 暂存,URI 行才产出。
  • URI 折叠去重 + 最小带宽代理------解 HLS 笛卡尔积坑。
  • 从 stream CODECS 偷音频 codec------补规范空隙。
  • DEFAULT+AUTOSELECT 双条件------对齐 hls.js 行为。
  • targetDuration 显式忽略------时长 = EXTINF 累加。
  • 容器嗅探------无 MAP + 扩展名 → 非 fMP4 MIME。

下一篇讲 ABR------带宽估计的双 EWMA 和质量选择的滞回算法。

相关推荐
IT_陈寒1 小时前
Redis键过期失效?这个坑我踩得明明白白
前端·人工智能·后端
web打印社区1 小时前
汽修 / 4S 店:维修工单、结算单网页怎么静默出纸
开发语言·前端·javascript·chrome·ecmascript
liangshanbo12151 小时前
Webpack的分包策略面试题
前端·webpack·node.js
ttwuai1 小时前
Golang Web后台管理框架推荐:Gin、GoFrame与数据面板项目怎么分
前端·golang·gin
广州华水科技1 小时前
北斗GNSS变形监测系统在水库安全监测中的应用与优势
前端
27669582921 小时前
youdao/有道翻译APP 算法协议分析
开发语言·前端·python·sign·youdao·有道翻译app算法·有道app协议请求
奕鼎竜瑆9 小时前
Solid 前端响应式开发从零到精通
前端·人工智能
动恰客流统计10 小时前
景区客流统计怎么做?兼顾管控与运营的实施方案解析
大数据·前端·人工智能