Skip to content

Commit

Permalink
feat(playlist-plugin): implement smart navigation based on playback p…
Browse files Browse the repository at this point in the history
…osition

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
    }
  }
});
```
  • Loading branch information
jboix committed Jun 21, 2024
1 parent 917ed75 commit 4cf92de
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 15 deletions.
22 changes: 12 additions & 10 deletions packages/pillarbox-playlist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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:

Expand Down
61 changes: 56 additions & 5 deletions packages/pillarbox-playlist/src/pillarbox-playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,32 @@ class PillarboxPlaylist extends Plugin {
* @private
*/
items_ = [];

/**
* The current index.
*
* @type {number}
* @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
Expand Down Expand Up @@ -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_);
}

Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/pillarbox-playlist/test/pillarbox-playlist.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit 4cf92de

Please sign in to comment.