☰
pure_live HLS 双路音视频预取的原子准入设计:`HlsPrefetchScheduler.selectAll` 与 `HlsPrefetchPlan.fromMaster` 实战解析
2026/10/6 7:23:37 网站建设 项目流程
  • 音视频
  • 直播
  • 移动开发

【免费下载链接】pure_live

纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。

项目地址:https://gitcode.com/gh_mirrors/pur/pure_live
点击查看免费下载

本篇技术指南围绕 pure_live 开源直播录播项目在 HLS 音视频预取(prefetch)链路中引入的"原子准入"机制展开,聚焦HlsPrefetchScheduler.selectAll与HlsPrefetchPlan.fromMaster两个核心组件的设计动机、实现细节与验证结果。读者将掌握:为什么双路(视频 + 音频)预取必须"全体先校验、再一次性准入",无歧义 master 清单如何被安全识别,以及从源码到原生 FFmpeg 实验验证的完整闭环方法,可直接复用于自己项目中的 HLS 录制/预取系统设计。

该审计文档(docs/HLS_PREFETCH_PLAN_AUDIT_2026_09_09.md)承接此前的停止冻结审计,本轮的核心目标是把"完整选择集合"的准入变成一个不可拆分的原子操作:要么整条 A/V 组合全部通过校验并一次性进入 feed 表,要么一条都不进入、调用方保持原有路径。

一、背景:为什么双路预取需要"原子准入"

HLS 直播录制通常涉及两路媒体流:一路视频(variant)、一路音频(独立的 AUDIO 组或伴随流)。如果调度器逐路 select——先准入视频、再准入音频——就会产生一个危险的中间态:视频已经进入活动状态、开始下载与轮询,而音频随后因"未支持标签、空/重复 ID、非法来源或集合超限"被拒绝。此时视频一路已经"先跑起来",整个 A/V 集合被拆成了残缺状态,录制输入端将丢失音轨。

文档明确指出这里的"原子"语义边界:

这里的"原子"是初始元数据与选择状态的原子准入,不是承诺未来两路网络请求同时成功。网络失败、运行中清单变化、续签及来源切换仍由后续运行期规则负责。

也就是说,原子性只覆盖"准入决策"这个瞬间:所有校验都在分配任何下载器或刷新定时器之前完成。网络层面的不确定性(断流、清单漂移、密钥续签)不属于准入合同的承诺范围,由已有的运行期刷新、覆盖缺口检测(onCoverageGap)与重试策略负责。

二、核心实现一:HlsPrefetchScheduler.selectAll的原子准入路径

源码位于 lib/domains/recorder/data/services/hls_prefetch_scheduler.dart。调度器的设计注释写得很清楚:一份录制源代(one recording source generation)、一个共享池、显式选中的媒体 feed;这个 owner 永远不自行切换清晰度或追随 master——选择权完全在调用方。

2.1 单路 select 复用同一准入路径

原有单路调用通过同一个准入路径继续工作:

bool select(String id, Uri fetchSource, HlsMediaSnapshot initial) => selectAll([(id: id, source: fetchSource, snapshot: initial)]);

select只是selectAll的退化形式,单路调用方无需改动。

2.2 selectAll 的完整校验序列

bool selectAll(Iterable<HlsPrefetchSelection> selections) { if (_closed || _finishing) return false; final pending = <String, _Feed>{}; try { for (final selection in selections) { if (pending.length + _feeds.length >= maximumFeeds || selection.id.isEmpty || selection.id.length > 256 || pending.containsKey(selection.id) || _feeds.containsKey(selection.id) || !_validSource(selection.source) || !_validSource(selection.snapshot.source)) { return false; } final window = HlsRetainedWindow(selection.snapshot.source, maximumSegments: maximumSegments); if (window.merge(selection.snapshot).isNotEmpty) return false; renderHlsRetainedManifest(window, localUri: (uri) => uri); final feed = _Feed(selection.id, selection.source, window, selection.snapshot.reloadFingerprint); _rebuild(feed); pending[selection.id] = feed; } } on FormatException { return false; } if (pending.isEmpty || _closed || _finishing) return false; _feeds.addAll(pending); _pump(); for (final feed in pending.values) { _schedule(feed); } return true; }

