Skip to content

Dolby Vision Profile 5 / 10.0 scrub previews drawn by the system compositor - #840

Merged
superuser404notfound merged 3 commits into
mainfrom
feat/dv-compositor-preview
Oct 19, 2026
Merged

superuser404notfound merged 3 commits into
mainfrom
feat/dv-compositor-preview

Conversation

@superuser404notfound

@superuser404notfound superuser404notfound commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

A Dolby Vision Profile 5 / AV1 Profile 10.0 base layer is IPT-PQ, and no still API on Apple platforms applies the RPU, so a CGImage of it is only correct with our own reshaping (DolbyVisionStillReshaping, off by default). The system compositor colours such a picture the same way it colours playback, so the scrub preview now goes through it.

How

  • CompositorPreviewBuilder cuts the keyframe that opens a resident segment into a standalone one-sample fMP4. The init is rebuilt from the segment's own parameters (dvh1, dvcC, colr, no audio track). Like scrubThumbnail, it needs no second connection.
  • HLSVideoEngine.compositorPreviewFile(atSeconds:) keeps the last 12 files per session, keyed by segment identity.
  • AetherScrubPreviewView shows a file through its own muted AVPlayer. Two players alternate, so the outgoing picture stays up until the next one is displayable. Requests are latest-wins, and clear() releases the decoders.
  • New public API: scrubPreviewDrawsInView, showScrubPreview(atSeconds:in:), ScrubPreviewPlacement. Documented in docs/api.md, with a CHANGELOG entry under Unreleased.

Measured

  • M1, real engine, Shrek the Third (4K Profile 5): 26 to 45 ms from request to picture for a new segment, 1 ms for the segment already shown. Colours match the reshaping converter.
  • Apple TV 4K (3rd generation), standalone probe app: a paused 4K Profile 5 preview player stayed next to 4K Profile 5 playback with display criteria set. Seeks took 13 to 25 ms, playback rate stayed at 1.0, and colours matched. The probe used ffmpeg-cut keyframe files, not this branch.
  • Trap: attaching an AVPlayerItemVideoOutput to the item switches the Dolby Vision processing off for the layer, which then shows raw IPT (green / magenta). The view never attaches one.

Tests

  • New CompositorPreviewBuilderTests (2).
  • Full suite locally: XCTest 779 and swift-testing 4623 green, plus the strict-pool run. One Vision OCR test timed out (180 s) under load in the first run and passed when rerun alone.

Held picture across an audio switch (second commit)

For Dolby Vision without a displayable base layer and without the reshaping trait, this hold used to be skipped. It now goes through the compositor too:

  • The resident segment's video is remuxed into a standalone file.
  • A player on the bound surface seeks that file frame-exactly to the outgoing position. Its target is computed on the player's own clock, from the track's timeRange start, because AVFoundation applies the init's edit list.
  • The offset adds the segment's B-frame delay. This was determined by sweeping the offset against the paused outgoing picture on three Profile 5 clips. Only that offset gave a pixel-identical hold; without the delay the held frame is two frames early.
  • The hold is up in 165 to 220 ms on an M1, within the existing 3 s budget.
  • ItemSwapStillTests' Profile 5 test now expects the compositor route. It passed against a real Profile 5 clip placed locally under the fixture name. The full suite is green again (779 + 4623, strict pool too).
  • Open: the trait path computes its offset without the delay as well, so it is probably two frames early too. Not measured.

Previews outside the resident window (72fff52)

The first device round showed the preview only near the playhead: a position the segment cache did not hold answered nil at once. Those positions now read the keyframe that opens their plan segment off the source with a side demuxer, the way the I-frame rendition reads its entries (one packet, nothing decoded), behind the same gates (no read on a sequential, serial or held-connection origin, waits out a seek or restart, superseded requests skipped).

  • aetherctl scrubpreview (new): 6 of 6 positions on the 4K Profile 5 clip, 18 to 25 ms for a source read off a local file; the packets are byte-identical to the source's keyframes, hvcC / dvcC / colr match the cache-built files.
  • Apple TV 4K over Jellyfin: first far jump 287 ms including the side reader's 163 ms open, later ones 89 to 233 ms, 31 of 31 positions shown, no playback stall while scrubbing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS

