From 57d6ab65ea8dbbb7718c90754f4421918c3b2c28 Mon Sep 17 00:00:00 2001 From: Alex Barstow Date: Mon, 25 Nov 2024 16:59:10 -0500 Subject: [PATCH 1/4] feat: Add option to disable seeking while scrubbing on mobile (#8903) ## Description On desktop, a user can hover over the progress bar while content plays, which makes it possible to seek to a relatively precise location without disrupting playback. On mobile there is no hovering, so in order to seek during inline playback the user can only tap a location on the progress bar (very hard to do precisely on a small screen) or scrub to try to hone in on a specific location (can be very clunky because seeks are constantly being executed). This PR adds a feature to treat scrubbing on mobile more like hovering on desktop-- while scrubbing, seeks are disabled and playback continues, only when the user finishes scrubbing is a single seek executed to the desired location. One key use-case for this feature is thumbnail seeking integrations on mobile, where the user can scrub through different thumbnail images until they find their desired seek location. ## Specific Changes proposed This behavior is similar to the existing `enableSmoothSeeking` behavior in that the `PlayProgressBar` slider visibly updates with the scrubbing movements, but differs in a few ways: - Playback continues while scrubbing, no seeks are executed until `touchend`. - The seek bar's `TimeTooltip` component displays the target seek time while scrubbing, rather than the `CurrentTimeDisplay` (which continues to show the current time of the playing content). --- src/css/components/_progress.scss | 10 +- .../progress-control/progress-control.js | 4 +- .../control-bar/progress-control/seek-bar.js | 50 +++++- src/js/player.js | 3 +- test/unit/controls.test.js | 14 ++ test/unit/player.test.js | 148 ++++++++++++++++++ 6 files changed, 216 insertions(+), 13 deletions(-) diff --git a/src/css/components/_progress.scss b/src/css/components/_progress.scss index e1c1851009..362901fc97 100644 --- a/src/css/components/_progress.scss +++ b/src/css/components/_progress.scss @@ -41,7 +41,8 @@ // This increases the size of the progress holder so there is an increased // hit area for clicks/touches. -.video-js .vjs-progress-control:hover .vjs-progress-holder { +.video-js .vjs-progress-control:hover .vjs-progress-holder, +.video-js.vjs-scrubbing.vjs-touch-enabled .vjs-progress-control .vjs-progress-holder { font-size: 1.666666666666666666em; } @@ -143,7 +144,8 @@ } .video-js .vjs-progress-control:hover .vjs-time-tooltip, -.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip { +.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip, +.video-js.vjs-scrubbing.vjs-touch-enabled .vjs-progress-control .vjs-time-tooltip { display: block; // Ensure that we maintain a font-size of ~10px. @@ -172,6 +174,10 @@ display: block; } +.video-js.vjs-scrubbing.vjs-touch-enabled .vjs-progress-control .vjs-mouse-display { + display: block; +} + .video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display { visibility: hidden; opacity: 0; diff --git a/src/js/control-bar/progress-control/progress-control.js b/src/js/control-bar/progress-control/progress-control.js index cbff618485..edb39d6920 100644 --- a/src/js/control-bar/progress-control/progress-control.js +++ b/src/js/control-bar/progress-control/progress-control.js @@ -141,7 +141,7 @@ class ProgressControl extends Component { } this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_); - this.off(this.el_, 'mousemove', this.handleMouseMove); + this.off(this.el_, ['mousemove', 'touchmove'], this.handleMouseMove); this.removeListenersAddedOnMousedownAndTouchstart(); @@ -172,7 +172,7 @@ class ProgressControl extends Component { } this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_); - this.on(this.el_, 'mousemove', this.handleMouseMove); + this.on(this.el_, ['mousemove', 'touchmove'], this.handleMouseMove); this.removeClass('disabled'); this.enabled_ = true; diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js index b59a65534f..b078b3d0f0 100644 --- a/src/js/control-bar/progress-control/seek-bar.js +++ b/src/js/control-bar/progress-control/seek-bar.js @@ -8,6 +8,7 @@ import * as Dom from '../../utils/dom.js'; import * as Fn from '../../utils/fn.js'; import {formatTime} from '../../utils/time.js'; import {silencePromise} from '../../utils/promise'; +import {merge} from '../../utils/obj'; import document from 'global/document'; /** @import Player from '../../player' */ @@ -40,7 +41,23 @@ class SeekBar extends Slider { * The key/value store of player options. */ constructor(player, options) { + options = merge(SeekBar.prototype.options_, options); + + // Avoid mutating the prototype's `children` array by creating a copy + options.children = [...options.children]; + + const shouldDisableSeekWhileScrubbingOnMobile = player.options_.disableSeekWhileScrubbingOnMobile && (IS_IOS || IS_ANDROID); + + // Add the TimeTooltip as a child if we are on desktop, or on mobile with `disableSeekWhileScrubbingOnMobile: true` + if ((!IS_IOS && !IS_ANDROID) || shouldDisableSeekWhileScrubbingOnMobile) { + options.children.splice(1, 0, 'mouseTimeDisplay'); + } + super(player, options); + + this.shouldDisableSeekWhileScrubbingOnMobile_ = shouldDisableSeekWhileScrubbingOnMobile; + this.pendingSeekTime_ = null; + this.setEventHandlers_(); } @@ -225,6 +242,12 @@ class SeekBar extends Slider { * The percentage of media played so far (0 to 1). */ getPercent() { + // If we have a pending seek time, we are scrubbing on mobile and should set the slider percent + // to reflect the current scrub location. + if (this.pendingSeekTime_) { + return this.pendingSeekTime_ / this.player_.duration(); + } + const currentTime = this.getCurrentTime_(); let percent; const liveTracker = this.player_.liveTracker; @@ -260,7 +283,12 @@ class SeekBar extends Slider { event.stopPropagation(); this.videoWasPlaying = !this.player_.paused(); - this.player_.pause(); + + // Don't pause if we are on mobile and `disableSeekWhileScrubbingOnMobile: true`. + // In that case, playback should continue while the player scrubs to a new location. + if (!this.shouldDisableSeekWhileScrubbingOnMobile_) { + this.player_.pause(); + } super.handleMouseDown(event); } @@ -324,8 +352,12 @@ class SeekBar extends Slider { } } - // Set new time (tell player to seek to new time) - this.userSeek_(newTime); + // if on mobile and `disableSeekWhileScrubbingOnMobile: true`, keep track of the desired seek point but we won't initiate the seek until 'touchend' + if (this.shouldDisableSeekWhileScrubbingOnMobile_) { + this.pendingSeekTime_ = newTime; + } else { + this.userSeek_(newTime); + } if (this.player_.options_.enableSmoothSeeking) { this.update(); @@ -371,6 +403,13 @@ class SeekBar extends Slider { } this.player_.scrubbing(false); + // If we have a pending seek time, then we have finished scrubbing on mobile and should initiate a seek. + if (this.pendingSeekTime_) { + this.userSeek_(this.pendingSeekTime_); + + this.pendingSeekTime_ = null; + } + /** * Trigger timeupdate because we're done seeking and the time has changed. * This is particularly useful for if the player is paused to time the time displays. @@ -513,10 +552,5 @@ SeekBar.prototype.options_ = { barName: 'playProgressBar' }; -// MouseTimeDisplay tooltips should not be added to a player on mobile devices -if (!IS_IOS && !IS_ANDROID) { - SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay'); -} - Component.registerComponent('SeekBar', SeekBar); export default SeekBar; diff --git a/src/js/player.js b/src/js/player.js index f5f3d22ed4..95349cdfdd 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -5567,7 +5567,8 @@ Player.prototype.options_ = { horizontalSeek: false }, // Default smooth seeking to false - enableSmoothSeeking: false + enableSmoothSeeking: false, + disableSeekWhileScrubbingOnMobile: false }; TECH_EVENTS_RETRIGGER.forEach(function(event) { diff --git a/test/unit/controls.test.js b/test/unit/controls.test.js index fcbc0be3e3..0a05551268 100644 --- a/test/unit/controls.test.js +++ b/test/unit/controls.test.js @@ -223,6 +223,20 @@ QUnit.test('SeekBar should be filled on 100% when the video/audio ends', functio window.cancelAnimationFrame = oldCAF; }); +QUnit.test('Seek bar percent should represent scrub location if we are scrubbing on mobile and have a pending seek time', function(assert) { + const player = TestHelpers.makePlayer(); + const seekBar = player.controlBar.progressControl.seekBar; + + player.duration(100); + seekBar.pendingSeekTime_ = 20; + + assert.equal(seekBar.getPercent(), 0.2, 'seek bar percent set correctly to pending seek time'); + + seekBar.pendingSeekTime_ = 50; + + assert.equal(seekBar.getPercent(), 0.5, 'seek bar percent set correctly to next pending seek time'); +}); + QUnit.test('playback rate button is hidden by default', function(assert) { assert.expect(1); diff --git a/test/unit/player.test.js b/test/unit/player.test.js index 4e6cbfa286..4608508808 100644 --- a/test/unit/player.test.js +++ b/test/unit/player.test.js @@ -3611,6 +3611,154 @@ QUnit.test('smooth seeking set to true should update the display time components player.dispose(); }); +QUnit.test('mouseTimeDisplay should be added as child when disableSeekWhileScrubbingOnMobile is true on mobile', function(assert) { + const originalIsIos = browser.IS_IOS; + + browser.stub_IS_IOS(true); + + const player = TestHelpers.makePlayer({ disableSeekWhileScrubbingOnMobile: true }); + const seekBar = player.controlBar.progressControl.seekBar; + const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); + + assert.ok(mouseTimeDisplay, 'mouseTimeDisplay added as a child'); + + player.dispose(); + browser.stub_IS_IOS(originalIsIos); +}); + +QUnit.test('mouseTimeDisplay should not be added as child on mobile when disableSeekWhileScrubbingOnMobile is false', function(assert) { + const originalIsIos = browser.IS_IOS; + + browser.stub_IS_IOS(true); + + const player = TestHelpers.makePlayer({ disableSeekWhileScrubbingOnMobile: false }); + const seekBar = player.controlBar.progressControl.seekBar; + const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); + + assert.notOk(mouseTimeDisplay, 'mouseTimeDisplay not added as a child'); + + player.dispose(); + browser.stub_IS_IOS(originalIsIos); +}); + +QUnit.test('Seeking should occur while scrubbing on mobile when disableSeekWhileScrubbingOnMobile is false', function(assert) { + const originalIsIos = browser.IS_IOS; + + browser.stub_IS_IOS(true); + + const player = TestHelpers.makePlayer({ disableSeekWhileScrubbingOnMobile: false }); + const seekBar = player.controlBar.progressControl.seekBar; + const userSeekSpy = sinon.spy(seekBar, 'userSeek_'); + + // Simulate a source loaded + player.duration(10); + + // Simulate scrub + seekBar.handleMouseMove({ pageX: 200 }); + + assert.ok(userSeekSpy.calledOnce, 'Seek initiated while scrubbing'); + + player.dispose(); + browser.stub_IS_IOS(originalIsIos); +}); + +QUnit.test('Seeking should not occur while scrubbing on mobile when disableSeekWhileScrubbingOnMobile is true', function(assert) { + const originalIsIos = browser.IS_IOS; + + browser.stub_IS_IOS(true); + + const player = TestHelpers.makePlayer({ disableSeekWhileScrubbingOnMobile: true }); + const seekBar = player.controlBar.progressControl.seekBar; + const userSeekSpy = sinon.spy(seekBar, 'userSeek_'); + + // Simulate a source loaded + player.duration(10); + + // Simulate scrub + seekBar.handleMouseMove({ pageX: 200 }); + + assert.ok(userSeekSpy.notCalled, 'Seek not initiated while scrubbing'); + + player.dispose(); + browser.stub_IS_IOS(originalIsIos); +}); + +QUnit.test('Seek should occur when scrubbing completes on mobile when disableSeekWhileScrubbingOnMobile is true', function(assert) { + const originalIsIos = browser.IS_IOS; + + browser.stub_IS_IOS(true); + + const player = TestHelpers.makePlayer({ disableSeekWhileScrubbingOnMobile: true }); + const seekBar = player.controlBar.progressControl.seekBar; + const userSeekSpy = sinon.spy(seekBar, 'userSeek_'); + const targetSeekTime = 5; + + // Simulate a source loaded + player.duration(10); + + seekBar.pendingSeekTime_ = targetSeekTime; + + // Simulate scrubbing completion + seekBar.handleMouseUp(); + + assert.ok(userSeekSpy.calledWith(targetSeekTime), 'Seeks to correct location when scrubbing completes'); + + player.dispose(); + browser.stub_IS_IOS(originalIsIos); +}); + +QUnit.test('Player should pause while scrubbing on mobile when disableSeekWhileScrubbingOnMobile is false', function(assert) { + const originalIsIos = browser.IS_IOS; + + browser.stub_IS_IOS(true); + + const player = TestHelpers.makePlayer({ disableSeekWhileScrubbingOnMobile: false }); + const seekBar = player.controlBar.progressControl.seekBar; + const pauseSpy = sinon.spy(player, 'pause'); + + // Simulate start playing + player.play(); + + const mockMouseDownEvent = { + pageX: 200, + stopPropagation: () => {} + }; + + // Simulate scrubbing start + seekBar.handleMouseDown(mockMouseDownEvent); + + assert.ok(pauseSpy.calledOnce, 'Player paused'); + + player.dispose(); + browser.stub_IS_IOS(originalIsIos); +}); + +QUnit.test('Player should not pause while scrubbing on mobile when disableSeekWhileScrubbingOnMobile is true', function(assert) { + const originalIsIos = browser.IS_IOS; + + browser.stub_IS_IOS(true); + + const player = TestHelpers.makePlayer({ disableSeekWhileScrubbingOnMobile: true }); + const seekBar = player.controlBar.progressControl.seekBar; + const pauseSpy = sinon.spy(player, 'pause'); + + // Simulate start playing + player.play(); + + const mockMouseDownEvent = { + pageX: 200, + stopPropagation: () => { } + }; + + // Simulate scrubbing start + seekBar.handleMouseDown(mockMouseDownEvent); + + assert.ok(pauseSpy.notCalled, 'Player not paused'); + + player.dispose(); + browser.stub_IS_IOS(originalIsIos); +}); + QUnit.test('addSourceElement calls tech method with correct args', function(assert) { const player = TestHelpers.makePlayer(); const addSourceElementSpy = sinon.spy(player.tech_, 'addSourceElement'); From f87a699f2d6834a256cbd0ce5e6ab2dd5b95f61a Mon Sep 17 00:00:00 2001 From: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:11:13 -0500 Subject: [PATCH 2/4] fix: update vhs version (#8930) bump vhs version (3.16.1) Co-authored-by: Dzianis Dashkevich --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad7e19f78a..2199c0038c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1791,9 +1791,9 @@ } }, "@videojs/http-streaming": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.16.0.tgz", - "integrity": "sha512-VL8l+JGbc9KqZ1fY2pYgBS1u3i6iQ/5mRAE6bwrI5R0RAtKxur1hjipVGwkkJSYRzwLgArt5Wg5abEjfoJN7yA==", + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.16.1.tgz", + "integrity": "sha512-+5FXE0q8sRD6lPVdGsDL0N8q4+IiQEjMtoCSu0js1oftdCchP9bbhR9ac8XUGJUCZ53KoeY02izkjyGYK2hpGw==", "requires": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.1.1", diff --git a/package.json b/package.json index 22600b7615..e0aafefc92 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "^3.16.0", + "@videojs/http-streaming": "^3.16.1", "@videojs/vhs-utils": "^4.1.1", "@videojs/xhr": "2.7.0", "aes-decrypter": "^4.0.2", From a7ba9f2fc5e1ccd0da153f52fd73fdf6b78fc30c Mon Sep 17 00:00:00 2001 From: Harisha Rajam Swaminathan <35213866+harisha-swaminathan@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:29:33 -0500 Subject: [PATCH 3/4] chore: update VHS version (#8933) --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2199c0038c..b41f82f9af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1791,9 +1791,9 @@ } }, "@videojs/http-streaming": { - "version": "3.16.1", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.16.1.tgz", - "integrity": "sha512-+5FXE0q8sRD6lPVdGsDL0N8q4+IiQEjMtoCSu0js1oftdCchP9bbhR9ac8XUGJUCZ53KoeY02izkjyGYK2hpGw==", + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.16.2.tgz", + "integrity": "sha512-fvt4ko7FknxiT9FnjyNQt6q2px+awrkM+Orv7IB/4gldvj94u4fowGfmNHynnvNTPgPkdxHklGmFLGfclYw8HA==", "requires": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.1.1", @@ -15061,12 +15061,12 @@ "dev": true }, "video.js": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.19.1.tgz", - "integrity": "sha512-MVuayhXpzTBv5Jk3nYEU2akawPhuBBlizEbpQGx2i+6FiBmqxGjkrkLdDLOzG54ut7xapjp26IfWQLGSpeLmcQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.20.0.tgz", + "integrity": "sha512-VyXY/DbtfaI22gpWJdo8bmTcpPRfKg0SeQJBusRdIJF1RMI+er1BHpRreg67s5Qfd9ZeSbfKShUOwaxRft/tBw==", "requires": { "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "^3.15.0", + "@videojs/http-streaming": "^3.16.0", "@videojs/vhs-utils": "^4.1.1", "@videojs/xhr": "2.7.0", "aes-decrypter": "^4.0.2", diff --git a/package.json b/package.json index e0aafefc92..24f9df12f5 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "^3.16.1", + "@videojs/http-streaming": "^3.16.2", "@videojs/vhs-utils": "^4.1.1", "@videojs/xhr": "2.7.0", "aes-decrypter": "^4.0.2", From ca6f8235453d0d45ae4d05c8b056e2a8c206bc26 Mon Sep 17 00:00:00 2001 From: hswaminathan Date: Thu, 5 Dec 2024 14:36:40 -0500 Subject: [PATCH 4/4] 8.21.0 --- CHANGELOG.md | 15 +++++++++++++++ README.md | 12 ++++++------ package-lock.json | 2 +- package.json | 2 +- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b33264d959..6de5275352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ + +# [8.21.0](https://github.com/videojs/video.js/compare/v8.20.0...v8.21.0) (2024-12-05) + +### Features + +* Add option to disable seeking while scrubbing on mobile ([#8903](https://github.com/videojs/video.js/issues/8903)) ([57d6ab6](https://github.com/videojs/video.js/commit/57d6ab6)) + +### Bug Fixes + +* update vhs version ([#8930](https://github.com/videojs/video.js/issues/8930)) ([f87a699](https://github.com/videojs/video.js/commit/f87a699)) + +### Chores + +* update VHS version ([#8933](https://github.com/videojs/video.js/issues/8933)) ([a7ba9f2](https://github.com/videojs/video.js/commit/a7ba9f2)) + # [8.20.0](https://github.com/videojs/video.js/compare/v8.19.2...v8.20.0) (2024-11-19) diff --git a/README.md b/README.md index 9dcb23c6bf..556e6d15ff 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Video.js was started in the middle of 2010 and is now used on over ~~50,000~~ ~~ Thanks to the awesome folks over at [Fastly][fastly], there's a free, CDN hosted version of Video.js that anyone can use. Add these tags to your document's ``: ```html - - + + ``` Alternatively, you can include Video.js by getting it from [npm](https://videojs.com/getting-started/#install-via-npm), downloading it from [GitHub releases](https://github.com/videojs/video.js/releases) or by including it via [unpkg](https://unpkg.com) or another JavaScript CDN, like CDNjs. @@ -34,12 +34,12 @@ Alternatively, you can include Video.js by getting it from [npm](https://videojs - - + + - - + + ``` Next, using Video.js is as simple as creating a `