Repository navigation
fix(hls): declare CHANNELS on the audio rendition, "16/JOC" for E-AC-3 Atmos - #727
Merged
superuser404notfound merged 1 commit intoOct 8, 2026
Conversation
…3 Atmos The loopback master's EXT-X-MEDIA:TYPE=AUDIO tag carried no CHANNELS attribute at all. Apple's HLS Authoring Specification makes it REQUIRED on an audio rendition, and Dolby's DD+ Online Delivery Kit makes it the one playlist-level statement that a rendition is object audio: the number of decodable objects, a slash, then JOC. CODECS cannot carry it -- a JOC track is signaled `ec-3`, the same string a non-JOC E-AC-3 5.1 track gets (superuser404notfound#34: `ec+3` is refused by tvOS 26.5) -- and the `dec3` box sits a layer below the playlist. So with no CHANNELS the master said nothing about objects anywhere, AVFoundation settled on the bed, and an Atmos bitstream that had been stream-copied intact reached an Atmos-capable receiver as 5.1 PCM. The engine already knows the answer. HLSVideoEngine latches audioIsAtmosStreamCopy from the source probe (E-AC-3 profile 30 = AV_PROFILE_EAC3_DDP_ATMOS) and logs "EAC3+JOC Atmos: stream-copy engaged"; nothing carried that fact up to the playlist builder. What changed - `Network/HLSLocalServer.swift`: `HLSSegmentProvider` gains `masterAudioChannels`, defaulted to nil in the protocol extension so every existing conformance (including the Issue458 test stub) compiles untouched. The master builder appends CHANNELS to the EXT-X-MEDIA attribute list when the provider answers, and omits the attribute when it does not. - `Video/VideoSegmentProvider.swift`: two new stored properties (`audioChannelCount`, `audioIsAtmosStreamCopy`, both defaulted) and the answer. `VideoSegmentProvider.atmosChannelsAttribute` is the "16/JOC" literal, named so docs/formats.md can be pinned to it. - `Video/HLSVideoEngine.swift`: threads both through at provider construction, beside the existing servedAudioLanguage. Two decisions worth keeping. The JOC arm is gated on `audioDelivery == .streamCopy` as well as on the probe flag, because audioIsAtmosStreamCopy is latched from the source probe BEFORE the audio cascade runs and so stays true on a JOC source whose stream-copy probe was rejected and which fell back to the bridge; that session has no objects left, and advertising 16/JOC over a bridged bed would be worse than saying nothing. And the channel count is read from `savedAudioConfig.codecpar`, the configuration the muxer actually got, so a bridged track reports the encoder's layout rather than the source's. On the literal 16: the first CHANNELS parameter should strictly be `complexity_index_type_a` from the EC3SpecificBox, which is not reachable at the playlist layer. 16 is the count Dolby's delivery kit and Apple's published Atmos masters carry. Nothing here is public, so docs/api.md is unaffected; docs/formats.md (Dolby Atmos) and docs/architecture.md (HLSLocalServer) state the rule, and DocumentedConstantsTests pins the documented "16/JOC" to the constant. Measured with `aetherctl serve` against a real 8-channel E-AC-3 JOC source over http (MKV, HEVC DV P8.1, 3840x1392 @ 23.976), macOS 15: before #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="English", LANGUAGE="eng",DEFAULT=YES,AUTOSELECT=YES after ...,DEFAULT=YES,AUTOSELECT=YES,CHANNELS="16/JOC" The EXT-X-STREAM-INF line is byte-identical before and after, and so is init.mp4 (1339 B both ways, `cmp` reports no difference): this does not touch the muxer. Test plan - `swift build`, full `swift test` (3347 tests in 452 suites) and `python3 Scripts/check-doc-links.py`, macOS 15 / Swift 6.4. - Device: Apple TV 4K (3rd gen), tvOS 26.6 -> LG G5 OLED -> Sonos Arc over eARC, reading the incoming format string in the Sonos app, with a known-Atmos title in the Apple TV app immediately before as the route control. - Media 1: "28 Years Later", MKV, HEVC Dolby Vision Profile 8.1, with an AC-3 core plus an E-AC-3 dependent substream (chanmap 0xA010, JOC). Before: Multichannel PCM 5.1. After: Dolby Atmos. - Media 2: "Dune: Part Two", MKV, 6-channel E-AC-3 JOC (objects in the independent substream). Before: Multichannel PCM 5.1. After: Dolby Atmos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
superuser404notfound
force-pushed
the
pr/hls-audio-channels-attribute
branch
from
October 8, 2026 21:42
ea61397 to
9227c1e
Compare
superuser404notfound
pushed a commit
that referenced
this pull request
Oct 8, 2026
…n too PR #727 declared CHANNELS on the audio rendition, but the rendition is only written for audio with a resolvable language (AE#458), so an und or untagged E-AC-3 JOC track, Dolby's own test signals among them, still reached the master without a word about objects. A stream-copied JOC track now gets a rendition without LANGUAGE (NAME="Dolby Atmos"), which forces the master on an SDR source the way a language does. An untagged track that is not object audio is unchanged, so no other source changes route. Measured on macOS with aetherctl play against Dolby's untagged JOC test signal: before, no audible group; after, one option "Unknown", plays through. The AE#458 readback line reports the session as served=untagged. Adds master-line tests for CHANNELS, which #727 pinned only in the docs. Re #726 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
Owner
|
Thank you, merged in 230c406. The receiver A/B on two titles was exactly the evidence this needed, and the stream-copy gate on the JOC arm is the right call. Two notes on what happened around the merge:
This ships in the next AetherEngine release. |
Merged
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The loopback master's
EXT-X-MEDIA:TYPE=AUDIOtag carried noCHANNELSattribute, which Apple's HLS Authoring Specification requires and which is the
only playlist-level statement that a Dolby Digital Plus rendition carries
objects — so a stream-copied Atmos bitstream reached an Atmos-capable receiver
described as an untyped rendition and was rendered as the 5.1 bed.
Re #726 (kept open for the follow-up on untagged audio)
What changed
Network/HLSLocalServer.swift—HLSSegmentProvidergainsmasterAudioChannels, defaulted tonilin the protocol extension so everyexisting conformance (including the
Issue458…Testsstub) compiles untouched.The master builder appends
CHANNELSto theEXT-X-MEDIAattribute list whenthe provider answers, and omits the attribute entirely when it does not.
Video/VideoSegmentProvider.swift— two new stored properties(
audioChannelCount,audioIsAtmosStreamCopy, both defaulted) and the answer:"16/JOC"for a stream-copied JOC track, the served channel count otherwise,nilwhen there is no rendition to attribute. The literal lives inVideoSegmentProvider.atmosChannelsAttributeso the docs can be pinned to it.Video/HLSVideoEngine.swift— threads both through at provider construction,beside the existing
servedAudioLanguage.CHANGELOG.mdunder[Unreleased];docs/formats.md(Dolby Atmos) anddocs/architecture.md(HLSLocalServer) state the rule; aDocumentedConstantsTestspin holds the documentedCHANNELS="16/JOC"to theconstant. Nothing here is
public, sodocs/api.mdis unaffected —git show <sha> | grep '^+.*public'is empty.Two decisions worth a reviewer's attention. The JOC arm is gated on
audioDelivery == .streamCopyas well as on the probe flag, becauseaudioIsAtmosStreamCopyis latched from the source probe before the audiocascade runs and so stays true on a JOC source whose stream-copy probe was
rejected and which fell back to the bridge; that session has no objects left, and
advertising
16/JOCover a bridged bed would be worse than saying nothing. Andthe channel count is read from
savedAudioConfig.codecpar, the configuration themuxer actually got, so a bridged track reports the encoder's layout.
On the literal
16: the first parameter should strictly becomplexity_index_type_afrom theEC3SpecificBox, which is not reachable atthe playlist layer.
16is what Dolby's delivery kit and Apple's published Atmosmasters carry. (It is also what the
dec3PR measures on the same source, so thevalue is corroborated rather than assumed — but this PR does not depend on that
one.)
CODECSis untouched and staysec-3(#34). TheEXT-X-STREAM-INFline isbyte-identical before and after, and so is
init.mp4: this does not touch themuxer.
Test plan
over eARC, reading the incoming format string in the Sonos app, with a
known-Atmos title in the Apple TV app immediately before as the route control.
Playlist and init-segment observations on macOS 15 / Swift 6.4; the branch is
rebased onto
mainat 7.32.3 (4f46e689) and the full suite re-run there.AC-3 5.1 core plus an E-AC-3 dependent substream,
chanmap 0xA010, JOC /DV Profile 8.1.
independent substream).
…,DEFAULT=YES,AUTOSELECT=YES. After:…,DEFAULT=YES,AUTOSELECT=YES,CHANNELS="16/JOC". TheEXT-X-STREAM-INFline is byte-identical, and
cmpreports no difference oninit.mp4(1339 B in both arms).
Atmos after.
mainat 7.32.3 (4f46e689):swift build,full
swift test— 4184 Swift Testing tests in 581 suites passed, everyXCTest suite 0 failures — and
python3 Scripts/check-doc-links.py, all onmacOS 15 / Swift 6.4.
DocumentedConstantsTestsandPublicAPIDocumentationTestspass.Checklist
CHANGELOG.mdupdatedfeat(...),fix(...),chore(...))by grep over the diff
Note for whoever merges: the
dec3PR touches the samedocs/formats.mdparagraph and adds its own
[Unreleased]bullet, so whichever of the two landssecond needs a trivial rebase. They are otherwise independent — that PR
cherry-picks onto
mainon its own and builds without this one.🤖 Generated with Claude Code