From 4572c3d0d92f5b1b79b34dbe2c7b6557a8a5b7e4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 Aug 2020 09:44:58 +0200 Subject: Handle basic playlist in embed --- client/src/assets/player/images/tick-white.svg | 5 +- .../src/assets/player/peertube-player-manager.ts | 18 ++- .../src/assets/player/peertube-videojs-typings.ts | 25 ++++- .../src/assets/player/playlist/playlist-button.ts | 61 ++++++++++ .../assets/player/playlist/playlist-menu-item.ts | 98 ++++++++++++++++ client/src/assets/player/playlist/playlist-menu.ts | 124 +++++++++++++++++++++ .../src/assets/player/playlist/playlist-plugin.ts | 35 ++++++ 7 files changed, 360 insertions(+), 6 deletions(-) create mode 100644 client/src/assets/player/playlist/playlist-button.ts create mode 100644 client/src/assets/player/playlist/playlist-menu-item.ts create mode 100644 client/src/assets/player/playlist/playlist-menu.ts create mode 100644 client/src/assets/player/playlist/playlist-plugin.ts (limited to 'client/src/assets') diff --git a/client/src/assets/player/images/tick-white.svg b/client/src/assets/player/images/tick-white.svg index d329e6bfb..8868a2481 100644 --- a/client/src/assets/player/images/tick-white.svg +++ b/client/src/assets/player/images/tick-white.svg @@ -1,8 +1,7 @@ - - - + + diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 6a6d63462..dcfa3a593 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -18,14 +18,21 @@ import './videojs-components/settings-menu-item' import './videojs-components/settings-panel' import './videojs-components/settings-panel-child' import './videojs-components/theater-button' +import './playlist/playlist-plugin' import videojs from 'video.js' -import { VideoFile } from '@shared/models' import { isDefaultLocale } from '@shared/core-utils/i18n' +import { VideoFile } from '@shared/models' import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' import { getStoredP2PEnabled } from './peertube-player-local-storage' -import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings' +import { + P2PMediaLoaderPluginOptions, + PlaylistPluginOptions, + UserWatching, + VideoJSCaption, + VideoJSPluginOptions +} from './peertube-videojs-typings' import { TranslationsManager } from './translations-manager' import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig, isIOS, isSafari } from './utils' @@ -71,6 +78,9 @@ export interface CommonOptions extends CustomizationOptions { autoplay: boolean nextVideo?: Function + + playlist?: PlaylistPluginOptions + videoDuration: number enableHotkeys: boolean inactivityTimeout: number @@ -203,6 +213,10 @@ export class PeertubePlayerManager { } } + if (commonOptions.playlist) { + plugins.playlist = commonOptions.playlist + } + if (commonOptions.enableHotkeys === true) { PeertubePlayerManager.addHotkeysOptions(plugins) } diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index 1506a04ac..b72c4b0f9 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts @@ -1,10 +1,11 @@ import { Config, Level } from 'hls.js' import videojs from 'video.js' -import { VideoFile } from '@shared/models' +import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' import { PlayerMode } from './peertube-player-manager' import { PeerTubePlugin } from './peertube-plugin' +import { PlaylistPlugin } from './playlist/playlist-plugin' import { EndCardOptions } from './upnext/end-card' import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' @@ -45,6 +46,8 @@ declare module 'video.js' { dock (options: { title: string, description: string }): void upnext (options: Partial): void + + playlist (): PlaylistPlugin } } @@ -105,6 +108,16 @@ type PeerTubePluginOptions = { stopTime: number | string } +type PlaylistPluginOptions = { + elements: VideoPlaylistElement[] + + playlist: VideoPlaylist + + getCurrentPosition: () => number + + onItemClicked: (element: VideoPlaylistElement) => void +} + type WebtorrentPluginOptions = { playerElement: HTMLVideoElement @@ -125,6 +138,8 @@ type P2PMediaLoaderPluginOptions = { } type VideoJSPluginOptions = { + playlist?: PlaylistPluginOptions + peertube: PeerTubePluginOptions webtorrent?: WebtorrentPluginOptions @@ -170,10 +185,18 @@ type PlayerNetworkInfo = { } } +type PlaylistItemOptions = { + element: VideoPlaylistElement + + onClicked: Function +} + export { PlayerNetworkInfo, + PlaylistItemOptions, ResolutionUpdateData, AutoResolutionUpdateData, + PlaylistPluginOptions, VideoJSCaption, UserWatching, PeerTubePluginOptions, diff --git a/client/src/assets/player/playlist/playlist-button.ts b/client/src/assets/player/playlist/playlist-button.ts new file mode 100644 index 000000000..a7996ec60 --- /dev/null +++ b/client/src/assets/player/playlist/playlist-button.ts @@ -0,0 +1,61 @@ +import videojs from 'video.js' +import { PlaylistPluginOptions } from '../peertube-videojs-typings' +import { PlaylistMenu } from './playlist-menu' + +const ClickableComponent = videojs.getComponent('ClickableComponent') + +class PlaylistButton extends ClickableComponent { + private playlistInfoElement: HTMLElement + private wrapper: HTMLElement + + constructor (player: videojs.Player, options?: PlaylistPluginOptions & { playlistMenu: PlaylistMenu }) { + super(player, options as any) + } + + createEl () { + this.wrapper = super.createEl('div', { + className: 'vjs-playlist-button', + innerHTML: '', + tabIndex: -1 + }) as HTMLElement + + const icon = super.createEl('div', { + className: 'vjs-playlist-icon', + innerHTML: '', + tabIndex: -1 + }) + + this.playlistInfoElement = super.createEl('div', { + className: 'vjs-playlist-info', + innerHTML: '', + tabIndex: -1 + }) as HTMLElement + + this.wrapper.appendChild(icon) + this.wrapper.appendChild(this.playlistInfoElement) + + this.update() + + return this.wrapper + } + + update () { + const options = this.options_ as PlaylistPluginOptions + + this.playlistInfoElement.innerHTML = options.getCurrentPosition() + '/' + options.playlist.videosLength + this.wrapper.title = this.player().localize('Playlist: {1}', [ options.playlist.displayName ]) + } + + handleClick () { + const playlistMenu = this.getPlaylistMenu() + playlistMenu.open() + } + + private getPlaylistMenu () { + return (this.options_ as any).playlistMenu as PlaylistMenu + } +} + +videojs.registerComponent('PlaylistButton', PlaylistButton) + +export { PlaylistButton } diff --git a/client/src/assets/player/playlist/playlist-menu-item.ts b/client/src/assets/player/playlist/playlist-menu-item.ts new file mode 100644 index 000000000..916c6338f --- /dev/null +++ b/client/src/assets/player/playlist/playlist-menu-item.ts @@ -0,0 +1,98 @@ +import videojs from 'video.js' +import { VideoPlaylistElement } from '@shared/models' +import { PlaylistItemOptions } from '../peertube-videojs-typings' + +const Component = videojs.getComponent('Component') + +class PlaylistMenuItem extends Component { + private element: VideoPlaylistElement + + constructor (player: videojs.Player, options?: PlaylistItemOptions) { + super(player, options as any) + + this.emitTapEvents() + + this.element = options.element + + this.on([ 'click', 'tap' ], () => this.switchPlaylistItem()) + this.on('keydown', event => this.handleKeyDown(event)) + } + + createEl () { + const options = this.options_ as PlaylistItemOptions + + const li = super.createEl('li', { + className: 'vjs-playlist-menu-item', + innerHTML: '' + }) as HTMLElement + + const positionBlock = super.createEl('div', { + className: 'item-position-block' + }) + + const position = super.createEl('div', { + className: 'item-position', + innerHTML: options.element.position + }) + + const player = super.createEl('div', { + className: 'item-player' + }) + + positionBlock.appendChild(position) + positionBlock.appendChild(player) + + li.appendChild(positionBlock) + + const thumbnail = super.createEl('img', { + src: window.location.origin + options.element.video.thumbnailPath + }) + + const infoBlock = super.createEl('div', { + className: 'info-block' + }) + + const title = super.createEl('div', { + innerHTML: options.element.video.name, + className: 'title' + }) + + const channel = super.createEl('div', { + innerHTML: options.element.video.channel.displayName, + className: 'channel' + }) + + infoBlock.appendChild(title) + infoBlock.appendChild(channel) + + li.append(thumbnail) + li.append(infoBlock) + + return li + } + + setSelected (selected: boolean) { + if (selected) this.addClass('vjs-selected') + else this.removeClass('vjs-selected') + } + + getElement () { + return this.element + } + + private handleKeyDown (event: KeyboardEvent) { + if (event.code === 'Space' || event.code === 'Enter') { + this.switchPlaylistItem() + } + } + + private switchPlaylistItem () { + const options = this.options_ as PlaylistItemOptions + + options.onClicked() + } +} + +Component.registerComponent('PlaylistMenuItem', PlaylistMenuItem) + +export { PlaylistMenuItem } diff --git a/client/src/assets/player/playlist/playlist-menu.ts b/client/src/assets/player/playlist/playlist-menu.ts new file mode 100644 index 000000000..7d7d9e12f --- /dev/null +++ b/client/src/assets/player/playlist/playlist-menu.ts @@ -0,0 +1,124 @@ +import videojs from 'video.js' +import { VideoPlaylistElement } from '@shared/models' +import { PlaylistPluginOptions } from '../peertube-videojs-typings' +import { PlaylistMenuItem } from './playlist-menu-item' + +const Component = videojs.getComponent('Component') + +class PlaylistMenu extends Component { + private menuItems: PlaylistMenuItem[] + + constructor (player: videojs.Player, options?: PlaylistPluginOptions) { + super(player, options as any) + + this.player().on('userinactive', () => { + this.close() + }) + + this.player().on('click', event => { + let current = event.target as HTMLElement + + do { + if ( + current.classList.contains('vjs-playlist-menu') || + current.classList.contains('vjs-playlist-button') + ) { + return + } + + current = current.parentElement + } while (current) + + this.close() + }) + } + + createEl () { + this.menuItems = [] + + const options = this.getOptions() + + const menu = super.createEl('div', { + className: 'vjs-playlist-menu', + innerHTML: '', + tabIndex: -1 + }) + + const header = super.createEl('div', { + className: 'header' + }) + + const headerLeft = super.createEl('div') + + const leftTitle = super.createEl('div', { + innerHTML: options.playlist.displayName, + className: 'title' + }) + + const leftSubtitle = super.createEl('div', { + innerHTML: this.player().localize('By {1}', [ options.playlist.videoChannel.displayName ]), + className: 'channel' + }) + + headerLeft.appendChild(leftTitle) + headerLeft.appendChild(leftSubtitle) + + const tick = super.createEl('div', { + className: 'cross' + }) + tick.addEventListener('click', () => this.close()) + + header.appendChild(headerLeft) + header.appendChild(tick) + + const list = super.createEl('ol') + + for (const playlistElement of options.elements) { + const item = new PlaylistMenuItem(this.player(), { + element: playlistElement, + onClicked: () => this.onItemClicked(playlistElement) + }) + + list.appendChild(item.el()) + + this.menuItems.push(item) + } + + menu.appendChild(header) + menu.appendChild(list) + + return menu + } + + update () { + const options = this.getOptions() + + this.updateSelected(options.getCurrentPosition()) + } + + open () { + this.player().addClass('playlist-menu-displayed') + } + + close () { + this.player().removeClass('playlist-menu-displayed') + } + + updateSelected (newPosition: number) { + for (const item of this.menuItems) { + item.setSelected(item.getElement().position === newPosition) + } + } + + private getOptions () { + return this.options_ as PlaylistPluginOptions + } + + private onItemClicked (element: VideoPlaylistElement) { + this.getOptions().onItemClicked(element) + } +} + +Component.registerComponent('PlaylistMenu', PlaylistMenu) + +export { PlaylistMenu } diff --git a/client/src/assets/player/playlist/playlist-plugin.ts b/client/src/assets/player/playlist/playlist-plugin.ts new file mode 100644 index 000000000..b69d82e3c --- /dev/null +++ b/client/src/assets/player/playlist/playlist-plugin.ts @@ -0,0 +1,35 @@ +import videojs from 'video.js' +import { PlaylistPluginOptions } from '../peertube-videojs-typings' +import { PlaylistButton } from './playlist-button' +import { PlaylistMenu } from './playlist-menu' + +const Plugin = videojs.getPlugin('plugin') + +class PlaylistPlugin extends Plugin { + private playlistMenu: PlaylistMenu + private playlistButton: PlaylistButton + private options: PlaylistPluginOptions + + constructor (player: videojs.Player, options?: PlaylistPluginOptions) { + super(player, options) + + this.options = options + + this.player.ready(() => { + player.addClass('vjs-playlist') + }) + + this.playlistMenu = new PlaylistMenu(player, options) + this.playlistButton = new PlaylistButton(player, Object.assign({}, options, { playlistMenu: this.playlistMenu })) + + player.addChild(this.playlistMenu, options) + player.addChild(this.playlistButton, options) + } + + updateSelected () { + this.playlistMenu.updateSelected(this.options.getCurrentPosition()) + } +} + +videojs.registerPlugin('playlist', PlaylistPlugin) +export { PlaylistPlugin } -- cgit v1.2.3