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
endedevent 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()vsmediaSource.durationadjustment) tracked as open questions. - Composition scope: VOD-engine-variant only. Live engine
variants don't carry this behavior — live sources have
Infinityduration and never approach a boundary. Same composition-variant pattern assetLiveSeekableRange(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 ormediaSource.durationadjustment (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
endedvia 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) vsmediaSource.durationadjustment (option b). Trade-offs: (a) more direct but interacts with mse-mms-pipeline's existingendOfStreamgate (which fires onisLastSegmentAppended+currentTime >= lastSegStart); the gate today doesn't account for "buffer falls slightly short of duration boundary." Adding a pseudo-ended trigger path means twoendOfStream()callers with different conditions. (b) mutatesmediaSource.duration, which is written by multiple behaviors (from non-zero-pts-support andupdateMediaSourceDuration); 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 ofpresentation.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
Infinityduration, 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
endeddespite 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()vsmediaSource.durationadjustment. 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.durationisNaNorundefined. Some sources load before duration is known. Detection should not fire in that state. Defensive check.
Related features
- non-zero-pts-support — cluster B
foundation. Pseudo-ended-detection's "approaching duration"
semantics need to use offset-corrected
currentTimeand duration (both naturally offset-corrected through the chosen PTS-offset mechanism, but worth confirming). - mse-mms-pipeline —
endOfStream()is the MSE API used by one action option; the existingendOfStreamgate 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
- clusters.md § Time normalization — cluster B description + Borderline content compensation sub-cluster framing
- clusters.md § Feature classification axes — Borderline / content-compensation category; Naive vs Full depth framing
- SPF Epics Working Doc — source material; epic #10 (Pseudo-Ended Detection)
- non-zero-pts-support.md — cluster B foundation
- mse-mms-pipeline.md —
endOfStream()API + existing gate behavior - MSE Spec —
MediaSource.endOfStream()— one of the candidate action mechanisms