- 音视频
- 直播
- 移动开发
【免费下载链接】pure_live
纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。
本篇以 pure_live 仓库中 2026-09-05 的虎牙连续性审计文档为主体,完整还原“Android/Windows 双端短暂停顿”这一问题的取证过程:从匿名房间 660000 的 FLV/HLS 长读探测、mpv 原生缓存预算的语义纠错,到续签期间暂停竞态的代次冻结方案,并对照上游c5944dd5/543a3ded等提交厘清本地分叉与上游实现的实际差异。读完可以掌握:直播客户端如何为“连接短租约”建立证据边界、如何正确配置 media_kit/mpv 的缓存属性组合、以及换源(source refresh)事务中用播放意图代次防止过期异步结果覆盖用户操作。
冻结范围与归因分类
本轮排查在明确的基线冻结下进行,避免“边改边测”污染证据链:
- 本地基线
812553c2;冻结的 upstream/master 为db067f193bba47ea08a91cc0b4f5cda5f291f5be。本轮没有合并上游,也没有连接手机。 - 用户确认 Android、Windows 均有短暂停顿,旧版本主要表现为黑屏;发生频率、声音是否中断和安装包对应内核尚未取得同场采样。
- 短租约、Windows 提前切换及备用播放器路径可追溯至维护分支
db7d0df33,不是本轮上游更新引入。 - 缓存预算由维护分支
3c15096e引入,但与锁定的 media_kit 原生默认属性组合不完整,分类为integration-conflict。 - 换源等待期间缺少播放意图代次检查,属于维护分支恢复逻辑的
fork-regression;它是独立的状态竞态,不当作所有停顿的同一原因。
归因上的关键纪律是:不把操作竞态等同于全部前台自然停顿。本文档与后续文档(如关闭与快速重进审计、原生令牌连续播放修复、恢复队列审计、缓存与内存趋势审计、事件合同审计)共同构成分层修复记录,各修复层独立归因、独立回滚。
外部接口证据:FLV 短连接与 HLS 签名失效(非实机播放验收)
审计先在 Flutter/播放器/自动换源之外,用匿名公开房间 660000 做独立探测,脱敏结果保存在本地local-artifacts/diagnostics/huya-continuity-20260905/。两个关键样本:
- 单连接 FLV:HTTP 200,首字节约 0.39 秒;读取 5,209,234 字节后,连接在约 120.949 秒后 EOF,客户端未主动重开。
- 同一个签名 HLS URL:播放列表序号持续递增,124.48 秒时仍为 HTTP 200;130.84 秒开始,播放列表本身返回 HTTP 403,后续三次请求仍为 403。
初步“只有 FLV 有短有效期、换 HLS 就消失”的假设被第二次分阶段探测否定。审计的处理方式是:撤销该假设对应的临时测试,保留现有 HLS 租约保护,不为让测试通过而改变正确行为。
这些样本支持的结论是有边界的:
- 支持“当前获取方式下存在短期传输/签名有效性约束”;
- 没有证明全部 CDN 的固定 SLA;
- 没有证明登录可以解除约束;
- 没有证明每次用户停顿恰好发生在该时点;
- 在官方机制文档缺失时,不把 403 直接命名为某种账户验证。
后续在同条件长期对照(见原生令牌连续播放修复)中,网页模板路径与上游 WUP 原生令牌路径的 A/B 结果进一步收窄了结论:网页模板 + 匿名身份约 120.8 秒 EOF,而 WUP 原生凭据在 420 秒(测试上限)内连接正常,24,206,698 字节。这印证了本文“尚未定位取流根因”的早期状态,本文的缓存/竞态结论仍有效,但取流策略结论以该后续文档为准。
客户端链路与第一个错误状态
续签地址不等于连续媒体
问题一:Android 的 proactive 路径只保存_prefetchedSourceRefresh——即预取的地址。当旧连接结束,仍需重新打开媒体源、建连接、探测和解码。这可以缩短“获取地址”的等待,但没有消除媒体重开间隔。
问题二:Windows 的_tryWarmSwapSource等到候选画面出帧后交接,能减少黑屏;但旧播放器先 pause,随后还要设置候选音量、切换订阅并发布纹理变更。两段媒体没有时间戳对齐,也没有 Flutter 实际呈现提交确认,所以“候选已出帧”并不证明用户看到的画面无间隔。审计明确判定:现有日志里的 seamless 描述过度,应改为仅描述完成 handoff。
据此,文档直接否决了四类无证据修法:
- 无证据缩短重试;
- 直接删除租约保护;
- 全平台启用双解码器;
- 盲目扩大缓冲。
原生缓存预算未完整生效
审计发现应用当时只写入前向 32 MiB、后向 4 MiB、readahead 2 秒,而锁定的 media_kit 默认开启cache=yes和cache-on-disk=yes。按 mpv 官方手册的语义:
- 开启磁盘缓存时,
demuxer-max-bytes约束的是内存中的包元数据,而不是磁盘中的媒体数据,文件可持续追加; - 网络缓存启用时,较大的
cache-secs优先于较小的demuxer-readahead-secs。
因此旧注释中的“36 MiB 压缩媒体内存预算、2 秒预读”并不是完整的有效配置。当时的修复设计是:显式启用网络缓存、关闭磁盘缓存,保留 32/4 MiB 字节预算,明确设置 6 秒网络缓存时间上限、2 秒基础预读。审计同时强调:6 秒是有界抖动余量,不是额外等待 6 秒才开播,也不是断流无缝续接的保证。主播放器与多画面共用同一属性入口,不修改录制管线、播放比例、音频模式和手动暂停语义。
对照当前主线源码,MediaKitLiveProperties 的直播调优表已演进为:forwardBytes = 96 MiB、backBytes = 8 MiB(低配 4 核以下机器降为 40/5 MiB)、readaheadSeconds = 8、cacheSeconds = 30、cache-pause-wait = 4,并显式写入cache: yes、cache-on-disk: no、demuxer-donate-buffer: no(防止已播放数据借用前向预留)。build()的注释还保留了一个真实教训:Twitch 复现中,按源写的force-seekable=no曾被这张全局表覆盖为yes——这正是把“全局属性”与“按源属性”拆成build()与sourceProperties()两个入口的原因:
- sourceProperties() 按源决定
http-proxy、demuxer-lavf-format、force-seekable、cache-pause:远端渐进式流(FLV 等)一律按直播对待,cache-pause只在点播(本机/回环输入)保留,否则会出现“先播两秒、停进缓冲、再续播”的观感; - 引擎跨源复用场景下,非 HLS 上游必须清空
demuxer-lavf-format的强制值,否则下一条 FLV 会被按 HLS 解析; - media_kit_source_properties_test.dart 对上述每条规则都有回归用例,包括“平台声明优先于 URL 形状”(Twitch 把签名塞在路径里、无
.m3u8后缀也需声明 hls)。
另外,player_preset.dart 中的预设属性(如lowLatency关闭缓存、stableNetwork把cache-secs提到 120 秒并开启重连)以“后写覆盖”的方式叠加在这张基础表之上,这也解释了为什么缓存预算必须是显式、完整、单入口的——任何一个写入者漏项都会让组合回落到引擎默认。
续签期间用户暂停后,旧异步结果仍可能提交
这是审计定位到的明确状态竞态:定时器触发换源时检查了播放意图,但 resolverawait完成后没有重新检查。用户在等待期间暂停,过期结果仍可创建并播放新候选,覆盖用户暂停。
修复设计:
- 在解析入口冻结 session 与 playbackIntentRevision;
- 异步结果返回及候选准备完成时检查代次、播放请求和临时挂起状态;
- 过期结果按“已消费事务”处理,避免继续走错误恢复重开;
- 保持正常主动续签、以及死亡连接同 URL 重开的原有行为不变。
当前主线在 playback_source_refresh.dart 中的canAdoptSourceRefresh()仍然执行同一族门禁:!disposed && sameRoom && !revisionMoved && result.hasSources——换源是一次往返请求,期间任何超播放的事务都会使过期结果被拒绝采用;player_controller.dart 中的 commit 则以commit.revision <= 已见revision做幂等去重。虎牙侧的续期决策还集中在 huya_transport_policy.dart 的selectRefreshedLine():按“身份而非旧快照中的位置”续线,优先保留原 CDN/原格式/原原生凭据家族,主播重开导致流名轮换时按 host+格式识别线路,只在单条原生线失败时才放宽到网页 FLV、再到 HLS。
上游实际修法与本地差异
用户在追加要求中只执行 fetch、未 merge/rebase/cherry-pick;此次审阅的最新上游固定为baab0eb4(2026-09-04),此前冻结基线仍用于本地来源追踪。
令牌获取与故障恢复是不同层
- 上游
c5944dd5(8 月 29 日,标题并未准确列出所有改动)将getPlayUrl改为先执行getCndTokenInfoEx,由 WUP 返回sFlvToken再构造地址;缓存两分钟并合并并发请求,当前上游 master 仍保留此路径。 - 本地优先使用可用的房间网页模板,WUP 是补救路径;另有匿名观看身份、按协议选令牌、完整 fm 模板、lease 元数据、主动续签与 Windows 候选播放器。
- 当前上游的
getPlayUrl即便后缀是 m3u8,令牌获取仍统一读取sFlvToken,且缓存键只有 stream——这些与协议/身份一致性相关的差异应单独验证,不整文件覆盖本地。 - 上游
6d62f7ae的作者信息与标题源于本维护分支的一批改动,但其实际差异没有完整携入本地 Huya lease/Windows warm-swap 管线;同名提交也不等于实现一致。
本地源码印证了“按协议选令牌”的隔离:huya_site.dart 中 HLS 线路走getNativeCdnTokenInfoEx()取得的凭据只用于构造 HLS 地址,注释明确写着 “Never reuse its FLV token for HLS”;HuyaCdnTokenLease 对齐官方播放器的租约语义——wsTime获得 5 分钟有效期宽限、提前 30 秒请求刷新(refreshAt = invalidAt - 30s),并在存在服务端iExpireTime时取更早的失效时间。续期时间取“签名失效提前量、令牌租约刷新点、传输年龄”三者的最早值(见 getPlayUrlRefreshAt),网页/未知家族保留 100 秒刷新、125 秒失效的短传输租约保护(_transportRefreshAge/_transportInvalidAge)。
证据顺序同样重要:先前测到的 121 秒 EOF 和 131 秒 403 使用的是本地网页模板路径。在对上游 WUP 路径做同条件长期对照之前,不把结果推广成“虎牙所有获取方式的服务器固定限制”——这也是后续应优先补齐的证据,早于增加双播放器和更频繁重试。
IJK 黑屏修复的最终状态
543a3ded(9 月 4 日)包含:没有视频尺寸时仍构建视图(16:9 临时占位);纹理 ID 判断从> 0改成>= 0;一度增加视图帧后自动启动逻辑;移除start-on-prepared=0。- 后续
a6494321完整还原了该次 FijkAdapter 改动,恢复autoPlay:true。当前上游 master 保留视图/纹理修正,但不保留那套帧后自动启动状态机。 - 本地的比例控制、来源代次、音频和 PiP 状态机有额外约束。借鉴视图挂载与合法纹理 ID 修正时,应加 IJK 独立回归;这并非 Windows media_kit/CDN 短暂停顿的直接解决方案。
Issue 证据边界
- 808 号 issue 中 8 月 28 日回复已修复,但 8 月 29 日仍有 Windows 循环/卡死、约两分钟卡住的后续反馈——“已关闭”不等同于覆盖所有客户端的连续播放验收。
- 816 号 issue 有“线路 4 正常”的样本反馈。它提示 CDN 差异值得测量,但不等于所有用户永久固定线路 4 就是正确做法。
最终修复路线(按证据推进)
- 相同房间、码率、CDN、请求头,隔离比较网页模板/上游 WUP 获取方式,单连接连续读流超过三个当前观测失效窗口;记录状态码、结束原因与数据进度,不只验首包。
- 若 WUP 明确改善,适配成协议/身份一致、受服务端有效期约束、播放与录制相互隔离的首选源;保留失效恢复,但只在证据支持后削减不必要的定时重开。
- 若仍需中途续接,修复实际呈现与音频所有权交接,区分 Android 与 Windows,而不是用固定 40 秒重启充当无缝;保留暂停/关闭代次和失败回滚。
- 分别验收媒体连续性、IJK 纹理启动、缓存资源平台期;任何一项失败都不把整项虎牙稳定性标成通过。
验证记录与剩余门禁
验证证据(均为该文档原始记录):
- 初次红测:缓存继承属性测试实际得到
cache-on-disk=yes,与目标no不符——正是“只写预算、没写组合”的失败样本。 - 一次静态分析通过:No issues found,176.9 秒,记录于
20260904T230142251Z-quality-focused.json;本轮不启动手机测试。 - 暂停竞态红测:resolver 返回后实际安装了新候选而非保留暂停的原播放器,记录于
20260904T225019407Z-quality-focused.json。 - 修复后的 69/69 定向回归通过,覆盖缓存、虎牙 URL、源恢复及候选预热中暂停;记录于
20260904T225415949Z-quality-focused.json。 - 未更改当时的签名优先级、协议租约、40 秒交接定时器;未声称这两项加固已经消除短暂停顿。
后续原生令牌连续播放修复给出了可重复的外网探针操作方式:tool/probes/huya_native_transport_probe_test.dart不参与默认离线测试、不写入媒体或凭据,显式设置PURELIVE_HUYA_LIVE_PROBE=1后经tool/local_ci.ps1 -Scope Focused -TestPath tool/probes/huya_native_transport_probe_test.dart -SkipPubGet运行,可用PURELIVE_HUYA_ROOM、PURELIVE_HUYA_SECONDS(130–600 秒)控制参数。
剩余工作同样被写进门禁:
- 此轮修复的是缓存配置与明确的换源竞态,不声称 Android/Windows 虎牙短暂停顿已全部消失。
- 真正连续换源需要分别验证候选媒体时间轴、当前/候选音频所有权、Flutter 实际呈现提交、用户暂停/关闭取消、失败保留旧源与双解码器资源上限。
- Android 的仅预取 URL 路径仍是已知缺口;不直接移植 Windows 纹理逻辑。
- 需要跨多个租约周期测量视频最长帧间隔、音频缺口、换源次数、CPU/RSS 和缓存占用;单个首帧测试、接口成功和假播放器测试均不替代这层证据。
- Bug 批次尚未结束,不作为 3.2.0 稳定版发布;回滚时按独立修复提交回滚,不删除签名、租约和故障恢复保护。
- 音视频
- 直播
- 移动开发
【免费下载链接】pure_live
纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。
相关推荐
pure_live 录制链路 HLS 有界预取池:未读响应取消修复与读取租约所有权设计
pure_live 录制链路 HLS 有界预取池:未读响应取消修复与读取租约所有权设计 导读 本文基于 pure_live(纯粹直播)仓库中 HLS_PREFE
音视频直播移动开发Netcatty SFTP 传输审计(2026-09):暂停/恢复竞态修复与断点续传可靠性验证
Netcatty SFTP 传输审计(2026 09):暂停/恢复竞态修复与断点续传可靠性验证 2026 年 9 月,Netcatty 仓库完成了一次范围明确(
pure_live 虎牙缓冲期限治理:区分三类卡顿故障、修复计时回归与合成抖动对照实验
pure_live 虎牙缓冲期限治理:区分三类卡顿故障、修复计时回归与合成抖动对照实验 本文基于 pure_live 仓库的审计文档 虎牙持续缓冲期限与抗抖边界
音视频直播移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考