Skip to content

Latest commit

 

History

History
180 lines (147 loc) · 58.1 KB

PATCHES.md

File metadata and controls

180 lines (147 loc) · 58.1 KB

This is the complete MovieClaw downstream patch inventory, imported from MovieClaw local commit e2a19002, following the vendored engine layout. The original Chinese rationale is retained for provenance. References to MovieClaw host design files can be found in the MovieClaw documentation. The English host API contract is in docs/api.md.

Standalone fork validation (2026-10-06)

  • Toolchain: Xcode 27.0 (27A266a), arm64 macOS 27.0.1.
  • swift build --jobs 4: passed with the upstream library, SMB, CLI and compiled example targets.
  • python3 Scripts/check-doc-links.py: passed.
  • Targeted documentation/integration/cache-index/converter/prefetch/Range checks: 19 XCTest cases and 50 Swift Testing cases passed. The Range fixtures explicitly request the 32 MB first span that their assertions describe; OCR-prefetch fixtures explicitly arm OCR and also check the unarmed case.
  • Full swift test --jobs 4 is not green: the engine XCTest run executed 718 tests (one skipped, one failure), and Swift Testing executed 4,129 tests across 570 suites with 41 issues. These failures include downstream/upstream assumptions about target segment length, keyframe-index trust, SDR transfer labels, initial range size, held-reader/backpressure behavior, source-probe input limits, and the LoadOptions inventory. The known APFS allocation-sensitive cache-trim test also fails, as in the original MovieClaw fork. These checks are retained for upstream reconciliation.
  • The app build and real-media results below were measured against the same downstream engine source in MovieClaw. This contribution is a draft for review of the complete extension set, rather than a merge-ready declaration that the upstream suite has passed.

