Commit Graph
90 Commits
Author SHA1 Message Date
Christian PillsburyandClaude Opus 4.8 c3e137cf39 feat(spf): recover end-of-stream stall on skewed A/V (spike)
Make a complete VOD reach native `ended` (and loop) reliably even when audio
and video tracks end a few ms apart, via two narrow changes in the behaviors
that already own end-of-playback:

1. `end-of-stream` reactor — add `LAST_SEGMENT_REACHED_SLACK` (0.5s) to the
   "playhead reached the last segment" gate. A tiny final segment (e.g. Apple's
   ~44ms last segment) starts at the buffered end and Chrome freezes the
   playhead ~50-70ms short of it, so a strict `currentTime >= lastSegStart`
   never opens -> `endOfStream()` never fires -> MediaSource stays `'open'` ->
   frozen. The slack fires EOS just before the freeze.

2. `recover-end-stall` (new DOM behavior) — on `waiting`, if the MediaSource is
   `'ended'`, duration is finite, and the playhead is within
   `endStallNudgeWindow` (0.2s, config) of the reachable buffered end, nudge
   `currentTime = duration` to force native `ended`. Event-driven, no poll
   (`waiting` fires at ~0ms). Inert for live and clean-ending streams.

Composed in both engines. ADR:
internal/decisions/end-of-stream-av-skew-recovery.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:37:46 -07:00
Christian PillsburyandClaude Opus 4.8 6d26e083a5 feat(spf): collapse relocation tiers into shared-min default (spike)
Replace the per-type "Tier 1" / shared-min "Tier 2" split with one default
`deriveSharedMinStartMediaTime`: relocate every track by the `min` across the
selected A/V origins. Subsumes per-type — aligned origins reduce to each type's
own, skewed origins keep every DTS >= 0 while preserving A/V skew, and a single
selected type reduces to its own origin. Returns undefined until all selected
types are discovered (shared-min barrier). `derivePerTypeStartMediaTime` stays
as a barrier-free opt-out.

The loader stamp is now tier-agnostic: `stampStartMediaTime` applies the same
injected `derive` over `mediaContainerData` (not the raw own-origin), async with
an `awaitDefined` holdback and liveness guard. Engines resolve one
`deriveStartMediaTime` and feed it to both the reactor and
`relocationPipelinesFor(type, derive)` so model and buffer agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:37:10 -07:00
Christian PillsburyandClaude Opus 4.8 b820064b77 wip(spf): per-type relocation keying + non-0th-segment origin fix
- Key mediaContainerData by track *type* (video/audio), not track id: one
  init+media pair per type, ABR rungs share the origin. relocationMessagePipelines
  -> relocationPipelinesFor(type); the engines pass per-type.
- #4 fix: record the discovered segment's 0-based startTime and derive the origin
  as baseMediaDecodeTime/timescale - segmentStartTime, so relocation is correct when
  the first loaded segment isn't the 0th (non-zero initial currentTime, live/DVR).
  Adds segmentStartTime to MediaContainerData.
- Rename derivePerTrackStartMediaTime -> derivePerTypeStartMediaTime; stampTracks
  applies each type's origin to every track of that type.
- Add establish-start-media-time derive unit test.

Unit-verified (segmentStartTime=100 still yields origin 60) and end-to-end
sandbox-verified: start-at-300 buffers around the playhead (0-based); default
start-at-0 A/V + audio-only remain 0-based.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 07:20:43 -07:00
Christian PillsburyandClaude Opus 4.8 05a0a6452d wip(spf): non-zero-PTS relocation as establishStartMediaTime reactor + relocation steps
Rebuild relocation from the config-bundle (createRelocation + self-discriminating
origin-discoverer) into a DOM-free per-source reactor plus DOM config-pipeline
steps, per the presentation-timeline design + review.

- behaviors/establish-start-media-time.ts (new, DOM-free): the reactor — 3 states
  (inactive clears the transient slot per source / monitoring runs the injected
  deriveStartMediaTime seam -> writes Track.startMediaTime / established disables it).
  Owns mediaContainerData on state; selection optional/defensive; never touches pipelines.
