Releases: superuser404notfound/AetherEngine
AetherEngine 1.1.0
AetherEngine 1.1.0
Bug-fix release on top of 1.0.0. Three days of public-beta feedback in Sodalite drove a producer-side A/V sync overhaul, HDR / Dolby Vision routing fixes, a dynamic subtitle-clock model, plus a handful of new public API hooks. Tag retargeted on 2026-05-16 to include the tvOS/iOS packaging fix (#5).
Playback pipeline (producer + muxer)
A/V sync after restart and from frame one. The producer's video gate is now unconditional on AV_PKT_FLAG_KEY, initial-start as well as restart sessions. The audio gate always waits for the video gate, so both streams' first kept sample comes from the same source-time. Previously, MKV remuxes whose first decode-order packet wasn't a sync sample (some Bluey BD remuxes) either stalled AVPlayer with -12860 or played the first few seconds with audio anchored at the file start and video at a later IDR.
Matroska seek imprecision tolerated end-to-end. Producer restarts scan forward from the matroska seek result to the next true IDR, then apply a per-stream dynamic PTS shift so the fragment tfdt lands at the playlist's cumulative-EXTINF origin. NOPTS dts repair (lastValidDts + 1) keeps B-frame-heavy MKVs from stalling the muxer. Per-stream PTS shift now uses each stream's own start_time rather than the format-level value, since broadcast remuxes can ship different per-stream offsets.
HEVC open-GOP CRA support. Producers now drop pre-keyframe leading B-frames (HEVC RASL) when their display-order pts lands before the CRA that opened the segment stream. Without the drop, AVPlayer's HEVC decoder fails on the first display sample and stalls in waitingToPlay forever (repro: Bombige Magenverstimmung, open-GOP HEVC with firstKeyframePts=88 and two leading B-frames at pts=88 and pts=131).
Per-frame fallback duration. When the source MKV doesn't ship DefaultDuration (HandBrake / web-rip pipelines often drop it), the producer backfills pkt->duration per stream via a look-behind. Stops the mp4 sub-muxer from writing trun.last.duration = 0, which broke seekability on some downstream consumers.
HDR / Dolby Vision routing
Match Content master-toggle aware HDR routing. HDR HEVC on a non-DV display now routes through the master playlist when the user's Match Content master toggle is on, and through the media playlist when it's off. On panels with the toggle off, the master's VIDEO-RANGE=PQ was failing AVPlayer with Cannot Open (AVFoundationErrorDomain -11848) since the panel is SDR-locked.
Dolby Vision P8.1 / P8.4 cross-compat tags. P8.1 and P8.4 now emit bare dvh1.<profile>.<dvLevel> on DV-capable displays for the direct DV engagement, and fall back to hvc1.2.4.LXX with SUPPLEMENTAL dvh1.08.LL/db4h for cross-player compatibility on P8.4's HLG-HEVC base layer.
SDR rate-only display criteria. DisplayCriteriaController.apply() used to early-return for .sdr sources, which also skipped the preferredDisplayCriteria assignment. SDR sessions now program a rate-only criteria so Match Frame Rate can engage independently of Match Dynamic Range. tvOS internally honours whichever Match Content sub-toggle the user has enabled.
HDR10+ runtime detection. videoFormat flips from .hdr10 to .hdr10Plus on first T.35 SEI detection in a packet's payload. Debounced once per session so producer restarts on scrub don't re-fire.
Effective format clamping. Published videoFormat is clamped to panel capabilities. A DV asset on a non-DV TV is reported as .hdr10 (the panel's actual experience) rather than the source's claimed format.
Subtitles
Cue timestamps mapped through the active producer's shift. Subtitle cues come from an independent side-demuxer in raw source PTS, but AVPlayer's HLS clock sits at source_pts - producer.videoShiftPts, and the shift varies per producer session (matroska seek imprecision means the shift can be ~4 s on restart sessions for the same source). The producer now reports its shift via onVideoShiftKnown; the engine forwards it through HLSVideoEngine.onPlaylistShiftChanged and publishes a derived sourceTime (= currentTime + shift). Hosts read sourceTime for cue lookup and side-demuxer seeking, while currentTime stays the AVPlayer-clock for transport / scrub / resume.
Public API additions
AetherEngine.currentAVPlayer(@Published AVPlayer?): exposes the active AVPlayer for MPNowPlayingSession hosting. Re-emitted on every reload so hosts that rebindMPNowPlayingSession.playerstay current.setExternalMetadata(_:): pushAVMetadataItemcollection into the engine for the next native load. Stashed beforeload()and replayed onto the freshly-createdNativeAVPlayerHostwhen the item is created, so hosts can drive the tvOS info panel without reaching into AVPlayer.LoadOptions.httpHeaders: custom headers (auth tokens, User-Agent overrides) attached to every demuxer + segment fetch, replayed across producer restarts and side-demuxer sessions.LoadOptions.keepDvh1TagWithoutDV: experimental, keep thedvh1codec tag on non-DV displays. Off by default; tooling lever for AVKit auto-criteria behaviour.LoadOptions.matchContentEnabled: mirror the host's tvOS Match Content master toggle so the engine can gate HDR HEVC master-playlist routing accordingly.engine.reloadAtCurrentPosition()preserves the originalLoadOptions(httpHeaders + matchContentEnabled + keepDvh1TagWithoutDV) across the rebuild. Same for the internal audio-switch reload path.
Packaging
aetherctl is no longer exposed as an SPM product (#5). The target uses Foundation.Process, which is unavailable on tvOS / iOS, so exposing it forced SPM consumers on those platforms to compile it and fail. The aetherctl target itself stays, so swift build on macOS still produces the CLI for upstream development.
aetherctl
aetherctl reorganised under probe / serve / validate subcommands. serve exposes the engine's loopback HLS server for AVPlayer-side debugging on macOS. A fixtures.sh script populates a small media-zoo for repro testing.
Internals
- HDR badge consolidated into the
videoFormatsubscription path, single source of truth. - EngineLog double-emit fixed (handler and stdout are now mutually exclusive so Xcode console no longer renders every line twice).
- Network: BSD-sockets HLSLocalServer refactor attempted and reverted; NWConnection-based server remains the shipping implementation.
- Cleanup: dead VP9 capability probe removed; dead symbols dropped; stale docstrings refreshed.
Engine pin
For Sodalite hosts: bump Package.resolved to 424b88e (or use the 1.1.0 tag).
AetherEngine 1.0.0
AetherEngine 1.0.0
First stable release. A video player engine for Apple platforms — drop the package in, hand it a file, get pixels on screen. Built on Swift 6 strict concurrency, LGPL 3.0 with App Store exception.
The engine handles the hard parts (HDR, Dolby Vision, Dolby Atmos, container coverage, codec coverage) and exposes a single render surface plus a handful of async methods. No AVPlayerViewController. No opinionated controls. No analytics. You ship the UI.
Architecture
Two playback pipelines coexist, picked once at load(url:) by the source's video codec and the device's decode capabilities. Hosts see a unified @Published state surface either way.
Native AVPlayer pipeline (default). Demux with libavformat, re-mux on the fly into HLS-fMP4, serve from a local HTTP loopback, point AVPlayer at the playlist. Apple's stack does all decode, HDR / Dolby Vision signaling, audio routing.
Source URL → Demuxer → HLSSegmentProducer → SegmentCache → HLSLocalServer
↓
AVPlayer
├→ VideoToolbox (HW)
└→ AVR (Atmos via MAT 2.0)
Used for HEVC / H.264 in all cases, and for AV1 on devices with HW AV1 decoders (M3+ Mac, iPhone 15 Pro+, future Apple TV chips). Atmos passthrough, Dolby Vision HDMI handshake, HDR10 / HDR10+ / HLG all live on this path.
Software decoder pipeline (gap-filler). Demux, run video through libavcodec (dav1d for AV1, FFmpeg's native VP9 decoder for VP9) into CVPixelBuffers, run audio through libavcodec into CMSampleBuffers, render via AVSampleBufferDisplayLayer + AVSampleBufferAudioRenderer with AVSampleBufferRenderSynchronizer as the master clock.
Source URL → Demuxer ┬→ SoftwareVideoDecoder (dav1d / VP9) → SampleBufferRenderer → AVSampleBufferDisplayLayer
└→ AudioDecoder → AudioOutput → AVSampleBufferRenderSynchronizer (drives sync)
Used for codecs AVPlayer's HLS-fMP4 pipeline doesn't accept:
- AV1 on devices without HW AV1 (all current Apple TV chips, M1/M2 Macs, pre-A17-Pro iPhones). Apple ships dav1d on macOS 14+ / iOS 17+ but it's only reachable via AVPlayer's HLS-fMP4 pipeline when the chip also has HW AV1 — verified empirically.
- VP9 unconditionally. AVPlayer's HLS manifest parser silently rejects the
vp09CODECS attribute (verified via aetherctl:master.m3u8+media.m3u8fetched, then no further requests,item.statusstays.unknown). VideoToolbox HW-decodes VP9 fine on A12+, but only outside the HLS pipeline.
Public API
AetherPlayerView(UIKit / AppKit) +AetherPlayerSurface(SwiftUI) — single render surface the host embeds. Polymorphic: hosts eitherAVPlayerLayer(native) orAVSampleBufferDisplayLayer(SW) per session, swapped automatically.engine.bind(view:)/engine.unbind(view:)— engine attaches its active layer to the view automatically.engine.load(url:options:)— single async entry point. Dispatches by codec internally.LoadOptionscontrols diagnostics-only toggles.- Transport:
play(),pause(),togglePlayPause(),seek(to:)(async),setRate(_:),stop(),volume. - Lifecycle:
reloadAtCurrentPosition()rebuilds the pipeline after background suspension. - Audio tracks:
selectAudioTrack(index:)— mid-playback switch with backend-aware reload, audio-source-stream override propagates through both pipelines. - Subtitles:
selectSubtitleTrack(index:)(embedded, runs a side demuxer at the playhead),selectSidecarSubtitle(url:)(sidecar SRT / ASS / VTT),clearSubtitle(). Text + bitmap unified viaSubtitleCue(body = .text(String)or.image(SubtitleImage)). - Capabilities:
AetherEngine.displayCapabilities— static snapshot of HDR / DV / HLG support. @Publishedstate:state,currentTime,duration,progress,audioTracks,subtitleTracks,activeAudioTrackIndex,videoFormat,playbackBackend(.native/.software/.none),subtitleCues,isLoadingSubtitles,isSubtitleActive.
Codec coverage
| Codec | Native path | SW path |
|---|---|---|
| H.264 / AVC (SDR, HDR10) | universal | — |
| HEVC / H.265 Main / Main10 (SDR / HLG / HDR10 / HDR10+) | Apple TV 2017+ / iOS A9+ / Apple silicon | — |
| HEVC + Dolby Vision (P5 / P8.1 / P8.4) | same hardware as HEVC; dvh1 / hvc1 track type + dvcC box |
— |
| AV1 Main / High (P0 / P1, SDR / HDR10 / HDR10+) | M3+ Mac, iPhone 15 Pro+ (HW AV1) | Apple TV (all generations), older Mac / iPhone without HW AV1 |
| AV1 + Dolby Vision (P10.0 / P10.1 / P10.4) | same hardware as plain AV1; dav1 / av01 track type + dvvC box per Apple HLS Authoring Spec |
DV not engaged on the SW path (rare in real content; AV1+DV almost always pairs with HW-AV1 hosts) |
| VP9 Profile 0/2 (8/10-bit) | — | all platforms (libavcodec native) |
| AV1 + DV P7 / DV P8.2 / DV P10.2 (SDR-base) / Profile 11+ | refused (unsupportedDVProfile) |
refused |
The native path's AV1 acceptance gates on VTCapabilityProbe.av1Available (strict VTIsHardwareDecodeSupported). Engine's load(url:) dispatch resolves the path per source; hosts don't see the distinction.
HDR pipeline
- HDR10 / HDR10+ / HLG / Dolby Vision (HEVC P5, P8.1, P8.4 + AV1 P10.0, P10.1, P10.4) all engage the HDMI HDR-mode handshake on the native path.
AVDisplayCriteriabuilt from real demuxer-probed format +r_frame_rate/avg_frame_rate(snapped to standard rates: 23.976 / 24 / 25 / 29.97 / 30 / 48 / 50 / 59.94 / 60).- Match Content / Match Frame Rate user settings honored.
- HDR10+ ST 2094-40 metadata stream-copied as user-data-registered ITU-T T.35 SEI NALs; AVPlayer forwards the SEI to the system compositor unchanged.
- Dolby Vision: codec tag promoted (
hvc1→dvh1for HEVC P5 / P8.1,av01→dav1for AV1 P10.0 / P10.1) with the source'sdvcC/dvvCbox preserved. P8.4 / P10.4 keep the base codec tag and signal DV via SUPPLEMENTAL CODECS so non-DV displays present the HLG base layer. - Dolby Vision dual-layer (P7), SDR-base (P8.2 / P10.2) explicitly refused.
- HDR-to-SDR mapping handled by AVPlayer and the system compositor; no host-side tonemap.
Audio
- Stream-copy into fMP4 for legal codecs that AVPlayer accepts (AAC, AC3, EAC3 incl. JOC Atmos, FLAC, ALAC, MP3) — bit-exact, no transcode CPU overhead.
AudioBridgeFLAC fallback for codecs that aren't legal in fMP4 (TrueHD, DTS, DTS-HD MA, PCM, MP2, Vorbis) or that AVPlayer rejects in HLS-fMP4 despite spec-legality (Opus). Decode → S16 PCM → FLAC re-encode. Lossless bed channels; for TrueHD-MAT and DTS-X Atmos sources the object metadata doesn't survive the PCM intermediate.- Atmos passthrough preserved via EAC3-JOC stream-copy. The engine emits explicit diagnostics on both success (
stream-copy engaged, MAT 2.0 passthrough intact) and on the theoretical downgrade path (WARNING: Atmos downgrade — EAC3+JOC stream-copy rejected by mp4 muxer ...) so silent quality regressions are loud in the log.
Subtitles
Subtitle packets are routed through a side demuxer running at the playhead, decoded inline through avcodec_decode_subtitle2. Results land in a single [SubtitleCue] published list:
- Text codecs (SubRip / ASS / SSA / WebVTT / mov_text) →
SubtitleCue.body = .text(String). ASS override blocks stripped;\Nbecomes a real newline. - Bitmap codecs (PGS / HDMV PGS / DVB / DVD) →
.image(SubtitleImage). Indexed pixel plane walked through its palette, premultiplied against alpha, wrapped asCGImage. Position normalised in[0..1]against the source frame so the host scales to any on-screen rect. - Sidecar files (separate
.srt/.ass/.vttURL) →selectSidecarSubtitle(url:)opens its own short-livedAVFormatContext, decodes the whole file once, atomically swaps the result intosubtitleCues.
The host paints the cues with whatever style and animation it wants.
Seek
- Native path: AVPlayer's own seek.
- SW path: pause demux loop → flush decoders + renderer + audio renderer → seek demuxer → set
skipUntilPTSso frames between keyframe-before-target and the target are dropped → jump the synchronizer clock atomically viaAudioOutput.seekClock(to:rate:)so PTS-stamped samples decoded post-seek align with the master clock. - Backward / far-forward scrubs on the native path tear down the
HLSSegmentProducerand restart it at the new segment base. Short-range forward scrubs ride the cached segment window without restart.
Streaming & resilience
- HTTP Range + chunked delegate reads via
URLSession. No third-party networking layer; TLS / HTTP-3 / proxies / MDM rules ride for free. - Exponential backoff on transient network errors.
- Background pause / display-link aware lifecycle.
Dependencies
| Package | License | Purpose |
|---|---|---|
| FFmpegBuild | LGPL-3.0 | Slim FFmpeg 7.1 (avcodec / avformat / avutil / swresample / swscale): demux + HLS-fMP4 mux + AudioBridge FLAC encode + SW-path dav1d / VP9 decode + sws_scale YUV → NV12 / P010 |
| VideoToolbox | System | Native-path video decode (HW where available) |
| AVFoundation | System | AVPlayer + AVDisplayManager (native); AVSampleBufferDisplayLayer + AVSampleBufferRenderSynchronizer (SW) |
| CoreMedia | System | Sample descriptions, format-description tagging, CMTimebase |
Non-goals
- No built-in UI, controls, transport bar, HUD.
- No analytics, telemetry, session reporting. Wire your own to the
@Publishedstate. - No playlist / queue management. Call
load(url:)when you want the next one. - No subtitle overlay. The engine emits
SubtitleCue; the host paints. - No Metal shaders. Everything renders through Apple's native display stack.
- No third-party networking.
Requirements
| | Min |
|---|-...
Diagnostic bundle for issue #2 (2026-05-09)
Standalone HLS-fMP4 capture from aetherctl against a Jellyfin direct-play DV Profile 8.1 source. See README.md inside the zip for what each file is and how to test on iPhone Safari.
Not a code release. Pure diagnostic artifacts. Will probably be deleted once the underlying issue is resolved.