Skip to content

6.4.5 - Cold-start round trips on non-faststart MP4

Choose a tag to compare

@superuser404notfound superuser404notfound released this 02 Aug 08:15

Opening a non-faststart MP4 no longer pays a round trip for bytes the open already had, and one of the two remaining ones now overlaps the first request instead of queueing behind it.

Fixed

  • The return trip after a parse seek is a copy, not a third connection. Opening a non-faststart MP4 costs a data connection from byte zero, a seek to the trailing moov, and a return to the first sample. The first two are the file layout. The third was self-inflicted: seekReconnect resets the window, so the seek to the tail discarded bytes that had been resident moments earlier, and the return re-fetched them.

    The open window is now parked across a parse seek (4 MB, cut from the window's start, which is where the demuxer comes back to) and serves that return as a copy. It is released once the demuxer reports its header pass done, through a new internal AVIOProvider.markOpenPhaseFinished(). After that point a far seek is a scrub, whose landing zone the old window cannot serve, so keeping it would be pure cost. No additional bytes are fetched for this half.

  • open() issues one speculative 64 KB suffix range, concurrent with the data connection. bytes=-n needs no size, so it goes out before any response header exists. It covers what an open actually reads at the tail: mfra on fragmented MP4, and a trailing moov whose sample tables fit.

    A feature-length file's moov runs into megabytes, does not fit, and still costs its own request. That is deliberate: fetching megabytes on a guess would compete with playback bytes on exactly the slow links this is meant to help. The parked window still removes the return trip there.

    Suffix ranges are not universally implemented, so the fetch runs on a delegate that decides at the response header rather than a completion handler that fires once the body has arrived. An origin answering 200 with the whole file is hung up on before its body, and only a 206 whose Content-Range agrees with the received byte count is installed, so a proxy answering with a different region cannot place bytes at the wrong offset. Redirects follow the same credential policy as the other reader delegates.

    Measured on a 51 MB moov-at-end fixture: three sequential requests become two concurrent ones, with the moov read and the return trip both gone. Reported with before/after traces by @digilearn-dev (#281).

  • Display-criteria settle times in the log are measured, instead of having the Stage 1 budget added back in. Stage 2 reported startGrace.ticks * 10 + stage2Ticks * 50, counting Stage 1's entire blind-poll budget whether or not it was spent, so a switch that settled one 50 ms tick after an immediately observed start logged ~1050ms. Every settle time collected so far reads up to a second slow, including the ones the .brief play-gate budget (#274) was reasoned about.

    The line now carries the real numbers plus how Stage 1 learned of the switch: start pre-gate after 0ms, total 90ms, where pre-gate means the panel was already switching when the gate opened and in-gate means it started inside it. The Stage 2 cap also stops attributing every unobservable switch to an unobservable DV panel.

Compatibility

No public API change. AVIOProvider is internal and its new member has a default implementation, so custom IOReader hosts are unaffected.

One behavioural change worth knowing about for hosts that count requests against an origin: a VOD open now issues one additional 64 KB request. It is cancelled at the response header on origins that do not implement suffix ranges.

Upgrading

.package(url: "https://github.com/superuser404notfound/AetherEngine", from: "6.4.5")

Source-compatible.