Skip to content

chore: sync upstream AetherEngine 7.28.3 into the Silo fork - #16

Merged
Quick104 merged 449 commits into
mainfrom
sync/upstream-7.28.3
Oct 9, 2026
Merged

Quick104 merged 449 commits into
mainfrom
sync/upstream-7.28.3

Conversation

@Quick104

@Quick104 Quick104 commented Oct 7, 2026 •

Copy link
Copy Markdown

Summary

Merges upstream AetherEngine through dd1d5f88 (7.14.0 to 7.28.3, plus upstream's unreleased transport, audio-selection and subtitle-delay fixes) into the Silo fork. Fork changes that upstream now covers are retired; the rest are refitted onto upstream's new code.

Supersedes #13: upstream AE#681 (7.25.0) releases the shared audio session only when the last engine leaves, and runs every engine's activation and release on one queue.

What changed

Retired, covered upstream

Kept, fitted onto upstream

  • Refreshable HTTPRequestAuthorization on upstream's sealed relay (NET-1). Static headers now follow upstream's CredentialScope (NET-7, NET-109); resolver output stays the host's per-URL decision, as the resolver contract already says. Taking upstream also fixes two static-header bugs the fork had: credentials went to every allowed origin, and the subtitle proxy granted them to the post-redirect URL.
  • Bounded fetches (maximumBytes) use upstream's held-body limit (NET-10). The resolver's playlist preflight keeps the 2 MB cap the static preflight has. Relayed fetches use upstream's per-fetch pending cap (NET-107) in place of the fork's high-water wait.
  • The HTTPS-to-HTTP downgrade filter, sidecar and font credentials, sidecar decode off the cooperative pool, the native subtitle 503 while extraction is pending, nativeTimelineOffsetSeconds, ASS for local primary rendering, the 16-thread software decode cap, TrueHD Atmos to APAC, and CI's separate authorization test process.

New

  • The premature-end recovery (AE#287) keeps a pause made through the engine during its re-seek. It called play() after the seek regardless of the play intent.
  • An overlay-drawn injected ASS sidecar keeps its plain rendition selected behind a suppressing legible output instead of deselecting it, so upstream's AE#616 keeps measuring sourceTime on the remote-HLS bypass. Silo draws ASS from sourceTime; with the deselect it stayed on item time, ahead of the picture after a seek on a transcode. The suppression is attached with the order AE#616's report gives (deselect, wait, suppress, reselect) and dropped right before any selection AVPlayer should draw.
  • The APAC bridge reports an object decoder that produces nothing, as the channel bridges do since AE#641, instead of playing the picture over silence.
  • The hidden ASS rendition is selected only once its whole-program WebVTT has been extracted and playback is running, and turning subtitles off takes it down immediately (e773c661, 2d221aaa). Selected earlier, it joined AVPlayer's startup buffer. With stall waiting disabled, that stopped playback in Silo's authorization playback tests on CI.
  • An origin-rendition pick re-checks the item and the active track after loading the legible group, so a newer selection keeps its suppression (1fda333a, from Kody's review).

Fixes to upstream code, from Kody's review

These files were identical to upstream dd1d5f88 and are unchanged in upstream 7.30.0. Each fix is small and has a regression test that fails without it:

  • An HTTP source no longer crashes on a 206 whose Content-Range total is zero or negative (9c50e0d6).
  • Log redaction covers an Xtream password containing , ' or ), and a registered secret that starts a credential name no longer hides the value after it (d261d747).
  • The audio-only host parks its clock when it reaches the last sample, so 0.5x and a pause in the tail no longer cut the end (4ec5ac92).
  • A delayed live revive is checked again when its restart is admitted (8fc0a12f).
  • The I-frame payload cache stays under its cap, and closing the DVR ring releases the writer's chunk (3e2e4a16).
  • Cue markup is stripped in one pass; a crafted 64 KiB cue took seconds on the main actor (e63a2d96).
  • aetherctl redacts origin URLs and resolves served-VTT URIs against the playlist that lists them; smb-test's timed pass is linear; the burst-origin fixture rejects impossible arguments (94fab68e).
  • Seven upstream tests no longer hang after a timeout, depend on Task.yield() ordering, race a closed socket, or race a fake's state (d45af9e3).
  • From Codex's review: a session correction refuses a sharedOutputRole change. The role was listed as load identity but never compared (4ebc8eb0).
  • From Kody's second round: aetherctl prints in full only URLs on the mounted item's own engine server, so a localhost origin's credential is redacted too (71234f57, 31047b05), and the new ring test releases its parked writer on a cancelled wait (a01a6a72).
  • The new pause-in-the-tail audio test raced the park on CI's 3-core runner; it now plays the tail at 0.25x and starts over if a run still misses it (7f5af614).

One finding is answered with a comment instead (9a92e8cf): a first seek's superuser404notfound#409 head sample can run past the seek budget. Both bounded versions I tried were worse, either settling the repair verdict on a partial sample or leaving the seek unpositioned, which the software host's resume ignores.

Silo's matching change (picker fix and pin bump) is Silo-Server/silo-apple#683.

Test plan

  • Device / OS: macOS 27 host (Xcode 27.0) for swift test; iPhone 17 Pro simulator (iOS 27.0) and Apple TV 4K simulator (tvOS 27.0) with Silo built against this branch, against the dev server. Simulator audio output off.
  • Source media: MKV H.264 with two FLAC 5.1 tracks and embedded plus external ASS, played direct and as a server transcode (remote-HLS bypass); an H.264/AAC title with an external SRT, direct and transcoded; a TrueHD Atmos + E-AC-3 M2TS test file.
  • Result:
    • swift test in CI's two processes: the Swift Testing and XCTest suites pass on CI at 31047b05 (4134 tests locally at 58037700); the authorization group's 56 tests pass. One of three local full runs failed Issue243ReaderRetentionTests, which measures process-wide malloc totals; it passed alone five times and in the other two full runs. Local runs of the other CI steps (docs links, packet-cache scripts, partial composition regression, transport probe, release aetherctl, iOS/tvOS test-bundle builds, visionOS build) pass.
    • On the bypass, the injected ASS rendition is selected and suppressed (drawn by the host overlay (rendition suppressed)), AE#616 measured a 1.97 s lead off it, and only Silo's styled line is on screen. A plain SRT rendition is drawn by AVPlayer with no suppression. Media and sidecars come through the authorization relay.
    • Direct play, pause, resume and seeks work on both the software and native paths.
    • CI: the first run timed out 38 deadline tests, and its rerun hung until the job limit. CI now runs swift test on the xcode-27 runner (macOS 27) instead of macos-15, so the fork's APAC tests, which return early below OS 26, ran for the first time there. The APAC click test called AVAssetReader.copyNextSampleBuffer() from the cooperative pool, holding one of a three-core runner's threads while the decode waited. It now decodes on a thread of its own (dc4d5433).
    • Not run here: APAC to an Atmos receiver (the simulator has no H.264 hardware decoder, so the native HLS path that carries APAC is never chosen), PiP and AirPlay switching of the suppressed rendition, and item death behind the tvOS screensaver. These need devices.

Evidence: https://evidence.siloserver.org/r/aetherengine/pr-16/

Checklist

  • CHANGELOG.md updated
  • Commit messages follow Conventional Commits (feat(...), fix(...), chore(...))
  • The fix lives in the engine, not in a host-side workaround
  • Public API changes are intentional and documented

Open fork PRs: #12 needs a rebase onto upstream's AVIOReader (+584/−106); #14 should be retested on this branch, since AE#616 measures off the same renditions; #15 may be superseded by upstream AE#659 and AE#697 (forward-only URL sources now route native) and needs a retest.

🤖 Generated with Claude Code

Note

Sync upstream AetherEngine 7.28.3 into Silo fork

Brings the Silo fork up to AetherEngine 7.28.3. The largest changes:

  • Adds an I-frame playlist/rendition pipeline: hosts can request an I-frame playlist before session start (HLSVideoEngine.swift), and the local HLS server serves I-frame master entries, playlists, and cached fMP4 fragments.
  • Reworks the DVR ring buffer (PacketRingBuffer.swift) to store packets in chunk files with compact in-memory metadata, byte budgets, and shared per-session retention claims across processes.
  • Adds generation/epoch guards across the engine so callbacks, escalations, restarts, and recordings from superseded sessions or producers are dropped instead of applied.
  • Hardens hostile-input handling: bounded HTTP fetches, bounded playlist/UDF/ISO/ISO-BMFF parsing, saturating timestamp arithmetic, and expanded percent-encoded credential redaction in logs.
  • Adds played-media bitrate telemetry, HDR Vivid (CUVA) probe support, AV1 hardware routing, HEVC codec-string level capping at 153, and off-main-actor subtitle OCR draining.
  • Risk: behavioral changes are broad — ByteFIFO/SoftwareStoredPacket on-disk formats changed, HLSOriginRelay seals origin references per-instance so pre-existing plaintext-query references are refused, and LiveTelemetrySampler now reports playhead-crossed bitrate instead of transfer bytes.

Macroscope summarized 31047b0.

Vincent Herbst and others added 30 commits September 30, 2026 06:10
… (audit LIF-104)

Only `load()` consumed the AE#597 flag. `reloadWithAudioOverride` (audio
pick, custom disc-title pick, the custom-source reload) kept the native
host and left the flag up, so the first such rebuild after a reset
mounted its item on the invalidated AVPlayer. It now consumes the flag,
drops the native host and the audio-only player like `load()` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…dit NET-109, NAT-105, DEC-103, Vcred-101)

HLSOriginRelay.admit both allowed an origin and made it a credential
anchor, and the superuser404notfound#316 build admitted the post-redirect URL, so after a
cross-host redirect the edge got the host's token and the host's own
origin lost it. admit is split into allow(_:) and
grantCredentials(to:httpHeaders:); the proxy grants the URL the host
handed over and only allows the redirect target, and relayURL(for:)
only allows.

The playlist-driven fetchers that still sent every host header to
every URI a playlist named now share one CredentialScope type (the
NET-7 rule the ingest readers already follow): the superuser404notfound#316 proxy's
variant fetch (anchor: the host's origin URL), the remote-HLS audio
tap (anchor: the loaded URL) and the live subtitle rendition fetch
(anchor: the live ingest reader's credential origin).

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

`reloadWithAudioOverride` ended in an unconditional `play()` and
`.playing`, so an audio-track pick on a paused session resumed it, and
on a custom source every `reloadAtCurrentPosition` (the audio-delay
nudge, a superuser404notfound#460 correction, the AE#561 rebuild) did the same, including
a mount with `autoplay = false`. The transport is now resolved before
`.loading` (`sessionRebuildResumesPlaying`, or the value
`reloadAtCurrentPosition` passes, which is the mount flag after a
background teardown) and drives the stacking park, `play()` and the
final state; a rebuild that does not resume is settled `.paused` by the
host's readiness, like a paused mount (superuser404notfound#124).

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

Only the session's first producer got onLiveSegmentFinalized and
liveResidentCapProvider (and the sequential twin), set in start() after
the provider was built. The live reopen, the in-place muxerFailed rebuild
and the AE#222 live rebuild all build through makeProducer, which wired
neither, so every in-engine live recovery cut segments the playlist never
listed: the media playlist froze at the dead producer's last segment
while "live reopen succeeded" was logged.

The wiring moves into wireProviderCallbacks. start() calls it once the
provider exists, and makeProducer calls it for every producer built while
a provider is installed, so no rebuild site can forget it. A live producer
started without its report now trips an assert.

Tests: a live session over an embedded 8 s MPEG-TS whose reader ends after
one pass keeps growing its playlist after the reopen, and a producer
rebuilt in place reports into the same provider.

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

The three plan builders converted durations and index timestamps with trapping `Int64(_:)` /
`Int(_:)` and sized arrays from the duration. A container header can state about 9.2e12 s and a
Matroska index can carry entries near both `Int64` extremes, so a hostile file trapped the plan
(1 ns timebase), asked the uniform builder for 2.3e12 segments, or overflowed the trust check.

- `Demuxer.effectiveDurationSeconds` clamps to one week, non-finite to 0.
- `indexedKeyframes` and `firstIndexedTimestamp` drop entries past 2^62 ticks or 4e9 s, and do
  the AE#105 fold and the superuser404notfound#409 offset with checked arithmetic, dropping an entry that overflows.
- The builders compute the trust-check spread in Double, go through a checked `planPts` helper
  that returns an empty plan on overflow, and the uniform builder widens its stride so a plan
  never exceeds 200,000 segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…eturn (audit LIF-101, LIF-105)

The CORE-2 owed decision settled on any move out of `.loading`. A native
load passes through `.paused` (item readiness, inside the tvOS play gate)
seconds before it returns, so on tvOS the teardown ran under the load and
the host's `load()` threw `CancellationError`, the signal for "a newer
load or stop took over". Sodalite takes that as a supersede and skips its
observers and the Jellyfin start report, leaving a spinner up.

`loadSession` and `reloadWithAudioOverride` now mark the generation in
flight and settle the owed decision from their own exit. The decision
still hops through a Task, so the host's continuation after `load()`
wires its observers first, then the teardown runs. A decision made at
didEnterBackground, on the settle hop or at the iOS grace expiry while a
load is in flight is owed instead. The tvOS PiP close goes through the
same funnel, so a window closed during an in-PiP load or seek is torn
down once it settles instead of never (LIF-105). The bookkeeping is
platform-neutral so the settle timing is testable on macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…elpers (audit DMX-113, BIT-105, DMX-103)

`Int64(seconds * 1e6)` and the stream-time-base form in `Demuxer.seek(to:)` and `seekBounded`
trapped on NaN, infinity and anything past about 9.2e12 s, and several public entry points pass
host values through unvalidated (`FrameExtractor.thumbnail/snapshot(at:)`, the SW and audio
hosts' `load(startPosition:)`, `seek(to:)` during `.loading`). `byteEstimateTarget` and
`byteEstimateCorrection` converted before their `min(fileSize, ...)` clamp, so an origin that
inflated its `Content-Range` total trapped the subtitle side reader's byte-estimate fallback.

- New `Demuxer.ticks(forSeconds:timeBase:)`: nil unless finite and under 2^62 ticks; both seeks
  and `FrameDecodeContext`'s target PTS go through it, and the seeks return false on nil.
- The byte estimates compare the raw Double against the total before converting.
- API boundary: `FrameExtractor` returns nil for a non-finite or negative time, `FrameCache`
  saturates its bucket, the SW and audio hosts ignore a non-finite start position, and the
  public `seek(to:)` ignores a non-finite target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… session alone (audit HLS-104, HLS-105, SEG-104)

One identity rule for three ways a producer the session already replaced
still acted on it.

SEG-104 (B): a restart replaced while its first post-seek read was in
flight could open its gate afterwards, record its EpochAxis (dropping
the table at and above its index) and wake re-cut waiters with an epoch
whose bytes are never stored. The installed producer's epoch is now
mirrored under anchorShiftLock on every assignment of `producer`, and the
gate-open and live rebase handlers are passed their producer's epoch and
ignore any other.

HLS-104: the superuser404notfound#377 metered revive slept 3 to 45 s and then re-anchored
whatever producer was current, typically the working one a
segment-driven restart had installed during the backoff. It now captures
its dead producer weakly plus the session epoch and restarts only if
both are still current.

HLS-105: the AE#412 re-cut mark was dropped when the seek's 2 s gate wait
timed out, while the restart it marked kept running; its late gate then
recorded isRecut false and composed the backoff into the axis. The mark
is now a RecutMark owned by the restart: performRestart binds it to the
producer it installs at the marked index, a restart elsewhere keeps a
pending mark and drops a bound one, the wait's timeout no longer touches
it, and a newer seek drops a mark whose restart never installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…udit NET-104, DMX-106)

A first fragment starting `00 00` takes its size from the next 32 bits, and
`DVDSubpictureAssembler` reserved all of it, up to 4 GiB, then appended every later fragment of
the stream until that size was reached. FFmpeg's `dvdsub_parser` would bound this, but the superuser404notfound#651
declared stream bypasses it. `Demuxer.readPacketLocked` also copied the assembler out of its
dictionary before mutating it, so each fragment's append copied the partial unit at full capacity.

The assembler now drops a unit stated above 1 MiB (a DVD SPU is at most 53,220 bytes; the cap
keeps the HD-DVD units `dvdsubdec` decodes and the 70 KB unit the superuser404notfound#651 tests pin) and reserves at
most 64 KiB up front. The demuxer mutates the assembler in place and sizes the joined packet with
`Int32(exactly:)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…tes (audit SEG-102)

While the video gate waits, every selected-audio packet is buffered with its own `AVPacket`, and
the only bound was 8 MiB of payload. A zero-size packet was charged nothing and a 1-byte one one
byte, so a crafted Matroska of EBML-laced 1-byte frames behind a video track that never flags a
keyframe pinned about 8.4 million packets (1.3 to 1.5 GB of heap) while the VOD gate waited
without a time bound.

`shouldBufferPregateAudio` gains a defaulted `bufferedCount` / `maxEntries` (65,536) and the
call site passes the buffer's count. The cap sits above what the byte cap admits for AAC or AC-3
and still covers about 22 minutes of 20 ms Opus frames.

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

The pump checked shouldStop only at the top of its loop, before it read.
A read already parked when the session stopped the producer (the superuser404notfound#79
restart aborts it with markClosed, which Matroska, MPEG-TS and fMP4
report as end of file) came back as source EOF: the pump exited with
.eof, flushed the bridge and adopted its half-written segment into the
shared cache under the full index, where a later backward seek hit a
short segment.

The pump now re-checks right after every source read (after both reads
of the side-audio merge) and frees the packet, and an .eof exit with a
stop pending is remapped to .stopRequested. Making the closed demuxer
itself refuse to report EOF is left to the demuxer package.

Test: a Matroska pump parked mid-file, stopped, and then answered with
end of file exits as a stop and adopts nothing (it exited .eof and
cached the partial segment before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…cessor (audit LIF-103, Vcore-101, Vcore-102)

Session callbacks hop onto the main actor, and a hop that landed after
the session ended acted on whatever owned the engine by then. The item
death escalation queued behind a `stop()` spent the budget, sent an
escalation event and flipped the stopped engine to `.error` through its
rebuild's `sessionNotReloadable`; a load in that turn was rebuilt on the
software path with the old failure. The HLS callbacks (shift, seek state,
network phase, consumer re-engage, rebase, HDR10+) and the software and
audio network-phase sinks wrote a torn-down session's state into the next.

One generation-stamped hop (`hop(for:)`, `loadGeneration` is the session's
identity on every path) now carries all of them, the escalation and the
master fallback take the generation they were raised under, and the
escalation's absorbed-failure decision asks the generation instead of
`state`. A reused native host clears its pending display rejection and
escalation at unload, since `@Published` replayed them into the next
load's sinks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ow (audit SEG-107)

`LoadOptions.dvrWindowSeconds` reached `Int(ceil(window / cadence))` in
`LiveWindowSizing.requestedSegmentCount` unvalidated, so a host that expressed "keep
everything" as `.infinity` (or passed NaN, or anything past about 4.6e18 s) trapped on the first
live manifest request and on the pump's resident park cap. The same class 078f1be fixed in
`wholeSecondsCovering`.

The sizing now reads the window through `effectiveWindowSeconds`: NaN or nil is the live-only
floor, the rest is clamped to [0, one day], which is far deeper than the 900-entry playlist
ceiling ever serves and keeps the clamp log's `requested x cadence` product in range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… NAT-101, DEC-101, FEA-101, SUB-105)

SEG-1 sanitized packet timestamps inside the loopback segment producer
only. The software host, the audio-only host, the still extractor, the
recording tap, the subtitle stores and every probe read the same Demuxer
and got matroskadec's raw uint64 cluster times and INT64_MAX block
durations, so a crafted file still trapped the process in the SW-host
fold, the DVR feeder, the decoders' `pts * num` and the subtitle drain.

`SourceTimestampBounds.sanitize(_:timeBase:)` unsets pts/dts (and zeroes
the duration) at |ticks| >= 2^62 or |seconds| >= 4e9.
`readDemuxedPacketLocked` applies it right after `av_read_frame`, before
the superuser404notfound#407 clearing, the AE#105 fold, the superuser404notfound#651 join and the superuser404notfound#409 repair, and
`readPacket` applies it again at every exit, because superuser404notfound#409 rewrites pts/dts
after the funnel with wrapping arithmetic. The sidecar `SubtitleDecoder`
runs its own read loop and gets one call there. Epoch-anchored 1 GHz
timestamps stay under 2^62 until about 2116 and epoch seconds under 4e9
until about 2096. The producer keeps its own 2^60 bound unchanged.

Since 4e9 x (2^31 - 1) < 2^63, a bounded tick times any AVRational
numerator fits Int64. Values made after the funnel still get local checks:
the decoders build their CMTimes through one checked helper (.invalid on
overflow), the SW-host fold converts and applies its offset with checked
arithmetic (unset on failure), and the DVR feeder and the still extractor
turn ring seconds back into ticks through a checked conversion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ng it (audit NAT-106)

`mSampleRate` is a Float64 AVFoundation fills from the container, and a QuickTime
SoundDescriptionV2 carries it verbatim, so on the remote-HLS bypass the origin controls it.
`RemoteHLSStreamDescription.audioTracks` checked only `isFinite` before `Int(_:)`, and the
readyToPlay track dump in `NativeAVPlayerHost.audioFormatDescription` checked nothing, so NaN,
infinity or a rate past `Int.max` trapped at every native readyToPlay.

One helper, `RemoteHLSStreamDescription.wholeSampleRate`, returns the rate as an Int when it is
finite and in [0, 1e9), else 0, and both sites use it.

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

The demuxer bound covers packets, but cue times also arrive from live
WebVTT renditions, external stores and a host calling the public
`ASSScriptBuilder.timestamp`, and each of them scaled a Double into an
Int with a trapping conversion.

`SubtitlePacketStore.harvestChunk`, which the pump, the software-host tap
and the prefetcher all store through, now applies the demuxer's rule:
a non-finite or implausible time is unset, such a duration is 0. The
drain's packet rebuild converts through a checked helper, and the cue
store's dedupe key, the ASS and the WebVTT formatters pin NaN to 0 and
anything past 4e9 s to that edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ic leaves Int64 (audit BIT-101)

After a seek the landing keyframe re-anchored the sequence with a plain
`dts - landingIndex * plan.step`. In fragmented MP4 the step comes from
the 64-bit `tfdt` and the landing picture order can sit near -2^31
through `delta_pic_order_cnt_bottom`, so a crafted file trapped the demux
thread on seek or resume. The product and the difference are now checked;
on overflow the picture stays unrepaired and the next keyframe re-anchors.
`presentationShift` subtracts with the same check and falls back to no
shift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
… total (audit SEG-101)

A packet with neither dts nor pts advanced by its own duration with no
cap, so nine crafted BlockDurations just under the producer's 2^60 bound
walked the synthesized dts to Int64.max, and `OutputTimestampSanitizer`
trapped on the next `last + 1`.

`repairStrideTicks` caps every arm at one second of ticks, the cap
`updatedStrideTicks` already applies to a learned stride, and a packet
whose synthesized dts would leave the plausible range is dropped until a
genuine timestamp re-anchors the stream. The sanitizer returns nil when no
dts after the last one exists and the muxer drops that packet. The dts
bumps, the buffered-fragment bound and the no-cut watchdog's advance use
checked or saturating arithmetic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…-end budget (audit LIF-106, DEC-105)

Both `waitForSwitch` loops polled with `try? await Task.sleep`, so a load
whose task the host cancelled (Sodalite on Back and on every zap) spun
the main actor until the switch cap, and a superseded load kept polling
to its end. Each tick now goes through `gateTick`, which returns on
cancellation or when the caller's `isCurrent` (the load generation at
all four call sites: pre-flight, the superuser404notfound#667 wait, the play gate and the
reload play gate) says the load is gone, and logs the abandonment.

The 6 s observed-end cap was per gate. The superuser404notfound#667 wait leaves the record
unspent for the play gate, so on a panel whose end is never announced or
whose in-progress flag sticks both spent the full cap on one switch. The
first gate that waits on an armed switch now sets a deadline, and a later
gate on the same arm gets what is left of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…exit 64 (audit OPS-110)

`takeDoubleFlag` rejected NaN and infinity only, so `play --seconds 1e30` loaded and then trapped
in the run loop's `Int(seconds)`; negative counts reached `0..<n` (`seektest --seeks -1`,
`smbtest --reads -1`, `hlslive --seconds -1`), `hlslive --segments ""` built `1..<0`, and the
host-call delays (`--switch-audio 0@<ms>`, `--reload-applying-at`) overflowed
`UInt64(ms) * 1_000_000`.

New bounded variants `takeDoubleFlag(_:in:from:)` and `takeIntFlag(_:in:from:)` exit 64 with a
message outside their range (duration flags up to a week, counts up to a million), `hlslive`
refuses an empty segment list, and the delays go through a saturating `sleepNanoseconds`.
Verified by running each case: every one now exits 64, and the saturated delay no longer traps.

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

`SubtitleRectText.parsePositionTag` and `playRes(fromASSHeader:)` parse with `Double(_:)`,
which accepts `nan`, `inf` and `1e999`. `\pos(nan,nan)` published a NaN
`SubtitleTextPlacement.position` and `PlayResX: inf` normalized every position to NaN, breaking
the documented [0, 1] placement contract; a UIKit host that lays out with it raises
`CALayer position contains NaN` (Sodalite filters it, AetherPlayer ignores placement).

Both now require finite values: a non-finite tag yields no position, so the cue falls back to its
alignment, and a non-finite play resolution is no declaration, so the default applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…de query values (audit SUB-104, SUB-108, SUB-109, NET-114)

The nameless matchers (registered secrets, encoded payloads, userinfo,
Xtream paths) only matched raw bytes, so an upstream URL carried
percent-encoded inside a wrapper's query (IPTV proxy, debrid) leaked
its password in load url=, asset.url= and URLError dumps. A line that
holds a valid escape now also runs them over a decoded view built
once, and each hit maps back to whole escapes. The key matcher stays
raw-only, so the NET-1 terminator rule is unchanged. A base64 blob may
now start after - or _ (/v1-eyJ...).

After = the terminators : ; , ) > end a value only where prose follows
them, so a query password holding one goes whole; the header form
keeps the wide set. Added the other backends' names (api-key, cookie,
passwd, pwd, sessionid, session_id, authtoken, auth_token,
refreshtoken, sessiontoken), Bearer and Basic credentials with a
16-byte floor, JSON and dictionary keys whose quote closes before the
separator, and counted secret registrations. The key-initial check is
a table instead of a Set, which keeps an escape-free line at the old
cost despite the extra matchers.

The four NET-114 sites log the URL error code and host instead of the
NSError dump with its failing URL.

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

An in-place swap reuses a player that is still `.playing`, and that
status reaches the fresh item about 1 ms after the load, before it is
ready. The timeControlStatus sink took it for the item rolling: it
released the AE#629 clock hold before the mount seek landed, so the
pre-landing readings reached the published clock, and it latched
`hasEverPlayed`, so a swapped item failing at startup took the
post-playback branch. Both now wait for `.playing` on a ready item (the
predicate the AE#440 one-shot already used, renamed
`playingIsThisItemsRoll`), and the readyToPlay sink re-asks when the
mirror already reads `.playing`. The mount-seek and host-seek releases
stay. `aetherctl play --host-calls playreload|pausereload` still ends
`VERDICT: OK`, with the carried edge visible before readiness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
`PacketTimingProbe.run` (behind `aetherctl pktdump`) subtracted consecutive dts from the probed
file with a plain `-`, so a hostile file whose dts sit on opposite sides near 2^62 trapped the
probe. The delta now goes through `subtractingReportingOverflow`; a step that does not fit is
counted as non-monotonic and kept out of the histogram.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…udit SUB-107, NET-114)

The 128-bit path token is the capability that keeps LAN hosts off the
0.0.0.0 listener, yet every request line, the superuser404notfound#227 first-request line,
NativeAVPlayerHost's load url= and asset.url= and the superuser404notfound#316 serving line
logged it verbatim. HLSLocalServer registers the token with the log
redactor in start() and unregisters it in stop() (registrations are
counted, so two servers never release each other's), and the request
line is now composed from the route after the token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…on check (audit LIF-107)

The probe block wrote `sourceVideoWidth`, `sourceVideoHeight`,
`sourceVideoPixelAspectRatio` and `lastDetectedVideoCodec` before the
post-probe generation check. The AE#532 RPU audit is a second read that
`stopInternal` cannot abort, so a load superseded inside it came back
after its successor's prologue and wrote its own values over the reset;
a successor that never probes (the remote-HLS bypass) kept them, and
Sodalite reads a non-zero width as "has video". The values are now
collected in the block and published after the check.

The test origin can hold every request after a point, which is how the
test parks the audit of a synthesized Dolby Vision Profile 5 over HDR10
source while the successor loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…on sequential origins (audit SEG-103)

7.22 made sequential-origin audio follow the video cutter
(audioFollowsVideoCut) at the per-feed bridge site and the stream-copy
sites, but the EOF bridge.flush() loop still routed its tail by plan
time. When the last GOP crossed the last plan boundary the flushed tail
opened the plan index the cutter never reached: the real final segment
was closed as not final, its duration never arrived, and ENDLIST landed
behind the segment before it, so AVPlayer ended the archive early.

Both bridge sites now route through bridgedAudioSegmentIndex, so the
live, sequential and plan rules live in one place.

Test: a 16 s forward-only Matroska archive (embedded, Opus bridged to
FLAC, keyframes at 0, 9 and 11 s under the uniform 4 s plan) lists all
160 video frames and its whole duration. Before, the playlist stopped
after 9 s with 90 of 160 frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ility rule (audit HLS-102)

The two fix branches each introduced the same 2^62 ticks / 4e9 s bound. The index
check now delegates to SourceTimestampBounds.plausible(_:timeBase:) so the rule has
one definition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…n the log (audit NET-111)

A connection that never presented the session token cost the host's
log two unthrottled lines per request (the rejection and the 404), and
its request line was attacker text split only on CRLF, so a bare LF
forged a line of its own. Every line such a connection can cause
(rejection, malformed or non-UTF8 request, oversize head, EOF or recv
error mid-head) now shares one LogThrottle with a tally, the rejection
and its 404 are one line, and request text is logged with C0 controls
and DEL escaped (a stranger's cut at 256 characters). Local to the
server, because EngineLog passes the multi-line playlist bodies this
server logs on purpose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
@Quick104
Quick104 marked this pull request as ready for review October 8, 2026 00:11
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T02:24:29.869895Z 31047b0 New commits
🔒 Security Review ✅ Completed 2026-10-08T02:25:04.110946Z 31047b0 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Quick104 and others added 10 commits October 7, 2026 21:59
…cting from it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ind a registered name

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, at any rate

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s tail on close

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dget

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mb-test pass

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, lock two fakes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@silo-kody

silo-kody Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 580377004d

ℹ️ 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".

Comment thread Sources/AetherEngine/AetherEngine+ReloadWithOptions.swift
Comment thread Sources/aetherctl/PlaybackCmd.swift Outdated
Comment thread Tests/AetherEngineTests/PacketRingBufferChunkSpoolTests.swift
Quick104 added a commit to Silo-Server/silo-apple that referenced this pull request Oct 8, 2026
Head of Silo-Server/AetherEngine#16: since 1fda333a it fixes the bugs Kody
found in upstream code the sync takes. For Silo: an HTTP source no longer
crashes on a 206 with a non-positive Content-Range total, the audio-only
host plays to the last sample below 1x, a crafted cue no longer stalls the
main actor, and a delayed live revive no longer queues a stale restart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Quick104 and others added 2 commits October 7, 2026 22:09
`sharedOutputRole` was listed in `loadIdentityFields`, but `refusedFields`
never compared it, so `reloadAtCurrentPosition(applying:)` rebuilt a running
multiview engine under the other role instead of throwing
`loadIdentityNotCorrectable`. A new test flips every identity field alone
and expects exactly that field refused, and fails when the list names a
field the comparison does not cover.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@silo-kody

silo-kody Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Quick104 and others added 4 commits October 7, 2026 22:16
printableURL treated every loopback host as the engine, so a nativeRemoteHLS
origin on localhost printed its query credential in SERVED. An engine server
is a loopback address on a port the source does not use; the rest, a
localhost origin and its absolute renditions included, is redacted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…runner

CI paused after the park instead of inside the 0.25 s tail, and a resume
after a host pause restarts a parked clock, so the test read 3.5 s on a 1 s
file. The tail now plays at 0.25x, about a second of wall time, and a run
that still misses it starts over. Without the fix the test still fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The same deferred release the other parked-reader tests have, so a time-limit
cancel cannot leave the append thread and close() blocked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@silo-kody

silo-kody Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

…server

A different loopback port did not make a URL the engine's: a localhost origin\ncan list an absolute rendition on another localhost port. The engine endpoint\nis now the mounted item's URL when it is a loopback URL other than the source\n(the no-stand-in route mounts the source itself), and only URLs on that\nscheme, host and port print raw.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@silo-kody

silo-kody Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Silo Kody — review complete

Review finished. Check the inline comments for findings and verify each suggestion against the code and tests.

Reviewing changes in Silo
  • Include the related issue, expected behavior, and validation steps in the PR description.
  • For API changes, describe the effect on Apple and Android clients and Jellyfin compatibility.
  • For plugin changes, identify the affected SDK contract, plugin, and catalog entry.
  • Follow this repository's AGENTS.md and CONTRIBUTING.md.
  • Request another review with @kody start-review in a PR comment.
  • React with 👍 or 👎 to give feedback on individual suggestions.
Review settings
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ❌

Quick104 added a commit to Silo-Server/silo-apple that referenced this pull request Oct 8, 2026
Head of Silo-Server/AetherEngine#16 after its review rounds: since 58037700
the engine library only adds a refusal for a sharedOutputRole change in a
session correction, which Silo does not use. The rest is aetherctl, tests
and docs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Quick104
Quick104 merged commit fe8ce51 into main Oct 9, 2026
7 checks passed
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.

4 participants