From 408785534e51fb9f3c6c1a29cc2cd4072c81a2d0 Mon Sep 17 00:00:00 2001 From: Josep Boix Requesens Date: Tue, 7 May 2024 10:21:40 +0200 Subject: [PATCH] feat: add playlist management plugin to pillarbox player Introduces a pillarbox playlist plugin. This plugin supports playlist loading, dynamic modifications, auto-advancement, and repeat functionalities. ***Key Features:*** - **Initialization Options**: Configurable through an options object that can include an initial playlist, and settings for repeat and auto-advance behaviors. - **Dynamic Playlist Management**: Methods to load, push, splice, and shuffle playlist items dynamically. - **Playback Control**: Functions to navigate through the playlist such as next, previous, and select specific items. - **State Properties**: Properties like `repeat` and `autoadvance` which can be toggled post-initialization to alter playback behavior. ***Example*** ```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.pillarboxPlaylist().load(...playlist); ``` --- package-lock.json | 10 + packages/pillarbox-playlist/.babelrc | 3 + packages/pillarbox-playlist/README.md | 131 +++++++ packages/pillarbox-playlist/index.html | 56 +++ packages/pillarbox-playlist/package.json | 38 ++ .../src/pillarbox-playlist.js | 292 +++++++++++++++ .../test/pillarbox-playlist.spec.js | 350 ++++++++++++++++++ packages/pillarbox-playlist/vite.config.js | 8 + .../pillarbox-playlist/vite.config.lib.js | 28 ++ packages/skip-button/index.html | 3 +- scripts/template/README.md.hbs | 2 +- scripts/template/index.html.hbs | 5 +- 12 files changed, 922 insertions(+), 4 deletions(-) create mode 100644 packages/pillarbox-playlist/.babelrc create mode 100644 packages/pillarbox-playlist/README.md create mode 100644 packages/pillarbox-playlist/index.html create mode 100644 packages/pillarbox-playlist/package.json create mode 100644 packages/pillarbox-playlist/src/pillarbox-playlist.js create mode 100644 packages/pillarbox-playlist/test/pillarbox-playlist.spec.js create mode 100644 packages/pillarbox-playlist/vite.config.js create mode 100644 packages/pillarbox-playlist/vite.config.lib.js 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..a25fa99 --- /dev/null +++ b/packages/pillarbox-playlist/README.md @@ -0,0 +1,131 @@ +# 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 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. | + +## 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..8d2bb86 --- /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..e137853 --- /dev/null +++ b/packages/pillarbox-playlist/src/pillarbox-playlist.js @@ -0,0 +1,292 @@ +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. + * + * @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); + + // Adjust currentIndex for items being deleted + if (this.currentIndex_ >= start) { + 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 index() { + 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()) { + if (this.repeat) this.select(0); + + return; + } + + this.select(this.currentIndex_ + 1); + } + + /** + * 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() { + let m = this.items_.length, t, i; + + // While there remain elements to shuffle… + while (m) { + + // Pick a remaining element… + i = Math.floor(Math.random() * m); + m -= 1; + + // And swap it with the current element. + t = this.items_[m]; + this.items_[m] = this.items_[i]; + this.items_[i] = t; + + // Check if the currentIndex was swapped, update if necessary + if (this.currentIndex_ === m) { + this.currentIndex_ = i; + } else if (this.currentIndex_ === i) { + this.currentIndex_ = m; + } + } + } +} + +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..72a61d2 --- /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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).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', async() => { + // 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.index).toBe(0); + expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); + }); + }); + + describe('splice', () => { + it('should push new items at any point of the playlist', async() => { + // 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.index).toBe(2); + expect(pillarboxPlaylist.currentItem).toBe(playlist[0]); + }); + + it('should delete items at any point fo the playlist', async() => { + // 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.index).toBe(1); + expect(pillarboxPlaylist.currentItem).toBe(playlist[2]); + }); + + it('should push and delete items at any point of the playlist', async() => { + // 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.index).toBe(3); + expect(pillarboxPlaylist.currentItem).toBe(playlist[2]); + }); + + it('should lose track of current item when deleted', async() => { + // 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.index).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