Repository navigation
feat(software-host): parity with the AVPlayer path: Now-Playing and remote commands, caption style, local scrub stills, Atmos, audio route and interruption handling - #808
Closed
pkajaba wants to merge 67 commits into
Conversation
…with state-mapping tests
…ession behind ownsVideoNowPlayingSession
Subtitles composited into software-path PiP frames read the user's MediaAccessibility caption style (font, size, text, background and window colour and opacity, edge style) and re-render when it changes. Fields the user has not overridden keep the cue's styling or the default look. SubtitleCaptionStyle.current() / observeChanges(_:) serve host overlays.
Such a session keeps no packet cache, so supportsCacheBackedStills was false and scrubThumbnail / scrubPreviewFrame answered nil. They now decode from the file through a FrameExtractor (base layer for dual-layer Dolby Vision), with no extra connection.
…ehind glyphs A field is the user's choice only when the user domain differs from the system default; the UseValue / UseContentIfAvailable flag is carried per field as a precedence instead of being read as a change. The uniform edge is a stroke pass under the fill, the overlay cache is keyed by the style it was drawn in, the system style is read once on first enable, and a released observation no longer reaches its handler.
The extractor entry polls isCancelled, applies the 750 ms / 900 packet bound and reports the decoded frame time, which becomes actualSeconds on the session axis. The past-the-end clamp measures from the file's own start time, so a file that does not begin at 0 is no longer clamped against its duration alone. The test fixture is built once per process.
E-AC-3 JOC is enqueued to the audio renderer compressed, with the Atmos decoder primed once per process. TrueHD Atmos and DTS:X render to APAC under the existing objectAudioRendering option. Everything else, live sessions and .off keep the PCM decode. Adds aetherctl play --atmos-bed.
…l detection, review fixes
…command state alone on stop
…line knock-out The past-the-end clamp measures the container's duration from the container's start time and keeps the video stream's start as the target axis. Choice.resolved(cue:) is public so a host overlay can apply precedence. A uniform outline knocks the glyph area out before the fill, so a translucent text colour no longer sits over the stroke. The standard-look test checks measured values, and the observation test delivers an in-flight notification after release.
…mmand enablement restored only if unchanged
Check for a stop after the route-choosing suspensions and treat it as a cancellation. Start the Atmos decoder registration when the route is chosen and await it at the end of load. Open the audio tap decoders on first use. Flush the APAC encoder at end of stream and log a refused bridge packet. Separate PCM-fallback reasons, retry the dec3 build on a few packets, search dec3 only inside moov. Log renderer errors in release builds. Offer the Atmos audio pick on the software host and update the docs and tests.
…ls without the gate The uniform outline is drawn as a stroke and a copy-blend fill inside a transparency layer, replacing the clear knock-out: line background and window stay intact under translucent text, and opaque glyph edges stay solid. The observation test makes the post-release delivery the first one on its observation, so only the gate can stop it. clampSeekSeconds gets its doc comment back.
The E-AC-3 look read the stream's codec parameters with no guard held, so a stop closing the demuxer could free them mid-read. Read inside the peek closure under withStream and keep only copied values. A closed demuxer yields no audio packet.
The description handed to the audio renderer for APAC packets had no dapa cookie and no packet size, so the renderer failed every packet inside while reporting no error and playing silence. Build it with the cookie and 1024 frames per packet where the encoder is created.
Require the cookie in the test instead of force-unwrapping it, give the decode callback stable buffers instead of escaping pointers, and refuse to build a description from an empty cookie.
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
…le and Atmos source files
…, wrap the Atmos changelog entry
… APAC producer comes back nil
… the hardware decoder cannot play
# Conflicts: # README.md # Sources/AetherEngine/Audio/Spatial/ObjectAudioRendering.swift # docs/api.md # docs/formats.md
…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>
This was referenced Oct 22, 2026
Conflicts: CHANGELOG.md (Unreleased › Changed takes main's two entries beside ours) and AtmosOutputActivator.swift (main's TrueHD-only rule for the AVPlayer path, in this branch's followAtmosOutput). The software host follows the same rule: its activator now runs for TrueHD Atmos rendered to APAC and for compressed E-AC-3 JOC, not for DTS:X rendered to APAC (SoftwareAtmosRoute.carriesDolbyAtmos, tested; docs/formats.md). Co-authored-by: Claude <noreply@anthropic.com>
Main now carries the HDMIOutput fix (814fef1), so this branch takes main's version of it and drops its own CHANGELOG entry. CHANGELOG (Unreleased › Added / Fixed) and docs/api.md (stereoPresentation row) keep both sides; SoftwarePlaybackHost keeps the first-buffer budget beside main's live-join keyframe wait (AE#844). Co-authored-by: Claude <noreply@anthropic.com>
…rts an airplay video receiver and wait for a display mode switch before playing A Profile 7 FEL source loaded on the dual-layer route draws into a local layer that no AirPlay receiver picks up, so a TV the viewer sent the title to played sound only. When the host reports that the receiver shows video (setAirPlayReceiverShowsVideo, default off) the load takes the native route instead; an audio-only AirPlay output keeps the dual-layer route. The audio session reports every AirPlay output as the same port type, so the engine cannot tell them apart itself. The route is read once per load. Also: airPlayPictureStaysLocal reads false while the host reports the screen as mirrored (setScreenMirrored), and the software host waits for a running display mode switch before it starts playing, as the native path does (tvOS).
# Conflicts: # CHANGELOG.md # README.md # Sources/AetherEngine/AetherEngine.swift # Sources/aetherctl/PlaybackCmd.swift # Sources/aetherctl/main.swift # docs/api.md # docs/architecture.md # docs/formats.md
…receiver gate and software play gate)
…ty/now-playing: superuser404notfound#808 review items 1-4, software-host Atmos activator and delivery reporting
… doc comment, apac first-buffer comment, changelog entries for the route follow-ups
…4notfound#854) into parity/now-playing-856-fold Two conflicts. AtmosOutputActivator.swift: the type doc comment keeps both the software-host paragraph and the superuser404notfound#848 pause-grace paragraph; update() has one shared 'guard playing else { holdThroughPause(); return }' after the route check, covering the AVPlayer branch (playing = timeControlStatus != .paused) and the renderer branch (playing from the host transport), so the software host gets the same 60 s pause grace; pauseAfterGrace is cancelled before player.play(), release() cancels it, detach() reaches release() through follow(nil). Issue834AtmosActivatorRouteTests.swift: kept both the fold's softwareTransportTruthTable and main's portTypeIsHDMIOutput (superuser404notfound#854) pin.
…e chunk and the priming frames
superuser404notfound
pushed a commit
that referenced
this pull request
Oct 25, 2026
…tyle, local scrub stills, Atmos, route removal, channel count, interruption hold
superuser404notfound
pushed a commit
that referenced
this pull request
Oct 25, 2026
… activator and delivery reporting Conflicts with main resolved: AtmosOutputActivator keeps #848's pause grace on the new renderer path, stopInternal keeps endClockHold(), formats.md and CHANGELOG take main's text with the PR's entries.
Owner
|
This landed on main together with #856: |
superuser404notfound
pushed a commit
that referenced
this pull request
Oct 26, 2026
…t, APAC first-buffer comment, changelog for the route follow-ups Folds a168689 and 5d75e12 from the #808 branch, pushed after it landed: a truth table for softwareAtmosOutputPlaying, the first-buffer budget comment counting the encoder's 2048 priming frames beside the 2048-frame chunk (about 4096 frames, some 100 TrueHD access units, as the changelog already says), and changelog entries for the channel-count follow and the interruption play hold, which landed with #856 without one. 3cb9dee (drop the LibTrueHD pin from Package.resolved) is left out: main depends on LibTrueHD 3.1.0 since the object renderer, so the pin is real now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
superuser404notfound
pushed a commit
that referenced
this pull request
Oct 26, 2026
… (thanks @pkajaba) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
Owner
|
The four commits pushed after the merge are handled in
Thanks again, Pavel. |
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.
The software-host parity work from #702 (the "parity: subtitles, PiP, scrub thumbnails" thread). The software host (the Dolby Vision FEL route and video the hardware decoder cannot play) has no
AVPlayer, so several things the AVPlayer path gives for free were missing. Nine pieces, on one branch. It also includes the changes from #856 by @PieterVO (review items 1 to 4 and the fixes from his Apple TV run): commits 6e88cb8, a1e7fb2, bf48b1c, 1d1b2f5, 6bd9b89, 1912cd3 and 527e2c1, plus his merges of main (51b4137, 1f287d6, 4628c0f, 6ddadc1). They are merged in, not rebased, so his authorship is kept.What's in it
Now-Playing card and remote commands, opt-in.
ownsSoftwareNowPlaying, off by default and independent ofownsVideoNowPlayingSession, so the AVPlayer paths are unchanged.MPNowPlayingInfoCenter.default()and registers play, pause, toggle, skip (remoteSkipInterval, default 10 s) and change-position onMPRemoteCommandCenter.shared(). It republishes on seek, pause, speed change, stall and end.softwareNowPlayingActivetells a host when to add its own next and previous.isEnabledunless another owner changed it, and clears the info only while the center still holds what it last published.System caption style on the software host. Subtitles composited into software-path PiP frames use the user's Settings > Accessibility > Subtitles & Captioning choices (font, size, text colour and opacity, background and window colour and opacity, edge style), and re-render when the setting changes. A field counts as chosen only where it differs from the system default.
SubtitleCaptionStyle.current()andobserveChanges(_:)expose the same style to a host that draws its own overlay. Bitmap subtitles are untouched.Scrub thumbnails on a software session that plays a local file.
scrubThumbnailandscrubPreviewFrameanswered nil there (supportsCacheBackedStillswas false), so a FEL title had no scrub stills unless the host built its ownFrameExtractor. The engine now decodes them from the file (the base layer for a dual-layer title), through the same calls the AVPlayer path answers, withscrubPreviewFramehonouringisCancelledand the 750 ms / 900 packet bound. A Dolby Vision Profile 5 / 10.0 file without theDolbyVisionStillReshapingtrait keepssupportsCacheBackedStillsfalse (review item 4). Remote software sessions already served stills from the packet cache.Atmos on the software host. The host decoded every track to PCM, so a JOC stream played as its 5.1 core and TrueHD Atmos or DTS:X as its 7.1 bed. With
LoadOptions.objectAudioRenderingset (the engine's existing option for object-audio delivery), E-AC-3 JOC is handed to the audio renderer compressed, after the system's Atmos decoder is registered once per process, and TrueHD Atmos and DTS:X are rendered into the chosen bed and encoded as APAC..off(the default), live sessions and every other codec keep the PCM decode. The automatic audio pick prefers a TrueHD Atmos or DTS:X track on the software host too. Two fixes came with it:From the feat(software-host): parity with the AVPlayer path: Now-Playing and remote commands, caption style, local scrub stills, Atmos, audio route and interruption handling #808 review (items 1 to 4) and the Apple TV run in fix(software-host): #808 review follow-ups (stacked on #808) #856:
setRate(0)and interruption resumes, and detaches with the sessionaudioDeliveryreports.bridged(APAC) and.streamCopy(compressed JOC) on the software host,activeAudioDecodernames the route ("TRUEHD Atmos → APAC 7.1.4"), and background audio counts both as audioPause when the output route is removed. On iOS and tvOS the renderer paths (software host and audio host) now pause when the active output goes away (headphones unplugged, a wireless output lost). The decision is platform-neutral and unit-tested; it fires only while playing or seeking. It pauses for headphones, Bluetooth (A2DP, LE, HFP), AirPlay and USB outputs only. HDMI is left out, so an ARC / eARC receiver switching on or off on an Apple TV keeps playing (review item 2). The AVPlayer path already does this itself and is untouched.
Follow a change of the output channel count. When the route changes (say a 7.1 receiver to 2.0 speakers), the engine re-evaluates the preferred output channel count against the source and the new maximum, and writes it only when it differs, on the shared audio-session queue, so a repeated notification ends at the first no-op.
play()during an audio-session interruption. On the renderer paths a play press during an interruption is held instead of starting a stalled clock; the session is reactivated and the held play runs when the interruption ends. A second press while one is held reactivates and plays at once, and a pause, a stop or a reload cancels the held play. Without a renderer path nothing is held.FEL yields to an AirPlay video receiver, opt-in. On iOS, an AirPlay output holds the audio route and the system reports every AirPlay output as the same port type, so the engine cannot tell an Apple TV from a HomePod. The host says it with
setAirPlayReceiverShowsVideo(_:)(defaultfalse, so nothing changes until a host calls it): with it on and a wireless AirPlay route active, a Profile 7 title keeps the native route (8.1 where the display takes Dolby Vision; the enhancement layer is lost), because a video receiver takes a stream, not a composed picture. An audio-only output such as a HomePod keeps FEL. The decision is the newDualLayerRouteDecision.Reason.airPlayVideoReceiver, logged with the rest of the route line, and is read once per load (a URL source decides again onreloadAtCurrentPosition(); a custom source keeps its route until the nextload()).setScreenMirrored(_:)(iOS) lets a host say the screen is mirrored, soairPlayPictureStaysLocalreads false while it is on: the picture goes to the receiver too.Software play waits for the display mode switch (tvOS). The software host now goes through the same display-criteria gate the AVPlayer path uses before it starts the clock, so a rate-only or HDR mode switch finishes before the first frame instead of after it. The gate does nothing when the display criteria did not change or the load is a secondary output (nobody writes criteria for it), and it has no effect off tvOS.
docs/api.md,docs/architecture.md,docs/formats.md,docs/dolby-vision-fel.mdand the README row describe the contracts. CHANGELOG entries are under Unreleased.How it was tested
The branch is merged with the current origin/main (20a2272, including #848, #854 and #855), and with the commits from #856 listed above.
Tested on Mac
On the merged tip (7e8b09e):
TrueHDObjectAudio,DTSXObjectAudio,DualLayerDolbyVisionandDolbyVisionStillReshapingtraits on, the same suites plus the spatial bridge (including the stale packet) pass (156 tests in 17 suites).xcodebuild build-for-testingpass. The tvOS build is the only compile check of the activator's software-host branch.swift test4950 Swift Testing tests in 685 suites and 800 XCTest cases (783 + 17 in the SMB target, 1 skipped), all passing; the strict-pool run (LIBDISPATCH_COOPERATIVE_POOL_STRICT=1) passed except the known wall-clock testVideoPrerollTests"holds the clock past the plain timeout", which passes alone. It was not re-run after merging main 20a2272, which changed the panel HDR proof in the display criteria, the demuxer's PGS probe parking and a preroll test (9 files). Only the panel HDR proof suite of those was run on the merged tip (above); main's own tests for the PGS probe parking and the audio probe come with the merge and were not re-run here.SoftwareNowPlayingTests,SubtitleCaptionStyleTests,SoftwareLocalScrubStillTests,SoftwareAtmosRouteTests,SoftwareAPACFormatDescriptionTests,RouteRemovalPauseTests,ChannelCountChangeTests,InterruptionPlayGuardTestsandAirPlayParityTests(the host-signal and mirror truth tables, the setter defaults, the software play-gate grace, and with the trait on the video-receiver route decision, including that an audio-only output keeps FEL), plus additions toFrameDecodeContextClampTestsandItemSwapStillTests.RouteRemovalPauseTests.hdmiDoesNotPauseandportTypesMatchTheSession,SoftwareAtmosRouteTests.dtsxNeedsAOneToOneBedanddolbyAtmosRoutes,SoftwareLocalScrubStillTests.profile5FollowsTheStillVerdict,SpatialAudioBridgeTests.stalePacketDoesNotAnchorThePostSeekRun,AudioDeliveryClassificationTests.atmosRoutesReportTheirDelivery, the activator's transport truth table inIssue834AtmosActivatorRouteTests, and the renderer path's pause grace inIssue848AtmosActivatorPauseGraceTests.SoftwarePacketMemoryFIFOTests,SoftwarePacketCacheStorageTests, the refusal tests,DoviFelFuzzTests,VideoPrerollTests, and once the wall-clockIssue848AtmosActivatorPauseGraceTests"a pause keeps the stream playing until the grace runs out") are wall-clock or free-disk based and pass when re-run alone.Tested on Apple TV (by @PieterVO, #856)
Apple TV 4K (1st generation), tvOS 26.6, HDMI to a Dolby Vision TV through an Atmos receiver: a Profile 7 FEL title with TrueHD Atmos takes the dual-layer route with TrueHD rendered to APAC 7.1.4; the TV shows Dolby Vision and the receiver Atmos; over three sessions 0 lost frames, 2 to 39 display-layer drops per session (mostly at start), no rebuffers, the clock armed from audio. A Profile 7 MEL title on the loopback route: the activator is active on HDMI.
Still needs Apple TV
This stays a draft until these have run on a device:
.loading. On the software path the state may read.pausedbefore play when the load reaches readiness during the wait (the same.pausedas the play-gate item below), so the activator can play, hold through the 60 s pause grace, and play again. Check that the receiver flips to Atmos once and stays there.setAirPlayReceiverShowsVideo(true)and an Apple TV picked as the AirPlay output, a Profile 7 title loads on the native route (the log line namesairPlayVideoReceiver); with a HomePod as the output the same title keeps FEL. The setter is inert until a host calls it..paused) is a caveat to look at.Follow-up (not in this PR)
Open items from the review, to be taken separately:
LiveTelemetry.objectAudiois nil on the software hostdocs/api.mdon host Now-Playing targets besidesoftwareNowPlayingActivevolumeIsLocalKey)When spatial rendering is unavailable, the software host falls back to the PCM decode, so asking for Atmos never costs a session its audio.
audioDeliveryandactiveAudioDecodernow say which route a session took (.bridged,.streamCopy,.decoded), but the reason for a fallback is still only a log line. It happens when:objectAudioRenderingis.off, or the session is livedec3box cannot be built from the first packets, or the passthrough producer cannot be createdspatialRenderingLayoutreturns nil), the OS is older than 26 or has no APAC encoder, the build lacks theTrueHDObjectAudioorDTSXObjectAudiotrait, a DTS-HD MA track is not DTS:X, or the object decoder or the APAC bridge fails to buildIn those cases JOC plays as its 5.1 core and TrueHD Atmos or DTS:X as its 7.1 bed, as before this PR. The follow-up is to publish why a session fell back so a host can show it, and to decide whether any of these cases should do something better than PCM.