Skip to content

A switch for hosts with their own fallback ladder: decline the AE#561 software-path escalation, or be told when it takes #629

Description

@cmcpherson274

We moved our pin from 7.8.1 to 7.15.1 (b93f9c4b) on 23 September (EDT), escalation included. This is not a regression report or a condition on that move: it asks for a switch or a signal, for hosts that already re-plan a failing title.

Our ladder: the #461 escape, our stall net, then a demote that re-plans the title off the engine. Underneath, the escalation cannot be declined, and publishes a videoRoute change but not its cause (anchors at b93f9c4b; at 7.16.1 neither escalation file changed, and the NativeAVPlayerHost.swift anchors below sit ten lines further down):

  • shouldEscalate's only host-set inputs, preferredDecodePath and nativeRemoteHLS (Native/SoftwarePathEscalation.swift:71-78), each change how the session is served. Neither file has a public symbol.
  • A taken offer returns without surfacing the failure (Native/NativeAVPlayerHost.swift:977, :1007), and a successful rebuild publishes none: it survives only in log lines (AetherEngine+SoftwarePathEscalation.swift:34-44). Our demote never sees it as such; when the rebuild unwinds a load() the host still awaits, the host is handed the bare CancellationError any superseded load throws (AetherEngine.swift:1937), and ours read it as a failed load (measured below).

At 7.15.1 (Debug builds; Apple TV 4K unless noted):

  • Our escape still takes, in libavcodec (dropped 0 stalls 0) [1], and on a DV Profile 5 title is refused before any teardown, session kept [2]. The software path is not our objection; who decides is.
  • On our ordinary 7.15.1 legs (169 slices, before the fixtures below) the escalation never fired: no #561 line, and the 10 top-level item failures are all AVFoundationErrorDomain, with no revive exhaustion or -19602 [3].

Measured on 24 September (Apple TV 4K, Debug builds) with your nal-overrun-fixture.py over the docstring's libx265 testsrc recipe plus an aac tone: 8 s and 40 s Matroska files damaged at 3.12 s and 20.0 s, DirectPlay from Jellyfin on the loopback route, and the 8 s healthy twin, which played clean as shipped (the 40 s twin was not played). Your AETHER_DISABLE_NAL_SANITIZER switch turned the sanitizer off, proved by the muxer's #561 ... cut line on the shipped arm and its absence on the switched arm of the same file [4]:

  • 7.15.1 as shipped: the sanitizer cuts the damaged sample and both damaged files play to the end; AVPlayer refuses nothing [5].
  • 7.15.1, sanitizer off, the 40 s file: with the playhead at 15.90 s, AVPlayer failed the playing item (failedToPlayToEndTime CoreMediaErrorDomain/-19602) 46 ms after fetching the segment that holds the damaged frame. The Loopback-HLS: a single backward seek on heavy 4K can wedge the segment producer (video stalls, audio continues -> A/V desync) #93 revive's fresh item, mounted at 15.90 s, was refused at startup 3.1 s after the first failure (#561 AVPlayer refused ... at 12.00s, the :977 branch), and the rebuild completed 33 ms later. The picture stopped for 3.1 s and resumed 3.9 s back, at the 12.00 s the refused item reported: the rebuild reads the clock (AetherEngine.swift:3273) of an item refused before it played, which your 7.16.1 media-fallback fix avoids by reading the placement. From there the session ran at rate=1.00 on every tick to the end, decoded in VideoToolbox rather than libavcodec ([SWHost] selected HardwareVideoDecoder (VT HEVC)), with a 2.08 s gap in the presented pictures at the damaged frame (dpts=40.0/2080.0, then a tick with enq=+0; lost=0). Nothing our host received named the failure: it saw the route and audio-delivery changes and counted one stall. The deferred branch (:1007) is not reached by this shape on our route: the playing item's failedToPlayToEndTime feeds the Loopback-HLS: a single backward seek on heavy 4K can wedge the segment producer (video stalls, audio continues -> A/V desync) #93 revive (:738-748), whose reload re-arms hasEverPlayed (:2065), so the refusal is a startup one [6].
  • 7.15.1, sanitizer off, the 8 s file: the same take at 0.00 s, 32 ms from refusal to #561 rebuilt on the software path. Our original load() was still waiting at the display-criteria gate; released 657 ms after the rebuild, it found its generation superseded and threw (load superseded (gen 1 -> 2); unwinding). Our host reads a thrown load as a failed load: it stopped the rebuilt session (ready, no frame presented yet), demoted the rung and showed its error card, as at 7.8.1. That half is ours to fix. It is also the case for this ask: the only hints were the videoRoute and audioDelivery changes, 657 ms before the throw, so a host must infer that this CancellationError, which also ends a load the host superseded itself, means the engine took the session [7].
  • 7.8.1 (no sanitizer, no escalation), both damaged files: -19602 reaches our host as a failure and our ladder demotes the rung to its error card (on the 40 s file after three in-place recoveries of ours, 25.6 s after the first -19602); a Retry, pressed by our test rig 6 s later, re-plans the title as a server transcode, which plays to the end [8].

The ask, any one of:

  1. A LoadOptions flag (say escalatesToSoftwarePath, default true, today's behaviour), read in shouldEscalate, so a declining host gets the failure surfaced for its own ladder (Native/NativeAVPlayerHost.swift:978, :1008).
  2. A published signal when the escalation takes, carrying the failure it absorbed.
  3. For the startup case above, a recognisable error on the unwound load(), or that load() kept waiting across the take as your Feature request: generation-scoped startup progress checkpoints for host loading UI #361 reroutes keep it (AetherEngine.swift:3581, :4031, :4787), so a host learns the engine took the session over rather than that its own load was cancelled.

[1] 7.15.1: DEV-E9-escape-dv-p8.log:185, :193, :233, :249.
[2] 7.15.1: DEV-E9-escape-dv-p5.log:249-253, :271.
[3] 67 Apple TV, 102 simulator.
[4] Fixtures: 1280x720 8-bit hevc Main + aac, Scripts/nal-overrun-fixture.py at b93f9c4b, four bytes changed per file. The switch: DEV-E9-esc-d8-default.log:88 and DEV-E9-esc-d40-default.log:113 ([MP4SegmentMuxer] #561 video sample at dts=... cut to the last whole unit), zero such lines on DEV-E9-esc-d8-nosan.log and DEV-E9-esc-d40-nosan.log. The healthy 8 s twin: DEV-E9-esc-h8-default.log:141, :146, :150.
[5] DEV-E9-esc-d8-default.log:141, :149; DEV-E9-esc-d40-default.log:148, :168.
[6] DEV-E9-esc-d40-nosan.log:147 (started, loopback, 3.4 s), :154-157 (21:07:18.132 the segment from 19.88 s, 18.178 the failure, 18.179 the stop), :159-164 (the revive at 15.90 s), :179-182 (21.284 to 21.285), :209-211, :217-219 (21.317), :221-223 (21.323, 12.000 s), :232-264 (27 [SWDiag] ticks, clk 12.61 to 38.61; the gap at :245-247), :244, :265 (our stall count).
[7] DEV-E9-esc-d8-nosan.log:119-122 (43.822 to 43.823), :151 (43.844), :156, :158-159, :161 (43.854), :163-164 (44.511, the gate and the unwind), :165-167 (44.517 to 44.518: our demote, our Aether load failed: CancellationError()).
[8] DEV-E9-esc-d8-7081.log:113-116, :140, :147, :158; DEV-E9-esc-d40-7081.log:149-175, :307, :441, :577, :597, :609, :617. The 7.8.1 build is main's pre-bump tree at its own pin, in private package and build directories.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions