From 2adfc7ea9a1f858db874df9fe322e7ae833db77c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 23 Jan 2019 15:36:45 +0100 Subject: Refractor videojs player Add fake p2p-media-loader plugin --- .../player/videojs-components/p2p-info-button.ts | 102 +++++++ .../videojs-components/peertube-link-button.ts | 40 +++ .../peertube-load-progress-bar.ts | 38 +++ .../videojs-components/resolution-menu-button.ts | 84 ++++++ .../videojs-components/resolution-menu-item.ts | 87 ++++++ .../videojs-components/settings-menu-button.ts | 288 ++++++++++++++++++ .../videojs-components/settings-menu-item.ts | 329 +++++++++++++++++++++ .../player/videojs-components/theater-button.ts | 50 ++++ 8 files changed, 1018 insertions(+) create mode 100644 client/src/assets/player/videojs-components/p2p-info-button.ts create mode 100644 client/src/assets/player/videojs-components/peertube-link-button.ts create mode 100644 client/src/assets/player/videojs-components/peertube-load-progress-bar.ts create mode 100644 client/src/assets/player/videojs-components/resolution-menu-button.ts create mode 100644 client/src/assets/player/videojs-components/resolution-menu-item.ts create mode 100644 client/src/assets/player/videojs-components/settings-menu-button.ts create mode 100644 client/src/assets/player/videojs-components/settings-menu-item.ts create mode 100644 client/src/assets/player/videojs-components/theater-button.ts (limited to 'client/src/assets/player/videojs-components') diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts new file mode 100644 index 000000000..03a5d29f0 --- /dev/null +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts @@ -0,0 +1,102 @@ +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { bytes } from '../utils' + +const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') +class P2pInfoButton extends Button { + + createEl () { + const div = videojsUntyped.dom.createEl('div', { + className: 'vjs-peertube' + }) + const subDivWebtorrent = videojsUntyped.dom.createEl('div', { + className: 'vjs-peertube-hidden' // Hide the stats before we get the info + }) + div.appendChild(subDivWebtorrent) + + const downloadIcon = videojsUntyped.dom.createEl('span', { + className: 'icon icon-download' + }) + subDivWebtorrent.appendChild(downloadIcon) + + const downloadSpeedText = videojsUntyped.dom.createEl('span', { + className: 'download-speed-text' + }) + const downloadSpeedNumber = videojsUntyped.dom.createEl('span', { + className: 'download-speed-number' + }) + const downloadSpeedUnit = videojsUntyped.dom.createEl('span') + downloadSpeedText.appendChild(downloadSpeedNumber) + downloadSpeedText.appendChild(downloadSpeedUnit) + subDivWebtorrent.appendChild(downloadSpeedText) + + const uploadIcon = videojsUntyped.dom.createEl('span', { + className: 'icon icon-upload' + }) + subDivWebtorrent.appendChild(uploadIcon) + + const uploadSpeedText = videojsUntyped.dom.createEl('span', { + className: 'upload-speed-text' + }) + const uploadSpeedNumber = videojsUntyped.dom.createEl('span', { + className: 'upload-speed-number' + }) + const uploadSpeedUnit = videojsUntyped.dom.createEl('span') + uploadSpeedText.appendChild(uploadSpeedNumber) + uploadSpeedText.appendChild(uploadSpeedUnit) + subDivWebtorrent.appendChild(uploadSpeedText) + + const peersText = videojsUntyped.dom.createEl('span', { + className: 'peers-text' + }) + const peersNumber = videojsUntyped.dom.createEl('span', { + className: 'peers-number' + }) + subDivWebtorrent.appendChild(peersNumber) + subDivWebtorrent.appendChild(peersText) + + const subDivHttp = videojsUntyped.dom.createEl('div', { + className: 'vjs-peertube-hidden' + }) + const subDivHttpText = videojsUntyped.dom.createEl('span', { + className: 'http-fallback', + textContent: 'HTTP' + }) + + subDivHttp.appendChild(subDivHttpText) + div.appendChild(subDivHttp) + + this.player_.on('p2pInfo', (event: any, data: any) => { + // We are in HTTP fallback + if (!data) { + subDivHttp.className = 'vjs-peertube-displayed' + subDivWebtorrent.className = 'vjs-peertube-hidden' + + return + } + + const downloadSpeed = bytes(data.downloadSpeed) + const uploadSpeed = bytes(data.uploadSpeed) + const totalDownloaded = bytes(data.downloaded) + const totalUploaded = bytes(data.uploaded) + const numPeers = data.numPeers + + subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + + this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) + + downloadSpeedNumber.textContent = downloadSpeed[ 0 ] + downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] + + uploadSpeedNumber.textContent = uploadSpeed[ 0 ] + uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] + + peersNumber.textContent = numPeers + peersText.textContent = ' ' + this.player_.localize('peers') + + subDivHttp.className = 'vjs-peertube-hidden' + subDivWebtorrent.className = 'vjs-peertube-displayed' + }) + + return div + } +} +Button.registerComponent('P2PInfoButton', P2pInfoButton) diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts new file mode 100644 index 000000000..fed8ea33e --- /dev/null +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts @@ -0,0 +1,40 @@ +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { buildVideoLink } from '../utils' +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import { Player } from 'video.js' + +const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') +class PeerTubeLinkButton extends Button { + + constructor (player: Player, options: any) { + super(player, options) + } + + createEl () { + return this.buildElement() + } + + updateHref () { + this.el().setAttribute('href', buildVideoLink(this.player().currentTime())) + } + + handleClick () { + this.player_.pause() + } + + private buildElement () { + const el = videojsUntyped.dom.createEl('a', { + href: buildVideoLink(), + innerHTML: 'PeerTube', + title: this.player_.localize('Go to the video page'), + className: 'vjs-peertube-link', + target: '_blank' + }) + + el.addEventListener('mouseenter', () => this.updateHref()) + + return el + } +} +Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) diff --git a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts new file mode 100644 index 000000000..9a0e3b550 --- /dev/null +++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts @@ -0,0 +1,38 @@ +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import { Player } from 'video.js' + +const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') + +class PeerTubeLoadProgressBar extends Component { + + constructor (player: Player, options: any) { + super(player, options) + this.partEls_ = [] + this.on(player, 'progress', this.update) + } + + createEl () { + return super.createEl('div', { + className: 'vjs-load-progress', + innerHTML: `${this.localize('Loaded')}: 0%` + }) + } + + dispose () { + this.partEls_ = null + + super.dispose() + } + + update () { + const torrent = this.player().webtorrent().getTorrent() + if (!torrent) return + + this.el_.style.width = (torrent.progress * 100) + '%' + } + +} + +Component.registerComponent('PeerTubeLoadProgressBar', PeerTubeLoadProgressBar) diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts new file mode 100644 index 000000000..2847de470 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts @@ -0,0 +1,84 @@ +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import { Player } from 'video.js' + +import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { ResolutionMenuItem } from './resolution-menu-item' + +const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') +const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') +class ResolutionMenuButton extends MenuButton { + label: HTMLElement + + constructor (player: Player, options: any) { + super(player, options) + this.player = player + + player.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) + + if (player.webtorrent) { + player.webtorrent().on('videoFileUpdate', () => setTimeout(() => this.trigger('updateLabel'), 0)) + } + } + + createEl () { + const el = super.createEl() + + this.labelEl_ = videojsUntyped.dom.createEl('div', { + className: 'vjs-resolution-value' + }) + + el.appendChild(this.labelEl_) + + return el + } + + updateARIAAttributes () { + this.el().setAttribute('aria-label', 'Quality') + } + + createMenu () { + return new Menu(this.player_) + } + + buildCSSClass () { + return super.buildCSSClass() + ' vjs-resolution-button' + } + + buildWrapperCSSClass () { + return 'vjs-resolution-control ' + super.buildWrapperCSSClass() + } + + private buildQualities (data: LoadedQualityData) { + // The automatic resolution item will need other labels + const labels: { [ id: number ]: string } = {} + + for (const d of data.qualityData.video) { + this.menu.addChild(new ResolutionMenuItem( + this.player_, + { + id: d.id, + label: d.label, + selected: d.selected, + callback: data.qualitySwitchCallback + }) + ) + + labels[d.id] = d.label + } + + this.menu.addChild(new ResolutionMenuItem( + this.player_, + { + id: -1, + label: this.player_.localize('Auto'), + labels, + callback: data.qualitySwitchCallback, + selected: true // By default, in auto mode + } + )) + } +} +ResolutionMenuButton.prototype.controlText_ = 'Quality' + +MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts new file mode 100644 index 000000000..cc1c79739 --- /dev/null +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts @@ -0,0 +1,87 @@ +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import { Player } from 'video.js' + +import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' + +const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') +class ResolutionMenuItem extends MenuItem { + private readonly id: number + private readonly label: string + // Only used for the automatic item + private readonly labels: { [id: number]: string } + private readonly callback: Function + + private autoResolutionPossible: boolean + private currentResolutionLabel: string + + constructor (player: Player, options: any) { + options.selectable = true + + super(player, options) + + this.autoResolutionPossible = true + this.currentResolutionLabel = '' + + this.label = options.label + this.labels = options.labels + this.id = options.id + this.callback = options.callback + + if (player.webtorrent) { + player.webtorrent().on('videoFileUpdate', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) + + // We only want to disable the "Auto" item + if (this.id === -1) { + player.webtorrent().on('autoResolutionUpdate', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) + } + } + + // TODO: update on HLS change + } + + handleClick (event: any) { + // Auto button disabled? + if (this.autoResolutionPossible === false && this.id === -1) return + + super.handleClick(event) + + this.callback(this.id) + } + + updateSelection (data: ResolutionUpdateData) { + if (this.id === -1) { + this.currentResolutionLabel = this.labels[data.resolutionId] + } + + // Automatic resolution only + if (data.auto === true) { + this.selected(this.id === -1) + return + } + + this.selected(this.id === data.resolutionId) + } + + updateAutoResolution (data: AutoResolutionUpdateData) { + // Check if the auto resolution is enabled or not + if (data.possible === false) { + this.addClass('disabled') + } else { + this.removeClass('disabled') + } + + this.autoResolutionPossible = data.possible + } + + getLabel () { + if (this.id === -1) { + return this.label + ' ' + this.currentResolutionLabel + '' + } + + return this.label + } +} +MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) + +export { ResolutionMenuItem } diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts new file mode 100644 index 000000000..14cb8ba43 --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts @@ -0,0 +1,288 @@ +// Author: Yanko Shterev +// Thanks https://github.com/yshterev/videojs-settings-menu + +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import * as videojs from 'video.js' + +import { SettingsMenuItem } from './settings-menu-item' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { toTitleCase } from '../utils' + +const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') +const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') +const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') + +class SettingsButton extends Button { + constructor (player: videojs.Player, options: any) { + super(player, options) + + this.playerComponent = player + this.dialog = this.playerComponent.addChild('settingsDialog') + this.dialogEl = this.dialog.el_ + this.menu = null + this.panel = this.dialog.addChild('settingsPanel') + this.panelChild = this.panel.addChild('settingsPanelChild') + + this.addClass('vjs-settings') + this.el_.setAttribute('aria-label', 'Settings Button') + + // Event handlers + this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) + this.disposeSettingsItemHandler = this.onDisposeSettingsItem.bind(this) + this.playerClickHandler = this.onPlayerClick.bind(this) + this.userInactiveHandler = this.onUserInactive.bind(this) + + this.buildMenu() + this.bindEvents() + + // Prepare the dialog + this.player().one('play', () => this.hideDialog()) + } + + onPlayerClick (event: MouseEvent) { + const element = event.target as HTMLElement + if (element.classList.contains('vjs-settings') || element.parentElement.classList.contains('vjs-settings')) { + return + } + + if (!this.dialog.hasClass('vjs-hidden')) { + this.hideDialog() + } + } + + onDisposeSettingsItem (event: any, name: string) { + if (name === undefined) { + let children = this.menu.children() + + while (children.length > 0) { + children[0].dispose() + this.menu.removeChild(children[0]) + } + + this.addClass('vjs-hidden') + } else { + let item = this.menu.getChild(name) + + if (item) { + item.dispose() + this.menu.removeChild(item) + } + } + + this.hideDialog() + + if (this.options_.entries.length === 0) { + this.addClass('vjs-hidden') + } + } + + onAddSettingsItem (event: any, data: any) { + const [ entry, options ] = data + + this.addMenuItem(entry, options) + this.removeClass('vjs-hidden') + } + + onUserInactive () { + if (!this.dialog.hasClass('vjs-hidden')) { + this.hideDialog() + } + } + + bindEvents () { + this.playerComponent.on('click', this.playerClickHandler) + this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler) + this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler) + this.playerComponent.on('userinactive', this.userInactiveHandler) + } + + buildCSSClass () { + return `vjs-icon-settings ${super.buildCSSClass()}` + } + + handleClick () { + if (this.dialog.hasClass('vjs-hidden')) { + this.showDialog() + } else { + this.hideDialog() + } + } + + showDialog () { + this.menu.el_.style.opacity = '1' + this.dialog.show() + + this.setDialogSize(this.getComponentSize(this.menu)) + } + + hideDialog () { + this.dialog.hide() + this.setDialogSize(this.getComponentSize(this.menu)) + this.menu.el_.style.opacity = '1' + this.resetChildren() + } + + getComponentSize (element: any) { + let width: number = null + let height: number = null + + // Could be component or just DOM element + if (element instanceof Component) { + width = element.el_.offsetWidth + height = element.el_.offsetHeight + + // keep width/height as properties for direct use + element.width = width + element.height = height + } else { + width = element.offsetWidth + height = element.offsetHeight + } + + return [ width, height ] + } + + setDialogSize ([ width, height ]: number[]) { + if (typeof height !== 'number') { + return + } + + let offset = this.options_.setup.maxHeightOffset + let maxHeight = this.playerComponent.el_.offsetHeight - offset + + if (height > maxHeight) { + height = maxHeight + width += 17 + this.panel.el_.style.maxHeight = `${height}px` + } else if (this.panel.el_.style.maxHeight !== '') { + this.panel.el_.style.maxHeight = '' + } + + this.dialogEl.style.width = `${width}px` + this.dialogEl.style.height = `${height}px` + } + + buildMenu () { + this.menu = new Menu(this.player()) + this.menu.addClass('vjs-main-menu') + let entries = this.options_.entries + + if (entries.length === 0) { + this.addClass('vjs-hidden') + this.panelChild.addChild(this.menu) + return + } + + for (let entry of entries) { + this.addMenuItem(entry, this.options_) + } + + this.panelChild.addChild(this.menu) + } + + addMenuItem (entry: any, options: any) { + const openSubMenu = function (this: any) { + if (videojsUntyped.dom.hasClass(this.el_, 'open')) { + videojsUntyped.dom.removeClass(this.el_, 'open') + } else { + videojsUntyped.dom.addClass(this.el_, 'open') + } + } + + options.name = toTitleCase(entry) + let settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any) + + this.menu.addChild(settingsMenuItem) + + // Hide children to avoid sub menus stacking on top of each other + // or having multiple menus open + settingsMenuItem.on('click', videojs.bind(this, this.hideChildren)) + + // Whether to add or remove selected class on the settings sub menu element + settingsMenuItem.on('click', openSubMenu) + } + + resetChildren () { + for (let menuChild of this.menu.children()) { + menuChild.reset() + } + } + + /** + * Hide all the sub menus + */ + hideChildren () { + for (let menuChild of this.menu.children()) { + menuChild.hideSubMenu() + } + } + +} + +class SettingsPanel extends Component { + constructor (player: videojs.Player, options: any) { + super(player, options) + } + + createEl () { + return super.createEl('div', { + className: 'vjs-settings-panel', + innerHTML: '', + tabIndex: -1 + }) + } +} + +class SettingsPanelChild extends Component { + constructor (player: videojs.Player, options: any) { + super(player, options) + } + + createEl () { + return super.createEl('div', { + className: 'vjs-settings-panel-child', + innerHTML: '', + tabIndex: -1 + }) + } +} + +class SettingsDialog extends Component { + constructor (player: videojs.Player, options: any) { + super(player, options) + this.hide() + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + createEl () { + const uniqueId = this.id_ + const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId + const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId + + return super.createEl('div', { + className: 'vjs-settings-dialog vjs-modal-overlay', + innerHTML: '', + tabIndex: -1 + }, { + 'role': 'dialog', + 'aria-labelledby': dialogLabelId, + 'aria-describedby': dialogDescriptionId + }) + } + +} + +SettingsButton.prototype.controlText_ = 'Settings' + +Component.registerComponent('SettingsButton', SettingsButton) +Component.registerComponent('SettingsDialog', SettingsDialog) +Component.registerComponent('SettingsPanel', SettingsPanel) +Component.registerComponent('SettingsPanelChild', SettingsPanelChild) + +export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild } diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts new file mode 100644 index 000000000..b9a430290 --- /dev/null +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts @@ -0,0 +1,329 @@ +// Author: Yanko Shterev +// Thanks https://github.com/yshterev/videojs-settings-menu + +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import * as videojs from 'video.js' + +import { toTitleCase } from '../utils' +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' + +const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') +const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') + +class SettingsMenuItem extends MenuItem { + + constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) { + super(player, options) + + this.settingsButton = menuButton + this.dialog = this.settingsButton.dialog + this.mainMenu = this.settingsButton.menu + this.panel = this.dialog.getChild('settingsPanel') + this.panelChild = this.panel.getChild('settingsPanelChild') + this.panelChildEl = this.panelChild.el_ + + this.size = null + + // keep state of what menu type is loading next + this.menuToLoad = 'mainmenu' + + const subMenuName = toTitleCase(entry) + const SubMenuComponent = videojsUntyped.getComponent(subMenuName) + + if (!SubMenuComponent) { + throw new Error(`Component ${subMenuName} does not exist`) + } + this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this) + const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] + this.settingsSubMenuEl_.className += ' ' + subMenuClass + + this.eventHandlers() + + player.ready(() => { + // Voodoo magic for IOS + setTimeout(() => { + this.build() + + // Update on rate change + player.on('ratechange', this.submenuClickHandler) + + if (subMenuName === 'CaptionsButton') { + // Hack to regenerate captions on HTTP fallback + player.on('captionsChanged', () => { + setTimeout(() => { + this.settingsSubMenuEl_.innerHTML = '' + this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) + this.update() + this.bindClickEvents() + + }, 0) + }) + } + + this.reset() + }, 0) + }) + } + + eventHandlers () { + this.submenuClickHandler = this.onSubmenuClick.bind(this) + this.transitionEndHandler = this.onTransitionEnd.bind(this) + } + + onSubmenuClick (event: any) { + let target = null + + if (event.type === 'tap') { + target = event.target + } else { + target = event.currentTarget + } + + if (target && target.classList.contains('vjs-back-button')) { + this.loadMainMenu() + return + } + + // To update the sub menu value on click, setTimeout is needed because + // updating the value is not instant + setTimeout(() => this.update(event), 0) + } + + /** + * Create the component's DOM element + * + * @return {Element} + * @method createEl + */ + createEl () { + const el = videojsUntyped.dom.createEl('li', { + className: 'vjs-menu-item' + }) + + this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', { + className: 'vjs-settings-sub-menu-title' + }) + + el.appendChild(this.settingsSubMenuTitleEl_) + + this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', { + className: 'vjs-settings-sub-menu-value' + }) + + el.appendChild(this.settingsSubMenuValueEl_) + + this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', { + className: 'vjs-settings-sub-menu' + }) + + return el + } + + /** + * Handle click on menu item + * + * @method handleClick + */ + handleClick () { + this.menuToLoad = 'submenu' + // Remove open class to ensure only the open submenu gets this class + videojsUntyped.dom.removeClass(this.el_, 'open') + + super.handleClick() + + this.mainMenu.el_.style.opacity = '0' + // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element + if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { + videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') + + // animation not played without timeout + setTimeout(() => { + this.settingsSubMenuEl_.style.opacity = '1' + this.settingsSubMenuEl_.style.marginRight = '0px' + }, 0) + + this.settingsButton.setDialogSize(this.size) + } else { + videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + } + } + + /** + * Create back button + * + * @method createBackButton + */ + createBackButton () { + const button = this.subMenu.menu.addChild('MenuItem', {}, 0) + button.name_ = 'BackButton' + button.addClass('vjs-back-button') + button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_) + } + + /** + * Add/remove prefixed event listener for CSS Transition + * + * @method PrefixedEvent + */ + PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { + let prefix = ['webkit', 'moz', 'MS', 'o', ''] + + for (let p = 0; p < prefix.length; p++) { + if (!prefix[p]) { + type = type.toLowerCase() + } + + if (action === 'addEvent') { + element.addEventListener(prefix[p] + type, callback, false) + } else if (action === 'removeEvent') { + element.removeEventListener(prefix[p] + type, callback, false) + } + } + } + + onTransitionEnd (event: any) { + if (event.propertyName !== 'margin-right') { + return + } + + if (this.menuToLoad === 'mainmenu') { + // hide submenu + videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + + // reset opacity to 0 + this.settingsSubMenuEl_.style.opacity = '0' + } + } + + reset () { + videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + this.settingsSubMenuEl_.style.opacity = '0' + this.setMargin() + } + + loadMainMenu () { + this.menuToLoad = 'mainmenu' + this.mainMenu.show() + this.mainMenu.el_.style.opacity = '0' + + // back button will always take you to main menu, so set dialog sizes + this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height]) + + // animation not triggered without timeout (some async stuff ?!?) + setTimeout(() => { + // animate margin and opacity before hiding the submenu + // this triggers CSS Transition event + this.setMargin() + this.mainMenu.el_.style.opacity = '1' + }, 0) + } + + build () { + this.subMenu.on('updateLabel', () => { + this.update() + }) + + this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) + this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) + this.panelChildEl.appendChild(this.settingsSubMenuEl_) + this.update() + + this.createBackButton() + this.getSize() + this.bindClickEvents() + + // prefixed event listeners for CSS TransitionEnd + this.PrefixedEvent( + this.settingsSubMenuEl_, + 'TransitionEnd', + this.transitionEndHandler, + 'addEvent' + ) + } + + update (event?: any) { + let target: HTMLElement = null + let subMenu = this.subMenu.name() + + if (event && event.type === 'tap') { + target = event.target + } else if (event) { + target = event.currentTarget + } + + // Playback rate menu button doesn't get a vjs-selected class + // or sets options_['selected'] on the selected playback rate. + // Thus we get the submenu value based on the labelEl of playbackRateMenuButton + if (subMenu === 'PlaybackRateMenuButton') { + setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250) + } else { + // Loop trough the submenu items to find the selected child + for (let subMenuItem of this.subMenu.menu.children_) { + if (!(subMenuItem instanceof component)) { + continue + } + + if (subMenuItem.hasClass('vjs-selected')) { + // Prefer to use the function + if (typeof subMenuItem.getLabel === 'function') { + this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel() + break + } + + this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label + } + } + } + + if (target && !target.classList.contains('vjs-back-button')) { + this.settingsButton.hideDialog() + } + } + + bindClickEvents () { + for (let item of this.subMenu.menu.children()) { + if (!(item instanceof component)) { + continue + } + item.on(['tap', 'click'], this.submenuClickHandler) + } + } + + // save size of submenus on first init + // if number of submenu items change dynamically more logic will be needed + getSize () { + this.dialog.removeClass('vjs-hidden') + this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) + this.setMargin() + this.dialog.addClass('vjs-hidden') + videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + } + + setMargin () { + let [width] = this.size + + this.settingsSubMenuEl_.style.marginRight = `-${width}px` + } + + /** + * Hide the sub menu + */ + hideSubMenu () { + // after removing settings item this.el_ === null + if (!this.el_) { + return + } + + if (videojsUntyped.dom.hasClass(this.el_, 'open')) { + videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') + videojsUntyped.dom.removeClass(this.el_, 'open') + } + } + +} + +SettingsMenuItem.prototype.contentElType = 'button' +videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem) + +export { SettingsMenuItem } diff --git a/client/src/assets/player/videojs-components/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts new file mode 100644 index 000000000..1e11a9546 --- /dev/null +++ b/client/src/assets/player/videojs-components/theater-button.ts @@ -0,0 +1,50 @@ +// FIXME: something weird with our path definition in tsconfig and typings +// @ts-ignore +import * as videojs from 'video.js' + +import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' + +const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') +class TheaterButton extends Button { + + private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' + + constructor (player: videojs.Player, options: any) { + super(player, options) + + const enabled = getStoredTheater() + if (enabled === true) { + this.player_.addClass(TheaterButton.THEATER_MODE_CLASS) + this.handleTheaterChange() + } + } + + buildCSSClass () { + return `vjs-theater-control ${super.buildCSSClass()}` + } + + handleTheaterChange () { + if (this.isTheaterEnabled()) { + this.controlText('Normal mode') + } else { + this.controlText('Theater mode') + } + + saveTheaterInStore(this.isTheaterEnabled()) + } + + handleClick () { + this.player_.toggleClass(TheaterButton.THEATER_MODE_CLASS) + + this.handleTheaterChange() + } + + private isTheaterEnabled () { + return this.player_.hasClass(TheaterButton.THEATER_MODE_CLASS) + } +} + +TheaterButton.prototype.controlText_ = 'Theater mode' + +TheaterButton.registerComponent('TheaterButton', TheaterButton) -- cgit v1.2.3 From 3b6f205c34bb931de0323581edf991ca33256e6b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 24 Jan 2019 10:16:30 +0100 Subject: Correctly implement p2p-media-loader --- .../player/videojs-components/p2p-info-button.ts | 16 ++++++----- .../videojs-components/resolution-menu-button.ts | 33 +++++++++++++++++++--- .../videojs-components/resolution-menu-item.ts | 18 +++++------- .../videojs-components/settings-menu-item.ts | 10 +++++-- 4 files changed, 53 insertions(+), 24 deletions(-) (limited to 'client/src/assets/player/videojs-components') diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index 03a5d29f0..2fc4c4562 100644 --- a/client/src/assets/player/videojs-components/p2p-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts @@ -1,4 +1,4 @@ -import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' +import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' import { bytes } from '../utils' const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') @@ -65,7 +65,7 @@ class P2pInfoButton extends Button { subDivHttp.appendChild(subDivHttpText) div.appendChild(subDivHttp) - this.player_.on('p2pInfo', (event: any, data: any) => { + this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => { // We are in HTTP fallback if (!data) { subDivHttp.className = 'vjs-peertube-displayed' @@ -74,11 +74,13 @@ class P2pInfoButton extends Button { return } - const downloadSpeed = bytes(data.downloadSpeed) - const uploadSpeed = bytes(data.uploadSpeed) - const totalDownloaded = bytes(data.downloaded) - const totalUploaded = bytes(data.uploaded) - const numPeers = data.numPeers + const p2pStats = data.p2p + + const downloadSpeed = bytes(p2pStats.downloadSpeed) + const uploadSpeed = bytes(p2pStats.uploadSpeed) + const totalDownloaded = bytes(p2pStats.downloaded) + const totalUploaded = bytes(p2pStats.uploaded) + const numPeers = p2pStats.numPeers subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts index 2847de470..abcc16411 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-button.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts @@ -14,11 +14,9 @@ class ResolutionMenuButton extends MenuButton { super(player, options) this.player = player - player.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) + player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) - if (player.webtorrent) { - player.webtorrent().on('videoFileUpdate', () => setTimeout(() => this.trigger('updateLabel'), 0)) - } + player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) } createEl () { @@ -49,11 +47,32 @@ class ResolutionMenuButton extends MenuButton { return 'vjs-resolution-control ' + super.buildWrapperCSSClass() } + private addClickListener (component: any) { + component.on('click', () => { + let children = this.menu.children() + + for (const child of children) { + if (component !== child) { + child.selected(false) + } + } + }) + } + private buildQualities (data: LoadedQualityData) { // The automatic resolution item will need other labels const labels: { [ id: number ]: string } = {} + data.qualityData.video.sort((a, b) => { + if (a.id > b.id) return -1 + if (a.id === b.id) return 0 + return 1 + }) + for (const d of data.qualityData.video) { + // Skip auto resolution, we'll add it ourselves + if (d.id === -1) continue + this.menu.addChild(new ResolutionMenuItem( this.player_, { @@ -77,6 +96,12 @@ class ResolutionMenuButton extends MenuButton { selected: true // By default, in auto mode } )) + + for (const m of this.menu.children()) { + this.addClickListener(m) + } + + this.trigger('menuChanged') } } ResolutionMenuButton.prototype.controlText_ = 'Quality' diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts index cc1c79739..6c42fefd2 100644 --- a/client/src/assets/player/videojs-components/resolution-menu-item.ts +++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts @@ -28,16 +28,12 @@ class ResolutionMenuItem extends MenuItem { this.id = options.id this.callback = options.callback - if (player.webtorrent) { - player.webtorrent().on('videoFileUpdate', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) + player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) - // We only want to disable the "Auto" item - if (this.id === -1) { - player.webtorrent().on('autoResolutionUpdate', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) - } + // We only want to disable the "Auto" item + if (this.id === -1) { + player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) } - - // TODO: update on HLS change } handleClick (event: any) { @@ -46,12 +42,12 @@ class ResolutionMenuItem extends MenuItem { super.handleClick(event) - this.callback(this.id) + this.callback(this.id, 'video') } updateSelection (data: ResolutionUpdateData) { if (this.id === -1) { - this.currentResolutionLabel = this.labels[data.resolutionId] + this.currentResolutionLabel = this.labels[data.id] } // Automatic resolution only @@ -60,7 +56,7 @@ class ResolutionMenuItem extends MenuItem { return } - this.selected(this.id === data.resolutionId) + this.selected(this.id === data.id) } updateAutoResolution (data: AutoResolutionUpdateData) { diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index b9a430290..f14959f9c 100644 --- a/client/src/assets/player/videojs-components/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts @@ -223,6 +223,11 @@ class SettingsMenuItem extends MenuItem { this.subMenu.on('updateLabel', () => { this.update() }) + this.subMenu.on('menuChanged', () => { + this.bindClickEvents() + this.setSize() + this.update() + }) this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) @@ -230,7 +235,7 @@ class SettingsMenuItem extends MenuItem { this.update() this.createBackButton() - this.getSize() + this.setSize() this.bindClickEvents() // prefixed event listeners for CSS TransitionEnd @@ -292,8 +297,9 @@ class SettingsMenuItem extends MenuItem { // save size of submenus on first init // if number of submenu items change dynamically more logic will be needed - getSize () { + setSize () { this.dialog.removeClass('vjs-hidden') + videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) this.setMargin() this.dialog.addClass('vjs-hidden') -- cgit v1.2.3 From 092092969633bbcf6d4891a083ea497a7d5c3154 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 29 Jan 2019 08:37:25 +0100 Subject: Add hls support on server --- .../src/assets/player/videojs-components/p2p-info-button.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'client/src/assets/player/videojs-components') diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index 2fc4c4562..6424787b2 100644 --- a/client/src/assets/player/videojs-components/p2p-info-button.ts +++ b/client/src/assets/player/videojs-components/p2p-info-button.ts @@ -75,11 +75,12 @@ class P2pInfoButton extends Button { } const p2pStats = data.p2p + const httpStats = data.http - const downloadSpeed = bytes(p2pStats.downloadSpeed) - const uploadSpeed = bytes(p2pStats.uploadSpeed) - const totalDownloaded = bytes(p2pStats.downloaded) - const totalUploaded = bytes(p2pStats.uploaded) + const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) + const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) + const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) + const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) const numPeers = p2pStats.numPeers subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + @@ -92,7 +93,7 @@ class P2pInfoButton extends Button { uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] peersNumber.textContent = numPeers - peersText.textContent = ' ' + this.player_.localize('peers') + peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) subDivHttp.className = 'vjs-peertube-hidden' subDivWebtorrent.className = 'vjs-peertube-displayed' -- cgit v1.2.3