Skip to content

feat(packages): add pauseOnDrag to time slider#1596

Merged
mihar-22 merged 5 commits into
videojs:mainfrom
shiminshen:feat/pause-while-scrubbing-1440
Jun 30, 2026
Merged

feat(packages): add pauseOnDrag to time slider#1596
mihar-22 merged 5 commits into
videojs:mainfrom
shiminshen:feat/pause-while-scrubbing-1440

Conversation

@R-Delfino95

@R-Delfino95 R-Delfino95 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #1440

What it does

Adds opt-in pauseWhileDragging mirroring Vidstack's API:

  • React: <TimeSliderRoot pauseWhileDragging /> (default false)
  • HTML: <media-time-slider pause-while-dragging> (boolean attribute)

Wraps the existing onDragStart / onDragEnd callbacks — user-provided callbacks still fire alongside. Reads playback state via the existing selectPlayback selector + PlayerController / useLatestRef, mirroring how time and buffer are already wired.

Tracks wasPlayingBeforeDrag internally so a player paused before the drag doesn't get accidentally auto-played on release.

Scope choice

Opt-in default (false), matching Vidstack's pauseWhileDragging prop. v8 and Plyr are always-on (with platform disables for mobile/STV iOS audio glitching). I went opt-in because:

  1. It's a behavior change consumers should consent to (subtle iOS audio glitch on some streams during scrub-pause-resume)
  2. Vidstack's prior art is the closest match to v10's API shape
  3. Easy follow-up to flip the default if maintainers prefer

play() rejection on resume (autoplay policy, ad-paused state) is swallowed here — the existing error feature surfaces it via the player error state.

Verified locally

  • pnpm -F @videojs/react test224/224 pass (+4 new behavioral tests: default no-op, pause/resume when playing, no-resume when already paused, user-callback forwarding)
  • pnpm -F @videojs/html test120/120 pass (+1 new: attribute reflection)
  • pnpm lint / pnpm typecheck / pnpm check:workspace — all clean

Open question

The React test uses a hoisted-ref pattern to capture onDragStart / onDragEnd from the createSlider mock. If you'd prefer not extending the slider mock that way, happy to drop the React behavioral tests and lean on Playwright E2E for slider behavior (matches the existing convention in the HTML element tests).


Note

Medium Risk
Touches playback pause/play during seek UI interactions; default-off limits blast radius, but resume and mid-drag teardown paths affect player state.

Overview
Adds an opt-in pauseOnDrag option (default false) on the time slider so playback can pause while scrubbing and resume on release only if it was playing when the drag started.

TimeSliderCore gains startDrag / endDrag using MediaPlaybackState, with #wasPlayingBeforeDrag so already-paused media is not auto-played; resume still runs if pauseOnDrag is turned off mid-drag. Failed play() on resume is swallowed.

React (TimeSliderRoot) and HTML (media-time-slider / pause-on-drag) wire drag lifecycle through the core via selectPlayback, still calling user onDragStart / onDragEnd. Both call endDrag on unmount/disconnect/destroy so a mid-drag teardown does not leave the player paused.

Tests cover core drag behavior, HTML attribute reflection, and React integration (including unmount resume and callback forwarding).

Reviewed by Cursor Bugbot for commit 71ef8a8. Bugbot is set up for automated code reviews on this repo. Configure here.

@netlify

netlify Bot commented May 25, 2026

Copy link
Copy Markdown

‼️ Deploy request for vjs10-site rejected.

Name Link
🔨 Latest commit e20eabe

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

@shiminshen is attempting to deploy a commit to the Mux Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread packages/react/src/ui/time-slider/time-slider-root.tsx Outdated
Comment thread packages/react/src/ui/time-slider/time-slider-root.tsx Outdated
@R-Delfino95

Copy link
Copy Markdown
Collaborator Author

Hi @shiminshen! Thanks for your contribution 🙌

I created the PR with your changes, as external PRs are currently restricted.

I gave it a try and it looks correct to me. Would you mind rebasing on main and giving it another quick test, just to make
sure nothing has changed in the meantime?

Also, Cursor Bot detected some potential issues; would you mind reviewing those as well?

Thanks again!

Opt-in option to pause playback while the user is dragging the time
slider thumb, resuming on release if it was playing before. Reduces
the jittery feel during long scrubs and avoids audio glitching.
Closes videojs#1440.

Mirrors Vidstack's API:
- React: `<TimeSliderRoot pauseWhileDragging />` (default false)
- HTML: `<media-time-slider pause-while-dragging>` (boolean attribute)

Implementation:
- Wraps the existing `onDragStart` / `onDragEnd` callbacks. User-provided
  callbacks still fire — pause/resume runs alongside them.
- Reads playback state via the existing `selectPlayback` selector + a
  new `PlayerController` / `useLatestRef` hookup, mirroring how time
  and buffer are already wired.
- Tracks `wasPlayingBeforeDrag` so a player paused before the drag
  doesn't get auto-played on release (avoids accidental autoplay).
- `play()` rejection on resume (autoplay policy, etc.) is swallowed
  here — the existing error feature surfaces it.

