Releases: superuser404notfound/AetherEngine
AetherEngine 2.0.2
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
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
AetherEngine 2.0.0
Stability milestone
AetherEngine 2.0.0 marks the project as ready for external adopters to depend on. The HDR / Dolby Vision routing path has settled across the DrHurt #4 panel-mode sweep, the producer / cache layer is hardened against scrub-cascade failure modes, and the project now ships tests, CI, examples, a stability contract, and a known-limitations register.
No breaking changes to the public API surface. Existing 1.5.0 callers compile and run unchanged. The major-version bump is a stability signal for downstream adopters who want to pin against a milestone instead of a moving target — it is not an API redesign.
Highlights since 1.5.0
Match Dynamic Range OFF correctly detected
tvOS exposes a single combined isDisplayCriteriaMatchingEnabled flag for the user's Match Content setting (rate + range). There is no API to tell whether Match Dynamic Range specifically is on. Users with Match Frame Rate ON and Match Dynamic Range OFF previously had the engine route HDR sources through master playlists with VIDEO-RANGE=PQ, which AVPlayer rejected with AVFoundationErrorDomain -11848 / -11868 since the panel stayed SDR-locked.
The engine now reads UIScreen.currentEDRHeadroom AFTER the criteria handshake settles (waitForSwitch returns) and uses that empirical reading for the master-vs-media routing decision. > 1.001 means the panel accepted HDR, == 1.0 means it refused. The two cases route differently and AVPlayer no longer sees variant-filter rejections.
Closes the residual DrHurt #4 edge case (rate-match ON + range-match OFF on HDR/DV content).
sourceVideoFormat published surface
Stats / debug overlays can now distinguish "what's in the file" from "what the panel is currently presenting":
engine.$sourceVideoFormat // .dolbyVision (source claim)
engine.$videoFormat // .hdr10 (what the HDR10-only panel actually shows)videoFormat continues to be the engine-clamped value (DV on a non-DV TV publishes as .hdr10 because AVPlayer tonemaps via the dvh1 sample entry). sourceVideoFormat is the unclamped source-detected value so hosts can render "Source → Target" displays. Late HDR10+ detection from T.35 SEI upgrades both surfaces independently.
HLS producer reliability cluster
Several scrub-cascade and back-scrub failure modes closed:
- Cache high-water reset AFTER restart returns, not before. The prior pre-restart reset opened a race where the new producer's first segment landed in cache while the high-water counter was still being reset, leading to a restart cascade.
- Proactive backward-jump restart applied to both code paths. The
mediaSegment(data) path and themediaSegmentURL(sendfile) path both calldeclareTargetfromHLSVideoEngine, but only the data path had the proactive restart logic. Backward scrubs through the sendfile path now restart cleanly instead of stalling AVPlayer in a "wait for evicted segment" branch. - Producer high-water tracked across cache prunes. Restart-decision gates now use the monotonic
highestStoredIndexcounter instead of the live(min, max)of resident entries, catching the forward-jump + back-scrub + pruned-gap case that previously stalled.
LiveTelemetry + memory probe restart on audio-track switch
reloadWithAudioOverride() (the fast-reload path used when the user picks a different audio track) called stopInternal() to tear down the previous session but never restarted the diagnostic samplers. Hosts' stats overlays fell back to "-" for every field after the first audio-track switch in a session. Both samplers now re-arm symmetric to the cold-load path.
Adoption-readiness package
- Tests.
Tests/AetherEngineTests/covers the pure-function surface (FrameRateSnap, VideoFormat, LoadOptions). Built on the Swift Testing framework that ships with the 6.0 toolchain. - CI. GitHub Actions runs
swift build+swift teston macOS plusxcodebuildsmoke builds for tvOS and iOS Simulators on every push and PR. See.github/workflows/ci.yml. - CHANGELOG.
CHANGELOG.mdindexes every release with a one-paragraph summary and a link to the GitHub Release notes. - Stability contract. README › Stability and versioning spells out the SemVer policy for the
publicAPI surface. - Known limitations register. README › Known limitations documents the deferred / accepted-loss items (TrueHD-MAT object metadata loss through AudioBridge, EAC3 7.1 cap pending FFmpeg PR 21668, MPNowPlayingInfoCenter race on HLS-loopback, HDMI ch=2 lock symptom, live MPEG-TS sliding window not yet implemented, AV1 software path on Apple TV).
- Example.
Examples/MinimalPlayer/MinimalPlayerApp.swift— a 90-line SwiftUI drop-in app showing the smallest viable AetherEngine integration. - Swift Package Index.
.spi.ymldeclares the iOS / tvOS / macOS build matrix so SPI populates compatibility badges automatically.
Internal
resolveCodecRoute(codecpar:) extracted out of HLSVideoEngine.start(). The 300-line H.264 / AV1 / HEVC codec + DV-profile dispatch switch is now a private function returning a CodecRoute struct. start() drops from ~830 lines to ~520, reading linearly through its eight numbered sections instead of through a deeply nested DV-profile switch. Pure refactor, all per-profile policy comments preserved verbatim.
Acknowledgements
@DrHurt for the on-device DV / HDR matrix testing that drove this milestone. Builds 154-176 across SDR / HDR10 / DV panels cross-matched with Match Content states exposed every routing edge case the engine now handles, including the residual rate-on / range-off case that the post-handshake EDR detection closes. The 2.0 stability claim rests on that empirical sweep.
Install
.package(url: "https://github.com/superuser404notfound/AetherEngine", from: "2.0.0")Full diff
AetherEngine 1.5.0
Highlights
Codec coverage
- VP8 software dispatch.
AV_CODEC_ID_VP8now routes through the SW pipeline alongside VP9. Same rationale: AVPlayer's HLS manifest parser rejects thevp08CODECS attribute even though VideoToolbox can HW-decode the codec. Closes the WebM-1.0 / VP8 legacy gap. - MLP decoder. FFmpegBuild bumped to
1a28db3for the standalone--enable-decoder=mlp. AV_CODEC_ID_TRUEHD and AV_CODEC_ID_MLP build from the same source in FFmpeg but ship as distinct decoder objects; the AudioBridge FLAC bridge can now open raw MLP streams (BD-MV remuxes, archival masters), not just TrueHD-framed ones.
Dolby Vision
- DV detection from side data, not color_trc.
detectVideoFormatpreviously branched onAVCOL_TRC_SMPTE2084first and only checked thedvcC/dvvCside-data box inside that branch. Profile 8.4 (HLG base) and Profile 5 (often unspecified base-layer trc) bypassed the DV check entirely and producedformat=hlg codec=hvc1orformat=SDR codec=hvc1in the criteria handshake, leaving DV-capable panels stuck outside DV mode. Side-data check now runs first; color_trc only decides the non-DV-panel fallback. Surfaced by @DrHurt's on-device matrix in #4. - Non-DV-panel fallback tightened. SMPTE2084 base or unspecified trc both collapse to HDR10 (was: unspecified → SDR). AVPlayer tonemaps via the dvh1 sample entry in both cases, so the criteria target is HDR10.
Reliability
- HLS producer restart on far-behind fetches. AVPlayer occasionally fetches segments well below the current
baseIndex(background recovery, post-pause prefetch). The producer now restarts cleanly in that case instead of starving the consumer. - Display criteria preserved across audio-track switch. Mid-session audio-track swap reloads no longer drop
preferredDisplayCriteria, fixing a brief EDR-headroom regression on DV / HDR sessions when the user picks a new language. - EAC3+JOC forces FLAC bridge on Bluetooth A2DP / LE. Atmos passthrough is impossible over Bluetooth; routing the bitstream to A2DP / LE made the AVR's Atmos light spin without actual decode. The bridge fallback now engages automatically on those routes.
ec-3CODECS marker. The non-standardec+3JOC marker tvOS 26.5's HLS variant validator rejected is gone; JOC stays intact via thedec3box in the segment, where Atmos clients actually read it.
Diagnostics
aetherctl swdecodesubcommand. OpensSoftwareVideoDecoderfor a source's video stream, feeds N packets, reports counters + first-frame metadata. Distinguishes decoder-open failures from decoder-runs-but-no-frames from end-to-end SW-decode healthy. Useful for VP9 / VP8 / MPEG-4 Part 2 / MPEG-2 / VC-1 sources without going through TestFlight.- Release-visible SW-path lifecycle logs.
[SWDecoder] Opened: …,[SWHost] session start: …,[SWHost] first video frame enqueued: …no longer#if DEBUG-gated, so TestFlight users can see exactly where a SW session breaks.
Documentation
- README's at-a-glance codec table, software-pipeline section, live MPEG-TS row, and VTCapabilityProbe footer reflect the broader SW dispatch (VP8 / VP9 / MPEG-4 Part 2 / MPEG-2 / VC-1).
- New "Host setup on tvOS" section documents the engine-driven sole-writer pattern for
AVDisplayManager.preferredDisplayCriteria(tvOS 26.5+ enforces the criteria-before-load ordering at the HLS variant validator). - aetherctl reference updated with the new
swdecodesubcommand.
Full diff
Acknowledgements
Big thanks to @DrHurt for the relentless on-device DV / Atmos matrix testing across multiple panel modes. The 1.5 DV detection fix and the MPEG-4 / VC-1 / VP8 dispatch path both came directly out of their builds 154-176 sweep in #4.
AetherEngine 1.4.4
Highlight
Fixes HDR / DV playback failure on tvOS 26.5 (AVFoundationErrorDomain -11868 / AVErrorNoCompatibleAlternatesForExternalDisplay). SDR playback was unaffected throughout.
Root cause is HDR-variant-specific: tvOS 26.5 enforces Apple Tech Talk 503's documented ordering ("perform this switch BEFORE assigning the AVPlayerItem to the AVPlayer object") synchronously at HLS variant validation, but only for variants whose VIDEO-RANGE requires a panel mode switch (HDR10 / HLG / DV). SDR variants are unaffected since SDR is a universally supported panel mode and no switch is needed. Earlier tvOS versions deferred this check even for HDR variants.
The symptom signature for HDR sources was distinctive: master + media playlists fetched OK, item.status goes .failed immediately, errorLog().events.count == 0, accessLog().events.count == 0, item.tracks.count == 0, item.duration indefinite, no GET /init.mp4 ever reaches the loopback server.
AVKit-auto (appliesPreferredDisplayCriteriaAutomatically = true) cannot satisfy the criteria-first contract for HLS multivariant HDR sources. AVKit reads criteria from AVAsset.preferredDisplayCriteria, which is synthesized from the chosen variant's CMVideoFormatDescription. For HLS that format description only exists after init.mp4 is parsed, and init.mp4 is only fetched after the variant passes the validator. Chicken-and-egg, no way around it via auto-criteria.
Engine-driven sole-writer is the only working pattern. Hosts should pass LoadOptions(suppressDisplayCriteria: false) and set appliesPreferredDisplayCriteriaAutomatically = false on their AVPlayerViewController. Sodalite reverted to that host config in 1b6bbae1.
Fixes
Cache-Control: no-store → no-cache reverted (prophylactic, not proven necessary)
caf33ee (1.4.x) flipped the loopback HLS server's response Cache-Control from no-cache to no-store as a speculative AVPlayer URLCache leak mitigation. The revert in this release is prophylactic: single-variable testing of no-store under engine-driven display criteria wasn't done, so whether no-store was contributing to the failure is unproven. SDR variants kept working with no-store throughout the broken window, which suggests it's not a general HLS killer. The actual memory leak in that timeframe was on the source-fetcher (URLSession task pool, fixed separately via per-request session); the loopback-response no-store was layered on top without measurement that it helped.
If a later DV 8.1 long-session memory-leak test shows URLCache footprint matters, re-trying no-store under engine-driven mode is the right next experiment before going to bigger guns (custom URLProtocol interception or AVAssetResourceLoaderDelegate with a custom URL scheme).
DV 8.1 signaling: hvc1 sample entry + SUPPLEMENTAL-CODECS dvh1.08.XX/db1p on DV panels
The prior P8.1 emission branched on panel state: bare dvh1 sample entry + dvh1.08.XX CODECS on DV panels, hvc1 on non-DV panels, both without SUPPLEMENTAL-CODECS. Per Apple's HLS Authoring Spec appendix on SUPPLEMENTAL-CODECS (post-WWDC22), backward-compatible DV ships with hvc1 sample entry + hvcC + dvvC boxes, primary CODECS hvc1.2.4.LXX, and SUPPLEMENTAL-CODECS dvh1.08.XX/db1p. The /db1p brand identifier marks the supplemental as DV with HDR10 base for AVPlayer's profile-matching; without it the variant is treated as plain HDR10 and the DV pipeline never engages. Mirrors the working P8.4 pattern.
DV 8.1 / 8.4 on non-DV panels: strip DV side data
For backward-compatible DV routed to HDR10-only panels: strip AV_PKT_DATA_DOVI_CONF from the codecpar before mp4 muxer init so init.mp4 has a clean hvc1 + hvcC sample entry with no dvvC box. Mirrors P7's existing strategy. Removes a class of "init.mp4 sample entry confuses tvOS 26 codec filter" risk on non-DV displays.
DisplayCriteriaController.reset() gated on whether apply() actually wrote
reset() now tracks whether apply() wrote preferredDisplayCriteria during the session and no-ops otherwise. AVKit-sole-writer hosts that pass LoadOptions.suppressDisplayCriteria = true now get ZERO engine writes against avDisplayManager for the entire session lifecycle. The prior unconditional nil-write on every stopInternal() (audio-track-switch reloads, source switch, etc.) raced AVKit's in-flight criteria negotiation under that host mode.
HDCP-LEVEL=TYPE-1 no longer emitted from master playlist
Apple Tech Talk 501 recommends TYPE-1 for 4K HDR / DV variants in CDN distribution for DRM enforcement. Our local loopback server has no DRM scope: the source file is already in the user's possession. The attribute caused AVPlayer's variant filter to reject the variant on edge-case HDMI HDCP-negotiation states. Removed.
P8.4 mirror of P8.1 branched emission
P8.4 now matches P8.1's two-branch emission: DV-capable panel gets hvc1 + SUPPLEMENTAL-CODECS dvh1.08.XX/db4h with DV preserved; non-DV panel gets plain hvc1 with DV stripped.
Diagnostics
Richer instrumentation on item.failed, evergreen value:
errorLog().eventsfull dump (was: only post-registration notifications vianewErrorLogEntryNotification; missed entries logged duringreplaceCurrentItem)accessLog().eventsfull dump- Item-level state:
item.trackswith FourCC per track,presentationSize,seekableTimeRanges.count,loadedTimeRanges.count,duration,appliesPerFrameHDRDisplayMetadata - HLSLocalServer logs first request headers per session (AVPlayer's User-Agent, X-Playback-Session-Id, Accept-Encoding, etc.)
- HLSLocalServer logs media.m3u8 TAIL as well as head
Acknowledgements
@DrHurt for the testing patience across multiple builds in #4 and for independently reaching the "go back to engine-driven manual display criteria" conclusion at 2026-05-26 07:59 UTC. That architectural call closed the bug.
Full changelog: 1.4.2...1.4.4
AetherEngine 1.4.2
Features
Codec dispatch: MPEG-4 Part 2, MPEG-2, VC-1 routed to the SW path
Added to the software dispatch in AetherEngine.load. AVPlayer's HLS-fMP4 pipeline does not accept mp4v.20.X, mp2v, or vc-1 in its CODECS attribute (none are in Apple's HLS Authoring Spec list), so prior to this they fell through the default branch and silently failed on the native AVPlayer path. They now route to SoftwarePlaybackHost and decode via libavcodec.
FFmpegBuild already ships the native decoders for all three (--enable-decoder=mpeg4, mpeg2video, vc1); SoftwareVideoDecoder is codec-generic via avcodec_find_decoder. Covers the XVID / DIVX / SP / ASP universe, DVB and over-the-air recordings (MPEG-2), and the long tail of WMV-in-MKV remuxes (VC-1).
Live-stream scaffolding
Initial scaffold for IPTV / live.ts playback:
LoadOptions.isLiveopt-in flag for hosts to signal the source is live.@Published var isLive: Boolon the engine for host UIs (hide scrubber, hide duration).seek(to:)becomes a no-op with a warning log whenisLive.SourceProbe.isLivehint (no advertised duration + network-feed URL scheme) so hosts can decide whether to flip the opt-in.
H.264 / HEVC inside MPEG-TS routes through the native AVPlayer path via the existing HLS-fMP4 remuxer. MPEG-2 / MPEG-4 Part 2 / VC-1 inside MPEG-TS routes through the SW path.
Not yet shipped: sliding-window segment eviction in SegmentCache, MEDIA-SEQUENCE advance, and producer restart on upstream disconnect. Long-form IPTV sessions on the native path will accumulate cached segments until those land. Tracked separately.
Fixes
DV 8.1 signaling: hvc1 sample entry + SUPPLEMENTAL-CODECS /db1p
The prior P8.1 emission branched on panel state (dvh1 sample entry on DV panels, hvc1 on non-DV panels), both without a SUPPLEMENTAL-CODECS attribute. Per Apple's HLS Authoring Spec appendix on SUPPLEMENTAL-CODECS (post-WWDC22), backward-compatible DV ships with hvc1 sample entry + hvcC + dvvC boxes, primary CODECS hvc1.2.4.LXX, and SUPPLEMENTAL-CODECS dvh1.08.XX/db1p. The /db1p brand identifier marks the supplemental as DV with HDR10 base for AVPlayer's profile matching; without it the variant is treated as plain HDR10 and the DV pipeline never engages.
Mirrors the P8.4 pattern that's been working empirically. The mp4 muxer with strict=-2 automatically writes a dvvC box alongside hvcC when the source codecpar carries DV side data (we preserve it for P8.1), so AVKit's auto-criteria parser reads the DV profile from the live AVPlayerItem.formatDescription via the private CoreMedia hook.
DisplayCriteriaController.reset() no-ops under sole-writer mode
reset() now tracks whether apply() actually wrote preferredDisplayCriteria during the session and no-ops otherwise. Under AVKit-sole-writer hosts (those passing LoadOptions.suppressDisplayCriteria = true), the engine now makes ZERO writes against avDisplayManager for the entire session lifecycle.
The prior unconditional nil-write on every stopInternal() (including audio-track-switch reloads) raced AVKit's in-flight criteria negotiation and surfaced as mid-session panel-mode regressions, including the "RESET RESET RESET" log sequence and EDR-headroom collapse to 1.0 @DrHurt reported on the succeeding DV 5 retry attempt in #4.
Acknowledgements
@DrHurt for Build 176 testing on #4 that surfaced both the missing /db1p brand identifier and the reset() race.
Full changelog: 1.4.1...1.4.2
AetherEngine 1.4.1
Fixes
Gate play() on panel handshake settle
In an AVKit-sole-writer architecture (host sets appliesPreferredDisplayCriteriaAutomatically = true and passes LoadOptions(suppressDisplayCriteria: true)), AVKit writes preferredDisplayCriteria from the live AVPlayerItem.formatDescription after asset.load reaches readyToPlay. The HDMI handshake kicks off a beat later than the engine's synchronous pre-flight would have, and nativeHost?.play() was firing before the panel finished switching into the target dynamic range. On DV / HDR sources this surfaced as a first-frame stall on the cold path.
Two changes:
DisplayCriteriaController.waitForSwitchStage 1 grace extended from 200 ms to 1000 ms so the later-firing AVKit auto write is reliably caught by the gate's first-stage poll.await displayCriteria.waitForSwitch()inserted before everynativeHost?.play()call (initial load path + audio-track-reload path). The 1.4.0 two-stage poll's existing logic handles both "panel already in target mode" (early return viacurrentEDRHeadroom > 1.001) and "switch in progress, wait for completion".
The gate is path-agnostic. Engine-sole-writer architectures continue to use the engine's pre-flight + waitForSwitch. AVKit-sole-writer architectures now get the same handshake guarantee, just with AVKit driving the criteria write instead.
Docs
README now credits @DrHurt for the on-device DV / HDR matrix testing.
Acknowledgements
Once again, @DrHurt for the diagnosis on #4: "I don't know if readyToPlay waits for display switch. If not, we could possibly gate play() on waitForSwitch / isDisplayModeSwitchInProgress." That's exactly the contract this release implements.
Full changelog: 1.4.0...1.4.1
AetherEngine 1.4.0
Features
LiveTelemetry
1 Hz diagnostic sampler covering both the native AVPlayer and software dav1d / VP9 paths. Exposes per-second snapshots of:
- Producer restart count
- Last A/V gap measurement (audio gate vs video gate, ms)
- Local HLS server request count
- Frames enqueued into the software path's display layer
- Process resident memory
New LiveTelemetry value type plus AetherEngine.liveTelemetry published surface for hosts to consume. Powers Sodalite's "Stats for Nerds" overlay.
FFmpeg log bridge
av_log output now routes into EngineLog under the .ffmpeg category instead of going straight to stderr. Lets hosts (Sodalite's diagnostic overlay, aetherctl) pipe FFmpeg's internal noise through the same channel as engine logs, with category-level filtering. (#17)
Fixes
waitForSwitch async-handshake race
DisplayCriteriaController.apply() writes preferredDisplayCriteria and then immediately calls await waitForSwitch(). The setter is async-initiated, so the HDMI handshake kicks off a beat after the setter returns. The previous guard isDisplayModeSwitchInProgress else { return } mis-classified that window as "no switch needed" and bailed, letting asset.load proceed while the panel was still in its old mode. On DV / HDR sources this surfaced as AVPlayer -11848 "Cannot Open".
Replaced with a two-stage poll:
- Stage 1 (200 ms grace, 10 ms ticks): wait for
isDisplayModeSwitchInProgressto flip true, OR forUIScreen.currentEDRHeadroom > 1.001(panel was already in HDR mode and no transition is needed). - Stage 2 (5 s cap, 100 ms ticks): wait for the switch to complete, then sanity-check
currentEDRHeadroomto confirm the panel reached HDR. Warns if not.
Engine-sole-writer architectures can now rely on waitForSwitch as the actual gate, not just a "no in-progress switch right now" check.
Audio codec tag for Atmos JOC on iOS
EAC3 + JOC (Atmos) content now emits the correct ec-3 codec tag in the HLS manifest on iOS too. Was native-tvOS-only before. (#16)
externalMetadata applied on iOS
Now Playing artwork, title, and dynamic range hint via AVPlayerItem.externalMetadata now propagate on iOS in addition to tvOS. (#15)
Muxer interleaver drain
libavformat's interleaver buffer is now explicitly drained before each fragment cut. Was leaving samples queued in the muxer between fragments, surfaced as occasional missing audio at fragment boundaries. (#14)
Docs
README now documents the LiveTelemetry surface, sampler timing, and host integration points.
Acknowledgements
Big thanks to @DrHurt for the relentless on-device DV / HDR testing across builds 159 through 172 in #4. His matrix of panel-mode + match-content combinations exposed the waitForSwitch race, and his hunch that play() should gate on isDisplayModeSwitchInProgress is exactly the contract the two-stage poll now implements.
Full changelog: 1.3.2...1.4.0
AetherEngine 1.3.2
AetherEngine 1.3.2
Patch release on top of 1.3.1. Two user-visible video fixes, one network performance win, plus an engine-wide logging cleanup.
Dolby Vision Profile 7 sources now play (#11)
UHD Blu-ray REMUX MKVs with DV Profile 7 were getting hard-rejected at engine load with unsupported Dolby Vision profile 7.-1 (the -1 was a placeholder; the source's actual dv_bl_signal_compatibility_id was 6). Apple has no P7 decoder, but the bitstream's HEVC Main10 base layer is HDR10 by construction (UHD-BD requires HDR10 backwards-compat), so the BL plays as plain HEVC HDR10 once routed that way.
P7 now packages as hvc1 sample entry, hvc1.2.4.LXX CODECS, PQ VIDEO-RANGE, no supplemental DV hint. AVPlayer's HEVC Main10 decoder ignores nuh_layer_id != 0 NAL units (HEVC Annex F multi-layer extension) so the EL+RPU layers ride along unused in the fMP4 samples. No DV mode is requested at the HDMI handshake.
Required two commits to land:
- Initial routing change (
34e1d04). - Strip the source's
dvcCconfiguration record from the output stream's codecpar beforeavformat_write_header. Without that strip, the mp4 muxer writes advcC profile=7inside thehvc1sample entry and VT's HEVC decoder selection rejects it withkVTVideoDecoderUnsupportedDataFormatErr(-12906). Fix inbe25435. Confirmed by @Delarkz against a Gemini Man 2160p REMUX (init.mp4 dropped from 2098 B to 2066 B, exactly the dvcC box getting stripped).
Scope: P7 only. P8.1 / P8.4 non-DV-display paths still keep dvcC because they work in the field (Apple has P8 decoders so the dvcC + hvc1 combination doesn't bite).
Resolved CDN URL cached across range fetches (#12)
Debrid / proxy playback URLs typically redirect each request from a stable proxy endpoint to a signed CDN object URL. The AVIO reader was paying the proxy + redirect hop on every Range fetch (~6 ops/sec at 4K HEVC bitrates).
AVIOReader now captures dataTask.currentRequest?.url (the URL after the redirect chain) inside ChunkFetchDelegate / ProbeDelegate.didReceive response and stashes it in a per-reader cache. Subsequent fetches use the cached URL instead of the source URL.
Safety rails:
- Cache only populates on 2xx / 206 responses, so 4xx redirect targets can't poison it.
- 401 / 403 / 404 / 410 against the cached URL drops the cache and triggers a one-shot retry against the original source URL so the proxy can re-issue a fresh signed redirect.
Rangeand caller-supplied extra headers are re-applied on cross-host redirects inChunkFetchDelegate(same hookProbeDelegatealready had).
Scope: seekable mode only. Streaming mode (live transcode) is a single GET so caching doesn't help.
Engine logging unified through EngineLog (PR #13)
Two distinct problems with the old layout fixed in one pass:
-
Xcode debug console duplicates.
EngineLog.emitwas writing to OSLog unconditionally, then also callingprint(line)when no host handler was installed. Xcode renders both OSLog activity and process stdout in the same debug-area console, so every emit surfaced twice. The intermediateisatty(STDOUT_FILENO)gate didn't help because Xcode wires GUI-app stdout to a pty. The stdio fallback is removed entirely; sinks are nowOSLog (always) + handler (optional). -
23 raw
print(...)calls scattered acrossAVIOReader,Demuxer,AudioDecoder,AudioOutput,SoftwareVideoDecoder,SampleBufferRenderer, plus a vestigial duplicate inAetherEngineitself. These bypassed OSLog entirely, weren't filterable by category, and never reached any host handler. All routed throughEngineLog.emitwith appropriate categories.
New swPlayback (sw.playback) category carves the custom playback subsystem (SoftwarePlaybackHost plus its decoders and renderers) out of the .engine catch-all so the AV1 / VP9 routes can be filtered on their own via log stream --predicate 'category == "sw.playback"'.
aetherctl probe installs its own stdout handler since it can no longer rely on the removed EngineLog stdio fallback. serve / validate already had timestamped handlers, behaviour unchanged.
Thanks to @Delarkz for the PR.
Engine pin
For Sodalite hosts: bump Package.resolved to b9a8710 (or use the 1.3.2 tag).
AetherEngine 1.3.1
AetherEngine 1.3.1
Patch release on top of 1.3.0. Two production fixes plus one diagnostic.
Producer: empty-cache restart now fires after far scrubs
mediaSegment(at:)'s empty-cache decision used to fall back to a "wait for cold-start producer" branch whenever index ≤ 2, assuming the only context in which the cache became empty was right after a fresh producer launch at baseIndex 0. A scrub-forward followed by a scrub-back exposed the assumption: after the cache window slid away from the current producer (now at baseIndex 1314 after a forward scrub), a subsequent back-scrub to seg-0 hit an empty cache, fell into the "wait" branch, and AVPlayer timed out for 30 s before getting a 404 with CoreMediaErrorDomain -12938.
Fix tracks lastRestartIndex on the segment provider and restarts when |index − lastRestartIndex| > 2. Cold start (lastRestartIndex = 0, AVPlayer requests seg-0) still waits; back-scrubs to a position far from the producer trigger a clean restart at the requested index.
DV Profile 5: master playlist on HDR-ready non-DV panels
1.3.0 routed every non-DV-capable panel's DV5 source via the media playlist. That works on SDR-locked panels (where tvOS 26 rejects bare dvh1.05 master CODECS with -11868), but on an HDR-capable non-DV panel currently in HDR mode it forced a DV→SDR tonemap when DV→HDR10 would have been the better choice. Per DrHurt's #63 morning test on AetherEngine#4, the master path is accepted on HDR-capable non-DV panels and tonemaps to HDR10 properly.
New per-state matrix for DV5:
| Panel state | Routing |
|---|---|
| DV-capable in DV mode | master (unchanged) |
| DV-capable SDR-locked, match off | media (unchanged) |
| Non-DV in HDR mode | master (changed, was media) |
| Non-DV SDR-locked, match off | media (unchanged, master gets -11868) |
DV8.1 and DV8.4 dispatch unchanged.
Diagnostic: A/V gap at audio gate
The [HLSSegmentProducer] audio gate open: log line now reports the source-time gap between video's first kept packet and audio's first kept packet (gapMs=X.X). A separate WARNING line fires when the gap exceeds 50 ms, surfacing lip-sync drift in support logs without needing to read frame headers.
Engine pin
For Sodalite hosts: bump Package.resolved to 44c3a05 (or use the 1.3.1 tag).