Skip to content

Clipped video seeks to its end on play since 1.14.0 (#resetPlaybackIfNeeded passes absolute seekableStart to a clip-relative seek request) #1858

Description

@ItalianScallian

Summary

Since 1.14.0, a clipped video (clipStartTime > 0) seeks to its own end on first play and on every replay, then pauses on the last frame. 1.13.1 and earlier play the clip.

Reproduction

Native provider, any file, clip window later in the file than the window is long (typical for a highlight clip):

<script type="module" src="https://cdn.vidstack.io/player@1.15.6"></script>
<media-player src="https://files.vidstack.io/sprite-fight/720p.mp4" clip-start-time="30" clip-end-time="33" autoplay muted playsinline>
  <media-provider></media-provider>
</media-player>

1.5 s after load, headless Chromium:

build media-seek-request detail video.currentTime paused
1.12.13 30 30.97 (playing) false
1.15.6 30 33.00 true
1.15.6 + fix below 0 31.12 (playing) false

Same result with clip-start-time="6" clip-end-time="9" on a local 10 s file. Replay after the clip ends is broken in both Chromium and WebKit on 1.15.6, via remote.play(), PlayButton, keyboard, and MediaSession.

Cause

MediaStateManager#resetPlaybackIfNeeded dispatches the reset seek with an absolute time:

this.dispatch('media-seek-request', { detail: seekableStart(), trigger });

MediaRequestManager['media-seek-request'] runs the detail through boundTime, which treats it as clip-relative (every other caller sends clip-relative: remote.seek, TimeSlider, MediaSession) and adds clipStartTime again:

const clippedTime = time + store.clipStartTime(),
  isStart = clippedTime <= store.seekableStart(),
  isEnd = clippedTime >= store.seekableEnd();

With clipStartTime = 30, seekableStart = 30, seekableEnd = 33: clippedTime = 60, isEnd is true, the provider seeks to 33, the next time-update is >= clipEndTime, and the state manager requests a pause.

The unit mismatch has existed since 95a3dad (fix(player): clipping broken, v1.12.6, #1382), which changed the detail from (clipStartTime() > 0 ? 0 : seekableStart()) + 0.1 to seekableStart(). It was hidden until 923e7c7 (fix(vidstack): preserve sub-second seek targets, #1824, v1.14.0), because the old check Math.floor(time) === Math.floor(store.seekableStart()) matched the absolute value and returned seekableStart() unchanged.

Why it fires on first play

In Chromium the media-fragment seeked (from the #t=30,33 source URL) arrives before can-play. At that point seekableEnd is still 0 and currentTime is 0, so the ['seeked'] handler's Math.floor(currentTime()) === Math.floor(seekableEnd()) branch calls end() and marks the clip ended before it has played. The play handler then sees ended() and resets. WebKit does not fire that early seeked, so first play works there, but every replay path resets (ended(), realCurrentTime() >= clipEndTime(), or |currentTime - duration| < 0.1) and hits the same double add.

After the bad seek, ended is left false (the seeked at 33 clears it), so a consumer cannot work around it by pre-seeking on ended.

Fix

this.dispatch('media-seek-request', {
  detail: seekableStart() - clipStartTime(),
  trigger,
});

Verified by patching the 1.15.6 CDN bundle: first play, replay via remote.play(), and seek(0) + play() all play the clip in Chromium and WebKit. An alternative is to bypass boundTime and call provider.setCurrentTime(seekableStart()), as seekToLiveEdge already does.

The ['seeked'] handler comparing clip-relative currentTime with absolute seekableEnd is a second unit mix in the same area; it is what produces the load-time ended above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions