Replace the signal-as-event live-reload scheduler with a runner-driven model.
The `resolveTrack` loader schedules its resolve work on a new `RecurringRunner`
that re-runs the task on an injected `reschedule` policy; the separate
`scheduleTrackReload` behavior and its per-type reload-epoch signals are deleted.
Core (`core/tasks`):
- `Task.run()` is now memoized (runs once, shares the result across calls) and
gains `clone()` (fresh, pending, structurally identical) — added to `TaskLike`.
- `RecurringRunner`: single-slot, id-keyed (dedup same id / abort-and-replace on
new id), time-free. Each cycle runs `Promise.all([task.run(), reschedule(task,
previous, signal)])` and re-runs a `clone()` while reschedule resolves `true`.
- `Reschedule<T> = (task, previous, signal) => PromiseLike<boolean>` — invoked
concurrently with the run, observes it via the memoized `run()`, owns its delay.
- `delayedReschedule(cadence)` builds a Reschedule from a pure ms-cadence fn,
start-anchored (subtracts the run's elapsed) so reloads are measured from
load-start per RFC 8216 §6.3.4, preserving half-on-unchanged.
Supporting:
- `@videojs/utils/time`: add cancellable `sleep(ms, signal)`.
- `media/hls/reload-policy`: `mediaPlaylistReloadDelay` (pure cadence; relocated
scheduler logic — target-duration, half-on-unchanged, stop-on-ENDLIST, retry).
- `resolve-track`: baked universal completeness gate + injected `reschedule`;
engine composes `delayedReschedule(mediaPlaylistReloadDelay)`.
WIP: not fully validated end-to-end against a live stream through this path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live-vs-complete decision was being re-derived in three places from
`metadata.endList`, which diverges from how the parser itself computes
completeness (`endList || PLAYLIST-TYPE:VOD`) — and the parser already bakes
that result into `Track.duration` (finite EXTINF sum when complete, Infinity
while it can still grow). So read that instead of re-deriving:
- resolveDuration default reverts to `getResolvedSelectedTrackDuration` (returns
`Track.duration`, already Infinity for live by construction). Drops the
redundant `resolveSelectedTrackDuration`, which also keyed off `endList` alone
and thus wrongly returned Infinity for a complete PLAYLIST-TYPE:VOD playlist
lacking #EXT-X-ENDLIST.
- end-of-stream and seek-to-live-edge guards switch from
`metadata.endList` to `Number.isFinite(track.duration)`.
Net: `Presentation.duration === Track.duration` by construction, and all three
behaviors share the parser's one completeness predicate. Removes code. Full spf
suite green (1070 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compose the live behaviors into the VoD engine so one composition handles
both VoD and live, keying every live-vs-VoD decision off playlist
completeness rather than streamType:
- resolveSelectedTrackDuration: new completeness-based duration resolver, now
the engine default. Returns the resolved track's finite duration when its
playlist is complete (#EXT-X-ENDLIST), else Infinity (still growing → live).
Keys off completeness because deriveStreamType marks any playlist lacking
#EXT-X-PLAYLIST-TYPE:VOD as 'live', which would wrongly force Infinity on a
plain VoD stream that only carries #EXT-X-ENDLIST.
- seekToLiveEdge: guarded to no-op for complete playlists, so it's inert for
VoD when composed unconditionally.
- createSimpleHlsEngine: composes scheduleVideoTrackReload/Audio/Text,
anchorLiveTracks, and guarded seekToLiveEdge — all inert for VoD (complete
playlists never reload; the anchor is a no-op without PDT / shift 0). Adds
the startSequence config knob.
Also make updateMediaSourceDuration's live write defer to a non-updating
SourceBuffer instant: Infinity needn't precede appends (it overrides any finite
live-edge value an append pinned), but the MSE spec forbids setting duration
while a buffer is updating, so a racing synchronous write threw.
Verified live playback end-to-end through createSimpleHlsEngine (real-time,
presentation.duration Infinity, seeked to edge) against a Mux LL-HLS stream;
full VoD suite green (1071 passed). The separate live engine is retained for
now. Known follow-up: mediaSource.duration can stay finite during initial
buffer fill (continuous appends leave no idle instant) — to be fixed by
writing Infinity synchronously at sourceopen, ahead of the buffer actors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decompose the live media-playlist reload into two single-responsibility
behaviors, matching the content-snapshot ([1]) vs refetch-policy ([3])
category split in live-presentation-modeling.md:
- resolve-track (loader, [1]) now also services live reloads: it watches a
per-type reload-epoch slot and re-fetches when it advances (gate: load if
unresolved OR epoch > last serviced), carries the prior snapshot's timeline
forward, and sets presentation.streamType. ConcurrentRunner id-dedup +
recording the epoch at schedule time gives drop-if-busy coalescing.
- schedule-track-reload (scheduler, [3]) is what remains of reload-track once
the fetching is removed: it bumps the reload epoch on a target-duration
cadence (half on unchanged), inert for VoD via #EXT-X-ENDLIST, and enters on
track *selection* so its bumps also drive first-resolve retries.
reload-track is removed; the live-hls and live-playlist-spike engines compose
resolve* + schedule* in its place.
Also fix updateMediaSourceDuration for live: it only wrote Infinity when the
MediaSource was already open at entry and returned without scheduling a wait
otherwise. The presentation can resolve to Infinity before setupMediaSource
opens the MediaSource, so the eager write was missed and the first append
pinned a finite live-edge duration — stalling once the window slid past it.
Now it defers to sourceopen when not yet open.
Verified live playback end-to-end (real-time, Infinity duration, seeked to
edge) against a Mux LL-HLS stream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>