Skip to content

fix(software-host): #808 review follow-ups, merged with main 7.46.0 - #1

Closed
PieterVO wants to merge 121 commits into
pkajaba:parity/now-playingfrom
PieterVO:pr/808-followups
Closed

PieterVO wants to merge 121 commits into
pkajaba:parity/now-playingfrom
PieterVO:pr/808-followups

Conversation

@PieterVO

Copy link
Copy Markdown

Summary

Follow-up to superuser404notfound#808, meant to be merged into parity/now-playing. It brings the branch up to date with main (7.45.0, then 7.46.0) with the conflicts resolved the way the review suggested, addresses the review's four "Before merge" items, and adds three fixes found while running this branch on an Apple TV with a Dolby Vision Profile 7 FEL title that has a TrueHD Atmos track.

Refs superuser404notfound#808, superuser404notfound#838, superuser404notfound#848, superuser404notfound#854, superuser404notfound#462.

What changed

Merge with main. stopInternal keeps both stopSoftwareNowPlaying() and softwareClockSessionZero = nil; aetherctl keeps main's --atmos-bed parser (the branch's duplicate parser and global are gone); CHANGELOG / README / docs/api.md / docs/formats.md take main's text with the software-host sentences added.

Review items 1–4

  1. SoftwareAtmosRouteTests.apacRoutes takes .l714 for DTS:X, and a new test pins that every bed without a one-to-one DTS:X layout stays PCM while TrueHD Atmos renders into any. The software host now logs why a TrueHD or DTS track stays on PCM, as it already did for E-AC-3.
  2. Route removal pauses only for headphones, Bluetooth (A2DP, LE, HFP), AirPlay and USB. HDMI is left out, so an ARC / eARC receiver switching on or off keeps playing.
  3. A packet read before a seek can no longer anchor the APAC bridge: SoftwareAudioProducing gains the AE#492-style feed epoch, the demux loop reads it before the read, and SoftwareAPACProducer checks it under the same lock as flush()'s startSegment. Covered by a test that failed before the change (post-seek audio stamped 59 s early).
  4. A local software session reports supportsCacheBackedStills == false for Dolby Vision Profile 5 / 10.0 without the DolbyVisionStillReshaping trait.

