Skip to content

6.17.0 - A rectangle the overlay can trust

Choose a tag to compare

@superuser404notfound superuser404notfound released this 09 Aug 15:56
· 835 commits to main since this release

One added property and two fixes on the software path. Drop-in from 6.16.2, additive API only.

The shape

#311 gave a host compositing its own overlay over software-decoded video the two things it asked for: the clock to pace against, and the presentation time of every frame. Adopting it surfaced the third, which nothing published: the rectangle to draw into.

player.softwareDisplaySize          // CGSize?, @Published

The size the picture presents at, which is the coded frame under the pixel aspect ratio the decoder attached. nil off the software path, before the first frame is built, and on sources with no video.

The only public size was sourceVideoWidth / sourceVideoHeight, and those are the coded dimensions. A host deriving its rect from them lays anamorphic content out against the wrong shape: 720x576 declaring 64:45 presents as 1024x576, so a 5:4 overlay sits inside a 16:9 picture. There is nothing to measure on the layer either, AVSampleBufferDisplayLayer has no videoRect the way AVPlayerLayer does.

A host also cannot compute it. The ratio is resolved per frame across three sources, first sane wins (#177), and one whose display aspect is impossible is dropped in favour of square pixels (#290). A reconstruction from container metadata therefore disagrees with the screen in exactly the cases that policy exists for: for a rejected ratio the engine shows square pixels while the container still declares what it rejected.

So this is read off the format description the renderer enqueues, via CMVideoFormatDescriptionGetPresentationDimensions, rather than recomputed from the SAR. That description is the object the layer is handed, and the cache key it lives behind already invalidates on a dimension or PAR change, which is exactly when the value has to be republished.

Optional rather than .zero: there is no display size before the first frame is built, and a placeholder in a non-optional field is indistinguishable from a measurement at the call site. Mirrored rather than latched, unlike hasFirstFrameReadyForDisplay: a live source that switches resolution re-shapes the rectangle under a host that already laid out against it. Cleared with the session, so the next source is never laid out against this one's picture.

Two defects the new reading found

Anamorphic HEVC rendered at coded dimensions. The VT-backed decoder attached no pixel aspect ratio at all, and the renderer builds its format description from the delivered CVPixelBuffer, so nothing carried the ratio to the layer. The libavcodec decoder on the same host has attached it since #177, and the two are chosen per codec inside one host, so the gap was one decoder wide and invisible from every source that takes the other one. Same resolution order, same two gates, now applied on the decoder that was missing them. Reached in production by the interlaced-content detour and by forward-only sources, which is where broadcast SD lands.

The software load path cancelled the sinks it had just wired. softwareCancellables.removeAll() stood between two groups of .store(in:) calls, so everything attached above it died on arrival. What was above it is the SW-PiP cue mirror, so subtitles in a software-path PiP window froze at whatever was on screen when PiP started.

Measured

aetherctl play --sw --frame-times now reports disp=WxH per tick and the settled size next to the coded one. One synthetic clip, encoded four ways:

Source Decoder Before After
720x576 SAR 64:45 VT (hevc) 720x576 1024x576
720x576 SAR 64:45 libavcodec (h264) 1024x576 1024x576
1280x720 square VT (hevc) 1280x720 1280x720
1280x720 square libavcodec (h264) 1280x720 1280x720

Test suite: 1677 tests in 245 suites.

Thanks to @edde746, who raised the missing rectangle while adopting #311 and drew the boundary of it before anyone had to ask: what the engine settles per load, not a ratio to re-derive.

Issues: #353, #354.