Skip to content

Releases: superuser404notfound/AetherEngine

2.3.0

06 Jun 21:52

Choose a tag to compare

New public API for media metadata, plus episode-autoplay playback-reliability fixes. No breaking API change, existing 2.x callers are unaffected.

Changes

  • MediaMetadata extracted on every load. The demuxer parses normalized container tags (title, artist, album, albumArtist, with whitespace cleanup) and pulls embedded cover art. The engine publishes it at load time and exposes it through SourceProbe, and aetherctl prints the parsed container metadata in its probe output. Driven by the AetherPlayer media-player work.
  • Episode autoplay no longer starts audio before video. The native AVPlayer reused across native-to-native reloads (since 2.2.1) carried its previous rate=1.0 into the next item, so the new episode auto-resumed before the display-criteria handshake and played audio while the panel was still mid Match-Frame-Rate switch. The host now pauses the player across the item swap, so the post-handshake play() gates the start.
  • No more mid-playback stall plus A/V desync a minute or two into a stream. SegmentCache evicted already-produced forward segments when AVPlayer did a transient backward refetch (an audio handover or decode flush moved the prune target back), which forced a cache-miss producer restart that re-muxed from a fresh init segment. The forward prune bound is now anchored on the highest stored index so produced-but-unconsumed segments survive the dip, and the restart decision no longer treats a resident segment the producer merely raced past as a pruned gap.

Full changelog: 2.2.2...2.3.0

2.2.2

06 Jun 18:12

Choose a tag to compare

Playback-clock correctness. The engine now presents a single source-PTS timeline. No breaking API change, existing 2.2.x callers are unaffected.

Changes

  • Unified the playback clock onto source PTS. On the native HLS path currentTime previously mirrored AVPlayer's loopback clock (source_pts - playlistShiftSeconds) while sourceTime carried source PTS, forcing every source-timeline consumer (subtitle scheduling, media-segment intro/outro detection, resume reporting) to pick the right one of two clocks. The shift is now folded into the published currentTime, so currentTime == sourceTime on every path (the software and audio paths already ran on source time). Resume and reloadAtCurrentPosition get slightly more accurate as a result, and on a rare imprecise restart seek the reported position now reflects the true landed frame.
  • seek(to:) is now source-PTS based and converts to the loopback clock internally (a no-op on the software and audio paths, where the shift is 0). A seek(toSourceTime:) alias exists but is deprecated, since seek(to:) now covers it. sourceTime stays public as a stable alias for callers that want to express source-timeline intent explicitly.

Full changelog: 2.2.1...2.2.2

AetherEngine 2.2.1

06 Jun 16:44

Choose a tag to compare

Patch release. Playback, audio, and Now-Playing fixes. No public API change, existing 2.2.x callers are unaffected.