Found on device

  • TrueHD on the APAC route started on the video clock. The "audio is not recovering" fallback fired after 50 audio packets, but the APAC route's first buffer is about 100 TrueHD access units in. The bound now comes from the producer (the bridge's own dead-decoder threshold for APAC).
  • Unlabelled TrueHD. The software host now resolves a TrueHD track whose profile the probe missed from its major sync, sharing main's fix(audio): read TrueHD Atmos from the major sync when the probe never reached the track (7.40.1) superuser404notfound/AetherEngine#838 helper with the loopback route.
  • Atmos indicator on the software host. The Atmos output activator now also follows a software host that plays APAC or compressed E-AC-3 JOC (including resumes after an interruption and setRate(0)), and detaches with the session. On the test receiver the Atmos indicator stayed off for the FEL route's APAC until it did.
  • HDMI port type. AVAudioSession.Port.HDMI is "HDMIOutput", so the activator never ran on HDMI. This commit is identical to fix(audio): the Atmos output activator matches tvOS's HDMI port type superuser404notfound/AetherEngine#854 and can be dropped once that is merged.
  • Reporting. On the software host, audioDelivery is .bridged for APAC and .streamCopy for compressed E-AC-3 JOC (the loopback route's terms), and activeAudioDecoder names the route (TRUEHD Atmos → APAC 7.1.4); the background-audio decision accepts both.

CHANGELOG (Unreleased) and docs/api.md / PlayerState.swift (AudioDelivery) are updated.

Test plan

  • macOS: swift test --enable-all-traits on this branch: 5211 Swift Testing tests and 785 XCTest cases. In the full parallel run, two load-sensitive tests failed (ThreadCPUSamplerTests, HLSOriginRelayStalledConsumerTests); both pass when re-run on their own. Earlier full runs on this branch showed the same kind of load-only failures in HLSOriginRelay*, EngineTLSHandshake, SubtitlePacketStoreScaling, PythonOriginLaunch and HLSPlaylistHostileInput, all passing alone. swift build --build-tests with default traits passes.
  • Apple TV 4K (1st generation), tvOS 26.6, Release build of a host app against this branch with all four traits, HDMI to a Dolby Vision TV through an Atmos receiver:
    • UHD Blu-ray remux, Dolby Vision Profile 7 FEL, TrueHD Atmos: the dual-layer route is taken and TrueHD Atmos is rendered to APAC 7.1.4 on the software host. The TV shows Dolby Vision and the receiver shows Atmos. Over three sessions of several minutes: 0 lost frames, 2–39 display-layer drops per session (mostly at start), no rebuffers, audio lead steady around 4 s, the clock armed from audio.
    • UHD Blu-ray remux, Profile 7 MEL, TrueHD Atmos (loopback route, APAC): the activator logs active on route ports=[HDMIOutput]; before the HDMI fix it logged off.
  • Not run on a device: route removal, channel-count change, interruption hold, Now Playing, caption style, local scrub stills, DTS:X. The device list in feat(software-host): parity with the AVPlayer path: Now-Playing and remote commands, caption style, local scrub stills, Atmos, audio route and interruption handling superuser404notfound/AetherEngine#808 is still open for these.

Checklist

  • CHANGELOG updated
  • Conventional Commit messages
  • Engine-side fix
  • Public API behaviour change documented (audioDelivery values on the software host)

Co-authored with Claude (Anthropic); the commits carry a Co-authored-by: Claude trailer.

williamliu0516 and others added 30 commits October 16, 2026 05:26
…ncement layer's RPUs folded into the base layer

A UHD Blu-ray carries Dolby Vision as two HEVC PIDs in one clip: the base layer on 0x1011 and the
enhancement layer on 0x1015, whose access units hold the RPU for the base picture with the same PTS.
No DOVI record sits on either stream, so the base layer played as plain HDR10. The demuxer now
declares Profile 7 on the base layer, appends each picture's RPU to its base-layer access unit
(Annex-B) and keeps the enhancement packets to itself, so the rest of the engine sees the
single-track Profile 7 shape a remux has and takes its usual route (8.1 by default).

The enhancement unit usually follows its base unit, but not always: base units wait in a short
queue (8 pictures) and enhancement units that come first leave their RPU by PTS. A first version
that held one base unit lost the RPU on 40% of pictures after a mid-film start.

Measured on a retail UHD disc (Blu-ray folder, 90 GB main clip, over WebDAV): one RPU per
enhancement unit, none in the base layer; aetherctl play 30 s from the start and from 5000 s:
fmt=dolbyVision dvProfile=7 dvConversion=profile7ToProfile81, every base picture paired (was
fmt=hdr10 before). Not yet watched on a Dolby Vision display.
The suite's tests changed process-wide statics while running in parallel, so one test's 1 s budget leaked into other readers.

Give-up is timed on the wall clock, so a stalled CI runner ended the retry ladder after 2 refusals.

The reader now takes both values as init parameters, the test-only static is removed, and the give-up test asserts time spent >= budget.
…in tests

Tests set process-wide statics that every reader built by a concurrently running suite read at init, so one test's 0.02 backoff scale or 0.2 s pin gap leaked into other readers.

AVIOReader now takes backoffScale and pinIdleSeconds as init parameters (nil keeps the shipped value), and the two AetherEngine statics are removed.

The superuser404notfound#255 peak body reserve is tracked per reader instead of in a static that every reader's fetches raised.
The truncation test now fails if no chunk fetch reported a reservation, so it cannot pass on a reader that never reported.

The peak body reserve comment now says it is lock-protected and raised from each fetch's delegate.
mediaChapters carried raw container times, but seek(to:) and currentTime
run from the container start. On a source with a non-zero container start
a host passing a chapter start to seek(to:) landed that many seconds late.

The chapters are now rebased per backend onto the clock that seek(to:)
uses: the container start on native, and the software clock's zero on the
software path. That zero is predicted from the origin the software host
anchors on (container start, backed up by the video stream start) and
replaced by the clock's own session zero once it has anchored. Live keeps
origin 0 on both routes. The probe snapshots the raw chapters, and the
list is republished when the backend is settled, so reopens keep working;
nothing is published while no backend is settled.

A chapter is left out only when it ends at or before the content start,
judged on its effective end (the next chapter's start, else a declared
end after its start, else the container duration), because MKVs often
declare end == start. Audio-only backends keep the raw axis.
…time

On a source whose container starts past the 2 s tolerance, a seek issued
before the clock is armed converted with session zero 0 and then armed the
clock itself, so the demux loop never resolved the zero and the seek landed
the container start early. The host now derives the zero from the source
origin on that path, as a resume does, and reports it so the published
chapters stay on the same axis.

The media test now asserts the raw source time both before and after play()
and waits for the session to be ready instead of a fixed settle window. Test
comments describe the clips neutrally, and the stale comment in stop() is
corrected.
…on-live source

The zero was only set for a resume or by the first seek before play, so the
subtitle-harvest anchor and the scrub stills of a cold load converted with zero
and named content that sits an origin earlier. The host now derives it from the
source origin when it loads, as a resume does, and reports it; the seek-time
block is gone. The demux loop still re-anchors on a play.

The media test adds a play-first pass on a fresh load, reads sourceTime once
about 0.35 s after each seek inside a tight window instead of the 4 s window,
asserts the converted zero before play, and checks sourceTime on the native
route.
…lay (superuser404notfound#803)

* feat(audio): LiveTelemetry.objectAudio reports the Atmos objects in play

While objectAudioRendering plays TrueHD Atmos or DTS:X, the 1 Hz
telemetry carries an ObjectAudioActivity: the bed it renders into, the
objects the presentation declares, how many play and how many sit
overhead, and its bed channels on height speakers (DTS:X's standard
heights). These are the counts the bed levels log line has carried
since superuser404notfound#731, for a host's stats panel without reading the log. The
bridge publishes them behind a lock of its own, so the sampler never
waits for a decode holding opLock.

aetherctl play takes --atmos-bed as serve does and prints the counts:
obj=13/13 up=4 bed=7.1.4 on a TrueHD Atmos remux.

* docs(audio): LiveTelemetry.objectAudio is nil without the object-audio traits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS

* style(diagnostics): keep audioBridgeOutputBytesLifetime's doc comment on it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS

---------

Co-authored-by: Vincent Herbst <superuser404@tuta.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A playlist can name any scheme, and URLSession reads file://localhost/...
from the sandbox. The relay's pump already cancelled any answer that is
not an HTTPURLResponse, so such a URI ended as a 502 rather than being
served, but that was a side effect. Admission now refuses every scheme
but http and https, and a request naming one answers 404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… 45 s exchange ceiling (superuser404notfound#732)

Under heldSourceConnection the HEVC-in-MPEG-TS VOD ingest downloaded every
segment whole before the demuxer saw a byte, its carriage probe downloaded
segment 0 whole, and one 45 s deadline covered head and body together. On a
slow origin a large segment could not finish inside it, and the expiry
surfaced as the socket's own POSIX timeout (Code=60), naming neither cause.

- PersistentHTTP1Transport.open returns the response head and a Body read
  piece by piece; get() is open plus collect.
- The 45 s ceiling covers only the wait for a response head. A body ends on
  its size limit or on 30 s without a byte (typed .stalled with
  received/expected bytes and elapsed time).
- One request per origin at a time: checkOut claims the origin and the next
  request waits for the connection to be pooled or closed, so a seek never
  opens a second connection beside a response still in flight.
- An abandoned body with at most 2 MB left drains for up to 3 s and keeps
  its socket; a larger remainder closes it.
- The probe reads the head and hands the open response to the producer.
- With the FIFO full the current segment is read to its end, so a pause
  idles the connection between requests, not mid-body.
- The h2 path's 45 s resource timeout is lifted the same way.

Measured on a loopback keep-alive origin at 200 KB/s with 11.6 MB segments:
before, the probe died at 45.1 s with Code=60 and the session left the
ingest; after, the ingest held and played.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…user404notfound#732)

- A probe response the producer's run does not open with (a seek onto
  another or an encrypted first segment) is abandoned before any request.
  It held the origin's claim unread, so the next request waited forever.
- A request cancelled while it waited for the origin does not take the
  connection; its cancellation handler would close the drained socket.
- A seek drains an abandoned body only when the rest arrives within the
  drain deadline at the rate seen so far; a slow remainder closes at once
  instead of holding the next request 3 s and closing anyway.
- next() checks cancellation before marking a read in flight, so abandon()
  never waits on a read that does not happen.
- A refused or redirecting body is drained for at most 45 s.
- No extra copy of each chunk into the pending buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…/fix/732-streamed-segment-bodies

fix(hls): stream segment bodies on the persistent HTTP/1.1 ingest, no 45 s exchange ceiling (superuser404notfound#732)
LiveStreamRefusalRetryTests failed on CI in six recent runs (main and four
PR branches): "a stream refused mid-session ..." and "a stream refused for
longer than the budget ..." with drained.failure != nil and too few requests.

Cause: AVIOReader read AetherEngine.reconnectBackoffScaleForTesting,
pinIdleSecondsForTesting and liveRefusalRetryBudgetForTesting from statics at
init. swift-testing runs these tests, and the five other suites that set the
scale, in parallel, so a reader picked up whatever its neighbours had set:
the budget test's 1 s budget plus a scale another suite's defer had put back
to 1.0 makes the mid-session test give up after its second refusal. Forcing
exactly that interleaving on main reproduces the three CI expectation
failures deterministically.

The three values are now AVIOReader init parameters (backoffScale,
pinIdleSeconds, liveRefusalRetryBudget) with the shipped defaults, the same
shape connStallTimeout and windowHighWater already take for this reason, and
the statics are gone. Six suites pass their values to the reader they build.
No behaviour change outside tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…/fix/live-refusal-test-race

test(io): reader test knobs are init parameters, not process-wide hooks
… split by object energy (superuser404notfound#731)

The TrueHD path no longer mixes object signals into the layout. The
decoder discards the matrix interpolation values and carries the
stream's channel-based presentation (Dolby's 7.1 or 5.1 rendering of
the same mix) alongside each block; BedSplitter divides every bed
channel per STFT bin between the floor speakers it covers and the
heights, by where the objects' energy lies, bounded by the bed and
balanced per band towards a direct render. Elements are plain point
sources. A 5.1 bed covers a 7.1 layout's sides and rears.

The splitter's 2048-frame latency is drained before the encoder
restarts elsewhere and at end of input, so no audio is lost there.
AETHER_TRUEHD_REFERENCE_RENDER=1 keeps the exact decode and mix for
measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…peruser404notfound#731)

The bed bound now compares an element with its bed channels per band
instead of per bin: in a single bin Dolby's mix can hold less than one
element where several cancel, and the per-bin bound cut legitimate
height content there. The balance only raises an output towards the
direct-render estimate and never lowers it, since the elements'
energies add up to less than a mix of them wherever they carry the
same signal. Against the exact render on nine height-heavy windows of
five films the heights went from -0.61 to -0.20 dB, the median
misplaced energy from 0.54 to 0.38 %; on Unfold from 4.6 to 3.0 %,
worst second 9.9 to 6.8 %.

The LFE delay line was one hop longer than the split outputs' latency.

Bridge tests run with and without the stream's bed; BedSplitterTests
pin the split, the 5.1 bed on a 7.1 layout and the timeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
Vincent Herbst and others added 28 commits October 21, 2026 07:24
…me size is wrong, changelog for superuser404notfound#828

The sample-clock model skipped the duration-less fallback whenever codec
geometry was admitted. A declared frame size that understates the packet
(implicit HE-AAC: codecpar 1024, packets 2048) fails the join on every
packet, so a duration-less track covered nothing and every seek refetched,
the regression superuser404notfound#775 fixed for TrueHD. The fallback now applies whenever the
join fails. Also keeps superuser404notfound#823's overflowing successor case as a regression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
# Conflicts:
#	CHANGELOG.md
…peruser404notfound#834)

The activator looped its silent Atmos stream beside APAC on every tvOS
route. Over AirPlay to a HomePod pair the programme stayed silent after
pause and resume, and froze after the second pause, until a seek. It now
plays only while every output port is HDMI, re-evaluates on route
changes, and logs its route verdict when that changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
# Conflicts:
#	CHANGELOG.md
…peruser404notfound#839)

Broadcast HLG (BBC iPlayer, UK DVB-T2 UHD) declares a backward-compatible
BT.2020 10-bit or BT.709 transfer and names HLG only in an
alternative_transfer_characteristics SEI (payloadType 147). Format
detection, the display criteria, the route's VIDEO-RANGE and the muxer's
colr all read codecpar.color_trc, so the source played as SDR.

The demuxer now peeks up to 8 video packets after open, gated on HEVC or
H.264 with BT.2020 primaries and a BT.709 / BT.2020 / unspecified
transfer, and an SEI naming HLG sets color_trc to ARIB STD-B67. Doing it
in the demuxer covers the load probe, probe(url:) and a live reopen with
one hook. The SEI walk is shared with the A53 caption parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…e behind the live edge

A live viewer on a link slower or less steady than the channel had no way to trade latency for a
buffer: each path kept its own distance (the loopback's 3 x TARGETDURATION, the software path's
delivery-unit lead, the origin's HOLD-BACK on the remote bypass), and `.fastZap` made it shorter.

The new option only ever moves the playhead further back, clamped to 45 s:

- Loopback: the served HOLD-BACK, the first-serve gate and its deadline follow the cushion, the
  window grows to hold it, and `.fastZap`'s shallow start is floored at it.
- Live HLS ingest: the join takes enough of the listing to cover it, so a listed backlog fills
  the cushion at link speed instead of at 1x.
- Software path: the join waits until the cushion stands behind the newest media and starts that
  far back, landings hold it and a rebuffer refills it before resuming. Without a DVR window the
  host keeps a packet ring that deep and still publishes no rewind window.
- nativeRemoteHLS: configuredTimeOffsetFromLive, preserved across stalls.

Measured with `aetherctl live --realtime --preroll 0` (raw TS, no backlog): first manifest after
19.2 s and ~17 s behind live without it, after 31.2 s and ~29 s behind with 30 s, no stall or
-16832 in either arm, isAtLiveEdge held. Software path (`--sw --preroll 8`): ~20 s behind with
20 s against 0.4 s without, with and without a DVR window.

`aetherctl play` and `live` take `--live-cushion SECONDS`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…own offset

Pre-merge review: on nativeRemoteHLS the cushion went straight into configuredTimeOffsetFromLive,
so a cushion shallower than the origin's HOLD-BACK (5 s against 18 s) moved the playhead CLOSER to
the edge, breaking the "only ever further back" contract. The offset is now raised to
recommendedTimeOffsetFromLive once the origin's playlist is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… a host-chosen distance behind the live edge
…, broadcast HLG

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…/now-playing)

Conflicts resolved as suggested in the review: stopInternal keeps stopSoftwareNowPlaying() and main's softwareClockSessionZero reset; aetherctl drops the PR's --atmos-bed parser and global in favour of main's objectAudioRendering(_:); docs take main's text with the software-host sentences added.
… shown

Selecting a PGS / DVB / DVD track armed the OCR worker for its native
WebVTT rendition at once, and with it the forward prefetch lead of 270 s.
On Matroska that side reader pulls every cluster, so a fullscreen viewer
paid for a rendition nothing displayed: on a 4K remux 434 MB of prefetch
in 90 s against 17 MB with the overlay-only lead, and an Apple TV 4K
(1st gen) dropped frames on an 89 Mbit/s source with a forced PGS track.

The worker now follows the native selection (setNativeSubtitleSelected,
reached by setNativeSubtitleRendering for PiP / AirPlay / an external
display) and raises or drops the prefetch lead with it. Because it now
starts the window empty, the native select of an OCR rendition waits until
the harvest has read and the worker has recognized every stored
composition through subtitleOCRLeadSeconds, instead of the cue-end
heuristic a sparse forced track never satisfies. The wait has no deadline
(a refused empty rendition is never retried, a partial one is cached empty
by AVKit), ends when the request changes, and reports its state after 60 s.

Measured with aetherctl against a 4K HEVC remux: fullscreen 17 MB of
prefetch in 90 s with the worker off; PiP at t=20 arms it, rebuilds the
prefetch at 270 s and selects with the cue after 2.6 to 72 s (the long
waits are Vision recognizing the first 4K composition).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…item

The loopback withheld its first media playlist until the live window
held the holdback or the host's liveEdgeCushionSeconds. That wait ran on
AVPlayer's playlist timer, which gives up after about 20 s on tvOS
(-16839 "Unable to get playlist before long download timer", then
-1008), measured in the tvOS simulator. A raw MPEG-TS channel filling at
1x after its connect burst never started with a 30 s cushion, and the
plain .standard holdback served after 19.2 s on a strict-realtime origin.

loadNative now waits out the same first-serve gate before host.load, so
the playlist request is answered at once (repeat request held 0.000 s)
and the session still plays the full cushion behind the edge.

Refs superuser404notfound#851

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… before AVPlayer is handed the item (AE#851)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ase (AE#682)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
ThreadCPUSampler states the CPU time each thread group consumed between
two samples, as a share of one core, with the main thread and threads
that ended inside the window on their own fields. The memprobe now emits
a cpuprobe line beside it every 30 s, so a field log from an Apple TV
nobody here owns reads as a coarse profile (DerPicknicker, A10X stutter
on 4K where Infuse plays clean).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ive selection, live cushion on tvOS

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…X bed test, no pause on HDMI, stale packet and the APAC anchor, DV P5 local stills

Before merge:
1. SoftwareAtmosRouteTests.apacRoutes takes .l714 for DTS:X (main renders DTS:X only into a bed
   that carries it one to one) and a new test pins that every other bed stays PCM while TrueHD
   Atmos renders into any. The software host now logs why a TrueHD or DTS track stays on PCM
   (SoftwareAtmosRoute.apacRefusal), as it already did for E-AC-3.
2. Route removal pauses only for outputs a viewer takes away: headphones, Bluetooth (A2DP, LE,
   HFP), AirPlay and USB, spelled as AVAudioSession.Port raw values (checked against the session
   on iOS / tvOS). HDMI is out, so an ARC / eARC receiver toggling on an Apple TV keeps playing.
3. A pre-seek audio packet can no longer anchor the APAC bridge: SoftwareAudioProducing gains the
   AE#492 feed epoch, the demux loop reads it before the read, and SoftwareAPACProducer checks it
   under the same lock as flush()'s startSegment.
4. A local software session reports supportsCacheBackedStills false for a Dolby Vision Profile 5 /
   10.0 stream without the DolbyVisionStillReshaping trait (FrameDecodeContext.yieldsStill).

Follow-ups on the FEL + TrueHD Atmos path:
- The demux loop's "decoder not recovering" clock fallback uses the producer's own first-buffer
  budget; the APAC producer's is the bridge's dead-decoder bound, since its first packet is about
  100 TrueHD access units in and the old 50 sent every TrueHD load to the video-anchored clock.
- The software host resolves a TrueHD track whose profile the probe missed from its major sync,
  sharing HLSVideoEngine.trueHDMajorSyncVerdict with the loopback route (superuser404notfound#838).

CHANGELOG: route removal and the setRate(0) interruption change are listed; the software-host
Atmos entry drops `aetherctl play --atmos-bed` (main has it).

Co-authored-by: Claude <noreply@anthropic.com>
AVAudioSession.Port.HDMI.rawValue is "HDMIOutput" (read on an Apple TV 4K, tvOS 26.6, and an iOS 26 simulator). Spelled "HDMI", superuser404notfound#834's route rule matched no real HDMI route, so the activator never ran: the device logged '[AtmosOutputActivator] off on route ports=[HDMIOutput]' beside an APAC 7.1.4 programme. The tests that spelled HDMI the same way now use the session's value.

Co-authored-by: Claude <noreply@anthropic.com>
…st plays object audio

The Atmos output activator followed only the engine's AVPlayer. It now also follows a software host whose audio reaches its renderer as APAC (TrueHD Atmos, DTS:X) or compressed E-AC-3 JOC, driven by the engine's transport state. On an Apple TV 4K (1st gen, tvOS 26.6) on HDMI to an AVR, the receiver showed Atmos for the loopback route's APAC but not for the same title's APAC on the software host (the Dolby Vision FEL route) until the activator ran there.

Co-authored-by: Claude <noreply@anthropic.com>
audioDelivery is .bridged for TrueHD Atmos / DTS:X as APAC and .streamCopy for compressed E-AC-3 JOC (the loopback route's terms), and activeAudioDecoder names them ('TRUEHD Atmos → APAC 7.1.4'). The background-audio check reads deliversAudio instead of == .decoded so those routes keep their background audio. On device the FEL route showed 'libavcodec TRUEHD → CoreAudio' / 'Decoded' while it played APAC 7.1.4.

Co-authored-by: Claude <noreply@anthropic.com>
…and detaches with its session

Review findings: play() after an audio interruption keeps state .playing, so the renderer-path activator is refreshed on every accepted play; a renderer session that is gone detaches the activator (route observer included) instead of only releasing the stream. AudioDelivery and docs/api.md describe the software host's .streamCopy / .bridged.

Co-authored-by: Claude <noreply@anthropic.com>
…g and clock fallback

Co-authored-by: Claude <noreply@anthropic.com>
setRate(0) parks the software host without moving state off .playing, so the activator kept its silent stream looping under a paused programme. It now reads the host's own playing flag in that state and is refreshed after every rate change.

Co-authored-by: Claude <noreply@anthropic.com>
@PieterVO

Copy link
Copy Markdown
Author

Moved to superuser404notfound#856 so it sits next to superuser404notfound#808 in the main repository. It is the same branch, and parity/now-playing can still fast-forward to it.

@PieterVO PieterVO closed this Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants