From 4cf92decd4d8810ee55be183902686083115ce69 Mon Sep 17 00:00:00 2001 From: Josep Boix Requesens Date: Thu, 6 Jun 2024 15:17:30 +0200 Subject: [PATCH] feat(playlist-plugin): implement smart navigation based on playback position Resolves #17 by implementing smart navigation to the previous item in the playlist based on the current playback position: - If playback is beyond the threshold, the current media is restarted. - If playback is within the threshold, the previous item is navigated to. - For live media, the previous items is always played regardless of the current playback position. - The threshold functionality can be disabled by setting the value to undefined. Example usage: ```javascript import pillarbox from '@srgssr/pillarbox-web'; import './src/pillarbox-playlist-ui.js'; window.player = pillarbox('player', { plugins: { pillarboxPlaylist: { // The threshold in seconds previousNavigationThreshold: 10 } } }); ``` --- packages/pillarbox-playlist/README.md | 22 ++++--- .../src/pillarbox-playlist.js | 61 +++++++++++++++++-- .../test/pillarbox-playlist.spec.js | 18 ++++++ 3 files changed, 86 insertions(+), 15 deletions(-) diff --git a/packages/pillarbox-playlist/README.md b/packages/pillarbox-playlist/README.md index 5e2c51d..aab969b 100644 --- a/packages/pillarbox-playlist/README.md +++ b/packages/pillarbox-playlist/README.md @@ -81,7 +81,7 @@ The following table outlines the key methods available in the this plugin: | `reverse()` | Reverses the order of the items in the playlist. Adjusts the current index if necessary. | | `sort(compareFn?)` | Sorts the items in the playlist using the provided compare function. Adjusts the current index if necessary. | | `next()` | Advances to the next item in the playlist, with support for repeat mode. | -| `previous()` | Moves to the previous item in the playlist. | +| `previous()` | Navigates to the previous item or restarts the current item based on playback position and threshold. | | `shuffle()` | Randomizes the order of the playlist items using the Fisher-Yates shuffle algorithm. | | `select(index)` | Selects and plays the item at the specified index in the playlist. | | `toggleRepeat(force?)` | Toggles the repeat mode of the player to the opposite of its current state, or sets it to the specified boolean value if provided. | @@ -92,21 +92,23 @@ The following table outlines the key methods available in the this plugin: When initializing the playlist plugin, you can pass an `options` object that configures the behavior of the plugin. Here are the available options: -| Option | Type | Default | Description | -|---------------|---------|---------|---------------------------------------------------------------------------------------------| -| `playlist` | Array | `[]` | An array of playlist items to be initially loaded into the player. | -| `repeat` | Boolean | `false` | If true, the playlist will start over automatically after the last item ends. | -| `autoadvance` | Boolean | `false` | If enabled, the player will automatically move to the next item after the current one ends. | +| Option | Type | Default | Description | +|-------------------------------|---------|---------|---------------------------------------------------------------------------------------------| +| `playlist` | Array | `[]` | An array of playlist items to be initially loaded into the player. | +| `repeat` | Boolean | `false` | If true, the playlist will start over automatically after the last item ends. | +| `autoadvance` | Boolean | `false` | If enabled, the player will automatically move to the next item after the current one ends. | +| `previousNavigationThreshold` | Number | 3 | Threshold in seconds for determining the behavior when navigating to the previous item. | #### Properties After initializing the plugin, you can modify or read these properties to control playlist behavior dynamically: -| Property | Type | Description | -|---------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `repeat` | Boolean | Enables or disables repeating the playlist once the last item has played. Changes take effect immediately and apply to subsequent operations. | -| `autoadvance` | Boolean | Toggles automatic advancement to the next item when the current item ends. | +| Property | Type | Description | +|-------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `repeat` | Boolean | Enables or disables repeating the playlist once the last item has played. Changes take effect immediately and apply to subsequent operations. | +| `autoadvance` | Boolean | Toggles automatic advancement to the next item when the current item ends. | +| `previousNavigationThreshold` | Number | Threshold in seconds for determining the behavior when navigating to the previous item. | The following properties are read-only: diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist.js b/packages/pillarbox-playlist/src/pillarbox-playlist.js index 34aff91..432dccc 100644 --- a/packages/pillarbox-playlist/src/pillarbox-playlist.js +++ b/packages/pillarbox-playlist/src/pillarbox-playlist.js @@ -18,6 +18,7 @@ class PillarboxPlaylist extends Plugin { * @private */ items_ = []; + /** * The current index. * @@ -25,6 +26,24 @@ class PillarboxPlaylist extends Plugin { * @private */ currentIndex_ = -1; + + /** + * Threshold in seconds for determining the behavior when navigating to the previous item. + * + * - If the media is live, {@link previous} will navigate to the previous item, + * regardless of the threshold. + * - If the playback position is within this threshold, {@link previous} will + * navigate to the previous item. + * - If the playback position is beyond this threshold, {@link previous} will + * restart the current media. + * + * To disable this functionality, set the value to undefined or infinity. + * + * @type {number} + * @default 3 + */ + previousNavigationThreshold = 3; + /** * Whether the repeat is enabled or not. If repeat is enabled once the last * element of the playlist ends the next element will be the first one. This @@ -73,18 +92,30 @@ class PillarboxPlaylist extends Plugin { /** * Creates an instance of a pillarbox playlist. * - * @param {import('video.js/dist/types/player.js').default} player The player instance. - * @param {Object} options Configuration options for the plugin. + * @param {import('video.js/dist/types/player.js').default} player - The player instance. + * @param {Object} options - Configuration options for the plugin. + * @param {Array} [options.playlist=[]] - An array of playlist items to be initially loaded into the player. + * @param {Boolean} [options.repeat=false] - If true, the playlist will start over automatically after the last item ends. + * @param {Boolean} [options.autoadvance=false] - If enabled, the player will automatically move to the next item after the current one ends. + * @param {Number} [options.previousNavigationThreshold=3] - Threshold in seconds for determining the behavior when navigating to the previous item. */ constructor(player, options) { super(player); + + options = this.options_ = videojs.obj.merge(this.options_, options); if (options.playlist && options.playlist.length) { player.ready(() => { this.load(...options.playlist); }); } - this.autoadvance = !!options.autoadvance; - this.repeat = !!options.repeat; + + this.autoadvance = Boolean(options.autoadvance); + this.repeat = Boolean(options.repeat); + this.previousNavigationThreshold = + Number.isFinite(options.previousNavigationThreshold) ? + options.previousNavigationThreshold : + this.previousNavigationThreshold; + this.player.on('ended', this.onEnded_); } @@ -296,12 +327,32 @@ class PillarboxPlaylist extends Plugin { } /** - * Moves to the previous item in the playlist. + * Navigates to the previous item in the playlist or restarts the current + * media based on playback position. + * + * - If the media is live, navigates to the previous item regardless of the threshold. + * - If playback is beyond the threshold, restarts the current media. + * - If playback is within the threshold, navigates to the previous item. + * + * @see previousNavigationThreshold */ previous() { + if (!this.isLive() && + this.player.currentTime() > this.previousNavigationThreshold) { + this.player.currentTime(0); + + return; + } + this.select(this.currentIndex_ - 1); } + isLive() { + const liveTracker = this.player.liveTracker; + + return liveTracker && liveTracker.isLive(); + } + /** * Whether an element exists before the one that is currently playing. * If `repeat` mode is enabled this function will still return `false` when the diff --git a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js index 4f1adb3..d804dbb 100644 --- a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js +++ b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js @@ -216,6 +216,24 @@ describe('PillarboxPlaylist', () => { expect(srcSpy).toHaveBeenLastCalledWith(playlist[1].sources); expect(posterSpy).toHaveBeenLastCalledWith(playlist[1].poster); }); + + it('should restart the current media if the current time is beyond the threshold', () => { + // Given + const currentTime = vi.spyOn(player, 'currentTime').mockImplementation(() => pillarboxPlaylist.previousNavigationThreshold + 1); + + // When + pillarboxPlaylist.load(playlist); + pillarboxPlaylist.select(2); + pillarboxPlaylist.previous(); + + // Then + expect(pillarboxPlaylist.hasPrevious()).toBeTruthy(); + expect(pillarboxPlaylist.hasNext()).toBeTruthy(); + expect(pillarboxPlaylist.items.length).toBe(4); + expect(pillarboxPlaylist.currentIndex).toBe(2); + expect(pillarboxPlaylist.currentItem).toBe(playlist[2]); + expect(currentTime).toHaveBeenLastCalledWith(0); + }); }); describe('autoadvance', () => {