- behaviors/dom/relocation-steps.ts (new): relocationMessagePipelines — a plain
  config step array. discover writes state.mediaContainerData; stamp reads the
  track's own discovered origin (Tier 1) -> timestampOffset. Steps read composition
  state from call-time deps; coordinate with the reactor only through the slot.
- segment-loader: StepDeps widened with the composition {state,context,config}
  (opaque conduit; the loader never reads them).
- primitives/head-peek.ts (new): generic eager head-peek (replaces origin-discoverer).
- media/types: Track.startMediaTime + MediaContainerData.
- setup-buffer-actors: relocation pipelines from config; threads composition deps.
- engine: composes the reactor + bakes relocationMessagePipelines + mediaContainerData
  + deriveStartMediaTime config, comment-marked for easy removal.
- Deleted relocation.ts, origin-discoverer.ts. Doc + backlog updated.

Deferred (backlog): per-type keying; non-0th-segment origin (#4 — non-zero starts
currently mis-relocate); tier-agnostic apply; text-cue relocation; Tier 2 min;
audio-only wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 14:05:36 -07:00
Christian PillsburyandClaude Opus 4.8 ae1b0beeec wip(docs): reactor-based relocation design + spike backlog
Rearchitect presentation-timeline-model.md around the establishStartMediaTime
reactor (mediaContainerData slot, pure deriveStartMediaTime seam,
Track.startMediaTime consume, abortable awaitDefined apply); resolve the
reduce-hinge and startMediaTime-storage open questions.

Add a living Status & backlog to the spike plan so commits never bury
outstanding refactor/review work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 12:04:42 -07:00
Christian PillsburyandClaude Opus 4.8 555f9fdacc docs(spf): add presentation timeline coordinate model design doc
Consolidates the design-discovery for the timestampOffset-relocation
approach to non-zero-PTS: the three-timeline coordinate model
(media / presentation / wall-clock, with timestampOffset derived from
startTime - startMediaTime), the orthogonal capability axes
(relocation / parser / coordination), the discover -> reduce -> apply ->
consume architecture with branch-free always-present actors, and the open
hinges -- chief among them generalizing the live establishment behavior
for VOD. Draft; spike-stage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 12:50:28 -07:00
Christian PillsburyandClaude Opus 4.8 d4b21954bc docs(spf): note EVENT detection is now consumer-reachable
The DVR-vs-live distinction open question's PLAYLIST-TYPE:EVENT option is
available today: playlistType is exposed via getMediaPlaylistMetadata on
@videojs/spf/hls and used by the sandbox.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:20:47 -07:00
Christian PillsburyandClaude Opus 4.8 78f33556dc docs(spf): link live/DVR hardening follow-ups to tracking issues
Add issue refs to the live/DVR Open-questions bullets (#1743-#1746) and
a Status pointer to the Live/DVR Seekable and Anchor Hardening epic
(#1742), so the design docs and the issue tracker are bidirectional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:01:58 -07:00
Christian PillsburyandClaude Opus 4.8 2820b844b8 docs(spf): record live-window-guard fix and live/DVR seekable + anchor-coupling follow-ups
- live-stream-support.md: correct the window-exit guard description and
  test matrix to match the shipped fix (guard no longer bails on
  `seeking`); add the resolved out-of-window-seek-stall note; add Open
  questions for the seekable-range "lip", clamp-to-seekable as the
  general mechanism, and back-of-window yank-to-edge on discrete window
  slides. Remove stray EOF tags.
- live-presentation-modeling.md: add Open question flagging the
  model <-> anchor <-> resolve-track coupling for a holistic revisit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:52:15 -07:00
Christian PillsburyandClaude Opus 4.8 dbddbf67bb refactor(spf): rename anchorLiveTracks to anchorPresentationTimeline
The behavior's mechanism — pin one track from buffer ground truth, derive one
shared offset, stamp every track onto it — is format-neutral; only the offset
source (PDT) is live-specific. Rename it and its published signal (liveAnchor →
presentationAnchor, matching the PresentationAnchor type it holds) so the name
reflects the general role, and note the non-zero-PTS reuse path through the
existing resolveBufferedAnchor seam. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:46 -07:00
Christian PillsburyandClaude Opus 4.8 0cb33bb532 refactor(spf): establish the live anchor from buffer truth and gate the seek on it
Rework live anchoring around a single shared anchor established only from
authoritative buffer ground truth, dropping the unreliable manifest estimate:

- anchorLiveTracks establishes the anchor once per source from the first
  actually-buffered A/V track (the buffer actor's `initTrackId`, not the
  selection), stamps it onto every track via `positionAllTracksToAnchor`
  (resolved tracks shift; not-yet-resolved shells get `startDate` for the
  parser's `placeOnAnchor`), and publishes it as `liveAnchor`.
- seekToLiveEdge gates its live-edge seek on `liveAnchor`: with no estimate the
  pre-anchor window is the raw one, and seeking there would strand the playhead
  when the pin later shifts the window. Gating holds the seek until the timeline
  is anchored, so it targets the final native-PTS window. (Smoke-tested on Mux
  LL-HLS: single seek to the live edge after the pin, clean startup.)
- The resolveBufferedAnchor seam becomes `(deps) -> { trackId, segmentId,
  actualStart }`, reading the buffer actor directly (video-first).

An intermittent ~2 s audio model-`startDate` offset (buffers stay aligned, so
playback is synced) remains, tracked as a separate startup race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:46 -07:00
Christian PillsburyandClaude Opus 4.8 7cde2ea28d refactor(spf): hold one shared presentation anchor for all live tracks
Convert anchor-live-tracks from N per-track buffer pins to a single
shared (media-time <-> PDT) anchor applied to every selected track —
video, audio, and now text. A two-state reactor (unanchored -> anchored)
positions from the manifest estimate until a selected A/V track has
SourceBuffer ground truth, then establishes the shared anchor once
(first track to buffer wins) and positions each track onto it by PDT,
leaving it to the parser's carry-forward thereafter.

The resolveBufferedAnchor seam now takes the standard (track, deps)
setup arguments instead of closing over engine scope; the HLS engine's
implementation lives in its own generic module (resolve-buffered-anchor)
so a future audio-only-live engine can reuse it. anchorLiveTracks is now
a makeAnchorLiveTracks<Context>() factory mirroring makeShareSignals.

Realizes internal/decisions/live-presentation-anchor.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:45 -07:00
Christian PillsburyandClaude Opus 4.8 f531e1055c docs(spf): decide the single presentation-level live timeline anchor
Capture the decision to hold one rolling (media-time ↔ PDT) anchor — learned
from the first A/V track to obtain SourceBuffer ground truth and applied to all
selected tracks (incl. text) by per-segment PDT — replacing today's per-track
buffer pins, which are redundant under the no-skew assumption and can't anchor
text (no SourceBuffer). New decision doc live-presentation-anchor.md; resolves
the deferred single-rolling-anchor question in live-timeline-anchoring and
promotes [4] in live-presentation-modeling to presentation-level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:45 -07:00
Christian PillsburyandClaude Opus 4.8 5e161837d8 docs(spf): drop stale repositionPolicy references after seam removal
The repositionPolicy seam was removed when seek-to-live-edge's guard became
reload-driven. Reframe the sibling references: the guard does window-exit, and
the edge-only [live-edge-only-mode] use-case supplies its own reposition
behavior (alternative-impl) rather than flipping a seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:44 -07:00
Christian PillsburyandClaude Opus 4.8 fb4b6b05fb docs(spf): reconcile live-stream-support to the reactor guard
seek-to-live-edge is now a reactor (inactive ↔ live): entry does the one-time
seek, effects runs the window-exit guard (window-update re-fire + a single
`play` listener — `play`, not `playing`, since a slid-past playhead is at an
unseekable position where `playing` never fires). The repositionPolicy seam
was removed once the guard became reload-driven (nothing consumed it); the
live-edge-only use-case defines its own reposition behavior when built.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:22 -07:00
Christian PillsburyandClaude Opus 4.8 ada3100209 docs(spf): reflect the live-latency seam in live-stream-support
liveWindowFor is now {start, end}; the holdback rule lives in liveLatencyFor
and reaches seek-to-live-edge via the resolveLiveLatency seam + getLiveEdge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:22 -07:00
Christian PillsburyandClaude Opus 4.8 57a426db6b docs(spf): explain why syncLiveSeekableRange doesn't clear on termination
Reframe the "clearLiveSeekableRange() on termination" follow-up as a
deliberate non-action rather than a gap. Per the W3C MSE spec, the live
seekable range is consulted only while duration === Infinity; when a live
stream ends, endOfStream() sets a finite duration and the UA derives seekable
from buffered + duration, ignoring the live range — so clearing is unnecessary.
Clearing on the ENDLIST->finite-Track.duration transition would also be
premature (duration is still Infinity until endOfStream) and shrink seekable to
buffered-only. No logic change; the existing behavior (bail on null window) is
already correct, and the 'no-ops for a complete playlist' test locks it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:21 -07:00
Christian PillsburyandClaude Opus 4.8 81fb61b354 docs(spf): note liveWindowFromState + audio-only live in live-stream-support
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:21 -07:00
Christian PillsburyandClaude Opus 4.8 0d7208d2d4 docs(spf): note duration sole-ownership in live-stream-support
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:21 -07:00
Christian PillsburyandClaude Opus 4.8 7ccac9168e docs(spf): reflect seek-to-live-edge split in live-stream-support
Update the Implementation surface + live-edge phase row to attribute
setLiveSeekableRange to syncLiveSeekableRange and the window derivation to the
pure liveWindowFor helper, rather than the (now leaner) seekToLiveEdge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:20 -07:00
Christian PillsburyandClaude Opus 4.8 1cb1297c07 docs(spf): mark live-window playhead guard implemented in live-stream-support
Promote the live-window playhead guard phase from planned to implemented
(window-exit reposition in seek-to-live-edge): update the Status block, phase
row, What's-not-implemented, Implementation/Config surface, and Verification
(landed unit matrix; e2e deferred). Record the resolved seam-shape decision
(behavior-scoped repositionPolicy, not a public engine-config field) and guard
placement (extends seek-to-live-edge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:20 -07:00
Christian PillsburyandClaude Opus 4.8 e3c00f1db9 docs(spf): reconcile live-stream-support to implemented state, document live-window guard
Bump live-stream-support to sketched depth grounded in feat/spf-hls-live
(reload loop, sliding-window tracking, Infinity duration, live-edge seek,
ENDLIST termination all implemented); resolve its 5 open questions. Add the
planned live-window playhead guard (reposition on window-exit, covering
paused-too-long and fallen-behind) with a repositionPolicy seam for a future
edge-only composition.

Cascade: register [live-edge-only-mode] use-case candidate; cross-ref
buffer-stall-recovery (in-window stall vs window-exit reposition) and
dvr-event-stream-support (seekable spectrum).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:00:19 -07:00
Christian PillsburyandClaude Opus 4.8 a46a94e80b docs(spf): record realized live refetch policy + LL-HLS change-detection gap
live-presentation-modeling: category [3] refetch policy is realized as the
pure §6.3.4 cadence (mediaPlaylistReloadDelay) + delayedReschedule +
RecurringRunner, fetch scheduling per-track. Record the decided error
semantics — a failed reload's rejection propagates and ends the recurrence;
transient-fetch retry is deferred to network-resilience, kept out of the
cadence — and mark the [3] placement open question decided.

ll-hls-support: note that regular-live change-detection keys off the
full-segment signature, so a parts-only update reads as unchanged; LL-HLS
must fold part progression into change-detection or switch to blocking reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:59:25 -07:00
Christian PillsburyandClaude Opus 4.8 a2edaba81d docs(spf): record live timeline anchoring + MSE timestampOffset decisions
Two decision docs for the live-HLS timeline work:
- mse-timestamp-offset — native-PTS default for live; timestampOffset reserved
  for relocation (the manifest→buffer / encoded layer).
- live-timeline-anchoring — align demuxed tracks and recover turnover via PDT,
  not sequence number (the manifest layer).

Resolve open questions [4] (sync anchor) and turnover startTime recovery in
live-presentation-modeling, pointing at the decisions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:58:45 -07:00
Christian PillsburyandClaude Opus 4.8 820f6fe45c docs(spf): add live-presentation-modeling design doc
Companion to presentation-modeling.md for streams that change over
time (live / DVR / event). Spine is a category decomposition that
keeps data separated by change-rate and consumer: content snapshot
(snapshot/merge segment identity via media-sequence + URL/byteRange),
streamType, completeness-as-duration, refetch policy as its own
category, the PROGRAM-DATE-TIME sync anchor, and the derived consumer
surface. Captures the debated decisions and open questions; scopes to
sliding-window live (LL-HLS / DVR / discontinuity / timestampOffset
deferred). Adds the reciprocal companion link in presentation-modeling.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 09:58:44 -07:00
Santiago PuppoandGitHub a74b8c0993 refactor: Rename SPF background-looping-video into background-video (#1731) 2026-06-24 11:09:20 -03:00
23538e3649 feat(spf): text tracks switching (#1687)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 07:54:51 -07:00
edfec1c93e revert(core): unmerge i18n stack base (#1707)
Co-authored-by: Wesley Luyten <me@wesleyluyten.com>
2026-06-18 15:20:35 -07:00
768bf09da0 feat(core): add i18n foundation with English locale and UI wiring (#1589)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Wesley Luyten <me@wesleyluyten.com>
2026-06-18 11:55:38 -07:00
Ronald UrbinaandGitHub 07428519a5 feat(packages): add poster placeholder blur-up pattern (#1632) 2026-06-18 12:05:41 -03:00
bce79ec424 feat(spf): capability probing (#1676)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:22:49 -07:00
b89f1e944c feat(spf): multi cdn failover (#1671)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:20:43 -07:00
00aa6247b8 feat(spf): multi-cdn support (#1668)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:09:16 -07:00
f15d2b5e78 docs(spf): track switching design (#1631)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:39:40 -07:00
057f32573e feat(spf): multi-track audio + skills building features and behaviors (#1605)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 09:53:13 -07:00
Sam PottsandGitHub c5f75166fd feat(packages): update menu group labels (#1643) 2026-06-02 20:26:19 +10:00
Santiago PuppoandGitHub 3741e8de12 feat(spf): background looping video (phase 1) (#1602) 2026-06-01 16:13:26 -03:00
1a3cb45b29 feat(spf): basic audio only use case + use-case-composition doc-type + implementation skills (#1584)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:48:40 -07:00
c9d6ccecdd docs(spf): seed feature registry, clusters reference, and document-feature skill (#1581)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:07:33 -07:00
c112b6248b fix(spf): conventions, per-type specialization, config threading (#1537)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:38:47 -07:00
Sam PottsandGitHub ac37259803 docs(design): i18n (#1122) 2026-05-18 10:02:55 +10:00
Sam PottsandGitHub ad831d25f0 feat(packages): add playback rate menu (#1527) 2026-05-14 08:37:52 +10:00
e3c1b28032 feat(core): menu core layer and DOM keyboard navigation (#1503)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 18:15:49 +10:00
0620814a67 feat(packages): add UI support for gestures and hotkeys (#1388)
Co-authored-by: Rahim <rahim.alwer@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 10:34:12 +10:00
Wesley LuytenandGitHub 6c81f2d190 docs(design): add design doc for live presets (#1395) 2026-05-01 13:01:04 -03:00
Sam PottsandGitHub cc1b5645e5 docs(design): menus (#1078) 2026-05-01 13:34:33 +10:00
Christian PillsburyandGitHub 69852ddeb0 docs/spf fundamental concepts (#1396)
Feedback from @decepulis treated as a fast follow/incremental improvement effort for expediency.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 11:16:56 -07:00
89594292d4 docs(root): fix stale references and document sandbox workflow (#1464)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 14:29:24 -05:00
rahimandGitHub 5e9a02c851 fix(react): rename MediaGesture and MediaHotkey to Gesture and Hotkey (#1374) 2026-04-19 23:46:46 -07:00
24b8b77c8a feat(cli): add @videojs/cli docs command for LLM-friendly installation (#1214)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:07:49 -05:00