关键点在于:所有校验在构建pending临时表阶段完成,任何一条不满足就整体返回false,已经构建的 feed 不会被写入_feeds。只有整个集合通过后,才执行_feeds.addAll(pending)一次性提交,随后才启动_pump()(下载泵)和_schedule(feed)(清单轮询定时器)。这从代码结构上保证了"第二路被拒时,前一路不会先进入活动状态"。

逐一拆解校验项:

校验项源码位置说明
集合总容量pending.length + _feeds.length >= maximumFeedsmaximumFeeds默认 2,构造时被约束在1..2,超出即整体拒绝
feed ID 合法性selection.id.isEmpty \|\| selection.id.length > 256拒绝空 ID 与超长 ID
ID 冲突pending.containsKey \|\| _feeds.containsKey同一集合内重复 ID、或与既有 feed 冲突均拒绝
来源合法性_validSource(selection.source)与_validSource(selection.snapshot.source)同时校验"选择来源"与"快照来源"
保留窗口完整性window.merge(selection.snapshot).isNotEmpty有片段被逐出即拒绝——准入必须保留初始前缀,包括有限媒体;窗口太小而静默丢首段不算选择
渲染合同renderHlsRetainedManifest(window, localUri: (uri) => uri)初始清单必须能通过保留清单渲染器的全部约束
属性解析on FormatException解析失败整体回退

其中_validSource的实现为:

static bool _validSource(Uri uri) => const {'http', 'https'}.contains(uri.scheme) && uri.host.isNotEmpty && uri.userInfo.isEmpty && !uri.hasFragment && uri.toString().length <= 65536;

来源必须显式 http/https、主机非空、无内嵌凭据(userInfo 为空)、无 fragment,且序列化长度不超过 64 KiB。

2.3 有界迭代输入:不无界复制外部 iterable

文档特别强调:迭代输入按剩余最多两个 feed 的容量检查,超限即退出,不把外部 iterable 无界复制进内存。selectAll直接消费调用方传入的Iterable,在循环内即时检查pending.length + _feeds.length >= maximumFeeds并return false,因此外部集合再大也只会被有界遍历,符合录制端对内存安全的一贯约束。

2.4 准入后的运行期生命周期

准入只是起点。一旦提交,调度器进入运行期管理:

  • _pump():按 feed 轮询(round-robin)准入下载条目,预留pool.maximumEntries / maximumFeeds的配额(quota = (pool.maximumEntries ~/ maximumFeeds).clamp(1, 32)),防止第一条长清单吃光配额而饿死第二路;条目总数上限 512,池满时返回null形成有界背压而非隐式重试;
  • _schedule(feed):按 RFC 8216 6.3.4 的节奏刷新——首次/变化加载用完整 target duration,未变化加载用半个 target duration(见 hls_prefetch_scheduler.dart 的_schedule);
  • _refresh(feed):每次刷新拉取新快照、合并进HlsRetainedWindow、重建 wanted 集合;刷新失败则置feed.failed = true并上报onRefreshFailure,绝不在死循环里反复重试陈旧清单;
  • freeze()/stopFetching()/drainPublished():停止阶段先把已发布代冻结出 ENDLIST,再在有限时间内排空已准入的缓存体,超时则标记"输入尾部被丢弃"(inputTailDiscarded)。

三、核心实现二:HlsPrefetchPlan.fromMaster的无歧义 master 识别

源码位于 lib/domains/recorder/data/services/hls_prefetch_plan.dart。它的类注释定义了自己的边界:

Only describes an unambiguous master; it never chooses a quality, language, subtitle or camera for native. Other masters retain their existing path.

它不是一个通用 HLS 解析器,也不猜测 native 会选择哪档。生产调用者应整体保留现有路径,而不是"选第一档"或"只启用其中一路"。

3.1 什么 master 会被识别为"无歧义"