Fixes

  • Persistent forward-streaming AVIO reader for CDN direct-URL playback (#25). The fragile chunked range reader is replaced with a VLC-style single forward-streaming connection that reconnects with backoff on drops. Waiting on data is now edge-triggered, and the reconnect cap is progress-aware so a stream that keeps advancing is not killed by a transient stall.
  • Multichannel audio no longer downmixes to stereo with continuous-audio off (#24). Audio-route capability is sampled after playback settles rather than at readyToPlay, when the HDMI route has not finished negotiating yet. The native path lets AVKit own audio-session activation, and the manual reassert is scoped to the renderer paths that actually need it. (Earlier session-reassert and route-renegotiation attempts in this cycle were disproven on device and reverted.)
  • System Now-Playing survives native-to-native reloads (#15). Episode autoplay and audio-track switches reuse the existing native AVPlayer via replaceCurrentItem instead of building a fresh one, which previously blanked the Control Center Now-Playing card on every swap.

See CHANGELOG.md for the release index.

AetherEngine 2.2.0

05 Jun 15:10

Choose a tag to compare

AetherEngine 2.2.0

New public API: an audio-only playback path. Minor bump, purely additive. Existing 2.1.x callers compile and run unchanged.

Audio-only path

LoadOptions.audioOnly routes a source into a lean audio pipeline that never builds the HLS loopback server, the display layer, or the video producer. Decode is native-first:

  • Codecs on the avPlayerCanDecodeAudio whitelist hand the URL straight to a bare AVPlayer (AudioAVPlayerHost).
  • Everything else falls back to an FFmpeg decode into AVSampleBufferAudioRenderer (AudioPlaybackHost).

The engine branches load() into the audio path, routes transport (play / pause / seek) to the active host, and tears the host down in stopInternal for a clean handoff back to the video path.

System Now-Playing (tvOS / iOS)

The AVPlayer host owns a persistent per-player MPNowPlayingSession (exposed via audioNowPlayingSession) that stays the active Now-Playing app across a background pause, auto-publishes now-playing info from the player, and carries externalMetadata. The host survives across tracks (no per-track teardown) and does not pause when the app backgrounds, so audio keeps playing with the system overlay live.

All Now-Playing code is gated #if os(tvOS) || os(iOS). The path builds clean on macOS (no system session there), iOS, and tvOS.

Tooling

New aetherctl audio subcommand for audio-path smoke testing: prints the active decoder and final duration, driven under CFRunLoop so end-of-track fires at playback end rather than demux EOF.

Compatibility

Purely additive public API, no breaking changes. Pin from: "2.0.0" continues to pick this up.

2.1.3

01 Jun 16:34

Choose a tag to compare

Playback fix: rapid play/pause no longer swallowed

Transport state sync. No public API change, existing 2.1.x callers are unaffected.

Fixed

  • Rapid play/pause presses no longer get swallowed on the native (AVPlayer) path. The engine never derived its state from the player, so when something other than engine.play() / pause() drove the AVPlayer (a host that keeps AVKit's transport bar active for Control Center skip routing, Control Center itself, or the hardware play/pause button AVKit handles internally), the engine's state went stale and the next togglePlayPause() resolved to the action already in effect, a visible no-op.

How it works now

  • NativeAVPlayerHost publishes timeControlStatus; the engine reconciles state (playing / paused) from it, guarded to the steady transport states so loading, seeking, error and idle are never clobbered. waitingToPlayAtSpecifiedRate maps to playing, so the play/pause icon does not flicker on a rebuffer.
  • togglePlayPause() decides from the live player rather than the published state, closing the async gap during fast presses.

Full diff: 2.1.2...2.1.3

AetherEngine 2.1.2

01 Jun 08:26

Choose a tag to compare

Playback fix. Head-of-stream A/V sync. No public API change. Existing 2.1.x callers are unaffected.

Fixed

Audio no longer leads video at the start of a file. On a fresh play (baseIndex 0) the producer snapped the first audio packet onto the video's tfdt (desired 0), which subtracted the audio track's intrinsic start offset from every audio packet. On sources whose first full audio frame lands well past video frame 0 (Cars: EAC3 first frame at +256 ms) this pulled the whole audio track that far ahead of the picture for the entire session, reported as a 256 ms A/V offset in the stats overlay.

Head-of-stream now derives the audio shift from the video's origin shift, so both streams undergo one shared transform and their true source-time relationship is preserved by construction. The audio fragment's tfdt then starts a little after the video's, which fmp4 / AVPlayer represent natively (audio is simply silent for the leading gap). Resume and scrub sessions were unaffected and keep the existing gate-on-video snap (sub-frame residual, part of the HEVC-resume alignment stack).

Verification

Verified on device with Cars: the diagnostic A/V gap now reports 0 ms at head-of-stream (was 256 ms). Build green.

Full changelog: 2.1.1...2.1.2

AetherEngine 2.1.1

31 May 11:21

Choose a tag to compare

FrameExtractor quality pass. Internal only, no public API change. Existing 2.1.0 callers are unaffected.

Fixed / Improved

HDR thumbnails now tone-map correctly. PQ (ST 2084) and HLG stills used to come out too dark / desaturated because the extractor scaled straight to sRGB with no transfer conversion. HDR frames now route through a zscale + tonemap libavfilter graph (BT.2020 PQ/HLG to SDR BT.709 RGBA, hable tone curve); SDR keeps the fast direct sws path. This relies on the avfilter + zimg additions to FFmpegBuild (already pinned by this release).

Faster, lighter remote extraction. A new .stillExtraction demuxer profile gives the extractor's AVIO a random-access shape: no read-ahead prefetch (a scrub discards it on the next seek, and it competed with playback bandwidth), a 1 MB seek chunk, and a small probe budget. Decode fast-flags (skip loop filter, fast decode) cut per-frame CPU on big HEVC keyframes.

Thumbnails fixed on sparse-keyframe HEVC. The thumbnail decode no longer sets skip_frame = NONKEY, which starved the decoder when a seek landed mid-GOP past a lone keyframe and produced nil thumbnails on some HEVC sources.

Verification

Build + full test suite green; leaks --atExit reports 0 leaks across repeated HDR extractions (the per-call filter graph is freed each time); SDR and HDR thumbnail / snapshot all produce images via aetherctl extract.

Known limitation

DV Profile 5 (IPT-PQ, no HDR10 base) thumbnails still render with wrong colours on the software decode path the extractor uses, the same class as the AV1 Profile 10.0 limitation. Full Profile 5 playback is unaffected (it routes through the native AVPlayer path).

Full changelog: 2.1.0...2.1.1

AetherEngine 2.1.0

31 May 07:56

Choose a tag to compare

New public API: off-playback still-image extraction.

Added

FrameExtractor — still CGImages from a media URL, fully isolated from playback.

FrameExtractor decodes through its own FFmpeg context with no contact with the playback pipeline, the HLS loopback server, or shared engine state, so a scrub-preview decode can't perturb the frame on screen. Two modes share one decode core:

  • thumbnail(at:maxWidth:) snaps to the nearest keyframe, no forward decode, downscaled to maxWidth (default 320). Cheap and fast, built for scrub previews and Recents lists.
  • snapshot(at:maxSize:) decodes forward to the exact PTS at full or maxSize-clamped resolution, built for user-triggered stills.

It is an actor: blocking FFmpeg work runs on a dedicated serial queue off the cooperative pool, the decode context opens lazily on first use, a superseded request cancels the in-flight decode so the latest scrub position wins, results land in a bounded LRU cache (mode-isolated stores, second-bucketed thumbnails), and the context idle-closes after 10 s (the next request reopens lazily). shutdown() is the explicit, permanent teardown that awaits release of the FFmpeg demuxer / codec / sws resources.

// Currently-playing item:
let frames = engine.makeFrameExtractor()           // nil if nothing is loaded
// Arbitrary item (e.g. a Recents row):
let frames = FrameExtractor(url: url, httpHeaders: headers)

await frames.prewarm()                             // optional: hide cold-start
let preview = await frames.thumbnail(at: 612.0)    // CGImage?, nearest keyframe
let still   = await frames.snapshot(at: 612.0)     // CGImage?, frame-accurate
await frames.shutdown()                            // prompt teardown

AetherEngine.makeFrameExtractor() vends an extractor for the currently loaded URL (carrying its HTTP headers). The engine does not retain it; the caller owns its lifecycle.

aetherctl extract subcommand for still extraction plus leak testing (--at, --snapshot, --width, --loops), backed by the same public API. --loops N pairs with leaks --atExit to validate clean decode-context teardown.

Compatibility

Purely additive public API, no breaking changes. Existing 2.0.x callers compile and run unchanged. Pinning from: "2.0.0" already picks this up.

Full changelog: 2.0.2...2.1.0

AetherEngine 2.0.2

28 May 09:32

Choose a tag to compare

Follow-up bugfix to 2.0.1's Dolby Vision Profile 5 work.

Fixed

Profile 5 MP4 sources whose hvcC carries only the configuration header (no VPS / SPS / PPS arrays) now play correctly.

2.0.1's colr fix put the PQ transfer signal on the output sample entry but AVPlayer still failed the asset with `CoreMediaErrorDomain -4` because `CMVideoFormatDescription` cannot be built from a `dvh1` sample entry whose configuration record has no parameter set arrays. The matroska demuxer doesn't hit this because matroska parameter sets live in `CodecPrivate`, which FFmpeg lifts into `codecpar.extradata` as a complete annex-B sequence that the mp4 muxer's `ff_isom_write_hvcc` then rebuilds properly.

The engine now scans the first IRAP packet for VPS / SPS / PPS NAL units, builds a proper hvcC byte sequence (header + 3 parameter set arrays), and replaces the output stream's `codecpar.extradata` before `avformat_write_header`. Gated on the precise signal: HEVC codec, extradata ≥ 23 B with byte 22 = 0, NALU length size 4.

Verified locally against the issue #19 sample: loopback playback advances in QuickTime / AVPlayer, init.mp4 has all four boxes (`dvh1` + `hvcC` 125 B with parameter sets + `colr nclx 9/16/9` + `dvcC` P5 L6 compat=0), colors render correctly.

Compatibility

No API or behavior changes outside the P5 path. Pinning `from: "2.0.0"` already picks this up.

Full changelog: 2.0.1...2.0.2

AetherEngine 2.0.1

28 May 08:57

Choose a tag to compare

Bugfix release.

Fixed

Dolby Vision Profile 5 MP4 sources with no explicit PQ signaling now play correctly.

Some P5 MP4 encoders write a dvh1 sample entry and a well-formed dvcC record but omit the HEVC SPS VUI transfer fields and the container colr atom. The engine was stream-copying that gap through to its output fMP4, so AVPlayer saw a dvh1 sample entry with no PQ signal and refused to engage the DV decoder. The same content as MKV played cleanly because matroska's Colour element gives FFmpeg explicit codecpar.color_* that the mp4 muxer writes as a colr nclx atom; the mp4 demuxer has no equivalent fallback.

The fix forces the canonical P5 color tuple (BT.2020 / PQ / BT.2020-NCL / limited range) on the muxer's stream codecpar before avformat_write_header. P5 is defined as IPT-PQ-c2, so the dvcC record alone implies that signaling, which makes the override safe (no risk of mislabeling a non-PQ source).

Reported by @strangeliu (issue #19), diagnosed with @DrHurt's broken-vs-Dolby-reference framing.

Compatibility

No API or behavior changes outside the P5 path. Pinning from: "2.0.0" already picks this up.

Full changelog: 2.0.0...2.0.1