From 608a585dcb184ede7d4a1901d2698ca4569810e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <34163393+amtins@users.noreply.github.com> Date: Tue, 2 Jan 2024 19:11:55 +0100 Subject: [PATCH] feat: Seek bar smooth seeking (#8287) * refactor(player): decrease the indentation level in the currentTime method * fix(player): cache_.currentTime is not updated when the current time is set Updating cache_.currentTime as soon as the currentTime is set avoids having to wait for the timeupdate event, which results in: - making cache_.currentTime more reliable - updating the progress bar on mouse up after dragging when the media is paused. See also: #6232, #6234, #6370, #6372 * feat: add an option to handle smooth seeking Adds a player option called enableSmoothSeeking, which is false by default, to provide a smoother seeking experience on mobile and desktop devices. Usage: ```javascript // Enables the smooth seeking const player = videojs('player', {enableSmoothSeeking: true}); // Disable the smooth seeking player.options({enableSmoothSeeking: false}); ``` - **player.js** add an `option` called `enableSmoothSeeking` - **time-display.js** add a listener to the `seeking` event if `enableSmoothSeeking` is `true` allowing to update the `CurrentTimeDisplay` and `RemainingTimeDisplay` in real time - **seek-bar.js** `update` the seek bar on `mousemove` event if `enableSmoothSeeking` is `true` - add test cases --- .../control-bar/progress-control/seek-bar.js | 4 ++ .../control-bar/time-controls/time-display.js | 16 ++++- src/js/player.js | 4 +- test/unit/player.test.js | 63 +++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js index 75bab62894..ad0f452ed2 100644 --- a/src/js/control-bar/progress-control/seek-bar.js +++ b/src/js/control-bar/progress-control/seek-bar.js @@ -324,6 +324,10 @@ class SeekBar extends Slider { // Set new time (tell player to seek to new time) this.userSeek_(newTime); + + if (this.player_.options_.enableSmoothSeeking) { + this.update(); + } } enable() { diff --git a/src/js/control-bar/time-controls/time-display.js b/src/js/control-bar/time-controls/time-display.js index fb9a0ffeeb..9c6af2d0f4 100644 --- a/src/js/control-bar/time-controls/time-display.js +++ b/src/js/control-bar/time-controls/time-display.js @@ -26,7 +26,7 @@ class TimeDisplay extends Component { constructor(player, options) { super(player, options); - this.on(player, ['timeupdate', 'ended'], (e) => this.updateContent(e)); + this.on(player, ['timeupdate', 'ended', 'seeking'], (e) => this.update(e)); this.updateTextNode_(); } @@ -71,6 +71,20 @@ class TimeDisplay extends Component { super.dispose(); } + /** + * Updates the displayed time according to the `updateContent` function which is defined in the child class. + * + * @param {Event} [event] + * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called. + */ + update(event) { + if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') { + return; + } + + this.updateContent(event); + } + /** * Updates the time display text node with a new time * diff --git a/src/js/player.js b/src/js/player.js index eaf8bf1801..2594041816 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -5413,7 +5413,9 @@ Player.prototype.options_ = { breakpoints: {}, responsive: false, audioOnlyMode: false, - audioPosterMode: false + audioPosterMode: false, + // Default smooth seeking to false + enableSmoothSeeking: false }; TECH_EVENTS_RETRIGGER.forEach(function(event) { diff --git a/test/unit/player.test.js b/test/unit/player.test.js index f39fca34d2..7f311c67a3 100644 --- a/test/unit/player.test.js +++ b/test/unit/player.test.js @@ -3423,3 +3423,66 @@ QUnit.test('should not reset the error when the tech triggers an error that is n errorStub.restore(); log.error.restore(); }); + +QUnit.test('smooth seeking set to false should not update the display time components or the seek bar', function(assert) { + const player = TestHelpers.makePlayer({}); + const { + currentTimeDisplay, + remainingTimeDisplay, + progressControl: { + seekBar + } + } = player.controlBar; + const currentTimeDisplayUpdateContent = sinon.spy(currentTimeDisplay, 'updateContent'); + const remainingTimeDisplayUpdateContent = sinon.spy(remainingTimeDisplay, 'updateContent'); + const seekBarUpdate = sinon.spy(seekBar, 'update'); + + assert.false(player.options().enableSmoothSeeking, 'enableSmoothSeeking is false by default'); + + player.trigger('seeking'); + + assert.ok(currentTimeDisplayUpdateContent.notCalled, 'currentTimeDisplay updateContent was not called'); + assert.ok(remainingTimeDisplayUpdateContent.notCalled, 'remainingTimeDisplay updateContent was not called'); + + seekBar.trigger('mousedown'); + seekBar.trigger('mousemove'); + + assert.ok(seekBarUpdate.notCalled, 'seekBar update was not called'); + + currentTimeDisplayUpdateContent.restore(); + remainingTimeDisplayUpdateContent.restore(); + seekBarUpdate.restore(); + player.dispose(); +}); + +QUnit.test('smooth seeking set to true should update the display time components and the seek bar', function(assert) { + const player = TestHelpers.makePlayer({enableSmoothSeeking: true}); + const { + currentTimeDisplay, + remainingTimeDisplay, + progressControl: { + seekBar + } + } = player.controlBar; + const currentTimeDisplayUpdateContent = sinon.spy(currentTimeDisplay, 'updateContent'); + const remainingTimeDisplayUpdateContent = sinon.spy(remainingTimeDisplay, 'updateContent'); + const seekBarUpdate = sinon.spy(seekBar, 'update'); + + assert.true(player.options().enableSmoothSeeking, 'enableSmoothSeeking is true'); + + player.duration(1); + player.trigger('seeking'); + + assert.ok(currentTimeDisplayUpdateContent.called, 'currentTimeDisplay updateContent was called'); + assert.ok(remainingTimeDisplayUpdateContent.called, 'remainingTimeDisplay updateContent was called'); + + seekBar.trigger('mousedown'); + seekBar.trigger('mousemove'); + + assert.ok(seekBarUpdate.called, 'seekBar update was called'); + + currentTimeDisplayUpdateContent.restore(); + remainingTimeDisplayUpdateContent.restore(); + seekBarUpdate.restore(); + player.dispose(); +});