Vincent Herbst and others added 3 commits October 19, 2026 05:19
…the system compositor

A Profile 5 / AV1 Profile 10.0 base layer is IPT-PQ, and no still API applies the RPU, so the
engine cannot hand out a correct CGImage of it without its own reshaping (the
DolbyVisionStillReshaping trait, off by default). The system compositor colours it the way it
colours the playing picture, so the preview now goes there:

- CompositorPreviewBuilder cuts the keyframe that opens a resident segment into a standalone
  one-sample fMP4, with the init rebuilt from the segment's own parameters (dvh1, dvcC, colr, no
  audio track). No second connection, like scrubThumbnail.
- HLSVideoEngine.compositorPreviewFile keeps the last 12 such files per session, keyed by the
  segment's identity.
- AetherScrubPreviewView shows them through its own muted AVPlayer; two players alternate so the
  outgoing picture stays up until the next is displayable, and requests are latest-wins.
- Public: scrubPreviewDrawsInView, showScrubPreview(atSeconds:in:), ScrubPreviewPlacement.

Measured with Shrek the Third (4K Profile 5): 26 to 45 ms from request to picture for a new segment
on an M1, 1 ms for the one already shown, colours matching the reshaping converter. A tvOS probe
showed the same preview next to 4K Profile 5 playback on an Apple TV 4K without disturbing it. An
AVPlayerItemVideoOutput on the item switches the Dolby Vision processing off, so none is attached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…ough the system compositor

The held picture of an audio switch was skipped for Dolby Vision without a displayable base layer in
a build without DolbyVisionStillReshaping. It now plays the frame out of the resident segment:

- CompositorPreviewBuilder gains a whole-segment mode that remuxes every video sample into a
  standalone video-only fMP4 and reports the segment's B-frame delay.
- HLSVideoEngine.compositorHoldFile builds it for the item time under the playhead; one file at a
  time.
- HeldStillPresenter gets a compositor form: a muted player on the surface seeks frame-exactly, on
  its own clock (the track's timeRange start, since AVFoundation applies the init's edit list), behind
  an invisible layer that turns visible once the frame is displayable. Both surfaces implement it.
- holdPictureAcrossItemSwap takes that route, within the same 3 s budget the reshaped still had.

The offset into the file is item time minus segment start plus the B-frame delay. Measured by
sweeping it against the paused outgoing picture on three Profile 5 clips (24 fps with 0.083 s, a
second cut 1.2 s off a keyframe, 30 fps with 0.067 s): pixel-identical only there, two frames early
without the delay. Up in 165 to 220 ms on an M1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
…he resident window

The compositor preview only covered the cached span around the playhead: a position the segment
cache did not hold answered nil at once, and a Profile 5 source has no still to fall back to, so
scrubbing ahead showed the time alone (device log: every far target nil in 0 ms, every near one
shown in 50 ms).

Outside the resident window the preview now reads the keyframe that opens the position's plan
segment off the source, the way the I-frame rendition reads its entries: a side demuxer seeks to
the segment's start and takes one packet, nothing is decoded, and the session's own sample entry,
dvcC and colr wrap it. `IFrameRendition.keyframePayload(at:isStale:)` serves it without a
neighbour's keyframe standing in, behind the same gates (no read on a sequential, serial or
held-connection origin, waits out a seek or restart) and skipping requests a newer one superseded.
The builder sits behind a lock that `stop()` closes before it frees the codec parameters.

`aetherctl scrubpreview` drives it. On the 4K Profile 5 Patterns of Nature clip with the source
throttled to 20 Mbit/s: resident positions 19 to 28 ms, non-resident ones 18 to 25 ms, 6 of 6
shown; the source-read packets are byte-identical to the source's keyframes at 48 s and 64 s, and
hvcC, dvcC and colr match the cache-built files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJcXkGQrMiBPixVEFTSBJS
@superuser404notfound
superuser404notfound merged commit 6e6c630 into main Oct 19, 2026
4 checks passed
@superuser404notfound
superuser404notfound deleted the feat/dv-compositor-preview branch October 19, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant