Files
v10/internal/design/spf/features/pseudo-ended-detection.md
T

11 KiB

status, date, definition
status date definition
draft 2026-05-20 coarse

Pseudo-ended detection

Heuristically detect "pseudo-ended" state — playback stalls near the duration boundary on sources with mismatched manifest-duration vs actual segment-data length — and trigger termination cleanly so ended fires correctly rather than letting playback hang indefinitely. Canonically a Safari quirk on slightly-malformed VOD content (the manifest declares duration N seconds; the last segment ends at duration N - 0.05; the playhead approaches N, never reaches it, doesn't fire ended). Cluster B Borderline content-compensation feature; sister to edit-list-compensation and buffer-stall-recovery in the Borderline sub-cluster.

A Borderline feature per clusters.md § Feature classification axes: "accounts for technically valid but suboptimally-formed-or-delivered content (content compensation). The source plays; the work makes it play better in specific quirky cases." Notion epic #10 explicitly classifies it as Borderline.

Status

  • Composition: not implemented. Engine relies on browser's native ended event firing; on Safari with mismatched-duration sources, this never fires and playback hangs at the end.
  • Definition depth: coarse — scope from Notion + Borderline framing; SPF touchpoints sketched. Implementation details (threshold values, action choice between MediaSource.endOfStream() vs mediaSource.duration adjustment) tracked as open questions.
  • Composition scope: VOD-engine-variant only. Live engine variants don't carry this behavior — live sources have Infinity duration and never approach a boundary. Same composition-variant pattern as setLiveSeekableRange (live-only) — pseudo-ended- detection is VOD-only, composed into the relevant variant rather than runtime-checked inside a uniform behavior.

Phases of complexity

Two phases (detection + action), each with Naive (don't compensate) vs Full (apply the heuristic) depth per the Notion classification.

Phase Depth What Notes
Pseudo-ended state detection Naive Don't detect. Engine relies on the browser's native ended event; sources with the duration mismatch quirk stall indefinitely on Safari. The current state Status quo; ≈ what hls.js does for this case
Full Heuristic monitor: playhead approaching presentation.duration (within a small epsilon) + non-progressing for a threshold duration + no buffer beyond the current playhead → flag pseudo-ended state. Write a pseudoEndedDetected: boolean state slot (or derived signal) The compensation mechanism. Behavior reads currentTime, presentation.duration, SourceBuffer.buffered; writes the state slot. Threshold tuning is the load-bearing implementation work — too sensitive triggers false positives on real network stalls, too lax leaves Safari users hanging
Pseudo-ended action Naive Pass through; browser stalls indefinitely. The current state Status quo
Full When detection fires, trigger clean termination. Two action mechanisms (choice open): (a) call mediaSource.endOfStream() — explicit signal to MSE that no more data is coming, triggers ended event; (b) write mediaSource.duration to match the buffered end + a small margin so the browser naturally fires ended when playhead reaches the adjusted duration (a) is more direct but interacts with mse-mms-pipeline's endOfStream gate (which today fires on isLastSegmentAppended + currentTime >= lastSegStart; pseudo-ended is a different trigger path). (b) avoids the endOfStream gate interaction but mutates duration (which other behaviors read — care with multi-writer characterization)

What's in scope vs out of scope

In scope:

  • Both phases for finite-duration HLS sources where the duration mismatch quirk surfaces (canonically Safari)
  • Pseudo-ended state slot (or derived signal) + detection behavior
  • Pseudo-ended action behavior — either MediaSource.endOfStream() call or mediaSource.duration adjustment (choice open)
  • Threshold-tuning config surface (e.g., pseudoEndedDetection: { epsilonSeconds, stallThresholdMs })
  • Composition into VOD engine variants (live variants don't carry this behavior)

Out of scope (separate Borderline sister features):

  • [edit-list-compensation] (candidate, this session) — init- segment edit-list offset detection. Different mechanism (middle pattern: parse-and-offset).
  • [buffer-stall-recovery] (candidate, this session) — mid- stream stall detection and recovery (not at duration boundary). Both this feature and buffer-stall-recovery detect "playback not progressing"; the discriminator is "is the playhead near the duration boundary?" Pseudo-ended owns the near-boundary case; buffer-stall-recovery owns the mid-stream case.

Out of scope (different architectural layer):

  • Customer-facing "playback ended unexpectedly" UI / notifications. Engine surfaces ended via standard DOM events; consumer renders UI from there.
  • Service-side fix to the original-content duration mismatch. The proper long-term fix is encoder/manifest correctness; this feature compensates client-side until then.

Likely cross-cutting impact

Things this feature probably forces decisions on, not just additions:

  • Action-mechanism choice. MediaSource.endOfStream() (option a) vs mediaSource.duration adjustment (option b). Trade-offs: (a) more direct but interacts with mse-mms-pipeline's existing endOfStream gate (which fires on isLastSegmentAppended + currentTime >= lastSegStart); the gate today doesn't account for "buffer falls slightly short of duration boundary." Adding a pseudo-ended trigger path means two endOfStream() callers with different conditions. (b) mutates mediaSource.duration, which is written by multiple behaviors (from non-zero-pts-support and updateMediaSourceDuration); becomes a multi-writer slot. Lean: empirical — pick whichever Safari handles cleanly.
  • Distinction from [buffer-stall-recovery]. Both monitors detect "playback not progressing." Coordination needed so they don't fight (e.g., buffer-stall-recovery triggers a seek-nudge to recover, but the stall is actually pseudo-ended and the seek takes the playhead past the buffered end). Discriminator: pseudo-ended fires only when playhead is within epsilon of presentation.duration; buffer-stall-recovery fires elsewhere. Composition order: pseudo-ended runs first; on negative result, buffer-stall-recovery considers the stall.
  • Composition-variant placement. Pseudo-ended detection composes into VOD engine variants only — live variants don't carry this behavior (live has Infinity duration, no boundary to approach). Per the failure-mode catalog's composition-variant entry: behavior is variant-specific, not a runtime branch in a uniform behavior. Cross-cuts with how live-stream-support composes — both are variant-specific.
  • Threshold tuning surface. Epsilon (how close to duration counts as "near end"; e.g., 0.5s, 1s, 0.1s?) and stall threshold (how long of no-progress counts as stalled; e.g., 250ms, 500ms, 1s?). Empirical tuning; defaults should ship; consumers may override per source.
  • False-positive avoidance. Some sources legitimately have slow-loading last segments (e.g., live → VOD transition where the segment is being finalized server-side). Distinguish from pseudo-ended: if the buffer is still growing toward duration, it's not pseudo-ended yet. Detection logic gates on "buffer not growing" too.
  • Browser-specificity. Canonically Safari. Other browsers (Chrome, Firefox) may handle duration-mismatch differently — some fire ended despite the gap. Whether the detection logic runs unconditionally or is Safari-gated is open. Lean: run unconditionally (a defensive feature should fire if conditions match, regardless of browser).

Open questions

  • Action-mechanism choice — endOfStream() vs mediaSource.duration adjustment. Per cross-cutting note. Empirical determination on Safari.
  • Detection thresholds. Epsilon to duration boundary + stall duration. Default values + customer override config.
  • Coordination with buffer-stall-recovery. Composition order + shared state-slot vs separate. Pseudo-ended-first lean.
  • Live + VOD transition. Some sources transition from live (Infinity duration) to VOD (finite duration) when the broadcast ends. Composition shape: does the pseudo-ended behavior compose in at transition time (when duration becomes finite), or is it always-uncomposed for sources that started live? Lean: not composed (live engine variant doesn't carry it, and transition to VOD is structurally a source replacement). Confirm when live-stream-support termination scenarios mature.
  • Browser detection gate. Run unconditionally vs Safari-only? Lean: unconditional (defensive).
  • Composition with [edit-list-compensation]. Edit-list offsets affect the perceived duration boundary. If the edit list offsets the source's content end relative to duration, pseudo-ended detection's "approaching duration" check needs to be offset- aware. Cross-feature with sister candidate.
  • presentation.duration is NaN or undefined. Some sources load before duration is known. Detection should not fire in that state. Defensive check.
  • non-zero-pts-support — cluster B foundation. Pseudo-ended-detection's "approaching duration" semantics need to use offset-corrected currentTime and duration (both naturally offset-corrected through the chosen PTS-offset mechanism, but worth confirming).
  • mse-mms-pipelineendOfStream() is the MSE API used by one action option; the existing endOfStream gate behavior is the other caller. Coordination between the two callers is a cross-cutting concern.
  • edit-list-compensation — sister Borderline feature; edit-list offsets affect the perceived duration boundary.
  • buffer-stall-recovery — sister Borderline feature; both detect "playback not progressing." Coordination needed via composition order + near-boundary discriminator.
  • live-stream-support — out of scope (live has Infinity duration); this feature composes into VOD variants only.
  • buffer-management — reads buffered ranges for the detection heuristic.

See also