Skip to content

Commit c8d6f8a

Browse files
docs: correct DV routing, CLI flags, and source map against 4.6.3 (#84)
Audited README + docs/{architecture,formats,cli}.md against the actual sources at HEAD (4.6.3) and fixed the drift found: formats.md (Dolby Vision signaling was the biggest miss): - DV routing table no longer collapses P5/P8.1/P8.4 under "dvh1/dvhe". P5 emits a bare dvh1 primary; P8.1/P8.4/P7 emit hvc1 primary + dvvC with dvh1.08/db1p or /db4h in SUPPLEMENTAL-CODECS on DV panels, plain HDR10/HLG base elsewhere. dvhe is never emitted. - AV1+DV corrected: P10.0/P10.1 are bare dav1 with no supplemental, only P10.4 carries the dav1/db4h supplemental. - Documented the SDR-compatible-base profiles (HEVC P8.2, AV1 P10.2) that strip the DV config and play the Rec.709 base on every panel. cli.md: - "Fifteen subcommands" was off by one (sixteen). - hlslive --segments is a required comma-separated list of .ts paths, not a numeric sizing flag; clarified --seconds/--segment-seconds/--disc. - serve --native-subs: the index value is legacy/ignored; the flag drives requestNativeSubtitleTrack + attachAllNativeSubtitleStores, not LoadOptions.prepareNativeSubtitles. - Added the undocumented flags: live --serve-only/--rewind-test/ --gen-highbitrate-seed and customio --audio-only. architecture.md source map: - Added the omitted files: DiscRecognitionCache, HTTPDiscIOReader, MovTextSampleBuilder, NativeSubtitleCueStore, DoviRpuConverter+Probe, Issue65LivelockBreakers. - Added the AetherEngineSMB target subtree and a pointer to cli.md for the aetherctl target. README.md: - Audio bridge row now lists DTS-HD MA. - Documented LoadOptions.nativeRemoteHLS for AVPlayer-native remote HLS. Claude-Session: https://claude.ai/code/session_01XZTEfmztPE8hAdjHdBr9BH Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2784738 commit c8d6f8a

4 files changed

Lines changed: 37 additions & 8 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ A scannable summary; the depth for each row lives in **[docs/formats.md](docs/fo
4242
| Video (HW) | H.264, HEVC, HEVC Main10 via VideoToolbox; AV1 where HW AV1 exists |
4343
| Video (SW) | AV1 (dav1d) without HW, VP9 / VP8, MPEG-4 Part 2 / MPEG-2 / VC-1; bwdif deinterlace |
4444
| HDR | HDR10, HDR10+ (per-frame ST 2094-40), Dolby Vision (P5, P7 as single-layer 8.1, P8.1, P8.4, AV1 P10.x), HLG |
45-
| Audio | AAC, AC3, EAC3, FLAC, ALAC stream-copy lossless; TrueHD / DTS / MP3 / Opus bridge to EAC3 5.1 (default) or lossless FLAC |
45+
| Audio | AAC, AC3, EAC3, FLAC, ALAC stream-copy lossless; TrueHD / DTS / DTS-HD MA / MP3 / Opus bridge to EAC3 5.1 (default) or lossless FLAC |
4646
| Dolby Atmos | EAC3+JOC stream-copied on every route (HDMI MAT 2.0, AirPods spatial, BT downmix) |
4747
| Surround | 5.1 / 7.1 with correct `AudioChannelLayout` |
4848
| Audio-only | `LoadOptions.audioOnly`: lean pipeline, no video machinery, system Now-Playing on tvOS / iOS |
@@ -219,6 +219,8 @@ try await player.load(
219219

220220
Direct ingest covers MPEG-TS with demuxed-audio and packed-audio renditions, in-line AES-128 clear-key decryption, and SSAI ad-pod direct play (versioned init segments, audio re-anchoring, no-cut watchdog). Unsupported encryption / fMP4 playlists surface a typed `HLSIngestError` so the host can fall back. Details in [docs/formats.md › Live ingest](docs/formats.md#live-ingest-aes-128-ssai).
221221

222+
For an upstream AVPlayer can play natively (a standard remote `master.m3u8`, e.g. a Jellyfin live channel), `LoadOptions.nativeRemoteHLS` skips the demuxer probe and the loopback server entirely and hands the URL straight to AVPlayer, which manages the live edge and reconnect itself. Pair it with `isLive: true`.
223+
222224
## Used by
223225

224226
<!-- used-by:start -->

docs/architecture.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Sources/AetherEngine/
131131
│ ├── UDFReader.swift Read-only UDF 2.50 reader (Blu-ray BDMV, including the metadata partition and fragmented-file allocation descriptors)
132132
│ ├── MPLSParser.swift Blu-ray `.mpls` playlist parser (clips, duration, PlayListMark chapters)
133133
│ ├── BDTitleSelector.swift Enumerates Blu-ray playlists as selectable titles (longest first; short menu / decoy playlists filtered)
134+
│ ├── DiscRecognitionCache.swift Memoises `DiscReader.wrap` per URL + title index so disc recognition does not re-run on every subtitle / track switch (load-bearing for remote-ISO track switches, #76)
134135
│ └── DiscInspector.swift Diagnostic mirror of `DiscReader.wrap` for `aetherctl disc-inspect` (titles, chapters, recognition stages)
135136
├── Display/
136137
│ ├── DisplayCriteriaController.swift AVDisplayManager content-rate / dynamic-range hints (native path)
@@ -146,6 +147,7 @@ Sources/AetherEngine/
146147
│ ├── IOReader.swift Public custom byte-source protocol + MediaSource (load(source:) input)
147148
│ ├── DataIOReader.swift Ready-made in-memory IOReader over an immutable Data buffer
148149
│ ├── FileIOReader.swift Seekable IOReader over a local file via FileHandle (multi-GB ISO images)
150+
│ ├── HTTPDiscIOReader.swift Seekable IOReader over a remote HTTP(S) disc image with adaptive read-ahead (the network-ISO counterpart to FileIOReader)
149151
│ └── HLSIngest/
150152
│ ├── HLSLiveIngestReader.swift Public forward-only IOReader ingesting a live HLS upstream (resolver, playlist poller, segment fetcher, companion audio-rendition reader)
151153
│ ├── HLSPlaylist.swift Line-oriented RFC 8216 subset parser (master / media playlists)
@@ -164,6 +166,8 @@ Sources/AetherEngine/
164166
│ └── SampleBufferRenderer.swift SW path: AVSampleBufferDisplayLayer + B-frame reorder, HDR10+ attachments
165167
├── Subtitles/
166168
│ ├── ASSScriptBuilder.swift Reassembles raw ASS event cues + TrackInfo.assHeader into a complete script for whole-file renderers
169+
│ ├── MovTextSampleBuilder.swift Stateless tx3g (mov_text) sample builder for the native legible-subtitle injection path (LoadOptions.prepareNativeSubtitles, #55)
170+
│ ├── NativeSubtitleCueStore.swift Owns the decoded-cue array backing a native mov_text track; the producer drains it per segment cut (#55)
167171
│ └── SubtitleRectText.swift Plain-text + raw ASS event-line extraction from subtitle rects, shared by the inline and sidecar decoders
168172
├── Video/
169173
│ ├── HLSVideoEngine.swift Native path: session orchestrator (start/stop, producer construction + restart, shift handling)
@@ -172,6 +176,8 @@ Sources/AetherEngine/
172176
│ ├── HLSVideoEngine+LiveReopen.swift Native path: live source-loss recovery (capped-backoff reopen on the same timeline)
173177
│ ├── CodecRoutePolicy.swift Native path: DV / HDR / codec routing decisions (track types, CODECS strings, VIDEO-RANGE)
174178
│ ├── DoviRpuConverter.swift Native path: per-packet DV Profile 7 → 8.1 RPU conversion via libdovi (NAL surgery: convert type-62 RPU, drop type-63 EL)
179+
│ ├── DoviRpuConverter+Probe.swift Diagnostic DV-conversion probe (`doviConvertProbe` / `DoviConvertProbeResult`), backs `aetherctl dovitest`
180+
│ ├── Issue65LivelockBreakers.swift Pure backpressure-wedge detection (`BackpressureWedgeDetector`) breaking the VOD HLS scrub-burst livelock (#65)
175181
│ ├── VideoSegmentProvider.swift Native path: playlist-facing segment provider (live sliding window, restart heuristics)
176182
│ ├── HLSSegmentProducer.swift Native path: pump loop reading from Demuxer, feeding MP4SegmentMuxer, cutting fragments at segment-plan boundaries; SSAI program-switch detection + no-cut watchdog
177183
│ ├── H264SPS.swift Hand-rolled H.264 SPS parser (SSAI ad-creative coded dimensions / codec config)
@@ -187,6 +193,19 @@ Sources/AetherEngine/
187193
└── AetherPlayerView.swift Polymorphic surface: hosts either AVPlayerLayer (native) or AVSampleBufferDisplayLayer (SW)
188194
```
189195

196+
The `AetherEngineSMB` product is a separate, opt-in target so its AMSMB2 (LGPL-2.1) dependency never enters the core engine binary. Hosts that need LAN-share playback link it and hand the engine an `smb://` source via the standard `IOReader` seam:
197+
198+
```
199+
Sources/AetherEngineSMB/
200+
├── AetherEngineSMB.swift Product entry point: opt-in SMB2/3 byte source, depends on AMSMB2 (libsmb2); never linked by the core engine
201+
├── SMBURL.swift Parses smb://[user[:password]@]host[:port]/share/path URLs (missing credentials default to guest)
202+
├── SMBConnection.swift Read-only SMB2/3 byte source over one share + path via AMSMB2 (per-read open/seek/close, no persistent handle)
203+
├── ByteRangeSource.swift Random-access read-only byte-source protocol, isolates the network backend from cursor / seek logic for testability
204+
└── SMBIOReader.swift Bridges a ByteRangeSource into the engine's IOReader (blocking read via a happens-before semaphore edge)
205+
```
206+
207+
The `aetherctl` CLI target (`Sources/aetherctl/`) is documented separately in [docs/cli.md](cli.md).
208+
190209
## Dependencies
191210

192211
| Package | License | Purpose |

docs/cli.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ swift run aetherctl smbtest <smb-url> # play a file off an SMB2/3 share via t
2121
swift run aetherctl <url> # alias for serve (backwards compat)
2222
```
2323

24-
Fifteen subcommands plus the bare-URL `serve` alias.
24+
Sixteen subcommands plus the bare-URL `serve` alias.
2525

2626
## probe
2727

@@ -42,7 +42,7 @@ open 'http://127.0.0.1:<port>/master.m3u8' # macOS QuickTime
4242

4343
`--no-dv` forces the SDR / HDR10 route even for a Dolby Vision source (compare the two playlists).
4444

45-
`--native-subs <index>` enables `LoadOptions.prepareNativeSubtitles`, which causes the engine to mux ALL text subtitle tracks as separate language-tagged `mov_text` (tx3g) traks in the init segment (all `disposition:default=0`, none auto-displayed). The `<index>` argument (zero-based among the subtitle tracks reported by `probe`) is passed to `setNativeSubtitleSelected(track:)` after the session starts, so the diagnostic selects that track via the host API. The init segment will carry one `mov_text` trak per text subtitle stream; inspect with `mp4dump` or open the playlist in QuickTime to verify the legible `AVMediaSelection` group enumerates every language. Omit the flag to reproduce the default behavior (no native subtitle traks, muxer output byte-identical to before).
45+
`--native-subs <index>` turns on the native mov_text subtitle path: the engine calls `requestNativeSubtitleTrack()` before `start()` (so the init `moov` declares the tracks), then `attachAllNativeSubtitleStores()` after start. ALL text subtitle tracks are muxed as separate language-tagged `mov_text` (tx3g) traks (all `disposition:default=0`, none auto-displayed). The `<index>` value is legacy and now ignored, kept only for CLI compatibility: every non-bitmap track is always declared, and actual track selection happens via the host API in a full session, not from this flag. Inspect the init segment with `mp4dump` or open the playlist in QuickTime to verify the legible `AVMediaSelection` group enumerates every language. Omit the flag to reproduce the default behavior (no native subtitle traks, muxer output byte-identical to before).
4646

4747
## validate
4848

@@ -84,7 +84,7 @@ Plays a source through the audio-only pipeline (default ten seconds, `--seconds
8484

8585
## customio
8686

87-
Wraps a local file in a custom `IOReader` and plays it through `load(source:)`. `--memory` reads via `DataIOReader`, `--forward-only` drops the seek capability, and `--reload` / `--switch-audio` / `--select-subs` / `--extract` exercise the optional capabilities (background reload, audio-track switch, embedded subtitles, scrub preview) end-to-end.
87+
Wraps a local file in a custom `IOReader` and plays it through `load(source:)`. `--memory` reads via `DataIOReader`, `--forward-only` drops the seek capability, `--audio-only` routes through the audio-only pipeline, and `--reload` / `--switch-audio` / `--select-subs` / `--extract` exercise the optional capabilities (background reload, audio-track switch, embedded subtitles, scrub preview) end-to-end.
8888

8989
## disc-inspect
9090

@@ -96,7 +96,7 @@ Activates two subtitle tracks simultaneously on one source (primary + secondary)
9696

9797
## live
9898

99-
Runs a live MPEG-TS session against a built-in fixture that serves an endless broadcast by looping a seed `.ts` with rewritten timestamps. Flags simulate the failure modes the live path hardens against: `--drop-after N` (mid-stream connection drop + reconnect), `--discontinuity-at N` (program-boundary PTS / PCR jump), `--realtime` (1x wall-clock pacing), `--dvr-window N` (timeshift), `--measure-rss` (sliding-window retention), `--reload-test` (live rejoin end to end, including the full-backlog replay shape some origins serve on reconnect). `--seed <ts>` overrides the seed clip, `--sw` forces the software live path, `--report-cache-bytes` tracks on-disk DVR footprint.
99+
Runs a live MPEG-TS session against a built-in fixture that serves an endless broadcast by looping a seed `.ts` with rewritten timestamps. Flags simulate the failure modes the live path hardens against: `--drop-after N` (mid-stream connection drop + reconnect), `--discontinuity-at N` (program-boundary PTS / PCR jump), `--realtime` (1x wall-clock pacing), `--dvr-window N` (timeshift), `--measure-rss` (sliding-window retention), `--reload-test` (live rejoin end to end, including the full-backlog replay shape some origins serve on reconnect). `--seed <ts>` overrides the seed clip, `--sw` forces the software live path, `--report-cache-bytes` tracks on-disk DVR footprint, `--serve-only` parks the fixture without attaching an engine (raw `curl` / `ffprobe` inspection), `--rewind-test` runs the DVR rewind-and-return matrix variant, and `--gen-highbitrate-seed` generates a ~22 Mbps 1080p H.264 MPEG-TS seed (for RSS-retention measurement) then exits.
100100

101101
## dvr
102102

@@ -112,7 +112,7 @@ Drives a real AVPlayer (native loopback-HLS path) through a burst of rapid seeks
112112

113113
## hlslive
114114

115-
Replays a synthetic SSAI ad-pod feed through the live-direct-play path to repro the FAST-channel ad-break handling (program-switch detection, muxer rotation with versioned `#EXT-X-MAP`, audio re-anchor, no-cut watchdog). `--segments N`, `--seconds N`, `--segment-seconds N` size the run; `--disc` injects discontinuities at the ad boundaries.
115+
Replays a synthetic SSAI ad-pod feed through the live-direct-play path to repro the FAST-channel ad-break handling (program-switch detection, muxer rotation with versioned `#EXT-X-MAP`, audio re-anchor, no-cut watchdog). `--segments a.ts,b.ts,c.ts` is required: a comma-separated list of real `.ts` segment files served in order (content / ad / content) without timestamp rewriting. `--seconds N` (default 40) and `--segment-seconds N` (default 5) size the run; `--disc i,j` marks which segment indices carry a leading `#EXT-X-DISCONTINUITY` (default: auto-detected on every file change).
116116

117117
## smbtest
118118

docs/formats.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,26 @@ Interlaced sources (DVD-rip MPEG-2, SD broadcast) are deinterlaced through a per
2323
| H.264, HEVC (SDR) | BT.709 |
2424
| HEVC Main10 (HDR10) | BT.2020 / PQ |
2525
| HEVC Main10 (HDR10+) | BT.2020 / PQ + per-frame ST 2094-40 SEI stream-copied |
26-
| HEVC Main10 (DV P5 / P8.1 / P8.4) | `dvh1` / `dvhe` track type with the source's `dvcC` box preserved |
26+
| HEVC Main10 (DV P5) | `dvh1` track type (DV-only, IPT-PQ base; forced even on SDR panels) |
27+
| HEVC Main10 (DV P8.1 / P8.4 / P7) | `hvc1` primary + `dvvC` box, DV engaged via `SUPPLEMENTAL-CODECS` on DV panels, plain HDR10 / HLG base elsewhere |
2728
| HEVC Main10 (HLG) | BT.2020 / HLG |
2829
| AV1 HDR | BT.2020 / PQ |
2930

3031
HDR-to-SDR mapping is handled by AVPlayer and the system compositor according to the connected display. AetherEngine doesn't tonemap on the host; it tells the system "this is BT.2020 PQ" (or DV, or HLG) via the HLS-fMP4 sample description and lets tvOS / iOS pick the right path. `DisplayCriteriaController` issues the HDMI content-frame-rate and dynamic-range hint via `AVDisplayManager` before the first segment is fetched, so the receiver-side handshake is in flight by the time `AVPlayer` is ready to render. (For why this ordering is mandatory on tvOS, see the README's "Host setup on tvOS" section.)
3132

3233
### Dolby Vision signaling
3334

34-
For DV streams the demuxer surfaces the source's `AVDOVIDecoderConfigurationRecord`. On DV-capable displays, `HLSVideoEngine` writes the matching ISO BMFF `dvcC` box into the HLS-fMP4 sample description and emits a bare `dvh1.<profile>.<dvLevel>` codec tag for Profile 5, 8.1, and 8.4 so AVKit's auto-criteria reads `dvh1` from the sample entries and engages DV mode directly. On non-DV displays the engine downgrades to plain `hvc1`: Profile 5 is unplayable there (no HDR10 base), and Profiles 8.1 / 8.4 fall back to their HDR10 / HLG base layer with AVPlayer's tone-mapping path. AV1+DV (Profile 10.0 / 10.1 / 10.4) uses the parallel `dav1` / `av01` track type plus `dvvC` box on hardware-AV1 hosts.
35+
For DV streams the demuxer surfaces the source's `AVDOVIDecoderConfigurationRecord`, and the route depends on the profile's base-layer compatibility:
36+
37+
- **Profile 5** (DV-only, IPT-PQ, no base layer) emits a bare `dvh1.05.<dvLevel>` codec tag in the primary `CODECS` attribute with the `dvcC` box preserved. `dvh1` is forced even on non-DV panels (AVPlayer's system DV decoder tonemaps IPT-PQ internally; without `dvh1` the IPT chroma reads as YCbCr and shows a green / purple cast), so P5-on-non-DV is routed through a media playlist to dodge the `-11868` variant rejection.
38+
- **Profiles 8.1 / 8.4** (HDR10- / HLG-compatible base) emit `hvc1.2.4.L<level>` as the primary `CODECS` tag. On a DV-capable display the muxer writes the `dvvC` box and the variant carries `dvh1.08.<dvLevel>/db1p` (8.1) or `/db4h` (8.4) in `SUPPLEMENTAL-CODECS`, which is what makes AVKit engage DV. On a non-DV display the `dvvC` is stripped (a lone `hvc1` + `dvvC` still trips `-11868`) and the stream plays as its plain HDR10 / HLG base with AVPlayer's tone-mapping.
39+
40+
AV1+DV emits a bare `dav1.10.<dvLevel>` primary for Profile 10.0 (DV-only) and Profile 10.1 (HDR10-compat base) with no supplemental entry, and an `av01...` primary plus `dav1.10.<dvLevel>/db4h` in `SUPPLEMENTAL-CODECS` for Profile 10.4 (HLG-compat base), on hardware-AV1 hosts.
3541

3642
**Profile 7** (dual-layer, the common UHD-Blu-ray remux profile) has no decoder on any Apple platform, so the engine converts it to single-layer **Profile 8.1** live during muxing: the RPU of each video packet is rewritten with [libdovi](https://github.com/superuser404notfound/LibDovi) (`dovi_convert_rpu_with_mode`, mode 2, the same transform as `dovi_tool -m 2`), the enhancement-layer NALs are dropped, and the container `dvvC` is set to Profile 8.1. On a DV-capable display this means real Dolby Vision (`dvh1.08/db1p` supplemental) instead of the plain HDR10 base; on a non-DV display Profile 7 still falls back to its HDR10 base, unchanged. The conversion is loss-free relative to what Apple could show before (the enhancement layer was never decodable on Apple hardware), and any per-packet conversion failure falls back to the HDR10 strip. MEL and FEL sources are both handled.
3743

44+
**SDR-compatible-base profiles** (HEVC **Profile 8.2**, AV1 **Profile 10.2**) carry a Rec.709 base layer that no Apple platform has a DV decoder for. The engine strips the `dvcC` / DV config and plays the base as plain `hvc1` / `av01` (logging `not DV-routable, playing Rec.709 base`); there is no Dolby Vision on any display for these, on DV panels and SDR panels alike.
45+
3846
### HDR10+ dynamic metadata
3947

4048
ST 2094-40 metadata stays attached to the HEVC bitstream as user-data-registered ITU-T T.35 SEI NALs. The HLS-fMP4 stream-copy preserves the SEI through to `AVPlayer`, which forwards it to the system compositor. HDR10+-capable TVs apply the per-scene tone-mapping curves; HDR10-only TVs fall back to the static HDR10 base.

0 commit comments

Comments
 (0)