Upstream 7.36.0 (2026-10-14)

  • Merged upstream 7.36.0 (DTS:X rendered to APAC over the new LibDTSX dependency, live replay de-duplication #747, origin-slot completion #746, the VideoToolbox session rebuilt after a screen lock #739, chunked playlists #733, LibTrueHD 2.3.0).
  • P27's read-starvation guard now wraps upstream's live replay reader (#747) on the software path's direct read; P60's bt709CurveColor sits next to upstream's seekToInitialProducerStart.
  • Full swift test --jobs 8: 764 XCTest cases (one skipped upstream) and 4,475 Swift Testing cases in 615 suites, all passing.

Upstream main after 7.35.1 (2026-10-13)

  • Merged upstream main (7.35.1 plus #737 and #738). The remote-disc patches now use upstream's implementation: P5 (BDMV folders, #734, with the maintainer's cancel fix 600fb6a0), P7 (CLPI EP-map seeks, #736, folder side d64d7848), P61 (UDF-only DVD images, #737) and P13 (DVD cell timeline and time-map seeks, #738). P26 (VIDEO_TS folders) is the commit of upstream PR #756, cherry-picked as is.
  • P15 skips libavformat's duration estimate for a disc title, so bit_rate is 0 there; Demuxer.bitRate takes the title's size over its playlist duration whether or not libavformat gave a rate (upstream does so only when it did). P37's estimate stays for non-disc sources.
  • P9 and P18 keep their hooks next to upstream's EP-map / time-map seeks. The fork's DownstreamPlaybackIntegrationTests reads upstream's resolved / item names.
  • Full swift test --jobs 8: 747 XCTest cases (one skipped upstream) and 4,390 Swift Testing cases in 606 suites, all passing.

Upstream 7.33.2 (2026-10-11)

  • Merged upstream 7.33.2. Upstream AE#724 (7.32.2) took P13's software-clock rule as its default (only a first sample ahead of the start re-anchors) and the resume half of P35 (resumeSessionZero). The P13 switch softwareClockIgnoresEarlyFirstSample is now a deprecated no-op; P35 keeps only what upstream does not do, presetting the origin for a VOD load that starts from the head, on top of resumeSessionZero.
  • Full swift test --jobs 8: 4,252 Swift Testing cases in 588 suites and the XCTest suite; three issues, none from the merge. ItemSwapStillTests "a playing native item hands over the frame on screen" fails identically on pristine upstream 7.33.2 on this machine (macOS 27.0.1); the HLSPlaylistHostileInputTests linear-time bound and Issue684PumpJoinSpentTests pass when rerun alone (timing under a full parallel run).

Upstream 7.32.0 (2026-10-08)

  • Merged upstream 7.32.0. P19 (#719), P8 (#720) and P57 (#721) were taken upstream and now use upstream's implementation, including the maintainer's follow-up fixes (P57: sealed-staging memory keyed by staging file, a seal racing the reader's stat; P8: a full volume behind a restored session directory, .storageExhausted carrying ENOSPC). P57's process-wide switches are gone: MovieClaw sets LoadOptions.progressiveSegmentDelivery per load, and the P28 start witness reads it from the native host's load contract. The P25 storage-full test hook reports through the per-session latch.
  • Upstream moved blocking engine work to BlockingWork.detached; the downstream call sites (P47 audio session, P56 head-start open, P25 read-ahead budget) use it too.
  • P19 counts the P29 background prefetch and the P5 disc-directory readers through upstream's SourceTransferCounting.
  • Full swift test --jobs 8: 736 XCTest cases (one skipped upstream) and 4,232 Swift Testing cases in 586 suites, all passing.

Opt-in defaults and upstream 7.28.3 (2026-10-07)

  • Merged upstream 7.28.3. P38 (in-band HEVC parameter sets) and the tail witness of P18 (partial keyframe index) were taken upstream in #705 and now use upstream's implementation.
  • Every downstream switch now defaults to the stock engine behavior, following the upstream review of PR #703. Previously hard-wired policies gained switches: vodFirstSegmentTargetSeconds (P3), presentsSDRAsSRGB (P60) and softwareClockIgnoresEarlyFirstSample (P13). MovieClaw enables each one explicitly in its AetherCore adapter, so the App's behavior is unchanged.
  • P61 now reads the UDF anchor once: Blu-ray first as upstream, UDF-only DVD only when no BDMV exists.
  • The P51 trim test now leaves holes of at least 16 MiB: APFS fills smaller holes between written ranges when it allocates on flush, which made the test's allocation accounting fail.
  • Full swift test --jobs 8 (Xcode 27.0): 718 XCTest cases (one skipped upstream) and 4,138 Swift Testing cases in 572 suites, all passing.

MovieClaw 对 AetherEngine 的补丁(我们自己维护的 fork)

基线:上游 AetherEngine 7.28.0(7de2d5a0dbb8bc60c09be5620a71f8904d7ab60f,2026-10-06 同步)(LGPL-3.0 + App Store 例外)。 这份源码就是 MovieClaw 自己维护的 fork(2026-09-27 起):引擎问题我们自己修、自己完善,不以「等上游合并」为前提。 补丁在源码里都标了 [MovieClaw P<n>] 或 [MovieClaw patch P<n>],动机与实测见 docs/design/player-engine.md 第 7、8 节与 docs/design/disc-direct-play.md。LGPL 义务:修改后的源码随仓库公开,关于页注明组件与源码地址。

2026-10-06:7.19.0 → 7.28.0

完整合入上游引擎目标的 112 个变更文件,保留 MovieClaw 原有的 56 个补丁编号。新增流与音频详情、HLS 旁路遥测、共享输出、I-frame 拖动缩略图、顺序读取声明、缺失 ctts 的 HEVC 修复及 HDR Vivid 探测;FFmpegBuild / LibDovi 依赖约束不变。

合并交界处:P17/P30 的显示时钟等待跟随上游新的软解/硬解重排窗口;P7 的 CLPI 位置记录跟随有界、去重后的标题组装;P22/P53 的 HTTP 原盘预取复用上游限制响应体大小的 delegate;P10 默认音轨规则进入上游线程安全快照。P56 的首次 512 KB 请求仍默认启用,上游假定首次 32 MB 请求的 Range 用例在关闭此开关时验证。

验证记录:

  • macOS、iOS Simulator、tvOS 的 Debug 构建通过。
  • 上游相关回归 145 项通过(70 项格式/缓存边界、68 项加载取消/共享输出/读取稳定性、7 项默认 Range 配置);新增的 4 项合并回归包含在 App 的单元测试中。
  • App 单元测试 392 项中 391 项通过。SourceByteCacheTests.launchTrimShrinksAnOversizedSourceKeepingMetadata 的磁盘分配量断言在未同步的 7.19 源码上复现同样的 7 个失败。
  • 真正登录 NAS 的 PlayerUITests:起播/续播 MP4、MKV、暂停/继续、前后跳转、下一集通过。testLandscapeAndLock 的 3 个 UI 断言在主干 7.19 上也全部复现。
  • iOS Simulator 的冷缓存语料回归:MP4、MKV、蓝光目录、DVD 目录、DVD ISO、VC-1 蓝光目录、蓝光 ISO 全部起播并完成前后两次跳转(14 次落点),没有服务端降级;MKV 中途换音轨与字幕后继续播放。
  • macOS 直接播放 NAS 的 4K HDR10 HEVC + TrueHD:硬解、音频桥接和播放头推进正常,观察期间无掉帧、无服务端降级。
  • 网络故障回归:冷起播拒连 6 秒后恢复,6 Mbps 限速约 7.6 秒出画并持续播放;播放中拒连 20 秒、其间跳到未缓存的 675 秒位置,实际新请求被拒,网络回来后自动重连,播放头推进至 702 秒,没有服务端降级。重连会重建引擎,因此此场景以目标位置之后持续出画/推进验证,不依赖被重建清掉的 SeekTrace。
补丁 位置 做什么 为什么
P1 Video/HLSVideoEngine.swift MPEG-TS / M2TS 源不做「索引预热」(跳到片中间那一次寻址) TS 没有容器索引,预热什么也读不到,分片规划照样退回均匀切分;从 NAS 读蓝光原盘时这一次寻址实测 1.9 秒,起播时换音轨重载还要再付一次
P2 PlayerState.swift、Native/NativeAVPlayerHost.swift、AetherEngine+Loading.swift、AetherEngine+ReloadWithOptions.swift 新增 LoadOptions.vodStartsImmediately:点播起播时,AVPlayer 已缓冲 ≥1.5 秒却还停在「为减少卡顿而等待」,一次性 playImmediately 机制、门槛与直播的 AE#440 完全相同,只是对点播也生效。本机回环的生产者远快于 1 倍速,AVPlayer 等码率估计只是白等(真机 0.2~0.6 秒)
P3 Video/HLSVideoEngine.swift、Video/HLSVideoEngine+SegmentPlanning.swift 第一个分片的切分目标从 4 秒改成 1 秒(其后仍按 4 秒:阈值 1、5、9……秒),均匀切分时第一段不短于实测关键帧间隔 AVPlayer 必须等第一个分片完整产出才能开播,它的大小就是起播延迟:87 Mbit/s 的 UHD Remux 第一片 43 MB,真机从 NAS 读要 0.84 秒
P4 Demuxer/Demuxer.swift URL 片段 #aether-disc-image 声明「这是光盘镜像」,HTTP 源直接走镜像读取器 MovieClaw 的取流地址 /playback/files/{id}/stream?token= 没有 .iso 后缀,引擎按后缀判断镜像,ISO 被当成普通媒体探测失败。片段只在本机、不随请求发出,重新装载时随 URL 沿用
P5 Disc/DiscDirectory.swift(新)、Disc/DiscReader.swift 原盘目录(BDMV 文件夹)经 HTTP 直推:DiscDirectoryReader 协议 + HTTPDiscDirectoryReader(每个文件一个 Range 读取器)+ 按文件首尾相接的拼接读取器;DiscReader.wrap 在识别缓存之前分支到目录版,复用 MPLS、选主片、多剪辑拼接与时间轴折叠;可按服务端指定的主播放列表名选主片(只读这一个播放列表) 多剪辑原盘原来要 NAS 起 ffmpeg concat 换封装,还丢杜比视界增强层、TrueHD 退 AC-3 核心、字幕不进流。改后服务端只按文件供字节
P7 Disc/DiscSeekTable.swift(新)、Disc/DiscMetadata.swift、Disc/DiscRecognitionCache.swift、Disc/BDTitleSelector.swift、Disc/DiscReader.swift、Disc/DiscDirectory.swift、Demuxer/Demuxer.swift 蓝光按 CLPI 的 EP map 定位:识别时读选中标题各剪辑的 CLPI,定位时按折叠后的时间找剪辑、换成剪辑内原始 PTS、查表得到关键帧字节偏移,一次按字节定位 原来按时间二分:各剪辑时间戳互相重叠时落到别的剪辑、跳转落不了地(《完美陌生人》);经 HTTP 读机械盘时每一步都是一次请求加一次寻道,续播起播 14.8 秒 → 2.7 秒(《不一样的天空》)
P8 Video/SegmentCache.swift、Video/HLSVideoEngine+LiveReopen.swift **已由上游 #720 收下(7.32.0),改用上游实现。**分片缓存目录因磁盘已满建不出来时记下来,VOD 泵因此失败时报「存储已满」而不是「音频无法封装」 手机可用空间 197 MB 时 4K60 片子起播失败,报错却是「Source audio cannot be muxed (-22)」,真因是 NSCocoaErrorDomain 640
P9 Demuxer/Demuxer.swift UHD 原盘双 PID 杜比视界:探测后把增强层(PID 0x1015)与基础层(PID 0x1011)配对,基础层补上 P7 记录(bl_present_flag=1;增强层带 DOVI 描述符就照抄,没有就按基础层分辨率/帧率合成、兼容 ID 6),增强层每个访问单元的 RPU 按 PTS 追加到同一时间戳的基础层包尾,增强层视频不送下游;定位时清空暂存 引擎只看基础层,UHD 原盘一律按 HDR10 播放;增强层还被「只保留选中流」丢掉。实测《疾速追杀4》原盘的节目映射表根本没有 DOVI 描述符(两路只有 HDMV 注册描述符,杜比视界只记在 CLPI/MPLS),所以按蓝光规格固定的 PID 配对。合并后下游现成的 P7 → 8.1 转换原样接手,真机画面「杜比视界 · 片源 DV P7(已转 8.1)」

| P10 | Demuxer/Demuxer.swift | 起播默认音轨:没有一条音轨标了默认(蓝光 / DVD / TS 都不标)时,语言以第一条音轨为准,同语言里优先 AVPlayer 能原样拷贝的编码(AC-3 / E-AC-3 / AAC) | av_find_best_stream 按帧数、码率挑,真机《怦然心动》蓝光镜像起播放的是西班牙语 AC-3(第一条是英语 DTS)。服务端的默认音轨是「标了默认的,否则第一条」,两边一致后 App 起播后不必为换语言再重载一次 |

| P11 | PlayerState.swift、AetherEngine.swift、AetherEngine+ReloadWithOptions.swift | 新增 LoadOptions.audioTrackOrdinal:起播音轨按「第几条音轨」指定,探测完换成流下标(显式 audioSourceStreamIndex 仍优先) | App 记着的是与服务端同口径的 embedded:N,装载前不知道流下标,原来只能首帧后 selectAudioTrack 重载一次:真机蓝光镜像续播记着的中文音轨,起播 2.5 → 4.2 秒 |

| P12 | Demuxer/Demuxer.swift | find_stream_info 期间把没有画布尺寸的 PGS 流、认不出编码的数据流(广播录像的 DSM-CC 数据轮播,编码号临时给 BIN_DATA 占位)、MP4 / MKV 里认不出编码的音轨(国产 4K 剧的菁彩声 Audio Vivid「av3a」)暂当附件(同 parkUnresolvableAudio),探测完放回 | libavformat 认定 PGS「参数不全」直到知道画布尺寸,字幕包又稀疏,探测一直读到 50 MB 预算:带 PGS 的片子起播里「探测流」0.6~1.1 秒,改后 0.2 秒以内(《权力的游戏》3.85 → 1.79 秒、《乱世佳人》1.22 → 0.46 秒);日本电视台录像两路数据流同样拖满预算,《再见我们的幼儿园》1.12 → 0.40 秒;菁彩声音轨同理,《交锋》探测 1.51 → 0.14 秒、起播 3.48 → 1.77 秒(TS / PS 的未知流可能靠嗅探认出来,不动)。引擎画 PGS 的画布尺寸取自画面,用不上探出来的尺寸 | | P13 | Disc/DVDIFOParser.swift、Disc/DiscReader.swift、Disc/DiscMetadata.swift、Disc/DiscRecognitionCache.swift、Demuxer/Demuxer.swift | DVD 主 PGC 的每个 cell 当成一个剪辑交给现成的时间轴折叠(ClipSpan),cell 的时间戳基准由导航包(PCI 的 VOBU 起始 PTS − cell 内已播时间)当场算出;有时间表(VTS_TMAPT)就「标题时间 → VOBU 字节偏移」一次定位。只处理主 PGC 从标题 VOB 开头起、cell 首尾相接、没有多角度块的盘;定位表也接进 seekBounded(软件通路与字幕旁路走它);SWClockAnchorPolicy 只在首个样本晚于起播点时才按它重锚(粗粒度定位落在前面时时钟仍锚在起播点) | 整个 VTS 的 VOB 当一条 MPEG-PS 读,很多盘每个 cell 的 PTS 从头开始:《公司的力量》三个 cell 各自从 0.37 秒起,标题时间 2670 秒处归零,播放头与按时间二分的定位从那里起全乱;《聪明的一休》时间表 16 秒一格,续播落在 12 秒前,原来时钟锚在落点、画面等 8.9 秒才出,改后 0.68 秒 | | P14 | Decoder/HardwareVideoDecoder.swift、Native/SoftwarePlaybackHost.swift、AetherEngine.swift、AetherEngine+Loading.swift | VP9(4:2:0 的 8/10 bit)走 VideoToolbox 硬解:iOS 26.2 起登记补充解码器(VTRegisterSupplementalVideoDecoderIfAvailable),格式描述用由流参数拼出的 vpcC;解码器标签按实际硬解/软解写 | 4K VP9 软解本进程 CPU 约 51%,连播 7 分钟温度到「偏热」;硬解后 18~25%(《The Age of A.I.》) |

| P15 | IO/HTTPDiscIOReader.swift、Demuxer/Demuxer.swift | 光盘镜像读取器按 LRU 留最近 8 个小块(≤ 1 MB);光盘标题有 MPLS / IFO 时长时不再让 libavformat 从文件尾倒读估时长 | 识别盘内结构时目录项与文件数据两处按扇区来回跳,单缓冲每跳一次整块重取(26 次请求里 20 次);估时长又从几十 GB 镜像的尾部倒着读十来次。蓝光镜像《怦然心动》起播 2.02 → 0.97 秒(开盘 0.63 → 0.14、探测 0.55 → 0.01) |

| P16 | Video/SegmentCache.swift、Native/SoftwarePacketDiskFIFO.swift、AetherEngine+Prewarm.swift | 死会话留下的缓存立刻清:分片目录与软件通路包缓存都带存活锁(flock,进程一死内核就放),锁没人拿着且建了 10 秒以上就删,不再等 1 小时 / 24 小时;新增 sweepStaleSessionCaches() 供宿主启动时清一遍(两种缓存原来只在建同类新会话时顺手清) | 被杀掉的播放会话每个留下最多 2 GB 分片、1 GB 上下包缓存:真机一夜测试 App 占用长到 16 GB(分片 8.9 GB + 包缓存 6.6 GB),此前 4K 片子因手机写满起播失败;改后临时目录 0.08 GB |

| P17 | Native/SoftwarePlaybackHost.swift | 软件通路跳转落地与起播锚时钟时,时钟先停在落点(速率 0),按渲染器当前重排深度等到落点帧送出(上游 7.23 起软件解码保留 1 帧、硬解保留 4 帧)再按当前速率走;兜底 0.6 秒,暂停取消等待 | 原来落地就走时钟,解码器还在从前一个关键帧往目标解,前十几帧一出来就迟到被丢,跳转后画面停半秒再跳:DVD 7 → 0、AVI 11 → 0、VP9 4 → 0 帧,起播时间不变 | | P18 | Demuxer/MatroskaCuesProbe.swift(新)、Demuxer/Demuxer.swift、Video/HLSVideoEngine.swift、Video/HLSVideoEngine+SegmentPlanning.swift | 没有可用 Cues 的 MKV:打开时读文件头解析 SeekHead(跟不到的链、读不全的头都不下结论),判定没有或指向文件尾之外就跳过起播前的索引预热;按时间定位前按字节比例估位置、往后找最近的 Cluster 读时间码(至多校正两次)得到目标前最近的一个,再按平均码率估到目标之后探一次得到目标后最近的一个,都登记成 libavformat 的索引项(反向定位落前一个,生产端「不早于目标」的正向定位落后一个;码率按探到的点现算,截断文件的片长不可信);分片计划的可信判定加「尾部覆盖」:最后一个关键帧离片尾超过 60 秒就退回均匀切分 | 片库 6675 个 MKV 里 8 个没有可用索引(《饥饿站台》下载不完整、Cues 指针在 18.6 GB 而文件只有 11.8 GB;7 个《哆啦A梦》番外是没下完的空壳),边下边播的文件也是这个状态。原来预热线性读 10 秒(上限)放弃,扫到的关键帧只覆盖开头 212 秒却被判可信,最后一段从 205 秒拉到片尾 5933 秒,永远不出画;改后续播到 900 秒 1.2 秒出画,往后跳 600 秒 0.93 秒、往回跳到 300 秒 1.49 秒且落点准确 |

| P19 | Demuxer/CustomIOReaderBridge.swift | **已由上游 #719 收下(7.31.2),改用上游实现;P5 原盘目录与 P29 预取接上了上游的计数。**自定义读取器的桥接层累计交给解复用器的字节数,作为「已从源拉取字节」上报遥测(原来写死 0) | 光盘镜像、原盘目录都经这层接进引擎:顶栏加载速度恒为「0 KB/s」、带宽估计为空(真机《蜘蛛侠:英雄无归》UHD 原盘,快进、换轨都是 0) |

| P20 | AetherEngine+ForwardBuffer.swift(新)、AetherEngine.swift、Video/HLSVideoEngine.swift、Video/HLSSegmentProducer.swift、Video/SegmentCache.swift、Native/SoftwarePlaybackHost.swift、Native/SoftwarePacketReadAhead.swift | 新增 setForwardBufferDuration(_:):运行中把前向缓冲窗口放大到「多少秒内容」(按本场分段计划的平均段长换算段数),当前生产者、分片缓存、之后因跳转重建的生产者一起按新值;生产者停泊中途按新窗口重算目标(只往低处调,放大即放行);值写回载入选项,换音轨等原地重建沿用。磁盘仍受开播时算好的留存预算约束(min(2 GiB, 剩余空间 1/4),高码率片子窗口装不满时由磁盘停泊兜底)。软件通路同一个接口放大读前的秒数(读前时长改成运行中可调,之后跳转新建的读前也沿用) | 上游窗口装载时定死,默认 10 段(约 40 秒)。线路比片子码率慢时窗口一满生产者就停,用户暂停「攒一会再看」也只能攒 40 秒(2026-09-28《哪吒》:外网 54 Mbit/s 放 76 Mbit/s 原片)。App 在真卡住且当秒加载速度低于码率 9 成时放大到 3 分钟(用户定:再长,暂停攒满后不看了白下的流量太多),线路够快时不放大 |

| P21 | AetherEngine+Subtitles.swift、AetherEngine+SubtitleOCR.swift、AetherEngine.swift、Subtitles/SubtitleDrainStopReason.swift | 内封字幕的旁路预读(上游 #151,读到播放点后 75 秒的第二条连接)只在图形字幕的文字识别在跑时才跑;文字识别只在系统真要画原生字幕轨(画中画 / 隔空播放 / 外接显示)时才启动,进画中画那一刻启动、识别出一段后「取消再选中」一次原生字幕轨冲掉系统缓存的空字幕窗口,退出画中画就停 | 旁路是第二条连接,在 MKV / TS 上会把音视频字节整份再下一遍:开着内封字幕全程流量翻倍(模拟器实测 69.5 → 140 Mbit/s),选图形字幕还会因为文字识别把预读拉到 270 秒,UHD 原盘每选一次多下约 2 GB。平时字幕只靠主力通路 / 软件通路顺带收下的包(#112,覆盖播放点后约 40 秒):起播、换字幕、跳转后字幕都照常上屏,图形字幕按时消失 | | P22 | Demuxer/SourceByteCache.swift(新)、Demuxer/AVIOReader.swift、IO/HTTPDiscIOReader.swift、PlayerState.swift、AetherEngine.swift、AetherEngine+Prewarm.swift | 片源字节缓存:LoadOptions.sourceCacheKey 把本次取流地址登记到宿主给的稳定键上,HTTP 读取器与光盘读取器从源站收到的每一块都按文件偏移写进本机临时目录里的稀疏文件;只在「本来要为这个位置新发请求」的分支(没有连接、远跳、回跳)先查缓存;重新打开时缓存里有文件头就当预热数据接管、一个请求都不发;连接报的文件大小对不上就整份作废;全进程预算 min(1 GiB, 可用空间 1/8),按 1 MiB 块的最近使用淘汰并打洞还空间;关播放器不清(退出再进同一部片、断线重连换了引擎实例都接得上),块丢光的片源整条删、新片源按当时可用空间重算预算,App 启动时清掉上次的残留(跳过本进程正在用的) | 换音轨、回前台都是整场重建,攒好的前向缓冲整段作废重下(MKV 换一次音轨多下约 35 秒的量;MP4 还要重下文件头索引,大片 26 MB、1.2 秒);往回跳进分片缓存、段后有空档时生产者从跳转目标重启,已缓存那几段的源字节再读一遍(UHD 原盘多下 460 MB)。改后换音轨、回跳都从本机拿:原盘换音轨从缓存供出 386 MB、0.65 秒恢复播放,MKV / MP4 换音轨 0.11 秒;暂停切后台被拆后回前台重建从缓存供出 373 MB;断线重连的新引擎实例、退出再进都从缓存起播(退出再进从建引擎到出画 1374 → 359 毫秒、零下载);光盘镜像回跳命中;缓存内容与 NAS 原文件抽样逐字节比对一致 | | P23 | AetherEngine+ForwardBuffer.swift、AetherEngine.swift、Video/HLSVideoEngine.swift、Native/SoftwarePlaybackHost.swift、Native/SoftwarePacketReadAhead.swift | 新增 setPrefetchSuspended(_:):换封装通路把生产者前向窗口压到 0(只产 AVPlayer 真来要的段),软件通路让读前停在已读到的地方,AVPlayer 直连服务端流时把预读压到 1 秒;状态记在引擎上,会话重建后照样停着 | 用户定的暂停策略:计费网络(蜂窝、个人热点、低数据模式)上按暂停就连下载也停,暂停着不看了就不会白下;Wi-Fi 上照常攒。模拟器模拟计费网络:放大窗口后暂停 40 秒只多下 34 MB(在途的一段加 AVPlayer 自己要的一两段),不计费时同样操作会攒到约 1 GB | | P24 | Native/SoftwarePathEscalation.swift、Native/NativeAVPlayerHost.swift | 换封装通路的 AVPlayer 报「解不了」时,外层是 AVFoundationErrorDomain(-11833 找不到解码器、-11821 解码失败)的也按片源媒体判决处理,转到软件通路在本机 CPU 上解码(effectiveErrorDomain:底层带 CoreMedia 错误、或外层就是这两个解码判决码) | 原来只认外层是 CoreMedia 的失败,这类直接报错;模拟器上 4K 10 bit HEVC 就是 -11833(底层 -12906),App 以前靠 MPV 接住。MPV 移除后(2026-09-28)由引擎自己兜,真解不了(软件通路也拒绝,如杜比视界 P5)才由 App 改走服务端流 | | P25 | AetherEngine+StorageBudget.swift(新)、PlayerState.swift、AetherEngine+Loading.swift、Video/HLSVideoEngine.swift、Video/HLSVideoEngine+LiveReopen.swift、Video/SegmentCache.swift、Video/MP4SegmentMuxer.swift、PlaybackErrorInfo.swift、Demuxer/SourceByteCache.swift、Native/SoftwarePlaybackHost.swift | 新增 LoadOptions.backwardBufferSegments(分片缓存后方窗口可设,默认 20、最少 2);写分片遇到 ENOSPC(封装写临时文件、落缓存)记下「存储已满」,泵失败时报新的错误类别 storageExhausted(原来只认建分片目录失败,其余报成笼统的「音频无法封装」);各处读临时卷剩余空间收到一个入口,另加两个测试钩子(假装剩余空间、模拟写满) | 手机存储快满时分片写不进去,起播直接失败,App 只好在可用空间低于 512 MB 时改走服务端流(真机《他是谁》可用 197 MB 时起播失败)。现在宿主按剩余空间收小前后窗口照样用自研引擎;播放中写满时宿主收到 storageExhausted,收到最小窗口原位重开,而不是当成解不了换播放器 | | P26 | Disc/DiscReader.swift、Disc/DiscDirectory.swift | DVD 目录(VIDEO_TS 文件夹)经 HTTP 直推:DiscReader.wrap 按目录里有没有 VIDEO_TS/ 分流到新增的 wrapDVDFolder;选标题、读片长章节语言的逻辑从镜像分支抽成 dvdTitleSet 两边共用,标题集的 VOB 按分段顺序用 MultiFileConcatIOReader 首尾相接(与镜像里连续的扇区是同一段字节,P13 的 cell 折叠表与时间表原样可用) | 目录分支原来只认蓝光(P5)。片库里的 DVD 目录(《情事》《哪吒闹海》)服务端当「原文件」给,取的是个文件夹,一律 404、App 只好降级;现在服务端按目录清单给(disc-direct-play.md §2.7),引擎按目录读 | | P27 | Native/ReadStarvationGuard.swift(新)、Native/SoftwarePlaybackHost.swift | 软件通路读源时挂一个看护:音频已走在已送入的最后一包前面(AudioLookaheadPolicy 判为该停下缓冲)而读取还卡在网络上,就暂停音频时钟,等读取返回后照原逻辑恢复 | 读源阻塞在网络上时泵线程出不来,原有「断粮就停钟」的判断根本轮不到执行,时钟空转:画面冻住、进度照走,宿主的「长时间没进展」看门狗也就永远不触发。故障注入实测(MKV 播放中断网 60 秒 / 一直断网):修前进度空跑到 211 秒、一直断网也不报错;修后 15 秒无数据触发重连,恢复后接着播,一直断网落到错误页 | | P28 | Native/NativeAVPlayerHost.swift | VOD 下 P2 的「提前开播」真正生效:等待期间的见证改为每 0.1 秒看一次缓冲,过 1.5 秒线就 playImmediately(直播保持上游只记录不动手);每次跳转重新武装一次 | P2 只在进入等待的那一刻看一眼缓冲,那时总是空的,于是从没生效过。真机 UHD 原盘起播时缓冲已 3.65 秒、AVPlayer 还多按了 0.6 秒;4K60 往回 5 秒、往前 20 秒的跳转目标都在本机分片里,仍各等 1.4–1.5 秒。2026-09-30:采样间隔 0.1 → 0.025 秒(vodStartWitnessIntervalSeconds,宿主可改):从头播时首个分片一到缓冲就过线,原来平均还要干等半个间隔 | | P29 | IO/HTTPDiscIOReader.swift | 原盘读取器单块预取:顺序读到满窗口(8 MB)后,处理当前块的同时把下一块请求出去;跳走了在下次取数时撤掉,预取失败回到原来带重试的取数。另加仅调试构建的测速日志(等锁、取数、写片源缓存) | 原来「取一块 → 处理完 → 再取」完全串行,每 8 MB 约 170 ms(≈ 48 MB/s),其中约 40 ms 是往返与处理的空档;起播要攒的两个分片(~53 MB)、缓冲外跳转后的首个分片都卡在读源速度上 | | P30 | Native/SoftwarePlaybackHost.swift | P17 跳转停钟的 0.6 秒兜底到点时,解码器若还没解出落点那一帧(跳帧门槛还在)就接着停,上限 20 秒;放开时记一行真实停钟时长 | DVD 时间表粗(《哪吒闹海》43 秒一格),跳转落在目标前几十秒,0.6 秒一到时钟就走:画面冻 2–3 秒、进度照走,追上时一次丢 56 帧,宿主的跳转计时却只记了 165 毫秒 | | P31 | Decoder/SoftwareVideoDecoder.swift | 追落点时少干活:离落点超过 1 秒的包让解码器跳过不被参考的帧(AVDISCARD_NONREF),离落点超过 0.25 秒解出的帧不再去隔行、直接丢 | 目标之前的帧一帧都不显示,原来却全解、全去隔行(上传 GPU、按场出两帧),DVD 追 23 秒内容用了 3 秒 | | P32 | Demuxer/SourceByteCache.swift | 片源字节缓存(P22)的共享实例改为在后台串行队列上写盘、记账与超预算淘汰:write 把数据交给队列就返回(积压超过 64 MB 的新写入不缓存);淘汰只在记账时持锁,打洞放在锁外。AetherEngine.sourceByteCacheWritesInBackground 可关(真机新旧对照用);测试自建的实例仍同步 | 真机(iPhone Air)实测缓冲外跳转后,读取器把第一块数据写进缓存要 1.2~2.6 秒(跳到几个 GB 之外,稀疏文件第一次在远处落盘很慢),取数线程一直等着:UHD 原盘 +600 秒跳转 3.8 秒里有 2.6 秒是它(播放体验打点定位,docs/design/playback-qoe.md) | | P33 | Video/HLSVideoEngine.swift、Video/HLSVideoEngine+SegmentPlanning.swift、Demuxer/SourceByteCache.swift | 点播换封装的分片目标时长 4 → 2 秒(AetherEngine.vodSegmentTargetSeconds,宿主可改,夹在 1~6 秒);宿主按比例放大前后窗口段数,缓冲时长不变。直播标准档、量不出关键帧间隔的均匀切分仍按上游 4 秒(步长短于 GOP 会切出没有关键帧的空段);长 GOP 的片子仍按关键帧间隔切 | AVPlayer 要等一整段产出、送达才开画:续播 / 缓冲外跳转落点那一段 4 秒 UHD 有 26~50 MB,光产出就 0.5~0.7 秒;提前开播(P28)后第二段跟不上,首帧后约 0.8 秒卡一下。真机 6 部 × 2 轮交替对照(与 P32 同批):首帧中位 / p90 1494 / 1755 → 1305 / 1420 毫秒,缓冲外跳转 706 / 1425 → 580 / 984,开播后卡顿 2 次 → 0 | | P34 | Demuxer/Demuxer.swift | MP4 / MKV 里第二条起的 TrueHD(容器已声明采样率与声道)在 find_stream_info 期间同 P12 暂当附件、探测完放回;第一条照常探。AetherEngine.parkSecondaryTrueHDDuringProbe 可关(真机新旧对照用) | 探测只差「采样格式」一项,要解出一帧才有;《变形金刚4》第二条(中文)TrueHD 解不出,探测读满 50 MB 预算、0.76 秒。TrueHD 一律经音频桥接,桥接自己开解码器、按解出的帧配重采样。真机验证切到这条音轨照常出声;对照结果见 docs/design/playback-qoe.md §9.3 | | P35 | Native/SWClockAnchorPolicy.swift、Native/SoftwarePlaybackHost.swift、AetherEngine.swift | 软件通路点播片源的容器起点明显不为 0(超过 2 秒)时,装载即把它预设为 session zero:起播点按「从 0 算的内容时间」加上起点再定位、锚时钟,发布的时间减去起点;首样本重锚叠在预设值上;光盘章节跳转的基准扣掉已折进 session zero 的部分 | 原来只在首样本晚于起播点时才得出 session zero:《戴珍珠耳环》VC-1 原盘 raw 从 600 秒起,续播到 600 时整条时间轴按 raw 发布——续播点差 600 秒、片尾 600 秒够不着,跳到 600 秒之前落在第一个包之前、时钟等不到画面永远卡住(真机语料每批必现)。主力通路按 AE#270 以容器起点为 0,两条通路口径对齐 | | P36 | Video/KeyframeSnapPolicy.swift(新)、Video/HLSVideoEngine.swift、AetherEngine.swift | 主力通路点播跳转按代价吸附关键帧:分片计划用上可信关键帧表(MKV Cues / MP4 stss)时留下关键帧时间与「每秒逐帧解码耗时」(帧率 × 按 4K 折算的像素 × 4.5 毫秒),宿主跳转时估精确落点要解多久,超过 0.2 秒就改落到最近的关键帧后 0.2 秒(不超过到下一关键帧的一半);往前跳不落到起点及其之前、往回跳不落到起点及其之后。AetherEngine.seekSnapDecodeBudgetSeconds ≤ 0 关闭 | 零容差跳转让 AVPlayer 从前一关键帧逐帧解到落点:低码率 4K60 WEB(《抓特务》关键帧最远 10 秒)缓冲内跳转 1.2~2.3 秒。真机对照缓冲内中位 541 → 215、p90 2053 → 665 毫秒,《抓特务》全部 ≤ 0.37 秒;B 帧 MP4 索引与播放轴差几帧,落在关键帧 + 0.01 秒仍会回退解码,故落点后移 0.2 秒。用户 09-29 拍板按代价自适应吸附 | | P37 | Demuxer/Demuxer.swift、Video/HLSVideoEngine.swift | 容器没声明码率(原盘 / 光盘镜像的 MPEG-TS)时按片源总字节 × 8 ÷ 时长估平均码率,填 HLS 的 AVERAGE-BANDWIDTH / BANDWIDTH | 原来兜底 25 Mbit/s、峰值声明 50 Mbit/s,UHD 原盘一段 2 秒 12~15 MB 超出声明,AVPlayer 报 -12318 Segment exceeds specified bandwidth(《黑豹2》估出 51 Mbit/s,声明 102 Mbit/s 后不再报) | | P38 | Video/AnnexBSampleConverter.swift(新)、Video/HLSVideoEngine+SegmentPlanning.swift、Video/HLSVideoEngine.swift、Video/HLSSegmentProducer.swift、Video/MP4SegmentMuxer.swift、Video/HLSVideoEngine+AudioRoute.swift | 点播 HEVC 的 Annex B 样本(MPEG-TS / 原盘)改由封装层自己转成 4 字节长度前缀并保留带内 VPS/SPS/PPS,配置记录给 hvcC,movenc 不再转换 | movenc 对 hvc1 转换时按 filter_ps 剥掉样本里的参数集,init 只剩片头一套。《黑豹2》118 秒、345.7 秒两次换 PPS:续播到片中或从头播过换点后,切片按旧 PPS 解,Mac 系统硬解报 Cannot Decode、真机只有声音没画面(续播 22 秒后靠跳转重建才出画)。样本里留着参数集后 Mac 离线硬解 48/48 帧、真机续播 1.7 秒出画并连续播过换点;其他 TS 原盘 / HLG / 4K60 TS 回归正常 | | P39 | Demuxer/SourceRangePrefetcher.swift(新)、Demuxer/SourceByteCache.swift、AetherEngine+Prewarm.swift | 新增 AetherEngine.prefetchSourceRanges(url:cacheKey:ranges:httpHeaders:):按宿主给的字节范围(按 1 MiB 块对齐、分 4 MiB 一段)拉下来写进片源字节缓存(P22),已在缓存里的跳过;沿用预热的源站名额规则(源站只允许一个请求时直接放弃,暂时没名额稍等、3 秒拿不到就放弃);写入前等缓存积压降到 32 MiB 以下(pendingWriteBytes),免得超过 P32 的积压上限被丢 | 刷片(docs/design/reels.md)要在上一条播放期间把下一条「从原片中间起播」要读的文件头、索引、起点后约 4 秒先下到本机,位置由服务端从容器索引算好给出;#551 的预热只认从 0 开始的文件头、按带令牌的完整地址存内存,接不住片中的几段。模拟器实测(本机临时后端、540p H.264):预取过的下一条从滑到出画面 82 毫秒,没预取的首条 1398 毫秒 |

| P39 | Video/KeyframeSnapPolicy.swift、AetherEngine+Loading.swift、AetherEngine.swift | 宿主起播(续播)时起播点按代价吸附到前一个关键帧:精确落点要逐帧解超过 0.05 秒(startSnapDecodeBudgetSeconds)就从关键帧后 0.2 秒开播,只往前、不跳过没看的内容,最多往回让 10 秒(关键帧隔得更远的片子照旧精确落点)。一次性开关 snapsNextStartToKeyframe 由宿主在起播的那次 load() 前设,入口取走;换音轨、回前台等引擎自己的重建不吸附、原位接上 | 起播定位与跳转一样是零容差,AVPlayer 拿到首个分片后从关键帧逐帧解到续播点才出第一帧:《金色》4K60 续播白等 1.2 秒、《权力的游戏》0.9 秒。真机交替对照(12 部 × 2 轮,续播 600 秒)首帧中位 941 → 573、p90 2432 → 1187 毫秒,12 部里 11 部变快、1 部持平 | | P41 | Demuxer/Demuxer.swift | 探测流(find_stream_info)打一行耗时与读量的诊断日志 | 起播「探测」一段慢在读数据还是在解码要能一眼看出:杜比视界 P5 的 4K 片 hvcC 不带参数集(23 字节),FFmpeg 只能把第一帧 IDR 整帧软解(真机约 140 毫秒、读 1.1 MB)才拿到像素格式;带参数集的片子这一步 0~2 毫秒 | | P42 | Demuxer/SourceByteCache.swift、AetherEngine+Prewarm.swift | 片源字节缓存(P22)跨启动保留:共享实例落在 Caches,每个片源按键的哈希存 .bin(稀疏数据)与 .idx(记账),记账随写入防抖落盘、淘汰时先落记账再打洞;某个片源第一次被访问时按需恢复;App 启动时整理(孤儿、30 天未用、总量超运行预算一半按最近使用整条删)。persistsSourceByteCache 可关(真机对照用) | 真实使用里续播占七成多,续播要读的文件头、索引与续播点那一段正是上一场下过的字节,原来每次启动都清掉重下。真机交替对照(10 部,每组先热身一遍、杀掉 App 再冷启动续播 600 秒)首帧中位 598 → 358、p90 817 → 410 毫秒,10 部全部变快:尾部 moov 的 MP4 打开容器不再下 2~11 MB 索引(741 → 410、817 → 447),长 GOP 的《权力的游戏》首个大分片也在本机(1208 → 384) | | P43 | Demuxer/AVIOReader.swift、AetherEngine+Prewarm.swift | 预连:AetherEngine.preconnect(url:) 在两个取源会话(文件头的 persistentSession、尾部预读的 chunkSession)上各发一个 HEAD,把 TCP / TLS 握手提前做掉 | 经反向代理的 HTTPS 源站,每次播放的首个取流请求中位 83 毫秒,其后同连接的冷区域请求 60 毫秒;宿主在取流地址还没拿到(起播协商在路上、详情页打开)时就能先连 | | P44 | AetherEngine+StorageBudget.swift | 可用空间查询(temporaryVolumeAvailableBytes)带 10 秒缓存,改为公开,宿主与引擎共用一份 | 「重要用途可用」在真机上每次约 17 毫秒,起播一次要查好几回:宿主定落盘计划(原来在主线程上、会话回来到装载引擎之间,调试包还查两次,共 33 毫秒)、分片留存预算(会话启动的关键路径上)、片源字节缓存预算(持着缓存的锁)。宿主点播放时从后台先查一次,之后都命中 |

| P45 | Video/HLSVideoEngine.swift、Demuxer/SourceByteCache.swift | MKV 索引预热(cue prewarm)跳到起播点而不是片中间;cuePrewarmTargetsStart 可关(真机对照用) | 任何一次跳转都会让 libavformat 加载延后的 Cues,跳到哪都一样拿到索引;跳完读到的那一簇正是生产者接下来要读的。原来跳片中间,那一簇是白读。单独对照(没有 P42 时)无差,大头是读 Cues 本身;有了 P42,续播时文件头、Cues 与续播点都在本机,片中间那一簇却多半早被淘汰,真机这一步仍有 45~70 毫秒是它的网络冷读 | | P47 | AetherEngine.swift | 新增 hostManagesAudioSessionCategory:宿主自己设音频会话的类别、策略、多声道并负责激活时,引擎建实例不再重设类别(MovieClaw 默认开,App 点播放时以「长视频」策略设好类别、声明多声道并激活) | 引擎每建一个实例就用默认策略重设一遍类别:把宿主要的「长视频」路由策略改掉;会话已激活时换策略要重新协商路由;装载还要先等这次跨进程调用(真机约 15 毫秒)。真机交替对照(从头播 10 部 × 2 轮)首帧中位 397 → 364、p90 543 → 447,开播 502 → 459 毫秒,7/10 部变快。注意:播放期间实际生效的路由策略因此从「默认」变成 App 本来指定的「长视频」,隔空播放要真机确认;做 tvOS 时按上游 #24(HDMI 声道锁定)重新评估由谁激活 | | P46 | Demuxer/SourceByteCache.swift | 片源字节缓存淘汰时,文件头 8 MiB、文件尾 32 MiB 的块(moov、Cues、SeekHead 所在)排在最后;这类块总量超过「预算四分之一,至少一个片源的头尾」时先按最近使用丢最旧片源的 | 它们只在打开时读一次,按最近使用总是最先被挤掉:看一个多小时高码率片,1 GiB 预算只装得下最后几分钟,下次续播(P42)就得重下文件头和索引。单测覆盖「长时间播放后头尾还在」与「元数据区超上限先丢最旧的」 | | P49 | Demuxer/MatroskaCuesLocator.swift(新)、Demuxer/AVIOReader.swift | Matroska 索引提前取:文件头第一块数据一到就按 SeekHead 找出 Cues 的位置(MatroskaCuesLocator,只读 Segment 开头的第一个 SeekHead,截断时只答「还不够」),Cues 在文件头 4 MB 之外、尾部预读 64 KB 之外、离文件尾不超过 2 MB 时,与开容器并行把「Cues 到文件尾」一次取回,装成与文件头 / 文件尾同样的常驻片段(在途时读到这段就等它;开流与索引预热结束后第一次读不到就放掉),也写进片源字节缓存;字节缓存里整段都有就直接装上;开头的 SeekHead 只指向文件尾附近的完整目录时(mkvmerge 预留位置放不下完整目录的写法)不提前取——试过盲取文件最后 1 MB,真机上要 90~180 毫秒,比按需读(约 37 毫秒)还慢。prefetchesMatroskaCues 可关(真机对照用) | libavformat 把 Cues 的解析推迟到第一次跳转(索引预热),那时才按 SeekHead 去读;mkvmerge 产物的 Cues 离文件尾 7.7 万~93 万字节,落在尾部预读之外,要单独再发一次请求(真机经外网域名每次 40~70、最长 380 毫秒),还会掐掉正在读文件头的连接、之后重连。语料 14 部 MKV 有 10 部如此(片库随机抽 40 个 MKV,定位结果与 NAS 上逐个核对的 Cues 位置全部一致,7 个超出尾部预读) | | P50 | Demuxer/SourceByteCache.swift | 片源字节缓存每块除主段外另记一段暂存范围(只在内存里):新写的、主段、暂存三者相接的并起来,最长的当主段(读、落盘只认它),次长的暂存;预算与淘汰把暂存算进去、整块打洞时一起扣。sourceByteCacheKeepsSpareRuns 可关;removePersistedSourceByteCache() 供实验台热身前清场 | 原来每块只记一段、不相连时「留长的」:尾部预读先写了文件最后 64 KB,moov / 索引再从最后一块中间顺序写过来时,每一小段都比已记的短,全被丢账,下次打开(P42)整块重下。真机跨启动续播 12 部里 7 部在开流时仍要向文件尾最后一块发请求。交替对照(10 部 MP4,热身前清空缓存):首帧中位 303 → 241、p90 499 → 333 毫秒,受影响的开容器 48~128 → 11~21 毫秒 | | P51 | Demuxer/SourceByteCache.swift | 启动整理跨启动缓存(trimPersisted)超过保留上限时,先从旧到新缩:每份丢元数据区(P46 的文件头 8 MiB、文件尾 32 MiB)以外的块,按块的最近使用从旧到新丢、丢够超额就停(续播点附近最后看的那段留着):先写缩过的记账(保留原修改时间),再给丢掉的块打洞;缩的那一步持缓存的锁、锁内再确认这份没被装载打开(恢复也在这把锁里,免得恢复出来的那份把刚打成洞的块当成数据);缩完仍超才从旧到新整条删。sourceByteCacheTrimKeepsMetadata 可关(真机对照用) | 保留上限是运行预算的一半(至多 512 MiB),原来超了就按最近使用整条删:看一会儿高码率片缓存就涨过上限,下次启动这一份(正要续播的那部)连 moov / Cues 一起被删,续播全部重下。真机:4K60 片热身 18 秒写了 398 MB,下次启动整理赶在装载前跑完,开容器重下 10 MB 的 moov(415 毫秒,首帧 1047);整理与装载谁先是竞速 | | P53 | Demuxer/AVIOReader.swift | 读到在途的提前取(尾部预读 64 KB、P49 的 Matroska 索引、P54 的 MP4 moov)时,原来最多只等「首字节耗时 × 2」(下限 0.25、上限 5 秒);现在一旦开始往回送(提前取的回调每收一批字节就记一次时刻),截止时刻跟着最近一次到货往后推,停下不动超过 1 秒(且超过原上限)才放弃。waitsOnProgressingPrefetch 可关 | 原上限只回答「首字节该到了没有」,假定字节一到整段转眼就齐:快线路成立,慢线路不成立——6 Mbit/s 时 1.2 MB 的索引要传 2 秒以上。等不及的那次读在同一位置另发请求,把同一段再下一遍(两个请求平分同一条慢线路,谁都更慢),还把正在读文件头的连接换掉,回到文件头时又得补取。模拟器 6 Mbit/s 共享限速冷起播(faultlab.py slow-start):4K MKV 索引下了两遍、文件头连接只下了 0.4 MB 就被换掉;改后 6 个请求变 4 个,首帧 18.6 → 13.2 秒。线上外网那次播放的服务端日志是同一模式(开播 1 秒内掐断文件头连接,随后 +1、+3、+7 秒陆续补取) | | P54 | Demuxer/MP4MoovLocator.swift(新)、Demuxer/AVIOReader.swift | MP4 / MOV 尾部 moov 提前取:文件头一到就按顶层盒子(ftyp 开头,逐个跳过 free / wide 等)找到 mdat,mdat 之后到文件尾不超过 16 MB 时,与开容器并行把这段一次取回,走 P49 同一套常驻片段、在途等待(P53)与字节缓存。moov 在 mdat 之前(faststart)、分片 MP4、看不懂的布局一律不取。prefetchesMP4TailMoov 可关 | 片库超过 100 MB 的 MP4 抽样 60 部有 34 部是 mdat 在前、moov 在后(moov 0.5~3 MB,最大 10.7 MB,都是最后一个盒子)。mov 解复用器读到 mdat 盒子头就跳去文件尾读 moov:读取器只能把正在读文件头的连接挪过去,读完回到 mdat 开头时文件头只留下连接被挪走前到手的一点,剩下的按 4 MB 整块补取,慢线路上整块在 4 秒限时内下不完、作废重来。模拟器 6 Mbit/s 冷起播 1080p MP4(moov 1.9 MB):请求 5 个变 3 个,文件头连接一次不断,首帧 8.0 → 5.7 秒。单开它在「moov 大、第一个分片小」的片子上反而更慢(两段平分线路),须与 P56 一起开 | | P55 | Demuxer/AVIOReader.swift | 读取器按每代连接从首字节起的平均到货速度(收满 256 KB 且超过 0.5 秒才算;推送式连接不看消费快慢,就是线路给它的速度)与整段提前取的耗时估线路速度;回跳要走旁路整块补取(4 MB、限时 4 秒)时,若按这个速度留两成余量整块到不齐(约低于 10 Mbit/s),就不取,直接在读的位置重连流式读。skipsDetourOnSlowLink 可关 | 旁路补取是同步的:整块到齐才交出第一个字节,到限时还没齐就整块作废再重连。快线路上一块转眼就到,省下的是一次重连(#69 防解析来回跳时的重连风暴);慢线路上注定作废:6 Mbit/s 模拟器实测回到文件头时这一块下到 2.6~2.9 MB 被丢掉、白等 4 秒。P53、P54 之后冷起播多半碰不到这条路,它兜的是看不懂布局的片源与慢线路上往回拖动 | | P56 | Demuxer/AVIOReader.swift | 冷打开时文件头的第一个请求只要 512 KB(调用方指定了范围时照它),不再一开始就要 32 MB;容器索引提前取(P49 的 Cues、P54 的 moov)在途时,文件头连接到了范围尽头先不做超前续连——解复用器真读到窗口尽头时读取循环照常在那里续连。从头播(打开配置新增 playbackStartsAtHead,引擎按有没有续播点填)时,索引、文件尾这些常驻片段一交出字节就把文件头续上——开流读完索引后引擎要规划分片、等 AVPlayer 来要第一个分片,零点几秒不从窗口读,只靠「读窗口时顺手续」会让文件头连接闲着;续播不续(马上要跳到续播点)。prioritizesIndexPrefetch 可关 | 首帧之前必须下的只有索引和第一个分片两段。并行时两段平分线路,索引晚到一倍,而文件头多下的字节首帧用不上:模拟器 6 Mbit/s 冷起播《大江大河》MP4(1.3 Mbit/s,moov 3.2 MB,第一个分片约 0.6 MB),P54 让 moov 与文件头并行后 moov 下了 5 秒、文件头白下 3 MB,首帧 9.3 秒,比旧做法(掐断文件头、moov 独占线路,5.4 秒)还慢。改后两轮中位 9.3 → 5.8 秒,1080p MP4(moov 1.9 MB)5.8 → 4.2 秒,4K MKV(第一个分片 8 MB,两段本来都要下完)持平 13.2 秒。快线路上多出的一次续连与解析重叠 | | P57 | Video/SegmentCache.swift、Video/HLSSegmentProducer.swift、Video/VideoSegmentProvider.swift、Video/MP4SegmentMuxer.swift、Network/HLSLocalServer.swift、Native/NativeAVPlayerHost.swift | **已由上游 #721 收下(7.32.0),改用上游实现与 LoadOptions.progressiveSegmentDelivery;P28 的开播见证经装载契约读这个选项。**点播分片边产出边送:生产者开新段时把正在写的暂存文件登记给分片缓存,AVPlayer 来要这一段时本机服务器不再等整段写完,按分块传输先送已写出的字节、跟着文件增长往下送;封装器在分片内每 0.5 秒刷出一个片段(原来每 8 秒)。段写完(改名进缓存)即收尾,被生产者放弃(跳走、出错)就断开连接让 AVPlayer 重新请求。边送的每一块也算一次取数(#65 看门狗不再把长时间的边送当成没在取);起播见证(P28)边送时最长等 45 秒(原 5 秒)。servesSegmentsProgressively 可关 | 分片必须从关键帧切,长 GOP 片的第一个分片很大(4K MKV 从头播约 8 MB、续播吸附到前一个关键帧后约 20 MB),原来 AVPlayer 要等整段产出才拿到第一个字节,慢线路上首帧就是整段的下载时间。Mac 实验(命令行 AVPlayer + 按线路速度产出的本机服务器):AVPlayer 边收边解,收到完整的片段就能出画,片段 0.5 秒最好;把长 GOP 拆成非独立小分片的方案被否(去掉 INDEPENDENT-SEGMENTS 后跳转报 -12710、GOP 中间的小分片当不了跳转落点)。模拟器 6 Mbit/s:4K MKV 从头播首帧 13.8 → 3.2 秒(开播 5.3 秒),续播 40.5 → 6.4 秒(开播 12.9 秒);快线路三轮交替,引擎就绪到首帧中位从头播 665 → 750、续播 766 → 789 毫秒,在抖动内(单次 374~809);故障注入 16/16 | | P58 | Demuxer/MatroskaCuesLocator.swift、Demuxer/AVIOReader.swift、Demuxer/Demuxer.swift、Video/HLSVideoEngine.swift、AetherEngine.swift、AetherEngine+Loading.swift、PlayerState.swift | 服务端给的 Matroska 精简索引(LoadOptions.matroskaCues → MatroskaHostCues:原 Cues 在文件里的位置 + 只含视频轨索引点的整个 Cues 元素,校验正好是一个完整的 Cues 元素才收):文件头一到、SeekHead 登记的 Cues 位置与之一致时直接装成常驻片段,不提前取原索引;装的是精简版时它在自己的范围里压过窗口与文件头尾(同一个元素不能半截原字节半截精简版),读到它的末尾就短读返回(凑满这次读会在精简版后面另起连接读原索引余下的字节,还掐掉文件头连接)。只挂在主播放的打开配置上(探测兼会话、HLS 生成器兜底打开与卡死重开),读字幕的旁路照旧读原索引;开头只有次级 SeekHead、核对不了位置的文件不用。usesHostMatroskaCues 可关 | mkvmerge 给每条字幕轨的每个事件都写索引点,字幕多的片子 Cues 很大(片库 79 部 MKV:中位 68 KB、九成在 560 KB 以内、最大 4.2 MB;62 条字幕轨的 4K 片 1.23 MB),P49 也得整段下完,6 Mbit/s 下 2.4 秒全压在首帧前。主播放的解复用器只用得到视频轨的索引点(libavformat 建索引只读 CueTime / CueTrack / CueClusterPosition,跳转按视频流),服务端挑出来原样重新编码只有 14.6 KB。模拟器 6 Mbit/s 冷起播这部片(两轮中位,服务端下发由调试开关 -mcDebugMatroskaCues 注入):从头播首帧 3.46 → 1.39 秒(引擎就绪到首帧约 0.8 秒),续播 6.48 → 4.47 秒;第一版没有短读时只省到 2.39 秒 | | P59 | Network/HLSLocalServer.swift、Video/SegmentCache.swift | 点播的媒体播放列表也写 #EXT-X-INDEPENDENT-SEGMENTS(原来只写在主播放列表里)。declaresIndependentMediaSegments 可关 | AVPlayer 只认媒体播放列表里的这个声明:缓冲外跳转后它先要目标前 3~5 段自己找关键帧(模拟器实测:目标在第 58 段先来要第 53 段,目标在第 244 段先要第 241 段),生产者只好从那里重启,把目标前 15~23 秒的内容整段产出完才到落点。点播分片都在关键帧处切(vodCutter 只在关键帧开新段,#92),声明属实,与主播放列表那一行是同一个事实。模拟器 6 Mbit/s、4K MKV 从第 600 秒续播后跳转:往后跳 28.3 → 8.5 秒,往回跳在观察窗口内一直没落地 → 9.5 秒;改后 AVPlayer 直接要目标段。不限速时往回跳 1.17 → 0.47 秒(旧组先要早 4 段),往后跳持平(1.06 / 1.07 秒,旧组只早要 2 段)。模拟器上别的格式走软解、不经 AVPlayer,真机上常见格式都走这条通路 | | P60 | Decoder/VideoDecoderTypes.swift、Decoder/ColorDescription.swift、Decoder/HardwareVideoDecoder.swift、Video/HLSVideoEngine.swift、Video/MP4SegmentMuxer.swift | iPhone / Mac 上按 BT.709 曲线显示的 SDR(含未标注)改按 sRGB 上屏:换封装通路在 init.mp4 写 colr nclx(传递函数 13,原色/矩阵按 VideoToolbox 的缺省补全);软解帧标 IEC_sRGB;硬解帧看 VideoToolbox 从 VUI 读出的标签,是 709 或没标才换。容器没写传递函数时,HEVC 先开一次解码器读 SPS 的 VUI(不解帧),确认不是 HDR 才改;其余编码没写就不动。tvOS、HDR、杜比视界不动 | 标 BT.709 时系统做场景参考的 Rec.709 转换(约 1.961 gamma),中间调比 Infuse / VLC 亮 8~10 级、发白。《我不是大师》S01E17 对原片解码帧逐像素实测:改前与 1.961 曲线误差 1.95,colr 改 sRGB 后与「码值原样显示」误差 1.46(Infuse 1.33)。AVPlayer 认 colr 优先于 SPS VUI,所以已标 709 的片子同样生效,也所以容器漏写、只在 VUI 标 PQ 的 HDR 必须先读 VUI(LoopbackColrTests 的 hdr-pq 样片)。tvOS 输出的是 Rec.709 信号、曲线归电视,标 709 本就是直通 | | P61 | Disc/DiscReader.swift | 只有 UDF、没有 ISO9660 桥接卷的 DVD-Video 镜像:VIDEO_TS 改经 UDFReader 列出(文件须是首尾相接的一段),之后与 ISO9660 的 DVD 同一套(wrapDVD:选标题、拼 VOB、cell 折叠与时间表) | 原来 DVD 镜像只认 ISO9660,这种盘(第 16 扇区直接是 UDF 的 BEA01)落到蓝光分支、找不到 BDMV,退回把整个镜像当裸文件解复用:《巴尔扎克与小裁缝》引擎片长 8 秒、放的是菜单,正片与大图预告都放不了。改后片长 6364 秒(与 IFO 一致),从 2000 秒起播正常。片库 19 个 DVD 镜像里有 1 个是这种 | | P62 | Video/DolbyVisionRecordAudit.swift、AetherEngine.swift | 容器记录写 P5、VUI 声明 BT.2020 YCbCr PQ/HLG、RPU 又没纠正成 7 / 8(AE#532 读出 5 或读不出)时,这次加载自动按 dolbyVisionHandling = .baseLayerOnly 放 HDR10 / HLG 基础层;开关 presentsBaseLayerOfContradictedProfile5 默认关、MovieClaw 打开;宿主显式选过处理方式时不动 | 发布组把流媒体版的 P5 RPU 原样注进另一版 HDR10 / HDR10+ 基础层(「DV HDR10+ Hybrid」),照记录走 dvh1.05 时系统把 YCbCr 当 IPT 还原,整片偏绿 / 偏紫(用户反馈《惩罚者:最后一击》;《驯龙高手》HDR10 段注入《雷神4》P5 RPU 复现,iPad Pro 真机修前发绿、修后与纯 HDR10 对照一致)。真 P5 的 VUI 一律不写,不受影响 |

(P6 已并入 P5:按主播放列表名选主片是目录读取协议的一个字段。P40 是「MKV 索引预热改跳起播点」的第一版,没有 P42 时对照无收益、撤回过,编号空着;同样的改动在 P42 之后以 P45 重新启用。)

升级上游版本时:在维护分支合并选定的 upstream 提交,逐项核对补丁与测试,再跑 App 三端编译与真实播放回归。MovieClaw 固定依赖经过验证的 fork 提交,不自动跟随 main;流程见 维护说明。