Skip to content

Releases: superuser404notfound/AetherEngine

5.0.2

Choose a tag to compare

@superuser404notfound superuser404notfound released this 11 Jul 18:51

Patch release.

Fixed

  • Embedded SRT cues no longer duplicate after rapid seeking (#121). The overlay drainer rebuilds the EmbeddedSubtitleDecoder on every seek (.resetAndDecode), which restarts its per-instance dedupe set and cue-id counter at zero. Because subtitleCues is intentionally retained across the seek, the backscan re-decoded cues still in the store, and the insert path only replaced same-start bitmap cues while always appending text cues. Identical lines therefore accumulated (a report saw the retained count grow 4 to 7 to 11) and the reset decoder ids collided with retained ids, so SwiftUI logged ForEach(id:) "the ID N occurs multiple times" and rendered duplicate overlays.

    Both invariants now live at the retained-store insert funnel, which sees the whole session rather than one decoder generation:

    • a text cue already present with the same start, end, and text is dropped (content, not id, so simultaneous distinct speaker lines and genuine repeats at new timestamps still insert), and
    • every cue that lands is stamped with a session-monotonic id, so ids stay unique regardless of how often the decoder is rebuilt.

    The PGS bitmap same-start replace (#112) is unchanged.

Thanks to wunax for the source-level diagnosis and the exact reproduction.

Full Changelog: 5.0.1...5.0.2

5.0.1

Choose a tag to compare

@superuser404notfound superuser404notfound released this 10 Jul 08:42

Patch release. Five fixes on top of 5.0.0, no API changes.

Fixed

  • Split MPEG-TS PGS display sets reassemble before decode (#112). On Blu-ray MPEG-TS a PGS display set spans several PES packets (PCS, WDS, PDS, ODS, END). The packet-tap store kept one entry per harvested packet keyed by unique PTS, so segments without a PTS died at the NOPTS guard and segments sharing one collapsed in the same-PTS replace; the decoder never saw the palette and object definitions and every set failed at its END segment ("Invalid palette id"). The store now reassembles armed streams' chunks into one self-contained entry at the PCS presentation PTS. Arming comes from the demuxer (PGS streams in an mpegts container) through both hosts' tap sinks; Matroska stays on the per-packet path.
  • iOS route-sharing policy no longer blocks host PiP (#116). The shared AVAudioSession declared .longFormAudio on every platform. On iOS that marks the process a long-form audio client, which pins AVPictureInPictureController.isPictureInPicturePossible to false for any host-built PiP controller around the engine's player layer. The policy is now platform-split: tvOS keeps .longFormAudio (HDMI route negotiation), iOS declares .default.
  • Dolby Vision first frame no longer waits out a fixed 5 s poll (#117). waitForSwitch() polled isDisplayModeSwitchInProgress for a fixed 5 s and never watched the OS mode-switch notifications, so on panels where a DV switch is unobservable to the app it ran the full timeout every time, and load() waits twice, roughly 10 s to first frame. It now settles the instant the panel reports done (AVDisplayManagerModeSwitchStart / End, or EDR headroom rising) and otherwise caps the wait at ~2 s. Observable panels settle immediately; SDR and rate-only loads are unaffected.
  • LoadOptions.httpHeaders reaches the nativeRemoteHLS path (#119). The remote-HLS bypass built its AVURLAsset without an options dictionary, silently dropping the headers, so header-enforcing IPTV origins (per-stream Referer / User-Agent / Authorization) answered 403 and those channels could not play at all. The headers now ride the asset via AVURLAssetHTTPHeaderFieldsKey, and the remote-HLS audio tap fetcher (#95) sends the same headers on its own playlist, segment, and AES-key requests.
  • Pre-bound surfaces show video on loadRemoteHLS (#120). The lean remote-HLS bypass assigned its host and early-returned without presenting the player layer, so a surface bound before load (the usual SwiftUI order: view appears, then the load task fires) never attached playerLayer and every remote-HLS live channel played audio over black video. The layer is now presented right after the host is assigned, mirroring loadNative.

Thanks to ijuniorfu and dlev02 for the reports and logs, and to thatcube for PR #118.

Full diff: 5.0.0...5.0.1

5.0.0

Choose a tag to compare

@superuser404notfound superuser404notfound released this 09 Jul 21:15

Major release. The embedded subtitle overlay pipeline was rebuilt from the ground up, and one public symbol was renamed.

Breaking

  • setNativeSubtitleForPiP(_:) is now setNativeSubtitleRendering(_:). The native WebVTT legible rendition is selected whenever the video leaves the host's own view hierarchy (a PiP window, an AirPlay receiver, or a wired external display), not just PiP. Pure rename, no behavior change: update the call site and you are done.

Changed

  • Embedded subtitle overlay pipeline reworked: packet tap replaces the side-demuxer readers (#112). The overlay path used to open a second demuxer per selected embedded subtitle track and seek it around to reconstruct stateful PGS lines after seeks, fast-forwards, and audio switches. That model needed eleven rounds of hardening and still regressed on index-starved remote sources. The producer now retains every embedded subtitle stream it already demuxes in a compressed SubtitlePacketStore (300 s / 32 MB), and a playhead-paced drainer decodes cues from the store, running the same stale-arrival gate, successor trim, and retention rules as before. The software host feeds the store through its own tap. The side readers, reuse pool, re-arm coalescing, condemned latch, and cross-thread read-abort chain are deleted (~1350 lines). No extra connections, no reconstruction seeks: post-seek recovery is bounded by decode speed, not remote I/O.

Added

  • bufferedPosition for host scrub bars (#33). Reports the disk cache's contiguous read-ahead frontier on native direct play so hosts can draw a buffered band.
  • Interlaced live TV (#107). Interlaced H.264 routes to the software path with bwdif deinterlacing; DVB teletext decodes through libzvbi into WebVTT cues.
  • Cache-backed VOD scrub stills (#106). Single-connection VOD sources render scrub thumbnails from the segment cache instead of opening a second origin connection.
  • Remote-HLS audio tap delivery (#95). installAudioTap() now delivers on the remote-HLS path (VOD and live), with audioTapHasDeliverySource for host fail-loud and aetherctl audiotap --remote for verification.
  • Subtitle-preserving reduced-master fallback (#98). Display-rejection fallback stages master to reduced to media, keeping HDR and subtitle renditions; nativeSubtitleRenditionsServed is published for host external-subtitle windows.
  • SubtitleImage.canvasSize (#112). Bitmap cues carry their composition canvas size so hosts can map them onto the rendered video rect (defaulted, source-compatible).
  • Stats surface. Declared source video bitrate (Matroska BPS fallback), nominal frame rate, audio bitrate, and bridge output.

Fixed

  • Multi-clip / multi-title Blu-ray timeline (#105). Fold on observed clip bases rather than MPLS inTime, 0-based display axis, decoy-playlist defeat, MPLS duration trust, selected-title stills.
  • DV P5 still tone-map (#103). Matches libplacebo BT.2390 and applies the RPU reshaping curves.
  • Backward-scrub cold read reconnects fast (#93, #96). The starved detour fetch is bounded to a ~4 s budget instead of riding the 15/35 s socket timeouts.
  • Stall-recovery nudge reads the rendered frame (#115), instead of a stale pre-grace capture that skipped VOD playback backward on re-engage.
  • AVAudioSession category declared off-main (#114), clearing the hang-risk diagnostic from the engine constructor.
  • Cold DV/HDR master start gates on real track readiness (#35), and the backpressure wedge detector is suspended until the first rendered frame.
  • Resolved content lengths shared across demuxer opens (#112) via SourceContentLengthCache, so a starved size probe no longer collapses an open to forward-only streaming.
  • Synthetic PGS END flush gated on a complete object (#112), clearing "Invalid object id 0" on split m2ts.
  • Native legible renditions (Sodalite#38): no FORCED=YES on host-managed renditions, deselected in fullscreen until the host selects. The deselect is pinned unconditionally as soon as the legible group loads and re-asserted on a 40 ms cadence for the first second, so a system caption preference no longer flashes cues for up to ~0.5 s at video start on iOS.
  • Wired HDMI external display keeps loopback + master (Sodalite#34).

Thanks to ijuniorfu, dlev02, fubag, tresby, and rrgomes for the reports, logs, and retests that drove this release.

Full diff: 4.12.1...5.0.0

4.12.1

Choose a tag to compare

@superuser404notfound superuser404notfound released this 05 Jul 14:27

Patch release.

Fixed

  • mov_text subtitle OOM on the host overlay (#104 follow-up). 4.12.0 added the video/audio discard to the native PiP/AirPlay subtitle rendition reader only. The tvOS host-overlay reader is a separate side demuxer and still lacked the discard, so selecting an embedded text subtitle pulled the whole video and audio through a second connection just to reach the sparse subtitle samples, with RSS climbing by playback position until jetsam (worst on files carrying many subtitle tracks). The overlay side demuxer now sets AVDISCARD_ALL on everything except the selected subtitle stream, so mov_read_packet skips the video/audio byte reads and the reader fast-walks the in-memory index between cues. Adds a regression test that drives a real h264 + mov_text MP4 through the demuxer and asserts only subtitle packets are returned after the discard.

Full diff: 4.12.0...4.12.1

4.12.0

Choose a tag to compare

@superuser404notfound superuser404notfound released this 05 Jul 13:59

Additive release since 4.11.0. No breaking API changes.

Added

  • Opt-in decoded PCM audio tap (#95). installAudioTap() streams playback audio as mono Float32 48 kHz buffers with source-PTS timestamps and discontinuity flags, for host-side speech/audio features (live transcription, ShazamKit). New aetherctl audiotap.
  • Configurable forward-buffer window (#102). LoadOptions.forwardBufferSegments (clamped 4...150; nil keeps the adaptive default) lets hosts trade memory for resilience on slow or unstable sources.
  • Dolby Vision profile exposed (#103). SourceProbe.dvProfile and the live sourceDVProfile publish the profile number (5 / 7 / 8 / 10) for Stats-for-Nerds labels.
  • CEA-608 as a native WebVTT rendition on iOS (#98). 608 captions survive PiP and AirPlay through the existing WebVTT path.

Fixed

  • Dolby Vision Profile 5 / AV1 Profile 10.0 thumbnail colour (#103). FrameExtractor read the IPT-PQ-C2 base layer as BT.2020 YCbCr (green / magenta cast). It now applies the DV colour transform from the RPU metadata on the CPU. No Apple still-image API resolves this on its own; playback was never affected.
  • mov_text subtitle memory growth (#104). The native subtitle side demuxer now discards non-subtitle streams instead of streaming the whole program to harvest sparse mov_text packets.
  • Bridged-resume alignment and post-EOF revive (#99).
  • PGS catch-up burst suppression (#100).
  • Reactive master to media fallback on display rejection, and macOS on-demand HDR routing (#98).
  • Bounded wedge-restart reopen and #93 stall-recovery seek-target fixes.

Changed

  • SMB now uses SMBClient (#97), fixing SMB shares on tvOS / iOS (AMSMB2 hit EPERM). Support boundary SMB 2.0.2 / 2.1.

Removed

  • Dead in-band mov_text / tx3g subtitle muxing.

4.11.0

Choose a tag to compare

@superuser404notfound superuser404notfound released this 03 Jul 09:08

Highlights

Post-recovery video judder fixed (#93 residual). The wedged-restart fresh reopen skipped avformat_find_stream_info, so the reopened demuxer never resolved the video stream's B-frame reorder depth and delivered matroska B-frame packets with NOPTS or presentation-ordered, non-monotonic dts. The producer's dts repair then telescoped sample durations or dropped roughly a quarter of all frames, so every region produced after a wedge recovery played with heavy sustained judder while stream-copied audio stayed clean. The reopen now keeps find_stream_info under a bounded probe budget (4 MB / 5 s), resolving the reorder depth from the first packets at a small bounded read cost. Device-verified on 4K DV MKV over WAN.

Pre-first-frame loader death now recovers (#93 startup). A CoreMedia -15628 before playback ever started never posts playbackStalled, so the dead-consumer watchdog never armed and the session sat in an endless spinner. The -15628 errorLog now surfaces as a stall signal, the backpressure-wedge re-anchor path arms its own stage-2 item-reload escalation, and the extractor yield gate gained hysteresis so a single post-load buffer spike can no longer let a multi-megabyte thumbnail pull kill the loader during the critical first-segment window.

PiP survives the stage-2 recovery reload (#93 residual). The reload's default teardown paused the player and dropped the item to nil before the fresh one existed; during Picture in Picture that nil-item gap invalidated AVKit's content source and dismissed the PiP window. The recovery reload now swaps items atomically: transport intent, clocks, and the old item stay alive until replaceCurrentItem hands AVPlayer the fresh one.

Active subtitles survive the recovery reload (#93 residual). Legible selection is per-item, so the reload silently dropped an active native subtitle rendition (worst in PiP, where the rendition is the only subtitle path). The engine now remembers the last requested rendition and replays it onto the fresh item; a deselect clears the memory so a reload never resurrects subtitles the user turned off.

Subtitle readers follow AVKit-side seeks. PiP's skip buttons seek the AVPlayer directly and never pass through the engine's seek API, so after a far PiP back-skip the native subtitle readers kept reading forward from the old region and AVKit permanently cached empty subtitle windows for the new one. A far rendered-time jump now schedules a debounced re-anchor: readers outside the playhead's coverage restart at the new position and the remembered selection replays, busting the cached empty windows.

Still extraction yields to a starved pipeline (#93 startup). Session-coupled scrub-preview and chapter-thumbnail extraction pulls megabytes over the same link the segment producer needs; at playback start on a marginal link that contention could tip the first segment past CoreMedia's media timeout. Extractors vended by makeFrameExtractor() now yield elective thumbnail decodes while a producer restart is in flight or the forward buffer is under 3 s. Snapshots and cache hits are never gated; standalone FrameExtractor(url:) instances are unaffected.

Added

aetherctl pktdump. Raw video packet timing (dts / pts / duration, NOPTS and monotonicity stats, delta histograms) as delivered by the demuxer under a selectable open profile (--profile playback|restartReopen|stillExtraction). The differential between profiles is what isolated the judder root cause; backed by the public PacketTimingProbe.

Compatibility

No breaking API changes. New API: makeFrameExtractor(url:httpHeaders:) (session-coupled extractor for a host-chosen still URL, e.g. playing originals during a transcode), FrameExtractor init yieldWhile: parameter, PacketTimingProbe.

Thanks to @rrgomes for the detailed #93 retests and traces that drove this cluster.

4.10.0 - External subtitle tracks, iOS DV routing, restart recovery chain

Choose a tag to compare

@superuser404notfound superuser404notfound released this 02 Jul 20:10

Highlights

External subtitles as first-class tracks (#88). External subtitle files now register with the engine and appear in subtitleTracks alongside embedded tracks, with a synthetic id and isExternal == true. Register at load via LoadOptions.externalSubtitles or any time via addExternalSubtitleTrack; select through the unified selectSubtitleTrack (primary and secondary channels). Load-declared tracks join the native WebVTT renditions, so external subtitles survive PiP, AirPlay, and external displays, and a finished cue store backfills the fullscreen overlay instantly on select. preferredSubtitleLanguages ranks external tracks too, and removeExternalSubtitleTrack unregisters.

iOS HDR/DV master routing. The master-vs-media playlist gate required a tvOS-style panel-in-HDR signal, so HDR and Dolby Vision films on iPhone routed media-direct and PiP subtitles silently never engaged for them. iOS now treats AVPlayer.eligibleForHDRPlayback as panel readiness (built-in panels engage EDR on demand).

Terminal stall self-recovery (#93 residual). After a CoreMedia -15628 error AVPlayer's media loader can die silently: playbackStalled fires, then zero segment requests follow while the player waits forever, and the item never fails. Previously only backing out of the player cleared this. Every stall now arms a fetch-counter watchdog: a consumer still silent after a grace window gets a zero-tolerance nudge seek, and if the loader stays dead, an in-place item reload on the same host. AVKit, PiP, and Control Center survive the reload, and segment retention serves it instantly. A spurious .paused during recovery is re-asserted instead of latched as a user pause.

Backward-seek restart latency cluster (#93 residual). Device-profiled fixes for the seek-past-retention path over slow links:

  • The wedged-restart fresh reopen skips the full first-open probe budget (44 s to about 7 s on device).
  • Waiting segment fetches ride an in-flight restart instead of burning fixed retry budgets into 503s.
  • A re-request for the index a restart just targeted waits for the fresh producer instead of tearing it down.
  • An index the active producer's forward march covers never fires or backstops a restart.
  • A request superseded by a newer declared target (a skip-storm orphan AVPlayer has already abandoned) never fires at all.
  • Lazy native subtitle readers defer while a restart is executing instead of competing for the starved link.

Resume-anchored first producer (#93). The first producer anchors at the segment covering the load's start position instead of producing seg0 into an immediate teardown; the old race could 404 the item into a host reload (double spinner, audio over a black frame).

Also fixed

  • Duplicate same-language subtitle rendition NAMEs collapsed AVFoundation's legible options; names are unique now, forced tracks declare FORCED=YES, and language matching goes through the ISO-synonym table with no cross-language fallback (a German track no longer selects an English rendition in PiP).
  • A pump-tap-fed subtitle selection kept forwarding cues into the overlay after switching to a sidecar file.

Compatibility

No breaking API changes. New API: LoadOptions.externalSubtitles, addExternalSubtitleTrack, removeExternalSubtitleTrack, TrackInfo.isExternal.

4.9.1 - tvOS build fix

Choose a tag to compare

@superuser404notfound superuser404notfound released this 02 Jul 10:47

Hotfix on top of 4.9.0: the segment-retention free-space clamp queried volumeAvailableCapacityForImportantUsage, which does not exist on tvOS and broke the tvOS build. tvOS now uses the plain volumeAvailableCapacity key; iOS and macOS keep the important-usage variant. No behavior change otherwise; see the 4.9.0 release notes for the feature set.

4.9.0 - PiP subtitles, restart timeline continuity, segment retention, subtitle pump tap

Choose a tag to compare

@superuser404notfound superuser404notfound released this 02 Jul 10:40

Device-verified on iPhone 17 Pro (iOS 26.5.1). All 517 tests green (228 XCTest + 289 Swift Testing), strict-concurrency clean.

Added

  • Native WebVTT subtitle renditions: subtitles survive PiP, AirPlay, and external display (Sodalite#32, #55). Opt-in via LoadOptions.prepareNativeSubtitles: every text subtitle track is served as a language-tagged HLS SUBTITLES rendition over the loopback master (windowed per-video-segment WebVTT), exposed through AVFoundation's legible AVMediaSelection group. Renditions ship DEFAULT=NO,AUTOSELECT=NO so a host overlay never double-renders; hosts select per surface via setNativeSubtitleSelected(track:), which re-asserts automatically when AVFoundation drops a selection made during a stall recovery. Replaces the earlier mov_text/tx3g in-stream approach (in-band timed text is not HLS-conformant).
  • Subtitle pump tap: embedded text cues harvested from the producer's own read. The segment producer keeps text subtitle streams in its keep-set and hands their packets to a session-level decode tap (generalizing the #77 CC tap), filling the per-track cue stores at zero side-channel bandwidth with coverage equal to the produced region, across seeks and restarts. The host overlay is fed from the same stores, so enabling embedded text subtitles is instant even on remote sources; ASS markup is preserved for the styled overlay and stripped for the WebVTT renditions.
  • Byte-budgeted VOD segment retention: seeks into watched content no longer restart the producer (#93 relevant, Sodalite#32). SegmentCache keeps already-produced segments beyond its hard sliding window resident under a byte budget (2 GiB, clamped to a quarter of the tmp volume's free capacity; farthest-from-target evicted first), so a backward seek into the retained span and the forward march after it are pure cache hits with zero producer teardowns. Live sessions keep window-only pruning.

Fixed

  • A producer restart now continues the fMP4 media timeline instead of zero-basing it (Sodalite#32, #93). Every restart allocates a fresh mp4 muxer, and movenc zero-based each instance's timeline, so a restart-produced segment carried tfdt=0 while the VOD playlist placed it at its plan offset: an implicit timeline discontinuity on every seek-restart, papered over for plain playback but fatal to AVKit's legible renderer and matching the #93 playhead/loaded-range decoupling signature. The muxer now sets movflags +frag_discont with avoid_negative_ts=disabled, the restart audio gate inherits the session shift instead of snapping audio onto the video seam, and leading head-of-stream audio that would map below zero is dropped. A restarted segment is byte-identical to its continuously produced twin modulo the mfhd sequence number, pinned by a witness test.
  • A teardown no longer caches a partial segment (VOD). Restart teardowns adopted the in-flight segment, caching content shorter than the playlist's EXTINF; with retention such a segment became replayable and produced ~2 s of A/V desync when seeked back into. VOD now adopts the in-flight segment only on a natural EOF; every other exit discards it and the next request re-produces it full-length.
  • E-AC-3/AC-3/TrueHD no longer wedge the fragmented-mp4 muxer on an out-of-cache backward seek (#94, unchanged from the pre-release entry).

Notes

  • The new witness fixtures are generated by Scripts/fetch-fixtures.sh; the related tests skip when the clips are absent (e.g. CI).
  • Native subtitle renditions require the master playlist route: SDR sources on any panel, HDR/DV on HDR-ready panels. HDR-on-SDR-panel and DV Profile 5 on non-DV panels stay media-direct and keep host-overlay subtitles only.

4.8.0 - five reported fixes, unified playbackPhase, SW background audio

Choose a tag to compare

@superuser404notfound superuser404notfound released this 30 Jun 17:09

AetherEngine 4.8.0 bundles five reported fixes, the unified playback-status API (#85), software-path background audio on iOS, and two new diagnostic CLI tools.

Playback status: unified playbackPhase (#85)

A single published playbackPhase enum is now the source of truth for playback status, replacing the separate loading / rebuffering / seeking / stall signals hosts had to reconcile. It carries a typed .stalled(reconnecting:) case, and a typed onNetworkPhaseChanged reader callback feeds it from the native, software, and audio hosts, so a reader-level network stall surfaces consistently regardless of decode path. Thanks @reckloon.

Software-path background audio (iOS)

Software-decode playback (AV1 without hardware decode, VP9) now keeps audio playing when the iOS app backgrounds, via a background-audio-only demux loop and a wedge-safe keepalive policy. The new aetherctl bgaudio harness exercises it headlessly.

Fixes

  • Open-GOP and B-frame VOD corruption (#92). VOD segment cutting is now keyframe-gated in decode order, so the IRAP opens its own segment (like the live path and FFmpeg's hls muxer). Previously each packet was routed to a segment by its DTS against PTS-valued plan boundaries; under B-frame reorder a keyframe's DTS falls below its PTS, so the boundary IRAP fell into the previous segment and the next segment started mid-GOP, decode-dependent on its predecessor. A fresh decode at such a boundary (rebuffer recovery) showed transient blocky corruption. Verified with the new segverify tool against ffmpeg ground truth (1/8 to 8/8 segments independently decodable). Thanks @rrgomes.
  • MKV with an unreadable Cues index loaded 0 tracks (#91). A bunched keyframe index that spanned less than one target segment passed the gap check but produced a single whole-file segment, which AVPlayer rejected with TrackNotFound. The planner now also requires the index to span at least one target segment (a coverage witness alongside the gap witness), else it falls back to a uniform plan. Thanks @reckloon.
  • Software-path seek black flash (#90). The software path blanked the display on a seek and flashed black until the post-seek keyframe. It now holds the last displayed frame across the seek, matching the hardware path. Thanks @rrgomes.
  • Software-decode audio crackle (#89). The software AudioDecoder stamped PTS from container-quantized timestamps, producing a per-frame click on frame durations that do not land on integer-millisecond boundaries (for example 1536-sample AC-3 at 44.1 kHz). Buffers are now stamped from a gapless running sample clock. Thanks @rrgomes.
  • Multi-second PGS subtitle startup stall (#87). The subtitle side demuxer ran a full find_stream_info (a flat ~5 s analyze ceiling) at load. It now skips that, reads the codec from the header or PMT, and falls back to a bounded resolve only when needed. Thanks @rrgomes.
  • iOS HDR and Dolby Vision format label in Stats for Nerds is now correct.

Tooling

  • aetherctl segverify <url> decodes each loopback segment in isolation and reports how many are independently decodable. This is the segment-independence ground truth used to confirm #92.
  • aetherctl --throttle-kbps <n> caps serve / segverify bandwidth to simulate a slow CDN and reproduce backpressure and recovery behaviour.

Compatibility

Minor release: additive public API (playbackPhase, onNetworkPhaseChanged, the background-audio policy). No breaking changes.