Skip to content

6.1.0 - Seek lifecycle events

Choose a tag to compare

@superuser404notfound superuser404notfound released this 30 Jul 03:01

A new observable on the seek path, plus the mechanics behind it. isSeeking / seekTarget keep their meaning; nothing an existing consumer reads changes shape.

Added

  • A seek now says what happened to it, not just that it stopped happening. isSeeking / seekTarget are a level, and a level's falling edge cannot distinguish a landing from a give-up from a supersede. It also cannot keep the target the edge belonged to: both properties clear in the same recompute, so a consumer that hops a queue between them sees them coalesced and reads nil. Reported by rrgomes from production use in a synchronized-playback host (#38), which had rebuilt the missing half out of a 1.5 s landing grace, a retained last non-nil target and a 180 s unreached-target map with a 4 s epsilon.

    player.seekEvents      // AnyPublisher<SeekEvent, Never>
        .sink { event in
            switch event.outcome {
            case .began:                       hold(broadcastOf: event.target)
            case .landed(let renderedTime):    resume(from: renderedTime)
            case .stalled:                     keepHolding()     // may still land later
            case .superseded, .rejected:       drop(event.id)
            }
        }

    Every accepted seek emits .began and exactly one of .landed(renderedTime:), .stalled or .superseded under the same id. A seek that never reaches a host emits a standalone .rejected(.noActiveSession) or .rejected(.liveWithoutDVR). Each event carries its target on the currentTime axis and its origin (programmatic, nativeScrub, deferred).

    The contract has one deliberate asymmetry, and it is the reason the stream exists: a seek that spends its recovery budget reports .stalled and drops out of isSeeking, but it stays alive inside AVPlayer as recovery intent, so a .landed under the same id can still arrive, minutes later on a stalled source. That transition has no edge left in a level signal.

    Every event is also an EngineLog line in the engine category (seek#12 programmatic landed rendered=90.00 target=90.00), so a device capture needs no wiring.

Fixed

  • A native scrub no longer reports a landing before the picture arrives. The scrub's in-flight window ended when the coalesced producer restart drained, which means the producer is now producing at the new index, not that AVPlayer rendered it; the picture follows a fetch and a decode later, 1.4 s on the reporting host's WAN capture. The window now ends when the rendered frame reaches the restarted region, watched on the same rendered-time axis and bounded at 8 s, after which the honest report is .stalled rather than a latched signal.

    A restart that a programmatic seek itself caused is left to that seek's own events. The restart aims at the segment containing the requested time, so it is a lower bound on the landing rather than a point: an aetherctl seektest run measured a restart at 84.0 for a landing at 90.0, and watching for a landing at the restart target there would contradict a seek that landed fine.

  • A seek issued before the session can take it is visible instead of silently optimistic. Seeks stashed during load or against a pre-ready item (#127 / #178) publish their target on currentTime so scrub UI follows, but left isSeeking false. For a host broadcasting position that is worse than the old silent drop: a place nothing has reached, with no in-flight flag to suppress it. The stash window now carries the seek signal and hands over to its replay without a gap in the level.

  • seekTarget no longer publishes a settled seek's destination. It folded over the last non-nil target ever written, so a finished programmatic seek's target stayed published while a scrub was in flight toward a different one. Each source owns its own target now, and the published value follows the most authoritative one in flight.

  • A stop landing mid-seek no longer leaves the subtitle side-reader link owned by the video path for the whole next session. The #240 gate is per engine, not per session, and the hard clear in stopInternal bypassed the recompute that releases it.

Tooling

aetherctl seektest ends with a #38 SEEK EVENT LEDGER: an unpaired .began is a stranded in-flight window and fails the run, and .stalled seeks are listed with any late .landed that followed them.

Upgrading

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

Drop-in from 6.0.2. seekEvents is additive; isSeeking and seekTarget are unchanged in type and meaning, and the only behavioural difference for a host that ignores the new stream is that a native scrub's in-flight window now spans the picture arriving.