Skip to content

Releases: superuser404notfound/AetherEngine

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

AetherEngine 2.0.0

27 May 09:18

Choose a tag to compare

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 the mediaSegmentURL (sendfile) path both call declareTarget from HLSVideoEngine, 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 highestStoredIndex counter 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 test on macOS plus xcodebuild smoke builds for tvOS and iOS Simulators on every push and PR. See .github/workflows/ci.yml.
  • CHANGELOG. CHANGELOG.md indexes 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 public API 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.yml declares 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

1.5.0...2.0.0

AetherEngine 1.5.0

26 May 22:39

Choose a tag to compare

Highlights

Codec coverage

  • VP8 software dispatch. AV_CODEC_ID_VP8 now routes through the SW pipeline alongside VP9. Same rationale: AVPlayer's HLS manifest parser rejects the vp08 CODECS attribute even though VideoToolbox can HW-decode the codec. Closes the WebM-1.0 / VP8 legacy gap.
  • MLP decoder. FFmpegBuild bumped to 1a28db3 for 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. detectVideoFormat previously branched on AVCOL_TRC_SMPTE2084 first and only checked the dvcC / dvvC side-data box inside that branch. Profile 8.4 (HLG base) and Profile 5 (often unspecified base-layer trc) bypassed the DV check entirely and produced format=hlg codec=hvc1 or format=SDR codec=hvc1 in 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-3 CODECS marker. The non-standard ec+3 JOC marker tvOS 26.5's HLS variant validator rejected is gone; JOC stays intact via the dec3 box in the segment, where Atmos clients actually read it.

Diagnostics

  • aetherctl swdecode subcommand. Opens SoftwareVideoDecoder for 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 swdecode subcommand.

Full diff

1.4.4...1.5.0

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

26 May 09:08

Choose a tag to compare

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().events full dump (was: only post-registration notifications via newErrorLogEntryNotification; missed entries logged during replaceCurrentItem)
  • accessLog().events full dump
  • Item-level state: item.tracks with 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

26 May 04:57

Choose a tag to compare

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.isLive opt-in flag for hosts to signal the source is live.
  • @Published var isLive: Bool on the engine for host UIs (hide scrubber, hide duration).
  • seek(to:) becomes a no-op with a warning log when isLive.
  • SourceProbe.isLive hint (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

25 May 17:53

Choose a tag to compare

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.waitForSwitch Stage 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 every nativeHost?.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 via currentEDRHeadroom > 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

25 May 15:30

Choose a tag to compare

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:

  1. Stage 1 (200 ms grace, 10 ms ticks): wait for isDisplayModeSwitchInProgress to flip true, OR for UIScreen.currentEDRHeadroom > 1.001 (panel was already in HDR mode and no transition is needed).
  2. Stage 2 (5 s cap, 100 ms ticks): wait for the switch to complete, then sanity-check currentEDRHeadroom to 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

23 May 21:26
b9a8710

Choose a tag to compare

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 dvcC configuration record from the output stream's codecpar before avformat_write_header. Without that strip, the mp4 muxer writes a dvcC profile=7 inside the hvc1 sample entry and VT's HEVC decoder selection rejects it with kVTVideoDecoderUnsupportedDataFormatErr (-12906). Fix in be25435. 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.
  • Range and caller-supplied extra headers are re-applied on cross-host redirects in ChunkFetchDelegate (same hook ProbeDelegate already 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:

  1. Xcode debug console duplicates. EngineLog.emit was writing to OSLog unconditionally, then also calling print(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 intermediate isatty(STDOUT_FILENO) gate didn't help because Xcode wires GUI-app stdout to a pty. The stdio fallback is removed entirely; sinks are now OSLog (always) + handler (optional).

  2. 23 raw print(...) calls scattered across AVIOReader, Demuxer, AudioDecoder, AudioOutput, SoftwareVideoDecoder, SampleBufferRenderer, plus a vestigial duplicate in AetherEngine itself. These bypassed OSLog entirely, weren't filterable by category, and never reached any host handler. All routed through EngineLog.emit with 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

23 May 12:16

Choose a tag to compare

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).