Tests:
- HTML: `pauseWhileDragging` default + attribute reflection.
- React: 4 cases — default no-op, pause/resume when playing, no-resume
  when already paused, user-callback forwarding. Behavioral tests use
  the existing slider-mock pattern (captures `onDragStart` /
  `onDragEnd` via a hoisted ref).
Address Cursor Bot findings on videojs#1596:

- Resume on drag-end based on the captured `wasPlayingBeforeDrag`
  flag only, not the current `pauseWhileDragging` prop/attribute.
  If the flag turns off between dragStart and dragEnd the user still
  expects the auto-pause to undo itself on release.
- Resume on unmount/disconnect if a drag paused playback —
  createSlider's destroy() does not fire onDragEnd, so without this
  the player would stay paused when the slider is removed mid-drag.

Tests cover both edge cases (React).
@shiminshen shiminshen force-pushed the feat/pause-while-scrubbing-1440 branch from e20eabe to 80d201d Compare May 26, 2026 06:59
@shiminshen

shiminshen commented May 26, 2026

Copy link
Copy Markdown
Contributor

Thanks @R-Delfino95! Rebased on main (no conflicts in the slider files) and both Cursor Bot findings addressed in 80d201d:

  • Resume gated on current flag: the drag-end guard now depends only on the captured wasPlayingBeforeDrag flag. That flag is already a strict subset (only set when pauseWhileDragging is true at drag start and playback is playing), so dropping the redundant prop check means a mid-drag toggle no longer leaves the player paused. Symmetry: "if we paused it, we resume it."
  • Unmount mid-drag skips resume: added a useEffect cleanup (React) and a top-of-disconnectedCallback check (HTML) that resume playback if the slider goes away during an active drag. The HTML version reads playback state before super.disconnectedCallback() so the PlayerController is still attached.

Two new React tests cover both edge cases. Existing tests still pass; lint and the React/HTML time-slider typecheck are clean.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 80d201d. Configure here.

Comment thread packages/html/src/ui/time-slider/time-slider-element.ts Outdated
createSlider's destroy() does not fire onDragEnd, so destroying the
element while a drag had paused playback would leave the player paused.
disconnectedCallback covered DOM removal, but destroy() can also be
called directly (e.g. keep-alive) without going through disconnect.

Extract the resume logic into a private helper and call it from both
disconnectedCallback and destroyCallback before super so the
PlayerController is still attached.

@mihar-22 mihar-22 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, Renzo. Before a deeper review I'd like to see more conformance with our core design.

  1. Lets rename this to pauseOnDrag. Little more terse.
  2. Move core state logic and props to the TimeSliderCore class. Please see other core classes and interfaces as reference. Make sure they're documented.
  3. The PR title feat(html,react) contains an invalid scope. Here it'd be feat(packages)

Comment on lines +122 to +123
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can inline the handlers here to avoid naming/defining them above and separating from config.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2ae34cc — and after 71ef8a8 they just call core.startDrag/endDrag.

Comment on lines +35 to +37
} satisfies PropertyDeclarationMap<Exclude<keyof TimeSliderCore.Props, 'value' | 'min' | 'max'>> & {
pauseWhileDragging: { type: BooleanConstructor; attribute: string };
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This union & of types shouldn't happen, types come from TimeSliderCore.Props

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2ae34cc — types come from TimeSliderCore.Props now.

Comment on lines +46 to +50
/**
* When true, pause playback while the user is dragging the thumb,
* resuming on release if it was playing before. Default `false`.
*/
pauseWhileDragging = false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Descriptions don't belong here, they should be derived from implementing TimeSliderCore.Props. See how above none of them have it. Default value comes from core too.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 2ae34cc — description and default come from core now.

…rops

Move the pauseWhileDragging prop type and default value into TimeSliderCore
so HTML and React layers can derive them. Drops the `&`-intersection workaround
on the satisfies clause, the duplicate JSDoc, and the inline `false` default.
Also inlines the React drag handlers into the useSlider config to keep the
config and handlers co-located.
…gic into TimeSliderCore

Drag pause/resume state now lives in TimeSliderCore as documented startDrag/
endDrag methods (following the PlayButtonCore.toggle pattern), so the HTML and
React layers only delegate. The HTML attribute is now pause-on-drag.
@damonshen17

Copy link
Copy Markdown

@mihar-22 done in 71ef8a8:

  1. Renamed to pauseOnDrag (pause-on-drag attribute on the element).
  2. Moved the pause/resume logic into TimeSliderCore as documented startDrag/endDrag methods (same shape as PlayButtonCore.toggle) — html/react just delegate now. Added core unit tests for it.
  3. On the title — I cannot edit it since @R-Delfino95 opened the PR. Renzo, mind changing it to feat(packages): add pauseOnDrag to time slider?

Tests, typecheck and lint all pass.

@R-Delfino95 R-Delfino95 changed the title feat(html,react): add pauseWhileDragging to time slider feat(packages): add pauseOnDrag to time slider Jun 8, 2026

@mihar-22 mihar-22 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty!!

@mihar-22 mihar-22 merged commit 131e176 into videojs:main Jun 30, 2026
19 of 21 checks passed
@luwes luwes mentioned this pull request Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Pause While Scrubbing

5 participants