Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Sources/AetherEngine/PlayerState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ public enum AudioDelivery: String, Sendable, Equatable, CaseIterable {
/// The source's audio bitstream is muxed into fMP4 unchanged: Atmos, DTS-HD and every other
/// bitstream reach the renderer exactly as authored.
case streamCopy
/// The audio is decoded and re-encoded (FLAC or E-AC-3) for the fMP4 pipeline, because its codec
/// is not fMP4-legal or AVPlayer rejects it there. Lossless for the bed channels; object metadata
/// in a TrueHD-MAT or JOC bitstream does not survive the PCM intermediate.
/// The audio is decoded and re-encoded for the fMP4 pipeline, because its codec is not fMP4-legal
/// or AVPlayer rejects it there: to FLAC (lossless bed channels) or E-AC-3, where object metadata
/// in a TrueHD-MAT or JOC bitstream does not survive the PCM intermediate, or, under
/// `LoadOptions.objectAudioRendering`, to lossy APAC with the TrueHD Atmos objects rendered into
/// the speaker bed.
case bridged
/// libavcodec decodes the audio and the engine renders it itself (the software path and the
/// software audio-only host).
Expand Down
6 changes: 5 additions & 1 deletion Tests/AetherEngineTests/DocumentedConstantsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,12 @@ final class DocumentedConstantsTests: XCTestCase {
guard #available(macOS 26.0, iOS 26.0, tvOS 26.0, visionOS 26.0, *) else { return }
XCTAssertEqual(SpatialAudioBridge.bitRatePerChannel, 320_000,
"docs/formats.md quotes 320 kbps per bed channel")
XCTAssertEqual(APACSampleEntry.codecsString(channelCount: 8), "apac.31.02",
"docs/formats.md and cli.md quote apac.31.02 for 5.1.2")
XCTAssertEqual(APACSampleEntry.codecsString(channelCount: 10), "apac.31.03",
"docs/formats.md and cli.md quote apac.31.03 for 10 channels")
XCTAssertEqual(APACSampleEntry.codecsString(channelCount: 12), "apac.31.03",
"docs/formats.md and cli.md quote apac.31.03 for up to 12 channels")
"docs/formats.md and cli.md quote apac.31.03 for 12 channels")
XCTAssertEqual(APACSampleEntry.codecsString(channelCount: 16), "apac.31.04",
"docs/formats.md and cli.md quote apac.31.04 for 16 channels")
}
Expand Down
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ typed fact rather than as something to reconstruct (AE#462).
| `.none` | no session |
| `.noAudioInSource` | the source carries no audio track, or none was selected |
| `.streamCopy` | the source bitstream is muxed into fMP4 unchanged (Atmos, DTS-HD and everything else reach the renderer as authored) |
| `.bridged` | decoded and re-encoded to FLAC or E-AC-3 for the fMP4 pipeline; lossless for the bed channels, object metadata does not survive the PCM intermediate |
| `.bridged` | decoded and re-encoded for the fMP4 pipeline: to FLAC (lossless bed channels) or E-AC-3, where object metadata does not survive the PCM intermediate, or, under `objectAudioRendering`, to lossy APAC with the TrueHD Atmos objects rendered into the speaker bed |
| `.decoded` | libavcodec decodes and the engine renders it (the software path, the FFmpeg audio-only host) |
| `.droppedNoPipeline` | the source HAS audio and none of it could be delivered: no decoder for it in this build, or the bridge could not be built or could not write its header. The session plays video-only and silently |
| `.playerManaged` | AVFoundation owns the audio (the remote-HLS bypass, the native audio-only host). The engine has no pipeline of its own to classify and does not answer on AVFoundation's behalf |
Expand Down Expand Up @@ -960,7 +960,7 @@ All flags default to safe values; the table is the full set. Depth for the media
| `nativeRemoteHLSIngestFallback` | true | The #168 / #293 carriage recovery and the #363 401/403 bypass refusal recovery. Setting it false turns both off. |
| `audioOnly` | false | Lean audio pipeline, no video machinery. Also set automatically when the probe finds no video stream. |
| `audioBridgeMode` | `.surroundCompat` | Bridge encoder for codecs that cannot stream-copy into fMP4. `.surroundCompat` uses EAC3 for a source with more than two channels and FLAC for one with two or fewer (no surround to carry). `.lossless` uses FLAC up to 7.1 throughout and needs a sink that accepts multichannel LPCM. |
| `objectAudioRendering` | `.off` | TrueHD Atmos delivery. `.apac(SpatialSpeakerLayout)` renders a TrueHD Atmos track's objects into that speaker bed (`SpatialSpeakerLayout` 5.1.2, 5.1.4, 7.1.2, 7.1.4 or 9.1.6, positions described by `SpatialSpeaker`) and delivers it as lossy APAC, which reaches an Atmos receiver as Dolby MAT with its heights; see `ObjectAudioRendering` and [formats.md › TrueHD Atmos (object rendering)](formats.md#truehd-atmos-object-rendering). TrueHD without Atmos, OS 25 and earlier, and any start-up failure keep `audioBridgeMode`. |
| `objectAudioRendering` | `.off` | TrueHD Atmos delivery. `.apac(SpatialSpeakerLayout)` renders a TrueHD Atmos track's objects into that speaker bed (`SpatialSpeakerLayout` 5.1.2, 5.1.4, 7.1.2, 7.1.4 or 9.1.6, positions described by `SpatialSpeaker`) and delivers it as lossy APAC, which reaches an Atmos receiver as Dolby MAT with its heights; see `ObjectAudioRendering` and [formats.md › TrueHD Atmos (object rendering)](formats.md#truehd-atmos-object-rendering). TrueHD without Atmos, OS versions before 26 (iOS, tvOS, macOS and visionOS 26), and any start-up failure keep `audioBridgeMode`. |
| `confirmAtmos` | false | Background per-track JOC confirmation, republishing `audioTracks` as tracks confirm. Never on the start path; skipped for live and forward-only readers. |
| `preferredAudioLanguages` | empty | First-frame audio pick from the engine's single probe. Ordered BCP-47 / ISO 639 tags; region and script are normalized and rank within one preference (#590). An explicit `audioSourceStreamIndex` still wins. |
| `preferredSubtitleLanguages` | empty | Post-load subtitle activation on the host-overlay path. Ranks language specificity (an explicitly opposite script is rejected, not demoted) before the descriptor axis. Pure convenience: no reload and no pre-probe, unlike the audio equivalent. |
Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ open 'http://127.0.0.1:<port>/master.m3u8' # macOS QuickTime

`--start-position S` starts the session at S seconds, the resume anchor a host passes to `load(url:startPosition:)`. Also available on `play`.

`--atmos-bed <layout>` is `LoadOptions.objectAudioRendering = .apac(layout)` (`5.1.2`, `5.1.4`, `7.1.2`, `7.1.4` or `9.1.6`): a TrueHD Atmos track is rendered into that bed and served as APAC, so the master reads `CODECS="…,apac.31.03"` (up to 12 channels) or `apac.31.04` (16) and the log carries `TrueHD Atmos: objects rendered into …`. `--audio-index <n>` serves source stream `n`, as a host's audio picker would; without it the engine prefers an E-AC-3 JOC track when the file has one. Dolby's Unfold demo: `serve --atmos-bed 7.1.4 --audio-index 1 dolby-unfold-lossless.m2ts`.
`--atmos-bed <layout>` is `LoadOptions.objectAudioRendering = .apac(layout)` (`5.1.2`, `5.1.4`, `7.1.2`, `7.1.4` or `9.1.6`): a TrueHD Atmos track is rendered into that bed and served as APAC, so the master reads `CODECS="…,apac.31.02"` (5.1.2, 8 channels), `apac.31.03` (10 or 12 channels) or `apac.31.04` (9.1.6, 16 channels) and the log carries `TrueHD Atmos: objects rendered into …`. `--audio-index <n>` serves source stream `n`, as a host's audio picker would; without it the engine prefers an E-AC-3 JOC track when the file has one. Dolby's Unfold demo: `serve --atmos-bed 7.1.4 --audio-index 1 dolby-unfold-lossless.m2ts`.

`--audio-delay <ms>` parks the server with the AE#464 audio offset already in its muxer, which is how the DELIVERED offset is measured rather than argued about: fetch the media playlist and walk its segments, then read the first audio and video packet timestamps out of `init.mp4` + a segment with `ffprobe -show_entries packet=stream_index,pts_time`. On a 30 fps H.264 + AAC fixture the source's own alignment is +21.8 ms, `--audio-delay 200` reads +221.8 ms and `--audio-delay -150` reads -128.2 ms, i.e. exactly the offset asked for, with the video timestamp unchanged in every arm.

Expand Down
2 changes: 1 addition & 1 deletion docs/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Matroska CodecPrivate doesn't usually carry the pre-parsed `dec3` / `dac3` box c

Off by default. With `LoadOptions.objectAudioRendering = .apac(layout)`, a TrueHD track FFmpeg marks as Atmos (`AV_PROFILE_TRUEHD_ATMOS`, 48 kHz) skips the channel bridges: `SpatialAudioBridge` decodes the object presentation (beds, objects and their positions), renders it into the host's speaker bed (5.1.2, 5.1.4, 7.1.2, 7.1.4 or 9.1.6) with a constant-power allocentric panner, and encodes that bed as Apple Positional Audio (APAC, 320 kbps per bed channel) through `AVAudioConverter`. tvOS decodes APAC and sends it to an Atmos receiver as Dolby MAT, heights included. The trade is a lossy encode in place of the lossless 7.1 presentation, which is why the host opts in. Requires OS 26 (the APAC encoder API); on anything older, or if the bridge cannot start, the session falls back to `audioBridgeMode` with its audio intact.

libavformat cannot write APAC (its `apac` codec id is an unrelated legacy codec), so the muxer is given an ALAC stand-in and the init segment's sound sample entry is replaced with an `apac` entry carrying the encoder's magic cookie, which is the complete `dapa` box (`APACSampleEntry`). The master playlist advertises `apac.31.LL`: profile 31 (multichannel) and the channel-count level, `03` for up to 12 channels and `04` for up to 24; AVPlayer rejects a bare `apac`. Every APAC packet the encoder produces is independently decodable, so every segment starts on an Audio Sync Packet. The encoder runs with dynamic range control off: its DRC analysis otherwise holds 1.56 s (`.capture`) or more than 5 s (`.movie`) of audio before the first packet, which would leave the first segment after a seek without sound. The encoder's 2048 frames of priming stay in the stream: AVFoundation presents an APAC packet's audio 2048 frames before its timestamp, so the priming packets take the source position's timestamp and the first content frame plays on it.
libavformat cannot write APAC (its `apac` codec id is an unrelated legacy codec), so the muxer is given an ALAC stand-in and the init segment's sound sample entry is replaced with an `apac` entry carrying the encoder's magic cookie, which is the complete `dapa` box (`APACSampleEntry`). The master playlist advertises `apac.31.LL`: profile 31 (multichannel) and the channel-count level, `02` for 8 channels (5.1.2), `03` for 9 to 12 channels and `04` for 13 to 24; AVPlayer rejects a bare `apac`. Every APAC packet the encoder produces is independently decodable, so every segment starts on an Audio Sync Packet. The encoder runs with dynamic range control off: its DRC analysis otherwise holds 1.56 s (`.capture`) or more than 5 s (`.movie`) of audio before the first packet, which would leave the first segment after a seek without sound. The encoder's 2048 frames of priming stay in the stream: AVFoundation presents an APAC packet's audio 2048 frames before its timestamp, so the priming packets take the source position's timestamp and the first content frame plays on it.

## Subtitles

Expand Down
Loading