fromMaster(String text, Uri source)只接受两种形态:

  1. 唯一视频流(无 AUDIO 组引用):#EXT-X-STREAM-INF恰好一个,且其AUDIO属性缺失,返回[video];
  2. 唯一视频 + 其唯一明确 AUDIO 组:恰好一个#EXT-X-STREAM-INF引用了一个#EXT-X-MEDIA的 AUDIO 组,且组内恰好一个成员,返回[video, audioUri]。

两种形态都要求:URI 使用 master 最终地址(source.resolve(value))解析;来源与媒体协议的 scheme 必须 http/https、host 非空、无 userInfo、无 fragment;HTTPS 引用降级(master 是 https 而子 URI 不是)会直接抛FormatException使整个计划返回null。返回的sources是List.unmodifiable,即不可变集合。

3.2 明确的拒绝清单

以下任一情形都会让fromMaster返回null(无预取计划),由调用方回退到原有路径:

  • 多清晰度(多个#EXT-X-STREAM-INF);
  • 多个语言/音频候选(多个#EXT-X-MEDIAAUDIO 组);
  • 字幕/隐藏字幕组(CLOSED-CAPTIONS存在且不为NONE);
  • session key、未知状态标签/属性(#EXT开头的未知标签、STREAM-INF 出现白名单外的属性);
  • 组关联不完整(STREAM-INF 引用 AUDIO 组但组缺失,或反之);
  • 重复字段(同一属性名出现两次,_attributes解析时直接抛FormatException);
  • 悬空 STREAM-INF(pendingVariant为 true 时没有跟随的媒体 URI 行);
  • 相同 URI 同时作为两路(audioUri == video);
  • master 超过 4 MiB(text.length > 4 * 1024 * 1024);
  • 首行不是#EXTM3U。

其中#EXT-X-STREAM-INF的属性白名单为BANDWIDTH / AVERAGE-BANDWIDTH / RESOLUTION / CODECS / FRAME-RATE / AUDIO / CLOSED-CAPTIONS,且BANDWIDTH必须匹配^[1-9]\d*$(正整数);#EXT-X-MEDIA的属性白名单为TYPE / GROUP-ID / NAME / URI / DEFAULT / AUTOSELECT / LANGUAGE / CHANNELS / CHARACTERISTICS,其中TYPE必须为AUDIO,GROUP-ID、NAME、URI均非空,DEFAULT/AUTOSELECT只能是YES/NO。这些严格约束正是"不猜测、只识别唯一确定组合"的体现。

3.3 属性解析器

_attributes使用([A-Z0-9-]+)=("[^"]*"|[^,\s"]+)正则逐段解析逗号分隔的属性列表,遇到重复键、非法分隔符(非逗号或结尾悬空逗号)都抛出FormatException,因此"重复字段"会被同一机制拦截。

四、原生实验适配层:从 master 到 selectAll 的一次性接入

文档指出,原生实验适配层已经使用该计划:读取实际源站 master → 获取两份初始媒体 snapshot → selectAll 一次性准入 → 返回 master;随后 native 请求子清单时复用已选 feed,不重复选择、不重复加载。此前由 native 顺序访问两路后逐个 select 的实验接线已替换。

在源码中,这一流程对应 lib/domains/recorder/data/services/hls_relay_prefetch.dart 的_preparePrefetch:

  1. HlsPrefetchPlan.fromMaster(master, source)解析 master;
  2. 若plan == null,回退为单路root快照(HlsMediaSnapshot.parse(master, source)),走原有路径;
  3. 若计划有效,Future.wait(plan.sources.map(load))并发拉取两份初始媒体快照——注意文档与代码都强调"Serial initial reads age the first short live window while waiting for the second",因此共享同一个取消令牌(cancellation),任一加载失败都会cancellation.cancel()使整个准备过程作废;
  4. 为每路生成本地资源 ID(取自_localResource的路径末段文件名),构建HlsPrefetchSelection列表;
  5. 创建HlsPrefetchPool(maximumEntries: 32, maximumConcurrent: 16, memoryBytesPerBody: 512 * 1024)与HlsPrefetchScheduler(maximumFeeds: selections.length);
  6. candidate.selectAll(selections)一次性准入,失败则整个候选调度器被close(),不产生任何部分启用的 A/V 集合;
  7. 准入成功后,_prefetch = candidate,后续请求通过_servePrefetchManifest/_servePrefetchBody复用已选 feed。

_servePrefetchBody中的关键语义还包括:acquire超时或失败时按失败状态码回 503/410(停止阶段未排空则置_inputTailDiscarded = true);BYTERANGE 缓存只服务于其声明的切片——若请求的 Range 与资源声明的requestHeader不一致,返回 416,绝不把缓存切片冒充完整对象。

这段适配同时保留了原始 master 和媒体字节:不改质量、音轨或时间戳。切换/停止阶段,_endedManifest只为媒体清单补#EXT-X-ENDLIST,master 目录清单原样返回。

五、验证范围:从定向测试到双场景原生实验

5.1 master 计划测试

文档列出新增 master 计划测试的覆盖面:唯一组合、不可变集合、组错配、多变体/多音轨、重复属性、空/凭据/降级/fragment URI、未知会话/字幕合同等。与之配套的批量测试断言了原子性的可观测结果:

  • 第二路被拒时:feed=0、download=0、pool 条目=0——没有任何一路进入活动状态;
  • 有效组合:第一次 loader 回调必须已观察到feed=2——两路在准入瞬间同时可用;
  • 失败后的重复准入:保持既有状态,不破坏已经准入的组合。

5.2 双场景原生实验

原生实验目录controls/scheduled-1788957891514939/使用同一 12 秒视频 body 或 headers、6 秒滚动窗口、34 秒曝光,验证"真实 TCP + native"链路:

场景TS 字节本地完整视频序号视频/音频包探针停止耗时 ms观察峰值条目/下载
body 12 秒3,329,6680–13,连续840 / 1407216520 / 8
headers 12 秒3,300,3400–13,连续840 / 1313218019 / 8

两场景均code=0、正常排空、无强制取消、无完整性错误、无活动覆盖缺口;尾部丢弃仍为 true。两路合计 66 次独立刷新,关闭后池条目和字节均归 0。视频最大排序包步长 0.033334 秒、音频 0.021334 秒;整体仍是约 28 秒视频,body 场景音频约 30 秒、headers 场景约 28 秒——文档谨慎声明"不据此宣布所有尾部对齐"。

5.3 全量解码与固定哈希校验

CLI 实际以-v error -xerror -map 0:v:0 -map 0:a:0 -f null解码两份 TS,均退出 0、无错误文本。body TS SHA-256 为ED27EFDAB0D4DD3944F6AACC10E88B4725B7B8BEFFA0F08AA5CCFE1F1772AFA3,headers TS 为54ED2AC7B02BE31D0D09BA81F3372FB8FB163D7C8EFFD1E3057CD1BBFC372098。本地夹具、FFmpegKit DLL、ffprobe 和 CLI 的固定哈希由脚本验证;本轮 hook 未取得远端 ZIP SHA,故不称作"远端包哈希通过"——这一表述边界体现了审计对证据等级的严格区分。

5.4 资源守卫与构建记录

重型验证经共享资源守卫排队,未停止其他任务的 Java 进程,排队期间没有继续编辑源码。证据根目录local-artifacts/hls-prefetch-plan-20260909/,资源记录均位于local-artifacts/build-records/:

文件前缀 / 阶段秒(含排队)观察峰值 CPU %峰值工作集 B阶段结束活跃重型进程
20260909T124040828Z / atomic-plan188.07715.57117252096000
20260909T124630392Z / atomic-fixed295.79175.39149216010241
20260909T124702156Z / decode19.7180.11110300733440

一个值得注意的工程细节:首次atomic-plan在分析阶段因三个多行 if 缺少花括号失败,未运行测试;补齐花括号后运行atomic-fixed。同时让实验 master 响应头和 body共用同一个响应预算,不在读取 body 时重新计时。所有失败、排队与成功记录均被保留。第二阶段结束时另有 Java 进程活跃,解码阶段继续在资源守卫中排队,该进程随后退出;监控值包含可见重型进程,不把 75.39% 全部归因于预取代码,也未声称第二阶段结束立即全机空闲。最终解码记录为 0,两个本任务执行句柄都已收到终态。

六、生产接入状态与剩余工作

文档明确了当前所处的阶段与边界:

本轮把完整 A/V 选择接通到真实 TCP + native 实验,但尚未把预取默认启用于应用的FFmpegHlsInputRelay。下一步复用这个全体先校验的入口,接通生产资源 ID/HTTP Range 和失败状态、统一资源预算与停止生命周期,而非再叠一层实验 HTTP。

结合源码看,生产侧的接入点已经就位:FFmpegHlsInputRelay在_handleRequest中当resourceId == 'root' && prefetchEnabled && !_finishing时触发_preparePrefetch(见 lib/domains/recorder/data/services/ffmpeg_hls_input_relay.dart),且enablePrefetch仅在drainOnStop同时开启时才生效(prefetchEnabled: enablePrefetch && drainOnStop)。此外,manifestMediaTransform != null && enablePrefetch会被显式拒绝——预取缓存尚未承接 manifest 级媒体前缀变换的所有权,因此两者不能同时启用。探针 tool/probes/hls_scheduled_delivery_probe.dart 通过环境变量PURELIVE_HLS_PRODUCTION_PREFETCH_PROBE=1选择直接生产预取或历史实验适配器,两者均为 opt-in 探针,不是默认的应用程序激活。

其他明确未完成项:

  • 多变体等返回无计划,不等于这些平台验收通过;
  • 真实 LL-HLS、尾部完整排空、所有平台/界面功能、稳定版 3.2.0 发布仍属于原目标;
  • 宏观仍为20 PASS / 32 RUN / 10 NOT RUN,42 个验收大项未闭环;
  • 没有手机操作、构建、版本修改、上游合并或发布。

七、相关源码与文档索引

关注点路径
原子准入调度器(selectAll / select / 运行期泵与刷新)lib/domains/recorder/data/services/hls_prefetch_scheduler.dart
无歧义 master 计划(fromMaster)lib/domains/recorder/data/services/hls_prefetch_plan.dart
有界预取池与票据/租约lib/domains/recorder/data/services/hls_prefetch_pool.dart
保留窗口与清单渲染合同lib/domains/recorder/data/services/hls_retained_window.dart、hls_retained_manifest.dart
生产 relay 接入与预取服务lib/domains/recorder/data/services/ffmpeg_hls_input_relay.dart、hls_relay_prefetch.dart
双场景原生实验探针tool/probes/hls_scheduled_delivery_probe.dart、tool/probes/hls_rolling_delivery_probe_test.dart
本轮审计文档docs/HLS_PREFETCH_PLAN_AUDIT_2026_09_09.md
承接的上游审计docs/HLS_PREFETCH_FREEZE_AUDIT_2026_09_09.md

结语

pure_live 的 HLS 预取原子准入为"双路 A/V 预取"树立了一个可复用的范式:把选择权与校验权彻底分离——HlsPrefetchPlan.fromMaster只负责识别"无需猜测的确定性组合",HlsPrefetchScheduler.selectAll只负责"全体先校验、再一次性提交",而运行期的一切不确定性(网络失败、清单漂移、密钥续签、来源切换)都交给既有的刷新、冻结与排空合同。这种设计让录制端在"多清晰度、多语言候选"的 HLS 世界里既不越权猜测,也绝不陷入"半套 A/V 流"的残缺状态。对任何需要稳定录制 HLS 直播的工程而言,这份从设计、实现到原生验证的完整链路都值得对照借鉴。

  • 音视频
  • 直播
  • 移动开发

【免费下载链接】pure_live

纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。

项目地址:https://gitcode.com/gh_mirrors/pur/pure_live
点击查看免费下载

相关推荐

上一篇:Matterwiki:团队知识管理的简单选择
下一篇:终极指南:Diem Move智能合约测试完整教程 - 单元测试与集成测试实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询