6.1.0 - Seek lifecycle events
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/seekTargetare 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
.beganand exactly one of.landed(renderedTime:),.stalledor.supersededunder the sameid. A seek that never reaches a host emits a standalone.rejected(.noActiveSession)or.rejected(.liveWithoutDVR). Each event carries its target on thecurrentTimeaxis 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
.stalledand drops out ofisSeeking, but it stays alive inside AVPlayer as recovery intent, so a.landedunder the sameidcan still arrive, minutes later on a stalled source. That transition has no edge left in a level signal.Every event is also an
EngineLogline in theenginecategory (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
.stalledrather 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 seektestrun 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
currentTimeso scrub UI follows, but leftisSeekingfalse. 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. -
seekTargetno 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
stopInternalbypassed 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.