Repository navigation
Merge upstream AetherEngine 7.36.0 into the Silo fork - #21
Conversation
LiveTelemetry.demuxerBytesFetched read 0 for every custom-reader source, because CustomIOReaderBridge reported a constant 0. A disc image played over HTTP therefore showed no origin bytes and, on the software path, no networkThroughputMbps, although HTTPDiscIOReader pulled the whole title. HTTPDiscIOReader now counts the response bodies it receives, ConcatIOReader passes its base reader's count through, and the bridge reports it. A host's own IOReader is not asked and still reports 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LoadOptions.progressiveSegmentDelivery (off by default) lets AVPlayer use a segment's first fragments instead of waiting for its cut, which on a slow link is the time to first frame. - The producer registers each VOD segment's staging file with the cache as it opens it, and the muxer flushes a fragment every 0.5 s instead of holding up to 8 s. - SegmentCache.fetchSource answers a segment being written with a ProgressiveSegmentReader over its staging file; the file is only appended to and renamed as it stands on adoption, so the reader sees a growing prefix of the final segment. - HLSLocalServer sends each read as a chunk and ends at the seal; an abandoned segment closes the connection without the final chunk so AVPlayer asks for it again. - Each chunk sent counts as fetch activity for the superuser404notfound#65 wedge watchdog, which otherwise reads one long transfer as AVPlayer no longer fetching. - aetherctl play --progressive-segments drives the option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the temporary directory's volume ran out of space, the session
directory, the segments or the muxer's staging files could not be written,
every muxer revive failed the same way, and the give-up arm surfaced
vodSourceFailed ("Source audio cannot be muxed"), or
audioBridgeProducedNoOutput on a bridged track. Both send a host's fallback
ladder the wrong way for a file that plays once space is freed.
SegmentCache now latches an out-of-space write from the session directory,
a stored segment or a muxer staging file (reported through a new
MP4SegmentMuxer callback), and handleVODMuxerFailure surfaces the new
PlaybackErrorKind.storageExhausted ahead of the audio verdicts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
superuser404notfound#711 keeps the outgoing item mounted while the source reopens, but replaceCurrentItem still drops the AVPlayerLayer to black until the new item decodes its first frame, and on device that read as a flash on every switch. The rebuild now reads the frame on screen from the paused item (NativeAVPlayerHost.captureDisplayedFrame, ~20 ms paused; a playing item needs ~300 ms before a fresh output sees anything, which is why it reads after the pause) and lays it over the hosted layer on the bound AetherPlayerView as a one-sample AVSampleBufferDisplayLayer, so an HDR frame goes through its colour attachments as the software path's frames do. It comes down when the next host session reports isVideoReadyForDisplay, on stop, load, a failed or superseded rebuild, unbind, or after 8 s. Skipped for Dolby Vision (the output vends the base layer, which has no displayable colour of its own for Profile 5), PiP and external playback. Covered on a generated 4 s H.264 + AAC file: a playing and a paused item hand over a frame, an audio rebuild holds the picture and releases it on the next item's first frame (red with the hold disabled), stop takes it down, and the skip rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…endered host The first device run of the held picture logged no `held picture` line at all: Sodalite renders the native path through AVPlayerViewController and binds AetherPlayerView only for the software path, so boundView was nil and the hold returned without a trace. A host shaped like that now binds a transparent, input-free AetherStillView (bindStillView / unbindStillView), placed above the video and below its chrome, and sets its videoGravity to its own player's fill. The engine holds on that view first and on the bound AetherPlayerView otherwise. Both share one HeldStillPresenter. "No surface bound" and a frame that cannot be wrapped are now logged skips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…re too Software path: an audio-switch rebuild replaces host and layer, and the old host's stop() flushed its layer to black for the whole startup of the new one. The renderer's picture is read back first (displayedPixelBuffer, or the newest frame enqueued, which SampleBufferRenderer now keeps until a flush) and held until the next host's first frame (armed where loadSoftware installs it). Dolby Vision: profiles with a displayable base layer (8.x, 10.1+) hold it from the native output as before, now shown as HDR. Profile 5 and AV1 Profile 10 have none; the frame is decoded from the resident segment through the scrub stills' RPU reshaping (DolbyVisionStillConverter), frame-accurately via a new internal FrameExtractor.snapshot(afterFirstFrameBy:) that measures from the segment's first frame (its tfdt may be absolute or zero-based). Measured on an M1 Pro, Profile 5 UHD: 1.53 s at 1920 wide, 0.40 s at 960, 0.11 s at 480, plus ~0.24 s of decode per second into the segment, so the still is 960 wide and the switch waits up to 3 s for it with the outgoing picture paused. A guard that dropped a read frame now logs why, and the native capture allows 500 ms (paused it answers in ~20 ms; one suite run under the Profile 5 decode load missed 250). Tests: frame-accurate relative snapshot, software rebuild hold and release, the reshaping rule, and, against the local Dolby fixtures (skipped without them), Profile 5 reshaped inside the budget and Profile 8.1 from the output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…d transport, yucelokan's superuser404notfound#708-superuser404notfound#711 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…nOS builds CI on a385289: the macOS runner's AVPlayerItemVideoOutput vends nothing for a paused item (a playing one it does; a Mac and an Apple TV answer paused in ~20 ms), so every native hold test failed there. After 100 ms without a frame the paused item is asked to render its own frame again with a zero-tolerance seek to where it stands; the item is about to be swapped out anyway. visionOS has no isExternalPlaybackActive, and displayedPixelBuffer needs 1.1; the tvOS + visionOS job failed on both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… only The zero-tolerance re-render did not change it (CI on superuser404notfound#722): the runner's AVPlayerItemVideoOutput vends no frame for a paused item at all, while a Mac and an Apple TV answer in ~20 ms. The seek is removed again, since it fixed nothing measured. The five tests that need a paused read are disabled under GITHUB_ACTIONS with that finding as the reason; the playing read, the software path, the frame-accurate snapshot and the rules still run there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…/fix/held-still-ci fix(held-picture): re-render a paused item that vends no frame; visionOS builds
…stamped-resident-previews
…ded-source-snapshots # Conflicts: # CHANGELOG.md # Sources/AetherEngine/FrameExtractor/FrameDecodeContext.swift # Sources/AetherEngine/FrameExtractor/FrameExtractor.swift # docs/api.md
…t-live-replay-guard # Conflicts: # CHANGELOG.md
Every live video and audio packet on the stream-copy path is signed, so the Data copy per packet and the [UInt8] digest box were an allocation pair per packet for nothing. The digest is now SHA256.Digest over the packet's own buffer. The side-audio branch in liveReplaySample was unreachable behind its own sideAudioDemuxer == nil guard and is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…snapshots, exact live replay guard (yucelokan's superuser404notfound#715-superuser404notfound#717) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ded-vod-source-opening
…and LoadOptions tables seek(to:) names the forward-only rejection and the 0.75 s landing rule, canSeek and isSourceSeekable sit beside it, and sourceOpenPolicy is a LoadOptions row with its defaults, instead of two prose sections under "Public but not host API". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…igurable-live-startup
…-dvr-retention-leases
…a thread, not a starved async poll CI run 37722262358 failed three SourceOpenRecoveryTests while every other suite on the runner ran 50 to 80 times slower than locally. Each test drove the reader into a short window (a slot wait of at most 4 s, a body stall, the alternate data request) and then acted on it from an async test task polling every 20 ms. On a starved cooperative pool that poll came back too late: the teardown test's slot wait had already given up, so its waiting == 1 never recurred and it hung into the 2-minute limit; the data retry and the held connection hit an engine timeout of their own and failed on an error they were not about (noResponse, a restart from byte 0). ProbeTestTrigger runs the step on its own thread, which is where ProbeTestJob already runs the engine half, and a missed window now fails at once with a message instead of hanging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…surface lives in the Live and LoadOptions tables seekToLiveEdge(offsetSeconds:), currentItemLiveEdgeTime, liveTargetDurationSeconds and the two DVR lease calls are rows of the Live and DVR table, the timestamp-reset contract sits beside them, and the two startup options plus softwareDVRRetention are LoadOptions rows, instead of three prose sections under "Public but not host API". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
Review follow-ups on superuser404notfound#755: - On a resume the verdict peeked the file head, and step 8's seek then dropped those packets: one wasted range read per start onto any 48 kHz DTS-HD MA track. The demuxer is now put at the first producer's start before the peek (`seekToInitialProducerStart`, step 8's seek moved into a helper), and step 8 skips its seek when that already happened, so the peeked packets are the producer's first ones. LibDTSX decides mid-stream as well (aetherctl serve --start-position 40: verdict 1 ms, audio gate gapMs=0.0; plain DTS-HD MA refused at 8 s, channel bridge opens on target). - The peek holds every stream's packets until the producer reads them, so it is capped at 24 MiB as well as 512 packets. - A stream that decodes at a rate other than 48 kHz is refused on its first block instead of failing every block in the bridge. - TFC sits on the front-top row (0.5, 0.25, 1) as in the TrueHD map since superuser404notfound#742; LSD/RSD map to the side speakers like TrueHD's. - Docs: the angular figures were measured before superuser404notfound#742, the 21 ms after a start or seek are skipped rather than filled with silence, the 1200-packet silence report is 12.8 s of DTS, and the OS < 26 pick change for TrueHD Atmos gets a Changed line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…y a budget a starved run spends The 5 s pin still failed once on main (cee2ccc, run 38362554779): the budget counts from the prefetch's issue, and that run was starved for about 25 s (every test in the superuser404notfound#281 suite took 24 to 29 s, including the one with no origin delay). The budget ran out before the read arrived while the body that had already landed was still queued for its delegate, so the reader reconnected at the read position. The test's subject is whether the reader waits, not for how long (the calibration has its own test), so the budget is now 90 s and the suite's two-minute limit is the bound. The failure message carries the reader's own log lines, so a future red run names the prefetch outcome instead of only the ranges. Checked both ways: with the budget forced to 0 it fails with exactly the CI range list; with the fix the full suite passes (4345 tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… to twelve rounds Failed once in the one-thread cooperative pool job (run 38364557181): bounded 1.11 s vs plain 0.074 s. Measured locally in the strict-pool suite, a loaded run stalls EITHER fetch at random for up to 2.7 s against 16 ms unloaded, so three samples can all land in a stall; the delegate path is not slower than the plain one. A stall only ever adds time, so the best sample converges on the real cost: the loop now stops as soon as the bound holds and takes up to 12 rounds before failing. The per-byte accumulation the test guards is slower in every sample and still fails (checked by swapping it back in: 0.84 s vs 16 ms after all 12 rounds). Both full suites pass, default and LIBDISPATCH_COOPERATIVE_POOL_STRICT=1 (4345 tests each). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ang from a starved run Failed once in the one-thread cooperative pool job (run 38373967029): the capture returned nil after its 2 s timeout while the item was still playing, in a run that stretched this test to 22.7 s (about 1 s alone). A playing item vends a frame continuously, so the timeout only has to catch an output that never does; 15 s is inside the suite's one-minute limit. The failure message now carries the engine state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ard-only open (AE#733) A playlist endpoint answering 200 with no Content-Length (chunked, typical for a dynamic /api/playlist?id= handler) never reached the AE#268 ingest. Audit DMX-101 hangs up the ranged open at the head and the open goes forward-only on one unranged GET, but the AE#140/superuser404notfound#154 #EXTM3U check only read the persistent window, which that path leaves empty. FFmpeg was then handed the m3u8 and the load ended in "Invalid data found when processing input", the exact error the reporter saw on device. The same classification now runs on the forward-only stream's first bytes. Measured with aetherctl against a loopback origin serving the long-GOP HEVC fixture through /api/ts?url=...: chunked and length-less playlists failed before and now route through AE#268 and seek to segment 57; the Content-Length variant is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…found#752, superuser404notfound#755) The repository moved from kristofferR to superuser404notfound. 1.0.1 exports rust_eh_personality weak, so a host with Objective-C and C++ links next to LibDovi and LibTrueHD again (superuser404notfound#752), and stops repeated damaged sections from pushing the input timeline ahead of the source, which delayed the audio after a second gap until a reset (found by @andreencar). Package URL, docs and changelog follow the move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…h way it went (superuser404notfound#744) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…uilds Starlight parses ":X" as a remark text directive, so its link validator slugs the heading as "dts-with-heights" and rejected every link to #dtsx-with-heights. The escape renders the same heading on GitHub and on the site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…-source-probe-error-cause fix(diagnostics): preserve the custom-source probe failure cause
…under Unreleased The PR branched before 7.35.0 was cut, so the merge put its line under that release's Fixed section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ck-origin-callback-priority fix(demux): align origin-slot completion with playback priority
…uperuser404notfound#746) superuser404notfound#746 moved the 429 accounting into syncRequest's completion callback so the slot is not handed on before the refusal is on the books, but the detour still charged the same answer again afterwards. Each charge halves the origin's concurrency and doubles its quiet period, so one 429 on a detour block quartered the budget. The detour now only reads the Retry-After it hands its caller. The new callbacks also passed an absent Retry-After as 0, which the budget reads as a zero quiet period instead of the exponential ladder; the old detour path mapped it to nil. One helper charges the response for all three finite request paths and keeps that mapping. The CHANGELOG entry moves under Unreleased: the branch predated the 7.35.0 cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…eplay-recovery
…SourceReset (superuser404notfound#747) The verified-replay exhaustion from superuser404notfound#747 published only the new liveSourceReplayExhausted subject. A host that never subscribed to it (every existing integrator, Sodalite on a Jellyfin transcode respawn among them) held the last frame for good, where it used to retune on liveSourceReset. The new subject now fires only under LoadOptions.hostRetriesExhaustedLiveReplay; by default the session reports the exhaustion through onLiveSourceReset. The layer-unready presentation repair was wired for every native session, VOD included, and an in-place swap that joins audio-first while video waits for its IDR could earn it a nudge and an item replacement. It now runs only under nativeVideoFrameRecovery, like the decoded-output half. CHANGELOG: the PR's entries collapsed into one under Unreleased; three of them had landed under 7.35.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…e source plays once, video returns after a screen lock Also files the missing CHANGELOG entries for superuser404notfound#739 and superuser404notfound#733. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…': renew the live playlist when a GOP outgrows the seal Conflicts with superuser404notfound#747 resolved by keeping both: the reopen/exhaustion handlers and the renewal handler are registered side by side, and reloadStalledConsumerItem takes both preserveLivePosition (superuser404notfound#747 presentation repair) and replacementURL (renewal); preserveLivePosition still wins the rejoin. The CHANGELOG line moved from 7.35.0 to Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
Takes upstream main through 74c81b3: 7.29.0 to 7.36.0 plus the unreleased live-playlist renewal (superuser404notfound#748). Retired fork changes, now covered upstream: - TrueHD Atmos to APAC (d903760, 455bcd6, fa70ab5): upstream superuser404notfound#731 adopted the fork's bridge, encoder, sample entry and renderer, with priming kept, and builds on it (encode pipeline, metadata hold, peak limiter, Atmos output activator, DTS:X). LibTrueHD moves from 2.0.0 to upstream's 2.3.0 pin; LibDTSX 1.0.1 is new. - TrueHD Atmos diagnostics (fork #17: 8ff23f8, 59d8406, 703f790, 18729d1, d6cb090): BedLevelMeter, the route description and the 1200-packet AE#641 report shipped in upstream 7.34.0 unchanged. - Sidecar decode off the cooperative pool (241dff5): now upstream's BlockingWork.detached, still calling the fork's authorized decodeBlocking. - The fork's CHANGELOG entries for the above. Kept, and fitted onto upstream's code: - Audio-switch tracking (5cda1c1, ba895a5, d6cb090): audioSwitchInFlight beside upstream's held-picture state, and pendingAudioSelection stays internal for BackgroundTeardownSelection. - httpRequestAuthorization stays a known LoadOptions field beside upstream's three new ones. - stopInternal cancels the remote-HLS preparation and stops the subtitle relay, and resets the scrub-preview generation. - The superuser404notfound#694 park suite stays serialized and also runs off the cooperative pool. - The TLS live suites keep the fork's structure; the accepted-read loop drops its wall-clock deadline as upstream's does. - DocumentedConstants keeps the 8- and 10-channel APAC CODECS checks in upstream's combined test, and cli.md keeps the accurate apac.31.02 / 31.03 / 31.04 levels. - docs/api.md keeps the paused software-video seek sentence, and the objectAudioRendering row names `ObjectAudioRendering` again, which PublicAPIDocumentationTests requires (upstream main fails it). New: - HTTPAuthorizationWait and the remote-HLS timestamp-anchor probe start their tasks with BlockingWork.detached, which upstream's lint (BlockingWorkTests) now requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Important Review skippedToo many files! This PR contains 268 files, which is 168 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configuration
⛔ Files ignored due to path filters: 2
📒 Files selected for processing: 268
You can disable this status message by setting the
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Silo Kody — review completeReview finished. Check the inline comments for findings and verify each suggestion against the code and tests. Reviewing changes in Silo
Review settingsReview OptionsThe following review options are enabled or disabled:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ab3e3cdeb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let first = await group.next() ?? nil | ||
| group.cancelAll() |
There was a problem hiding this comment.
Enforce the Dolby Vision hold budget outside the task group
When a Profile 5/10 audio switch needs more than three seconds to decode the resident frame—for example with a long GOP or unusually expensive segment—the timeout child can finish first, but returning from withTaskGroup still waits for every child to exit. This cancelAll() does not stop the decode because snapshot(afterFirstFrameBy:) passes isCancelled: { false } into decodeFrame, so the outgoing player can remain paused and the audio switch delayed well beyond heldPictureDolbyVisionBudgetSeconds; use a cancellation-aware bounded extraction or an unstructured task whose completion is not awaited after the deadline.
Useful? React with 👍 / 👎.
| let first = await group.next() ?? nil | ||
| group.cancelAll() | ||
| return first |
There was a problem hiding this comment.
Timeout enforcement fails for Dolby Vision frame extraction: the three-second task-group race still waits for decoding after the timer wins because FrameExtractor.snapshot(afterFirstFrameBy:) awaits a dispatch-queue continuation and passes isCancelled: { false } to its decoder, keeping the outgoing native item paused and delaying the entire audio switch beyond its budget. Propagate cancellation into the extractor's read/decode loop rather than relying on group.cancelAll().
Prompt for LLM
File Sources/AetherEngine/AetherEngine+HeldPicture.swift:
Line 151 to 153:
Timeout enforcement fails for Dolby Vision frame extraction: the three-second task-group race still waits for decoding after the timer wins because `FrameExtractor.snapshot(afterFirstFrameBy:)` awaits a dispatch-queue continuation and passes `isCancelled: { false }` to its decoder, keeping the outgoing native item paused and delaying the entire audio switch beyond its budget. Propagate cancellation into the extractor's read/decode loop rather than relying on `group.cancelAll()`.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| // #127: readiness + deferred host seeks are session-scoped; the host-side sink can't clear them | ||
| // once nativeCancellables are gone. | ||
| isSessionReady = false | ||
| isSourceSeekable = nil |
There was a problem hiding this comment.
Seekability state remains invalid after internal software reloads clear isSourceSeekable to nil because loadSoftware never republishes the reopened demuxer's capability, leaving canSeek permanently false after readiness when switching audio tracks on seekable software VOD. Publish dem.isSourceSeekable after a successful, generation-checked software open.
Prompt for LLM
File Sources/AetherEngine/AetherEngine.swift:
Line 7545:
Seekability state remains invalid after internal software reloads clear `isSourceSeekable` to `nil` because `loadSoftware` never republishes the reopened demuxer's capability, leaving `canSeek` permanently false after readiness when switching audio tracks on seekable software VOD. Publish `dem.isSourceSeekable` after a successful, generation-checked software open.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| prefersObjectAudio: options.objectAudioRendering != .off && ObjectAudioRendering.isAvailable | ||
| && !options.isLive && !options.audioOnly | ||
| && !VideoRoutingPolicy.requiresSoftwarePath( | ||
| codecID: detectedCodecID, fieldOrder: detectedFieldOrder, | ||
| av1Available: VTCapabilityProbe.av1Available), |
There was a problem hiding this comment.
Object-audio ranking ignores explicit preferredDecodePath == .software, although later routing honors it, replacing a same-language default track with TrueHD Atmos/DTS:X for HEVC video on an APAC-capable OS even though SoftwarePlaybackHost decodes only its conventional bed. Exclude the explicit software preference from prefersObjectAudio.
Prompt for LLM
File Sources/AetherEngine/AetherEngine.swift:
Line 4759 to 4763:
Object-audio ranking ignores explicit `preferredDecodePath == .software`, although later routing honors it, replacing a same-language default track with TrueHD Atmos/DTS:X for HEVC video on an APAC-capable OS even though `SoftwarePlaybackHost` decodes only its conventional bed. Exclude the explicit software preference from `prefersObjectAudio`.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| if #available(visionOS 2, *) { | ||
| return Task.detached(executorPreference: BlockingExecutor.shared, priority: priority, operation: operation) | ||
| } | ||
| return Task.detached(priority: priority, operation: operation) |
There was a problem hiding this comment.
Cooperative-executor starvation can occur when the availability check fails because this throwing overload schedules documented blocking work on Swift's cooperative executor, potentially starving async authorization resolvers. Refactor the async operation boundary to isolate synchronous blocking work in a GCD-backed fallback and bridge completion through withCheckedThrowingContinuation, preserving errors, the cancellation flag, and the cancellation handler; creating its Task inside a GCD block alone does not fix the problem.
Also found in:
Sources/AetherEngine/BlockingWork.swift:25-25Tests/AetherEngineTests/Issue260FrameTimeAxisTests.swift:120-120Tests/AetherEngineTests/EngineTLSHandshakeTests.swift:88-88Tests/AetherEngineTests/Issue259A53CaptionAxisTests.swift:77-77Tests/AetherEngineTests/Support/OffCooperativePool.swift:20-20Tests/AetherEngineTests/Issue314FrameTimeSequenceTests.swift:138-138Tests/AetherEngineTests/SourcePrewarmAdoptionTests.swift:155-155Tests/AetherEngineTests/Issue450ConcurrentReaderCeilingTests.swift:185-185Tests/AetherEngineTests/Issue732PersistentHTTP1Tests.swift:26-26Tests/AetherEngineTests/IFrameSessionTests.swift:171-171Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:102-102Tests/AetherEngineTests/IFrameSessionTests.swift:189-189Tests/AetherEngineTests/BoundedFrameExtractorTests.swift:53-53Tests/AetherEngineTests/SourcePrewarmAdoptionTests.swift:177-177Tests/AetherEngineTests/SourcePrewarmAdoptionTests.swift:199-199Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:121-121Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:49-49Tests/AetherEngineTests/Issue732PersistentHTTP1Tests.swift:55-55Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:78-78Tests/AetherEngineTests/Issue732PersistentHTTP1Tests.swift:59-59Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:86-86Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:123-123Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:109-109Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:162-162Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:64-64Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:144-144Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:95-95Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:46-46Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:53-53Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:112-112Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:134-134Tests/AetherEngineTests/LiveSourceReplayTests.swift:451-451Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:92-92Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:63-63Tests/AetherEngineTests/LiveSourceReplayTests.swift:480-480Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:75-75Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:102-102Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:133-133Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:71-71Tests/AetherEngineTests/LiveSourceReplayTests.swift:459-459Tests/AetherEngineTests/LiveSourceReplayTests.swift:478-478Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:128-128Tests/AetherEngineTests/LiveSourceReplayTests.swift:448-448Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:113-113
Kody rule violation: Move blocking decodes off Swift's cooperative executor
Prompt for LLM
File Sources/AetherEngine/BlockingWork.swift:
Line 36:
Cooperative-executor starvation can occur when the availability check fails because this throwing overload schedules documented blocking work on Swift's cooperative executor, potentially starving async authorization resolvers. Refactor the async `operation` boundary to isolate synchronous blocking work in a GCD-backed fallback and bridge completion through `withCheckedThrowingContinuation`, preserving errors, the cancellation flag, and the cancellation handler; creating its `Task` inside a GCD block alone does not fix the problem.
**Also found in:**
- `Sources/AetherEngine/BlockingWork.swift:25-25`
- `Tests/AetherEngineTests/Issue260FrameTimeAxisTests.swift:120-120`
- `Tests/AetherEngineTests/EngineTLSHandshakeTests.swift:88-88`
- `Tests/AetherEngineTests/Issue259A53CaptionAxisTests.swift:77-77`
- `Tests/AetherEngineTests/Support/OffCooperativePool.swift:20-20`
- `Tests/AetherEngineTests/Issue314FrameTimeSequenceTests.swift:138-138`
- `Tests/AetherEngineTests/SourcePrewarmAdoptionTests.swift:155-155`
- `Tests/AetherEngineTests/Issue450ConcurrentReaderCeilingTests.swift:185-185`
- `Tests/AetherEngineTests/Issue732PersistentHTTP1Tests.swift:26-26`
- `Tests/AetherEngineTests/IFrameSessionTests.swift:171-171`
- `Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:102-102`
- `Tests/AetherEngineTests/IFrameSessionTests.swift:189-189`
- `Tests/AetherEngineTests/BoundedFrameExtractorTests.swift:53-53`
- `Tests/AetherEngineTests/SourcePrewarmAdoptionTests.swift:177-177`
- `Tests/AetherEngineTests/SourcePrewarmAdoptionTests.swift:199-199`
- `Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:121-121`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:49-49`
- `Tests/AetherEngineTests/Issue732PersistentHTTP1Tests.swift:55-55`
- `Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:78-78`
- `Tests/AetherEngineTests/Issue732PersistentHTTP1Tests.swift:59-59`
- `Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:86-86`
- `Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:123-123`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:109-109`
- `Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:162-162`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:64-64`
- `Tests/AetherEngineTests/RemoteDiscSessionReleaseTests.swift:144-144`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:95-95`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:46-46`
- `Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:53-53`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:112-112`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:134-134`
- `Tests/AetherEngineTests/LiveSourceReplayTests.swift:451-451`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:92-92`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:63-63`
- `Tests/AetherEngineTests/LiveSourceReplayTests.swift:480-480`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:75-75`
- `Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:102-102`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:133-133`
- `Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:71-71`
- `Tests/AetherEngineTests/LiveSourceReplayTests.swift:459-459`
- `Tests/AetherEngineTests/LiveSourceReplayTests.swift:478-478`
- `Tests/AetherEngineTests/Issue551RedirectHandoffTests.swift:128-128`
- `Tests/AetherEngineTests/LiveSourceReplayTests.swift:448-448`
- `Tests/AetherEngineTests/Issue647DiscReaderPrewarmTests.swift:113-113`
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
* chore(deps): pin AetherEngine with upstream 7.36.0 Move the AetherEngine pin to 23889c5b, the fork main that merges upstream AetherEngine 7.29.0 through 7.36.0 plus upstream's live-playlist renewal (#748), Silo-Server/AetherEngine#21. Resolution moves LibTrueHD 2.0.0 to 2.3.0 and FFmpegBuild 3.6.0 to 3.7.1, and adds LibDTSX 1.0.1, the DTS:X decoder behind objectAudioRendering. Bundle LibDTSX's NOTICE and Apache license, list it in the acknowledgements, refresh the LibTrueHD NOTICE and the provenance notes, and refresh the reviewed build-input and selector snapshots for the new project.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: count LibDTSX in the release source proof's package graph The source proof requires the locked package graph to have an exact size. The AetherEngine pin adds LibDTSX, so the graph has 12 packages and the complete source graph 19 entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tvos): show Dolby Vision in playback stats when the TV switches to it An Apple TV set to SDR with Match Dynamic Range on switches the TV to the content's range, but Aether can't prove the panel left SDR (its EDR headroom stays 1.0), so it keeps reporting SDR. The stats showed "Dolby Vision Profile 7 → SDR" while the TV displayed Dolby Vision. When matching is on and the display supports the source's range, label that range, as Aether serves it to the panel either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tvos): label the stats range from the load's own matching decision A load made with HDR off in Silo's settings doesn't match the content's range, so the TV stays in SDR; the stats now follow that load's matchContentEnabled instead of the system setting alone. On a display without Dolby Vision, a Profile 5 or 7 source shows HDR10, the PQ base Aether routes there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tvos): label a Dolby Vision base layer by its transfer On a matched display without Dolby Vision, Aether presents the Dolby Vision base layer: HLG for an HLG base, otherwise HDR10. Read the base's transfer from sourceVideoStreamFormat instead of inferring it from the profile number, so Profile 8.1 shows HDR10 and 8.4 shows HLG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tvos): mark the stats range unconfirmed when the TV can't prove it tvOS reports one setting for frame-rate and range matching, so the app can't tell whether the TV switched to HDR. Instead of claiming the TV's mode, the stats name the range Aether sent and mark it unconfirmed, e.g. "Dolby Vision Profile 7 → Profile 8.1 (unconfirmed)". A Dolby Vision source with an SDR base layer (Profile 8.2) keeps the SDR label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tvos): keep an SDR Dolby Vision base labelled SDR on any display Aether strips Dolby Vision from an SDR base layer (Profiles 8.2 and 10.2) and plays that base as SDR even on a Dolby Vision TV, so the stats check the base layer's transfer before naming Dolby Vision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Merges upstream AetherEngine
mainat74c81b3c(7.29.0 to 7.36.0, plus the unreleased live-playlist renewal superuser404notfound#748) into the fork. The previous sync was #16 (7.28.3). This is a merge commit, so the PR must be merged with a merge commit, not squashed: squashing would lose upstream's history and the merge base the next sync depends on.Fork changes upstream now covers (removed from the fork's
[Unreleased]changelog):BlockingWork, around the fork's authorized decode.Fork changes kept and fitted onto upstream's code:
audioSwitchInFlight), placed next to upstream's held-picture state.httpRequestAuthorizationstays in the reload field list.stopInternalstill cancels remote-HLS preparation and stops the subtitle relay.cli.md, and the paused-seek sentence inapi.md.New in this merge:
Task.detachedcall sites (HTTPAuthorizationWait, the remote-HLS timestamp-anchor probe) now useBlockingWork.detached, as upstream'sBlockingWorkTestslint requires.objectAudioRenderingrow inapi.mdnamesObjectAudioRenderingagain.PublicAPIDocumentationTestsrequires this, and upstreammaincurrently fails that test.The merge commit message lists every conflict resolution.
Verification
Run on MacDev:
swift test(CI split): 4475 Swift Testing tests pass, plus XCTest with 0 failures. The authorization suites pass 56 of 56.build-for-testingboth succeed, and the six packet-cache regression scripts pass.Evidence: https://evidence.siloserver.org/r/aetherengine/upstream-7.36.0-sync/
🤖 Generated with Claude Code