6.85.0 - A rebuilt run carries the normalization its bytes were written with
One defect, found twice: the VOD axis is two quantities added together, and every rule about it was written on fixtures where they are the same number.
What changes for a consumer
Nothing on a source whose timestamps start at zero. There the displacement AVPlayer's timeline carries and the source-to-item normalization the bytes carry are one number, which is every fixture the AE#418 and AE#481 rounds were measured on, and every arm of them reads exactly as it did before, run for run.
On a source whose timestamps begin above zero (a recording cut out of a longer one, plenty of TS and MKV remuxes) two things were wrong, both of them in the item-to-source mapping the host places subtitle cues and positions with:
- A seek landing on a run AVPlayer rebuilt published a zero axis. The AE#481 landing rule reads the axis off the run holding the landing, and where that run opens at the segment's own playlist position it published what the segment is worth to a PLACEMENT, which inside a run is nothing. The bytes still carry the normalization the producer folded into them. On a source starting at 600 s the session then mapped item time onto itself: bitmap subtitles were queried 600 s away from the picture, and the reported position went negative.
- A second placement composed the source origin in again. The composition added the whole shift rather than the gate's backoff, so a placement worth 596.833 s composed onto a standing 599.625 s published 1196.458 s and put the seam at item -531.625 s. It self-corrects wherever the placement can be read back out of AVPlayer's buffer; AE#418 round 7 is the case where it cannot, and there it stands for the rest of the session.
Measured
Scripts/timecode-fixture.sh's tc-cues-lie.mkv, remuxed with -output_ts_offset 600 so the item axis runs 0 to 120 s over a source that runs 600 to 720 s, served over Scripts/slowrange.py at 600 kbps / 300 ms, with play --picture-probe as the witness. The AE#481 arm (--start-position 53 --seek-count 5 --seek-pattern 65,60,70,58,75):
before #481 ... opens at 72.000s, seg18's own playlist position, so it carries 0.000s
capErr +0.017, cur=-515.10, VERDICT: clock did not advance
after #481 ... opens at 72.000s, seg18's own playlist position, so it carries 600.000s
capErr -599.983, cur=84.90, VERDICT: OK
and the composition, resuming at 73 s where the gate re-aims 0.375 s, then seeking:
before #418 seg17 placed ... axis shift 599.625s -> 1196.458s from item -531.625s
after #418 seg13 placed ... axis shift 599.625s -> 590.625s from item 81.000s
(the reading then lands on 591.000s, which is what the picture reads)
The zero-origin control is byte-identical between the two on every arm: the same publications, a capErr mean of +0.000 over the 25 ticks after the landing, 3 of 3 runs.
The gate-open pair is the whole thing in two lines, same bytes, same resume at 53 s:
zero origin actual=43000 desired=52000 pinnedTo=43000 shift=0 presentedShift=-9000
600 s origin actual=643000 desired=52000 pinnedTo=43000 shift=600000 presentedShift=591000
One gate, one backoff of 9 s, two numbers 600 s apart. shift is what the bytes carry, presentedShift is where AVPlayer shows the run's opening segment, and the composition works in the difference.
Internals
The two quantities live in one EpochAxisTable entry now, with the pruning rule written once, and a placement carries its own epoch instead of looking it up again after the fact (a backward restart between a composition and its reading drops that entry by design). placementOffset (was axisShift), placementBase and placementDisplacement are the same arithmetic with honest labels: they always described the displacement, and every number pinned in their tests is unchanged. publishPlaylistShift takes the displacement the axis is made of, so no call site can publish one half without stating the other.
Notes
The sub-second axis snap at a seek is deliberately untouched and stays inert on a source whose timestamps do not start at zero: on the one arm that separates the two geometries, inert is the right answer, and the rule's own premise turned out to be the weaker link (AE#534, with both arms measured).
Full suite green on both runners: XCTest 631 with 1 skipped, swift-testing 2953 in 398 suites.
Reported and first fixed by @orut34iop in PR #533.