3.0.0: Playback clock split, probe API for custom readers, raw ASS
A breaking release centered on one API redesign, plus the probe / custom-IO extensions and the subtitle memory fix from this week's issue work.
Breaking: the playback clock moved to engine.clock
currentTime ticked at ~10 Hz as @Published on the engine itself, so every SwiftUI view observing the engine re-rendered per tick no matter what it read. On tvOS that re-render tears down native Menu dropdowns mid-interaction, which is the flicker reported in #29.
The continuously ticking values (currentTime, sourceTime, progress, liveEdgeTime, seekableLiveRange, isAtLiveEdge, behindLiveSeconds) now live on PlaybackClock, a separate ObservableObject exposed as engine.clock. The engine's objectWillChange no longer fires on clock ticks. This mirrors AVFoundation's own shape: AVPlayer exposes time via addPeriodicTimeObserver, not as observable state.
Migration
- Plain reads compile unchanged:
engine.currentTimeetc. remain as read-only forwarders. - Combine subscriptions move:
engine.$currentTimebecomesengine.clock.$currentTime(same forsourceTime,progress, and the live-edge fields). - Time-driven UI (transport bar, time labels) should observe
engine.clockin the leaf view that renders time; everything else observes the engine and stays quiet during playback.
Added
probe(source:)accepts aMediaSource, so customIOReadersources can be probed like URLs. The caller keeps reader ownership; the probe never callsclose(). Closes #27.load()returnsSourceProbe(@discardableResult, existing callers compile unchanged): video size, codec, duration, tracks, and container tags in one shot, without a second probe round-trip.sourceVideoWidth/sourceVideoHeightare also public read-only now. Closes #28.- Opt-in raw ASS event lines.
LoadOptions.preserveASSMarkupemits ASS / SSA cues as the raw event line (override tags, style references, escapes intact), andTrackInfo.assHeadercarries the track's script header so hosts can resolve style references. Default off. Part of #30; full libass rendering stays open there. - Live DVR scrub thumbnails via
liveScrubThumbnail, decoded straight from the DVR segment cache. DataIOReader, a ready-made in-memoryIOReaderover an immutableDatabuffer.- Native remote-HLS path (
LoadOptions.nativeRemoteHLS): play a server-provided HLS URL directly with AVPlayer, bypassing the demux / remux / loopback pipeline. - SW-path deinterlacing through a persistent bwdif / yadif graph (DVD-rip MPEG-2, SD broadcast).
- HE-AAC / LATM bridging: LATM/LOAS AAC live audio bridges instead of dropping; plain ADTS-AAC stream-copies into fMP4.
Fixed
- Embedded-subtitle side demuxer no longer races to EOF. It paces against the playhead (90 s read-ahead) and TCP backpressure throttles its connection to playback rate. Previously it re-downloaded the entire remaining file alongside playback and pinned every future PGS bitmap cue in memory, which on 50-80 GB UHD remuxes ran the app into jetsam. Subtitle part of #31; the no-subtitle memory spike reported there is still under investigation.
- Live hardening batch: server-side stream-replay detection after reconnect, program-boundary timeline rebase, A/V-sync rebase pairing, source-loss auto-reopen with backoff, deterministic pause/resume, LL-HLS blocking playlist reload, fast give-up on dead tuners, abortable in-flight probes on stop / channel zap.
- VOD robustness batch: muxer-wedge exit, audio-bridge EOF / restart flush, Range-ignored (200-at-offset) guard, cache-gated backward restart, paused-seek clock anchor, corrupt-source-audio resilience.
Thanks
@ohjey for the precise root-cause analysis behind the clock split (#29), @strangeliu for the probe / load API proposals (#27, #28) and the raw-ASS suggestion (#30), @edde746 for the libass request (#30), @RomanLiberda for the large-file memory report (#31), and @DrHurt for review input across the board.
Full Changelog: 2.5.0...3.0.0