diff --git a/package-lock.json b/package-lock.json
index c1b3d5e..525d193 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5360,6 +5360,10 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true
},
+ "node_modules/@srgssr/pillarbox-playlist": {
+ "resolved": "packages/pillarbox-playlist",
+ "link": true
+ },
"node_modules/@srgssr/pillarbox-web": {
"version": "1.12.1",
"resolved": "https://npm.pkg.github.com/download/@srgssr/pillarbox-web/1.12.1/3f44c09ac56e6356c98ef3a9b583bcc4b5108e1c",
@@ -18270,6 +18274,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "packages/pillarbox-playlist": {
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@srgssr/pillarbox-web": "^1.12.1"
+ }
+ },
"packages/skip-button": {
"name": "@srgssr/skip-button",
"version": "0.0.1",
diff --git a/packages/pillarbox-playlist/.babelrc b/packages/pillarbox-playlist/.babelrc
new file mode 100644
index 0000000..ac08da0
--- /dev/null
+++ b/packages/pillarbox-playlist/.babelrc
@@ -0,0 +1,3 @@
+{
+ "extends": "../../babel.config.json"
+}
diff --git a/packages/pillarbox-playlist/README.md b/packages/pillarbox-playlist/README.md
new file mode 100644
index 0000000..1213f6f
--- /dev/null
+++ b/packages/pillarbox-playlist/README.md
@@ -0,0 +1,139 @@
+# Pillarbox Web: Playlist Plugin
+
+This plugin extends the pillarbox-web player with playlist management capabilities. It allows to
+load, manage, and control playback of a sequence of videos with options for auto-advancing,
+repeating content, and dynamic playlist modification.
+
+## Requirements
+
+To use this plugin, you need the following installed on your system:
+
+- Node.js
+
+## Quick Start
+
+To get started with this plugin, follow these steps:
+
+Add the `@srgssr` registry to your `.npmrc` file:
+
+```plaintext
+//npm.pkg.github.com/:_authToken=TOKEN
+@srgssr:registry=https://npm.pkg.github.com
+```
+
+Generate a personal access token on the [Personal Access Tokens page][token-settings]. For more
+information on using tokens with GitHub packages,
+visit: [Authenticating with a Personal Access Token][token-guide].
+
+You can now install it through `npm` the following command:
+
+```bash
+npm install --save @srgssr/pillarbox-web @srgssr/pillarbox-playlist
+```
+
+For instructions on setting up Pillarbox, see
+the [Quick Start guide](SRGSSR/pillarbox-web#quick-start).
+
+Once the player is installed you can activate the plugin as follows:
+
+```javascript
+import Pillarbox from '@srgssr/pillarbox-web';
+import '@srgssr/pillarbox-playlist';
+
+const player = new Pillarbox('my-player', {
+ plugins: { pillarboxPlaylist: { autoadvance: true, repeat: true } }
+});
+
+const playlist = [
+ { sources: [{ src: 'video1.mp4', type: 'video/mp4' }], poster: 'poster1.jpg' },
+ { sources: [{ src: 'video2.mp4', type: 'video/mp4' }], poster: 'poster2.jpg' }
+];
+
+player.playlistPlugin().load(playlist);
+```
+
+### API Documentation
+
+#### Methods
+
+The following table outlines the key methods available in the this plugin:
+
+| Function | Description |
+|----------------------------------------|--------------------------------------------------------------------------------------------------------|
+| `load(items)` | Initializes the playlist with the given items and starts playback from the first item. |
+| `push(...items)` | Adds new items to the end of the current playlist. |
+| `splice(start, deleteCount, ...items)` | Modifies the playlist by adding, removing, or replacing items. 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. |
+| `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. |
+
+#### Options
+
+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. |
+
+#### 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. |
+
+The following properties are read-only:
+
+| Property | Type | Description |
+|----------------|--------|------------------------------------------------------------------------------------------------------------------------------|
+| `currentIndex` | Number | Retrieves the index of the currently playing item. |
+| `currentItem` | Object | Retrieves the currently playing item. |
+| `items` | Array | Retrieves all items in the playlist. Modifications to the returned array will not affect the internal state of the playlist. |
+
+## Contributing
+
+For detailed contribution guidelines, refer to the main project’s [README file][main-readme]. Please
+adhere to the specified guidelines.
+
+### Setting up a development server
+
+Start the development server:
+
+```bash
+npm run start
+```
+
+This will start the server on `http://localhost:4200`. Open this URL in your browser to view the
+demo page.
+
+The video player (`player`) and the Pillarbox library (`pillarbox`) are exposed on the `window`
+object, making it easy to access and manipulate from the browser's developer console for debugging.
+
+#### Available URL parameters
+
+The demo page supports several URL parameters that modify the behavior of the video player:
+
+- `debug`: Set this to enable debugging mode.
+- `ilHost`: Specifies the host for the data provider.
+- `language`: Sets the language for the player interface.
+
+You can combine parameters in the URL like so:
+
+```plaintext
+http://localhost:4200/?language=fr
+```
+
+## Licensing
+
+This project is licensed under the MIT License. See the [LICENSE](../../LICENSE) file for more
+details.
+
+[main-readme]: ../../docs/README.md#Contributing
+[generate-token]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token
diff --git a/packages/pillarbox-playlist/index.html b/packages/pillarbox-playlist/index.html
new file mode 100644
index 0000000..2faa4ce
--- /dev/null
+++ b/packages/pillarbox-playlist/index.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Pillarbox-Playlist Demo
+
+
+
+
+
+
+
+
diff --git a/packages/pillarbox-playlist/package.json b/packages/pillarbox-playlist/package.json
new file mode 100644
index 0000000..841ae30
--- /dev/null
+++ b/packages/pillarbox-playlist/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@srgssr/pillarbox-playlist",
+ "version": "0.0.1",
+ "type": "module",
+ "main": "dist/pillarbox-playlist.cjs.js",
+ "module": "dist/pillarbox-playlist.es.js",
+ "style": "./dist/pillarbox-playlist.min.css",
+ "exports": {
+ ".": {
+ "import": "./dist/pillarbox-playlist.es.js",
+ "require": "./dist/pillarbox-playlist.cjs.js"
+ },
+ "./*": "./*"
+ },
+ "files": [
+ "dist/*",
+ "scss/*"
+ ],
+ "targets": {
+ "main": false,
+ "github-page": {
+ "publicUrl": "./",
+ "isLibrary": false,
+ "outputFormat": "esmodule"
+ }
+ },
+ "scripts": {
+ "build": "npm run build:lib && npm run build:css",
+ "build:css": "sass ./scss/pillarbox-playlist.scss:dist/pillarbox-playlist.min.css --style compressed --source-map --load-path node_modules",
+ "build:lib": "vite build --config vite.config.lib.js",
+ "github:page": "vite build",
+ "start": " vite --port 4200 --open",
+ "test": "vitest run --silent --coverage --coverage.reporter text"
+ },
+ "peerDependencies": {
+ "@srgssr/pillarbox-web": "^1.12.1"
+ }
+}
diff --git a/packages/pillarbox-playlist/src/pillarbox-playlist.js b/packages/pillarbox-playlist/src/pillarbox-playlist.js
new file mode 100644
index 0000000..484c108
--- /dev/null
+++ b/packages/pillarbox-playlist/src/pillarbox-playlist.js
@@ -0,0 +1,291 @@
+import pillarbox from '@srgssr/pillarbox-web';
+
+/**
+ * @ignore
+ * @type {typeof import('video.js/dist/types/plugin').default}
+ */
+const Plugin = pillarbox.getPlugin('plugin');
+const log = pillarbox.log.createLogger('pillarbox-playlist');
+
+/**
+ * Represents a Plugin that allows control over a playlist.
+ */
+class PillarboxPlaylist extends Plugin {
+ /**
+ * The items in the playlist.
+ *
+ * @type {PlaylistItem[]}
+ * @private
+ */
+ items_ = [];
+ /**
+ * The current index.
+ *
+ * @type {number}
+ * @private
+ */
+ currentIndex_ = -1;
+ /**
+ * 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
+ * mode only works forwards, i.e. when advancing to the next element.
+ *
+ * @type boolean
+ */
+ repeat = false;
+ /**
+ * Whether auto-advance is enabled or not.
+ *
+ * @type boolean
+ */
+ autoadvance = false;
+
+ /**
+ * Handles the 'ended' event when triggered. This method serves as a proxy to
+ * the main `ended` handler, ensuring that additional logic can be executed or
+ * making it easier to detach the event listener later.
+ *
+ * @private
+ */
+ onEnded_ = () => this.handleEnded();
+
+ /**
+ * Creates an instance of a pillarbox playlist.
+ *
+ * @param {import('@srgssr/pillarbox-web').Player} player The player instance.
+ * @param {Object} options Configuration options for the plugin.
+ */
+ constructor(player, options) {
+ super(player);
+ if (options.playlist && options.playlist.length) {
+ player.ready(() => {
+ this.load(...options.playlist);
+ });
+ }
+ this.autoadvance = !!options.autoadvance;
+ this.repeat = !!options.repeat;
+ this.player.on('ended', this.onEnded_);
+ }
+
+ dispose() {
+ this.player.off('ended', this.onEnded_);
+ }
+
+ /**
+ * Loads a playlist into the player. This method will load the first element
+ * in the playlist. Use it to initialize the playlist.
+ *
+ * Note: A copy of the playlist items array is made internally to ensure that
+ * external modifications to the array do not affect the internal state and
+ * vice versa.
+ *
+ * @param {PlaylistItem[]} items The playlist items to load.
+ */
+ load(items) {
+ this.items_ = [...items];
+ this.select(0);
+ }
+
+ /**
+ * Adds one or more items at the end of the playlist. This method will not
+ * load any of the elements. Use it to add items while the playlist is
+ * running.
+ *
+ * @param {...PlaylistItem} items the items to add to the playlist.
+ */
+ push(...items) {
+ this.items_.push(...items);
+ }
+
+ /**
+ * Modifies the contents of the playlist by removing or replacing existing
+ * elements and/or adding new elements.
+ *
+ * The method also adjusts currentIndex accordingly if items are added or
+ * removed in such a way that it affects the currentIndex.
+ *
+ * If the current item is deleted then the currentIndex becomes -1, the
+ * current element will continue playing but the next element will be the
+ * first element in the playlist.
+ *
+ * @param {number} start The zero-based location in the array from which to
+ * start removing elements.
+ * @param {number} deleteCount The number of elements to remove.
+ * @param {...PlaylistItem} items The items to add to the playlist.
+ *
+ * @return {PlaylistItem[]} An array containing the deleted elements.
+ */
+ splice(start, deleteCount, ...items) {
+ const itemsAddedCount = items.length;
+ const effectiveDeleteCount =
+ Math.min(deleteCount, this.items_.length - start);
+
+ if (this.currentIndex_ < start) {
+ return this.items_.splice(start, deleteCount, ...items);
+ }
+
+ // Adjust currentIndex for items being deleted
+ if (this.currentIndex_ < start + effectiveDeleteCount) {
+ // Current item is removed, set currentIndex to -1
+ this.currentIndex_ = -1;
+ } else {
+ // Adjust currentIndex based on the net items added/removed
+ this.currentIndex_ =
+ this.currentIndex_ - effectiveDeleteCount + itemsAddedCount;
+ }
+
+ return this.items_.splice(start, deleteCount, ...items);
+ }
+
+ /**
+ * Get the currently playing index.
+ *
+ * @returns {number} the currently playing index.
+ */
+ get currentIndex() {
+ return this.currentIndex_;
+ }
+
+ /**
+ * Get the currently playing item.
+ *
+ * @returns {PlaylistItem} the currently playing item.
+ */
+ get currentItem() {
+ return this.items_[this.currentIndex_];
+ }
+
+ /**
+ * Get the current playlist items.This is a copy of the internal list
+ * modifying this list will not affect the playlist. use `push` and `splice`
+ * to modify the internal list.
+ *
+ * @returns {PlaylistItem[]} the current list of items.
+ */
+ get items() {
+ return [...this.items_];
+ }
+
+ /**
+ * Plays the playlist item at the given index. If the index is not in
+ * the playlist this method has no effect.
+ *
+ * @param {number} index The index of the item to play.
+ */
+ // eslint-disable-next-line max-statements
+ select(index) {
+ if (index < 0 || index >= this.items_.length) {
+ log.warn(`Index: ${index} is out of bounds (The current playlist has ${this.items_.length} elements)`);
+
+ return;
+ }
+
+ if (index === this.currentIndex_) {
+ log.warn(`Index: ${index} is already selected`);
+
+ return;
+ }
+
+ const item = this.items_[index];
+
+ this.player.src(item.sources);
+ this.player.poster(item.poster);
+ this.currentIndex_ = index;
+ }
+
+ /**
+ * Advances to the next item in the playlist. If repeat mode is enabled, then
+ * once the last item of the playlist is reached this function will play
+ * the first one.
+ */
+ next() {
+ if (this.hasNext()) {
+ this.select(this.currentIndex_ + 1);
+
+ return;
+ }
+
+ if (this.repeat) this.select(0);
+ }
+
+ /**
+ * Whether an element exists in the playlist after the one that is currently playing.
+ * If `repeat` mode is enabled this function will still return `false` when the
+ * current position is the last item in the playlist.
+ *
+ * @returns {boolean} true if there is an element after, false otherwise.
+ */
+ hasNext() {
+ return this.currentIndex_ + 1 < this.items_.length;
+ }
+
+ /**
+ * Moves to the previous item in the playlist.
+ */
+ previous() {
+ this.select(this.currentIndex_ - 1);
+ }
+
+ /**
+ * Whether an element exists before the one that is currently playing.
+ * If `repeat` mode is enabled this function will still return `false` when the
+ * current position is the first item in the playlist.
+ *
+ * @returns {boolean} true if there is an element before, false otherwise.
+ */
+ hasPrevious() {
+ return this.currentIndex_ > 0;
+ }
+
+ /**
+ * Handles the `ended` event. If auto-advance is enabled then the next item
+ * will be played, otherwise nothing happens.
+ */
+ handleEnded() {
+ if (!this.autoadvance) {
+ return;
+ }
+
+ this.next();
+ }
+
+ /**
+ * Shuffles the order of the items in the playlist randomly.
+ * This method implements the Fisher-Yates shuffle algorithm to
+ * ensure each permutation of the array elements is equally likely.
+ */
+ shuffle() {
+ for (let i = this.items_.length - 1; i > 0; i -= 1) {
+ // Pick a remaining element…
+ const j = Math.floor(Math.random() * (i + 1));
+
+ // And swap it with the current element.
+ [this.items_[i], this.items_[j]] = [this.items_[j], this.items_[i]];
+
+ // Check if the currentIndex was swapped, update if necessary
+ if (this.currentIndex_ === i) {
+ this.currentIndex_ = j;
+ } else if (this.currentIndex_ === j) {
+ this.currentIndex_ = i;
+ }
+ }
+ }
+}
+
+pillarbox.registerPlugin('pillarboxPlaylist', PillarboxPlaylist);
+
+export default PillarboxPlaylist;
+
+/**
+ * Represents a single item in the playlist.
+ *
+ * @typedef {Object} PlaylistItem
+ * @property {any[]} sources The array of media sources for the playlist item.
+ * @property {string} poster A url for the poster.
+ * @property {Object} data The metadata for the playlist item. In this object
+ * you can store properties related to the playlist
+ * item such as `title`, the `duration`,
+ * and other relevant metadata.
+ */
+
+
diff --git a/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js
new file mode 100644
index 0000000..a92a5ad
--- /dev/null
+++ b/packages/pillarbox-playlist/test/pillarbox-playlist.spec.js
@@ -0,0 +1,350 @@
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import pillarbox from '@srgssr/pillarbox-web';
+import PillarboxPlaylist from '../src/pillarbox-playlist.js';
+
+const playlist = [
+ { sources: [{ src: 'first-source', type: 'test' }], poster: 'first-poster' },
+ { sources: [{ src: 'second-source', type: 'test' }], poster: 'second-poster' },
+ { sources: [{ src: 'third-source', type: 'test' }], poster: 'third-poster' },
+ { sources: [{ src: 'fourth-source', type: 'test' }], poster: 'fourth-poster' }
+];
+
+window.HTMLMediaElement.prototype.load = () => {
+};
+
+describe('PillarboxPlaylist', () => {
+ let player, pillarboxPlaylist, videoElement;
+
+ beforeAll(() => {
+ document.body.innerHTML = '';
+ videoElement = document.querySelector('#test-video');
+ });
+
+ beforeEach(() => {
+ player = pillarbox(videoElement, {
+ plugins: {
+ pillarboxPlaylist: true
+ }
+ });
+ pillarboxPlaylist = player.pillarboxPlaylist();
+ });
+
+ afterEach(() => {
+ vi.resetAllMocks();
+ player.dispose();
+ });
+
+ it('should be registered and attached to the player', () => {
+ expect(pillarbox.getPlugin('pillarboxPlaylist')).toBe(PillarboxPlaylist);
+ expect(player.pillarboxPlaylist).toBeDefined();
+ });
+
+ describe('load', () => {
+ it('should load a playlist', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+
+ // Then
+ expect(pillarboxPlaylist.hasPrevious()).toBeFalsy();
+ expect(pillarboxPlaylist.hasNext()).toBeTruthy();
+ expect(pillarboxPlaylist.items.length).toBe(4);
+ expect(pillarboxPlaylist.currentIndex).toBe(0);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[0]);
+ expect(srcSpy).toHaveBeenCalledWith(playlist[0].sources);
+ expect(posterSpy).toHaveBeenCalledWith(playlist[0].poster);
+ });
+ });
+
+ describe('select', () => {
+ it('should select an item by index', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.select(3);
+
+ // Then
+ expect(pillarboxPlaylist.currentIndex).toBe(3);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[3]);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[3].sources);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[3].poster);
+ });
+
+ it('should not load an item if its already selected', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.select(0);
+
+ // Then
+ expect(pillarboxPlaylist.currentIndex).toBe(0);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[0]);
+ expect(srcSpy).toHaveBeenCalledTimes(1);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources);
+ expect(posterSpy).toHaveBeenCalledTimes(1);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster);
+ });
+
+ it('should not load an item if its outside of the playlist range', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.select(5);
+
+ // Then
+ expect(pillarboxPlaylist.currentIndex).toBe(0);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[0]);
+ expect(srcSpy).toHaveBeenCalledTimes(1);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources);
+ expect(posterSpy).toHaveBeenCalledTimes(1);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster);
+ });
+ });
+
+ describe('next', () => {
+ it('should play next on a registered playlist', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.next();
+
+ // Then
+ expect(pillarboxPlaylist.hasPrevious()).toBeTruthy();
+ expect(pillarboxPlaylist.hasNext()).toBeTruthy();
+ expect(pillarboxPlaylist.items.length).toBe(4);
+ expect(pillarboxPlaylist.currentIndex).toBe(1);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[1]);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[1].sources);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[1].poster);
+ });
+
+ it('should not play next if the current index is the last of the playlist', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.select(3);
+ pillarboxPlaylist.next();
+
+ // Then
+ expect(pillarboxPlaylist.hasPrevious()).toBeTruthy();
+ expect(pillarboxPlaylist.hasNext()).toBeFalsy();
+ expect(pillarboxPlaylist.items.length).toBe(4);
+ expect(pillarboxPlaylist.currentIndex).toBe(3);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[3]);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[3].sources);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[3].poster);
+ });
+
+ it('should play the first element if repeat is true when next is called and the current index is the last of the playlist', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.repeat = true;
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.select(3);
+ pillarboxPlaylist.next();
+
+ // Then
+ expect(pillarboxPlaylist.hasPrevious()).toBeFalsy();
+ expect(pillarboxPlaylist.hasNext()).toBeTruthy();
+ expect(pillarboxPlaylist.items.length).toBe(4);
+ expect(pillarboxPlaylist.currentIndex).toBe(0);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[0]);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster);
+ });
+ });
+
+ describe('previous', () => {
+ it('should play previous on a registered playlist', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // 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(1);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[1]);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[1].sources);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[1].poster);
+ });
+ });
+
+ describe('autoadvance', () => {
+ it('should play next element on ended if autoadvance is enabled', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.autoadvance = true;
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.handleEnded();
+
+ // Then
+ expect(pillarboxPlaylist.hasPrevious()).toBeTruthy();
+ expect(pillarboxPlaylist.hasNext()).toBeTruthy();
+ expect(pillarboxPlaylist.items.length).toBe(4);
+ expect(pillarboxPlaylist.currentIndex).toBe(1);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[1]);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[1].sources);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[1].poster);
+ });
+
+ it('should not play next element on ended if autoadvance is disabled', () => {
+ // Given
+ const srcSpy = vi.spyOn(player, 'src').mockImplementation(() => {});
+ const posterSpy = vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.handleEnded();
+
+ // Then
+ expect(pillarboxPlaylist.hasPrevious()).toBeFalsy();
+ expect(pillarboxPlaylist.hasNext()).toBeTruthy();
+ expect(pillarboxPlaylist.items.length).toBe(4);
+ expect(pillarboxPlaylist.currentIndex).toBe(0);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[0]);
+ expect(srcSpy).toHaveBeenLastCalledWith(playlist[0].sources);
+ expect(posterSpy).toHaveBeenLastCalledWith(playlist[0].poster);
+ });
+ });
+
+ describe('shuffle', () => {
+ it('should randomize the order of playlist items', () => {
+ // Given
+ vi.spyOn(player, 'src').mockImplementation(() => {});
+ vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.shuffle();
+
+ // Then
+ expect(playlist).not.toEqual(pillarboxPlaylist.item);
+ expect(playlist.length).toBe(pillarboxPlaylist.items.length);
+ });
+ });
+
+ describe('push', () => {
+ it('should push new items at the end of the playlist', () => {
+ // Given
+ vi.spyOn(player, 'src').mockImplementation(() => {});
+ vi.spyOn(player, 'poster').mockImplementation(() => {});
+ const items = [
+ { sources: [{ src: 'fifth-source', type: 'test' }], poster: 'fifth-poster' },
+ { sources: [{ src: 'sixth-source', type: 'test' }], poster: 'sixth-poster' }
+ ];
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.push(...items);
+
+ // Then
+ expect(pillarboxPlaylist.items.length).toBe(6);
+ expect(pillarboxPlaylist.currentIndex).toBe(0);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[0]);
+ });
+ });
+
+ describe('splice', () => {
+ it('should push new items at any point of the playlist', () => {
+ // Given
+ vi.spyOn(player, 'src').mockImplementation(() => {});
+ vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ const items = [
+ { sources: [{ src: 'fifth-source', type: 'test' }], poster: 'fifth-poster' },
+ { sources: [{ src: 'sixth-source', type: 'test' }], poster: 'sixth-poster' }
+ ];
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.splice(0, 0, ...items);
+
+ // Then
+ expect(pillarboxPlaylist.items.length).toBe(6);
+ expect(pillarboxPlaylist.currentIndex).toBe(2);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[0]);
+ });
+
+ it('should delete items at any point of the playlist', () => {
+ // Given
+ vi.spyOn(player, 'src').mockImplementation(() => {});
+ vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.select(2);
+ pillarboxPlaylist.splice(0, 1);
+
+ // Then
+ expect(pillarboxPlaylist.items.length).toBe(3);
+ expect(pillarboxPlaylist.currentIndex).toBe(1);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[2]);
+ });
+
+ it('should push and delete items at any point of the playlist', () => {
+ // Given
+ vi.spyOn(player, 'src').mockImplementation(() => {});
+ vi.spyOn(player, 'poster').mockImplementation(() => {});
+ const items = [
+ { sources: [{ src: 'fifth-source', type: 'test' }], poster: 'fifth-poster' },
+ { sources: [{ src: 'sixth-source', type: 'test' }], poster: 'sixth-poster' }
+ ];
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.select(2);
+ pillarboxPlaylist.splice(1, 1, ...items);
+
+ // Then
+ expect(pillarboxPlaylist.items.length).toBe(5);
+ expect(pillarboxPlaylist.currentIndex).toBe(3);
+ expect(pillarboxPlaylist.currentItem).toBe(playlist[2]);
+ });
+
+ it('should lose track of current item when deleted', () => {
+ // Given
+ vi.spyOn(player, 'src').mockImplementation(() => {});
+ vi.spyOn(player, 'poster').mockImplementation(() => {});
+
+ // When
+ pillarboxPlaylist.load(playlist);
+ pillarboxPlaylist.splice(0, 1);
+
+ // Then
+ expect(pillarboxPlaylist.items.length).toBe(3);
+ expect(pillarboxPlaylist.currentIndex).toBe(-1);
+ expect(pillarboxPlaylist.currentItem).toBeUndefined();
+ });
+ });
+});
diff --git a/packages/pillarbox-playlist/vite.config.js b/packages/pillarbox-playlist/vite.config.js
new file mode 100644
index 0000000..18699ea
--- /dev/null
+++ b/packages/pillarbox-playlist/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ base: './',
+ test: {
+ environment: 'jsdom'
+ }
+});
diff --git a/packages/pillarbox-playlist/vite.config.lib.js b/packages/pillarbox-playlist/vite.config.lib.js
new file mode 100644
index 0000000..4779d8f
--- /dev/null
+++ b/packages/pillarbox-playlist/vite.config.lib.js
@@ -0,0 +1,28 @@
+import { defineConfig } from 'vite';
+import babel from '@rollup/plugin-babel';
+
+/**
+ * Vite's configuration for the lib build.
+ *
+ * Outputs:
+ * - 'dist/pillarbox-playlist.es.js': ESModule version with sourcemaps.
+ * - 'dist/pillarbox-playlist.cjs.js': CommonJS version with sourcemaps.
+ */
+export default defineConfig({
+ esbuild: false,
+ build: {
+ sourcemap: true,
+ lib: {
+ formats: ['es', 'cjs'],
+ name: 'PillarboxPlaylist',
+ entry: 'src/pillarbox-playlist.js'
+ },
+ rollupOptions: {
+ external: ['@srgssr/pillarbox-web'],
+ plugins: [babel({
+ babelHelpers: 'bundled',
+ exclude: 'node_modules/**'
+ })]
+ }
+ }
+});
diff --git a/packages/skip-button/index.html b/packages/skip-button/index.html
index a0bc440..f943639 100644
--- a/packages/skip-button/index.html
+++ b/packages/skip-button/index.html
@@ -46,7 +46,8 @@
// Load the video source for the player
player.src({
src: urn,
- type: 'srgssr/urn'
+ type: 'srgssr/urn',
+ disableTrackers: true
});
// Expose player for debugging
diff --git a/scripts/template/README.md.hbs b/scripts/template/README.md.hbs
index 9fbc791..35ebd31 100644
--- a/scripts/template/README.md.hbs
+++ b/scripts/template/README.md.hbs
@@ -7,7 +7,7 @@ essential context and addressing fundamental questions.
## Requirements
-To use this component, you need the following installed on your system:
+To use this {{lowerCase type}}, you need the following installed on your system:
- Node.js
diff --git a/scripts/template/index.html.hbs b/scripts/template/index.html.hbs
index 5e49264..2f79583 100644
--- a/scripts/template/index.html.hbs
+++ b/scripts/template/index.html.hbs
@@ -39,7 +39,7 @@
srgOptions: {
dataProviderHost: ilHost
},
-{{#ifEq type 'plugin'}}
+{{#ifEq type 'Plugin'}}
plugins: { {{camelCase name}}: true }
{{else}}
{{properCase name}}: true
@@ -49,7 +49,8 @@
// Load the video source for the player
player.src({
src: urn,
- type: 'srgssr/urn'
+ type: 'srgssr/urn',
+ disableTrackers: true
});
// Expose player for debugging