AetherEngine 2.1.0
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 tomaxWidth(default 320). Cheap and fast, built for scrub previews and Recents lists.snapshot(at:maxSize:)decodes forward to the exact PTS at full ormaxSize-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 teardownAetherEngine.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