Improve player

Add a settings dialog based on the work of Yanko Shterev (@yshterev):
https://github.com/yshterev/videojs-settings-menu. Thanks!
pull/489/head
Chocobozzz 2018-03-30 17:40:00 +02:00
parent 6b9af12936
commit c6352f2c64
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
17 changed files with 1381 additions and 359 deletions

View File

@ -21,6 +21,7 @@ import { MarkdownService } from '../shared'
import { VideoDownloadComponent } from './modal/video-download.component'
import { VideoReportComponent } from './modal/video-report.component'
import { VideoShareComponent } from './modal/video-share.component'
import { getVideojsOptions } from '../../../assets/player/peertube-player'
@Component({
selector: 'my-video-watch',
@ -341,45 +342,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.playerElement.poster = this.video.previewUrl
}
const videojsOptions = {
controls: true,
const videojsOptions = getVideojsOptions({
autoplay: this.isAutoplay(),
playbackRates: [ 0.5, 1, 1.5, 2 ],
plugins: {
peertube: {
videoFiles: this.video.files,
playerElement: this.playerElement,
videoViewUrl: this.videoService.getVideoViewUrl(this.video.uuid),
videoDuration: this.video.duration
},
hotkeys: {
enableVolumeScroll: false
}
},
controlBar: {
children: [
'playToggle',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'liveDisplay',
'flexibleWidthSpacer',
'progressControl',
'webTorrentButton',
'playbackRateMenuButton',
'muteToggle',
'volumeControl',
'resolutionMenuButton',
'fullscreenToggle'
]
}
}
inactivityTimeout: 4000,
videoFiles: this.video.files,
playerElement: this.playerElement,
videoViewUrl: this.videoService.getVideoViewUrl(this.video.uuid),
videoDuration: this.video.duration,
enableHotkeys: true,
peertubeLink: false
})
this.videoPlayerLoaded = true

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>settings</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
<g id="Artboard-4" transform="translate(-796.000000, -159.000000)" stroke="#fff" stroke-width="2">
<g id="38" transform="translate(796.000000, 159.000000)">
<path d="M7.20852293,4.3800958 C8.05442158,3.84706631 8.99528987,3.45099725 10,3.22301642 L10,1.99980749 C10,1.44762906 10.4433532,1 11.0093689,1 L12.9906311,1 C13.5480902,1 14,1.44371665 14,1.99980749 L14,3.22301642 C15.0047101,3.45099725 15.9455784,3.84706631 16.7914771,4.3800958 L17.6569904,3.5145825 C18.0474395,3.12413339 18.6774591,3.12110988 19.0776926,3.52134344 L20.4786566,4.92230738 C20.8728396,5.31649045 20.8786331,5.94979402 20.4854175,6.34300963 L19.6199042,7.20852293 C20.1529337,8.05442158 20.5490027,8.99528987 20.7769836,10 L22.0001925,10 C22.5523709,10 23,10.4433532 23,11.0093689 L23,12.9906311 C23,13.5480902 22.5562834,14 22.0001925,14 L20.7769836,14 C20.5490027,15.0047101 20.1529337,15.9455784 19.6199042,16.7914771 L20.4854175,17.6569904 C20.8758666,18.0474395 20.8788901,18.6774591 20.4786566,19.0776926 L19.0776926,20.4786566 C18.6835095,20.8728396 18.050206,20.8786331 17.6569904,20.4854175 L16.7914771,19.6199042 C15.9455784,20.1529337 15.0047101,20.5490027 14,20.7769836 L14,22.0001925 C14,22.5523709 13.5566468,23 12.9906311,23 L11.0093689,23 C10.4519098,23 10,22.5562834 10,22.0001925 L10,20.7769836 C8.99528987,20.5490027 8.05442158,20.1529337 7.20852293,19.6199042 L6.34300963,20.4854175 C5.95256051,20.8758666 5.32254093,20.8788901 4.92230738,20.4786566 L3.52134344,19.0776926 C3.12716036,18.6835095 3.12136689,18.050206 3.5145825,17.6569904 L4.3800958,16.7914771 C3.84706631,15.9455784 3.45099725,15.0047101 3.22301642,14 L1.99980749,14 C1.44762906,14 1,13.5566468 1,12.9906311 L1,11.0093689 C1,10.4519098 1.44371665,10 1.99980749,10 L3.22301642,10 C3.45099725,8.99528987 3.84706631,8.05442158 4.3800958,7.20852293 L3.5145825,6.34300963 C3.12413339,5.95256051 3.12110988,5.32254093 3.52134344,4.92230738 L4.92230738,3.52134344 C5.31649045,3.12716036 5.94979402,3.12136689 6.34300963,3.5145825 L7.20852293,4.3800958 Z M12,16 C14.209139,16 16,14.209139 16,12 C16,9.790861 14.209139,8 12,8 C9.790861,8 8,9.790861 8,12 C8,14.209139 9.790861,16 12,16 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
<g id="Artboard-4" transform="translate(-356.000000, -115.000000)" stroke="#fff" stroke-width="2">
<g id="8" transform="translate(356.000000, 115.000000)">
<path d="M21,6 L9,18" id="Path-14"></path>
<path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 738 B

View File

@ -0,0 +1,20 @@
import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
class PeerTubeLinkButton extends Button {
createEl () {
return videojsUntyped.dom.createEl('a', {
href: window.location.href.replace('embed', 'watch'),
innerHTML: 'PeerTube',
title: 'Go to the video page',
className: 'vjs-peertube-link',
target: '_blank'
})
}
handleClick () {
this.player_.pause()
}
}
Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)

View File

@ -0,0 +1,96 @@
import { VideoFile } from '../../../../shared/models/videos'
import 'videojs-hotkeys'
import 'videojs-dock/dist/videojs-dock.es.js'
import './peertube-link-button'
import './resolution-menu-button'
import './settings-menu-button'
import './webtorrent-info-button'
import './peertube-videojs-plugin'
import { videojsUntyped } from './peertube-videojs-typings'
// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
function getVideojsOptions (options: {
autoplay: boolean,
playerElement: HTMLVideoElement,
videoViewUrl: string,
videoDuration: number,
videoFiles: VideoFile[],
enableHotkeys: boolean,
inactivityTimeout: number,
peertubeLink: boolean
}) {
const videojsOptions = {
controls: true,
autoplay: options.autoplay,
inactivityTimeout: options.inactivityTimeout,
playbackRates: [ 0.5, 1, 1.5, 2 ],
plugins: {
peertube: {
videoFiles: options.videoFiles,
playerElement: options.playerElement,
videoViewUrl: options.videoViewUrl,
videoDuration: options.videoDuration
}
},
controlBar: {
children: getControlBarChildren(options)
}
}
if (options.enableHotkeys === true) {
Object.assign(videojsOptions.plugins, {
hotkeys: {
enableVolumeScroll: false
}
})
}
return videojsOptions
}
function getControlBarChildren (options: {
peertubeLink: boolean
}) {
const children = {
'playToggle': {},
'currentTimeDisplay': {},
'timeDivider': {},
'durationDisplay': {},
'liveDisplay': {},
'flexibleWidthSpacer': {},
'progressControl': {},
'webTorrentButton': {},
'muteToggle': {},
'volumeControl': {},
'settingsButton': {
setup: {
maxHeightOffset: 40
},
entries: [
'resolutionMenuButton',
'playbackRateMenuButton'
]
}
}
if (options.peertubeLink === true) {
Object.assign(children, {
'peerTubeLinkButton': {}
})
}
Object.assign(children, {
'fullscreenToggle': {}
})
return children
}
export { getVideojsOptions }

View File

@ -1,49 +1,11 @@
// Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher
import * as videojs from 'video.js'
import * as WebTorrent from 'webtorrent'
import { VideoConstant, VideoResolution } from '../../../../shared/models/videos'
import { VideoFile } from '../../../../shared/models/videos/video.model'
import { renderVideo } from './video-renderer'
import './settings-menu-button'
import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
import { getStoredMute, getStoredVolume, saveMuteInStore, saveVolumeInStore } from './utils'
declare module 'video.js' {
interface Player {
peertube (): PeerTubePlugin
}
}
interface VideoJSComponentInterface {
_player: videojs.Player
new (player: videojs.Player, options?: any)
registerComponent (name: string, obj: any)
}
type PeertubePluginOptions = {
videoFiles: VideoFile[]
playerElement: HTMLVideoElement
videoViewUrl: string
videoDuration: number
}
// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
// Don't import all Angular stuff, just copy the code with shame
const dictionaryBytes: Array<{max: number, type: string}> = [
{ max: 1024, type: 'B' },
{ max: 1048576, type: 'KB' },
{ max: 1073741824, type: 'MB' },
{ max: 1.0995116e12, type: 'GB' }
]
function bytes (value) {
const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1]
const calc = Math.floor(value / (format.max / 1024)).toString()
return [ calc, format.type ]
}
// videojs typings don't have some method we need
const videojsUntyped = videojs as any
const webtorrent = new WebTorrent({
tracker: {
rtcConfig: {
@ -60,199 +22,19 @@ const webtorrent = new WebTorrent({
dht: false
})
const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
class ResolutionMenuItem extends MenuItem {
constructor (player: videojs.Player, options) {
options.selectable = true
super(player, options)
const currentResolutionId = this.player_.peertube().getCurrentResolutionId()
this.selected(this.options_.id === currentResolutionId)
}
handleClick (event) {
super.handleClick(event)
this.player_.peertube().updateResolution(this.options_.id)
}
}
MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
class ResolutionMenuButton extends MenuButton {
label: HTMLElement
constructor (player: videojs.Player, options) {
options.label = 'Quality'
super(player, options)
this.label = document.createElement('span')
this.el().setAttribute('aria-label', 'Quality')
this.controlText('Quality')
videojsUntyped.dom.addClass(this.label, 'vjs-resolution-button-label')
this.el().appendChild(this.label)
player.peertube().on('videoFileUpdate', () => this.update())
}
createItems () {
const menuItems = []
for (const videoFile of this.player_.peertube().videoFiles) {
menuItems.push(new ResolutionMenuItem(
this.player_,
{
id: videoFile.resolution.id,
label: videoFile.resolution.label,
src: videoFile.magnetUri,
selected: videoFile.resolution.id === this.currentSelectionId
})
)
}
return menuItems
}
update () {
if (!this.label) return
this.label.innerHTML = this.player_.peertube().getCurrentResolutionLabel()
this.hide()
return super.update()
}
buildCSSClass () {
return super.buildCSSClass() + ' vjs-resolution-button'
}
buildWrapperCSSClass () {
return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
}
}
MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
class PeerTubeLinkButton extends Button {
createEl () {
const link = document.createElement('a')
link.href = window.location.href.replace('embed', 'watch')
link.innerHTML = 'PeerTube'
link.title = 'Go to the video page'
link.className = 'vjs-peertube-link'
link.target = '_blank'
return link
}
handleClick () {
this.player_.pause()
}
}
Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)
class WebTorrentButton extends Button {
createEl () {
const div = document.createElement('div')
const subDivWebtorrent = document.createElement('div')
div.appendChild(subDivWebtorrent)
const downloadIcon = document.createElement('span')
downloadIcon.classList.add('icon', 'icon-download')
subDivWebtorrent.appendChild(downloadIcon)
const downloadSpeedText = document.createElement('span')
downloadSpeedText.classList.add('download-speed-text')
const downloadSpeedNumber = document.createElement('span')
downloadSpeedNumber.classList.add('download-speed-number')
const downloadSpeedUnit = document.createElement('span')
downloadSpeedText.appendChild(downloadSpeedNumber)
downloadSpeedText.appendChild(downloadSpeedUnit)
subDivWebtorrent.appendChild(downloadSpeedText)
const uploadIcon = document.createElement('span')
uploadIcon.classList.add('icon', 'icon-upload')
subDivWebtorrent.appendChild(uploadIcon)
const uploadSpeedText = document.createElement('span')
uploadSpeedText.classList.add('upload-speed-text')
const uploadSpeedNumber = document.createElement('span')
uploadSpeedNumber.classList.add('upload-speed-number')
const uploadSpeedUnit = document.createElement('span')
uploadSpeedText.appendChild(uploadSpeedNumber)
uploadSpeedText.appendChild(uploadSpeedUnit)
subDivWebtorrent.appendChild(uploadSpeedText)
const peersText = document.createElement('span')
peersText.classList.add('peers-text')
const peersNumber = document.createElement('span')
peersNumber.classList.add('peers-number')
subDivWebtorrent.appendChild(peersNumber)
subDivWebtorrent.appendChild(peersText)
div.className = 'vjs-peertube'
// Hide the stats before we get the info
subDivWebtorrent.className = 'vjs-peertube-hidden'
const subDivHttp = document.createElement('div')
subDivHttp.className = 'vjs-peertube-hidden'
const subDivHttpText = document.createElement('span')
subDivHttpText.classList.add('peers-number')
subDivHttpText.textContent = 'HTTP'
const subDivFallbackText = document.createElement('span')
subDivFallbackText.classList.add('peers-text')
subDivFallbackText.textContent = ' fallback'
subDivHttp.appendChild(subDivHttpText)
subDivHttp.appendChild(subDivFallbackText)
div.appendChild(subDivHttp)
this.player_.peertube().on('torrentInfo', (event, data) => {
// 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 numPeers = data.numPeers
downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
peersNumber.textContent = numPeers
peersText.textContent = ' peers'
subDivHttp.className = 'vjs-peertube-hidden'
subDivWebtorrent.className = 'vjs-peertube-displayed'
})
return div
}
}
Button.registerComponent('WebTorrentButton', WebTorrentButton)
const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin')
class PeerTubePlugin extends Plugin {
private readonly playerElement: HTMLVideoElement
private readonly autoplay: boolean = false
private readonly savePlayerSrcFunction: Function
private player: any
private currentVideoFile: VideoFile
private playerElement: HTMLVideoElement
private videoFiles: VideoFile[]
private torrent: WebTorrent.Torrent
private autoplay = false
private videoViewUrl: string
private videoDuration: number
private videoViewInterval
private torrentInfoInterval
private savePlayerSrcFunction: Function
constructor (player: videojs.Player, options: PeertubePluginOptions) {
super(player, options)
@ -274,10 +56,20 @@ class PeerTubePlugin extends Plugin {
this.playerElement = options.playerElement
this.player.ready(() => {
const volume = getStoredVolume()
if (volume !== undefined) this.player.volume(volume)
const muted = getStoredMute()
if (muted !== undefined) this.player.muted(muted)
this.initializePlayer()
this.runTorrentInfoScheduler()
this.runViewAdd()
})
this.player.on('volumechange', () => {
saveVolumeInStore(this.player.volume())
saveMuteInStore(this.player.muted())
})
}
dispose () {
@ -311,16 +103,19 @@ class PeerTubePlugin extends Plugin {
return
}
// Do not display error to user because we will have multiple fallbacks
// Do not display error to user because we will have multiple fallback
this.disableErrorDisplay()
this.player.src = () => true
this.player.playbackRate(1)
const oldPlaybackRate = this.player.playbackRate()
const previousVideoFile = this.currentVideoFile
this.currentVideoFile = videoFile
this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, done)
this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, () => {
this.player.playbackRate(oldPlaybackRate)
return done()
})
this.trigger('videoFileUpdate')
}
@ -337,7 +132,7 @@ class PeerTubePlugin extends Plugin {
renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => {
this.renderer = renderer
if (err) return this.fallbackToHttp()
if (err) return this.fallbackToHttp(done)
if (!this.player.paused()) {
const playPromise = this.player.play()
@ -414,13 +209,17 @@ class PeerTubePlugin extends Plugin {
private initializePlayer () {
this.initSmoothProgressBar()
this.alterInactivity()
if (this.autoplay === true) {
this.updateVideoFile(undefined, () => this.player.play())
} else {
this.player.one('play', () => {
this.player.pause()
this.updateVideoFile(undefined, () => this.player.play())
})
// Proxify first play
const oldPlay = this.player.play.bind(this.player)
this.player.play = () => {
this.updateVideoFile(undefined, () => oldPlay)
this.player.play = oldPlay
}
}
}
@ -473,7 +272,7 @@ class PeerTubePlugin extends Plugin {
return fetch(this.videoViewUrl, { method: 'POST' })
}
private fallbackToHttp () {
private fallbackToHttp (done: Function) {
this.flushVideoFile(this.currentVideoFile, true)
this.torrent = null
@ -484,6 +283,8 @@ class PeerTubePlugin extends Plugin {
this.player.src = this.savePlayerSrcFunction
this.player.src(httpUrl)
this.player.play()
return done()
}
private handleError (err: Error | string) {
@ -498,6 +299,25 @@ class PeerTubePlugin extends Plugin {
this.player.removeClass('vjs-error-display-enabled')
}
private alterInactivity () {
let saveInactivityTimeout: number
const disableInactivity = () => {
saveInactivityTimeout = this.player.options_.inactivityTimeout
this.player.options_.inactivityTimeout = 0
}
const enableInactivity = () => {
// this.player.options_.inactivityTimeout = saveInactivityTimeout
}
const settingsDialog = this.player.children_.find(c => c.name_ === 'SettingsDialog')
this.player.controlBar.on('mouseenter', () => disableInactivity())
settingsDialog.on('mouseenter', () => disableInactivity())
this.player.controlBar.on('mouseleave', () => enableInactivity())
settingsDialog.on('mouseleave', () => enableInactivity())
}
// Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
private initSmoothProgressBar () {
const SeekBar = videojsUntyped.getComponent('SeekBar')
@ -520,4 +340,6 @@ class PeerTubePlugin extends Plugin {
}
}
}
videojsUntyped.registerPlugin('peertube', PeerTubePlugin)
export { PeerTubePlugin }

View File

@ -0,0 +1,33 @@
import * as videojs from 'video.js'
import { VideoFile } from '../../../../shared/models/videos/video.model'
import { PeerTubePlugin } from './peertube-videojs-plugin'
declare module 'video.js' {
interface Player {
peertube (): PeerTubePlugin
}
}
interface VideoJSComponentInterface {
_player: videojs.Player
new (player: videojs.Player, options?: any)
registerComponent (name: string, obj: any)
}
type PeertubePluginOptions = {
videoFiles: VideoFile[]
playerElement: HTMLVideoElement
videoViewUrl: string
videoDuration: number
}
// videojs typings don't have some method we need
const videojsUntyped = videojs as any
export {
VideoJSComponentInterface,
PeertubePluginOptions,
videojsUntyped
}

View File

@ -0,0 +1,68 @@
import * as videojs from 'video.js'
import { 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: videojs.Player, options) {
options.label = 'Quality'
super(player, options)
this.controlText_ = 'Quality'
this.player = player
player.peertube().on('videoFileUpdate', () => this.updateLabel())
}
createEl () {
const el = super.createEl()
this.labelEl_ = videojsUntyped.dom.createEl('div', {
className: 'vjs-resolution-value',
innerHTML: this.player_.peertube().getCurrentResolutionLabel()
})
el.appendChild(this.labelEl_)
return el
}
updateARIAAttributes () {
this.el().setAttribute('aria-label', 'Quality')
}
createMenu () {
const menu = new Menu(this.player())
for (const videoFile of this.player_.peertube().videoFiles) {
menu.addChild(new ResolutionMenuItem(
this.player_,
{
id: videoFile.resolution.id,
label: videoFile.resolution.label,
src: videoFile.magnetUri
})
)
}
return menu
}
updateLabel () {
if (!this.labelEl_) return
this.labelEl_.innerHTML = this.player_.peertube().getCurrentResolutionLabel()
}
buildCSSClass () {
return super.buildCSSClass() + ' vjs-resolution-button'
}
buildWrapperCSSClass () {
return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
}
}
MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)

View File

@ -0,0 +1,31 @@
import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
class ResolutionMenuItem extends MenuItem {
constructor (player: videojs.Player, options) {
const currentResolutionId = player.peertube().getCurrentResolutionId()
options.selectable = true
options.selected = options.id === currentResolutionId
super(player, options)
this.label = options.label
this.id = options.id
player.peertube().on('videoFileUpdate', () => this.update())
}
handleClick (event) {
super.handleClick(event)
this.player_.peertube().updateResolution(this.id)
}
update () {
this.selected(this.player_.peertube().getCurrentResolutionId() === this.id)
}
}
MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
export { ResolutionMenuItem }

View File

@ -0,0 +1,285 @@
// Author: Yanko Shterev
// Thanks https://github.com/yshterev/videojs-settings-menu
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) {
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 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, 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, data) {
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) {
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, options) {
const openSubMenu = function () {
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) {
super(player, options)
}
createEl () {
return super.createEl('div', {
className: 'vjs-settings-panel',
innerHTML: '',
tabIndex: -1
})
}
}
class SettingsPanelChild extends Component {
constructor (player: videojs.Player, options) {
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) {
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 Button'
Component.registerComponent('SettingsButton', SettingsButton)
Component.registerComponent('SettingsDialog', SettingsDialog)
Component.registerComponent('SettingsPanel', SettingsPanel)
Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild }

View File

@ -0,0 +1,313 @@
// Author: Yanko Shterev
// Thanks https://github.com/yshterev/videojs-settings-menu
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, 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)
this.eventHandlers()
player.ready(() => {
this.build()
this.reset()
})
}
eventHandlers () {
this.submenuClickHandler = this.onSubmenuClick.bind(this)
this.transitionEndHandler = this.onTransitionEnd.bind(this)
}
onSubmenuClick (event) {
let target = null
if (event.type === 'tap') {
target = event.target
} else {
target = event.currentTarget
}
if (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.subMenu.controlText_
}
/**
* Add/remove prefixed event listener for CSS Transition
*
* @method PrefixedEvent
*/
PrefixedEvent (element, type, callback, 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) {
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 () {
const saveUpdateLabel = this.subMenu.updateLabel
this.subMenu.updateLabel = () => {
this.update()
saveUpdateLabel.call(this.subMenu)
}
this.settingsSubMenuTitleEl_.innerHTML = 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?: Event) {
let target = 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
}
switch (subMenu) {
case 'SubtitlesButton':
case 'CaptionsButton':
// subtitlesButton entering default check twice and overwriting
// selected label in main manu
if (subMenuItem.hasClass('vjs-selected')) {
this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
}
break
default:
// Set submenu value based on what item is selected
if (subMenuItem.options_.selected || subMenuItem.hasClass('vjs-selected')) {
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 }

View File

@ -0,0 +1,72 @@
function toTitleCase (str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
// Don't import all Angular stuff, just copy the code with shame
const dictionaryBytes: Array<{max: number, type: string}> = [
{ max: 1024, type: 'B' },
{ max: 1048576, type: 'KB' },
{ max: 1073741824, type: 'MB' },
{ max: 1.0995116e12, type: 'GB' }
]
function bytes (value) {
const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1]
const calc = Math.floor(value / (format.max / 1024)).toString()
return [ calc, format.type ]
}
function getStoredVolume () {
const value = getLocalStorage('volume')
if (value !== null && value !== undefined) {
const valueNumber = parseFloat(value)
if (isNaN(valueNumber)) return undefined
return valueNumber
}
return undefined
}
function getStoredMute () {
const value = getLocalStorage('mute')
if (value !== null && value !== undefined) return value === 'true'
return undefined
}
function saveVolumeInStore (value: number) {
return setLocalStorage('volume', value.toString())
}
function saveMuteInStore (value: boolean) {
return setLocalStorage('mute', value.toString())
}
export {
toTitleCase,
getStoredVolume,
saveVolumeInStore,
saveMuteInStore,
getStoredMute,
bytes
}
// ---------------------------------------------------------------------------
const KEY_PREFIX = 'peertube-videojs-'
function getLocalStorage (key: string) {
try {
return localStorage.getItem(KEY_PREFIX + key)
} catch {
return undefined
}
}
function setLocalStorage (key: string, value: string) {
try {
localStorage.setItem(KEY_PREFIX + key, value)
} catch { /* empty */ }
}

View File

@ -0,0 +1,101 @@
import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
import { bytes } from './utils'
const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
class WebtorrentInfoButton 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: 'peers-number',
textContent: 'HTTP'
})
const subDivFallbackText = videojsUntyped.dom.createEl('span', {
className: 'peers-text',
textContent: 'fallback'
})
subDivHttp.appendChild(subDivHttpText)
subDivHttp.appendChild(subDivFallbackText)
div.appendChild(subDivHttp)
this.player_.peertube().on('torrentInfo', (event, data) => {
// 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 numPeers = data.numPeers
downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
peersNumber.textContent = numPeers
peersText.textContent = ' peers'
subDivHttp.className = 'vjs-peertube-hidden'
subDivWebtorrent.className = 'vjs-peertube-displayed'
})
return div
}
}
Button.registerComponent('WebTorrentButton', WebtorrentInfoButton)

View File

@ -279,3 +279,27 @@
width: $size;
height: $size;
}
@mixin chevron ($size, $border-width) {
border-style: solid;
border-width: $border-width $border-width 0 0;
content: '';
display: inline-block;
transform: rotate(-45deg);
height: $size;
width: $size;
}
@mixin chevron-right ($size, $border-width) {
@include chevron($size, $border-width);
left: 0;
transform: rotate(45deg);
}
@mixin chevron-left ($size, $border-width) {
@include chevron($size, $border-width);
left: 0.25em;
transform: rotate(-135deg);
}

View File

@ -1,7 +1,7 @@
@import '_variables';
@import '_mixins';
$primary-foreground-color: #eee;
$primary-foreground-color: #fff;
$primary-foreground-opacity: 0.9;
$primary-foreground-opacity-hover: 1;
$primary-background-color: #000;
@ -11,9 +11,12 @@ $control-bar-height: 34px;
$slider-bg-color: lighten($primary-background-color, 33%);
$setting-transition-duration: 0.15s;
$setting-transition-easing: ease-out;
.video-js.vjs-peertube-skin {
font-size: $font-size;
color: #fff;
color: $primary-foreground-color;
.vjs-dock-text {
padding-right: 10px;
@ -22,16 +25,16 @@ $slider-bg-color: lighten($primary-background-color, 33%);
.vjs-dock-description {
font-size: 11px;
&:before, &:after {
&::before, &::after {
display: inline-block;
content: '\1F308';
}
&:before {
&::before {
margin-right: 4px;
}
&:after {
&::after {
margin-left: 4px;
transform: scale(-1, 1);
}
@ -41,7 +44,7 @@ $slider-bg-color: lighten($primary-background-color, 33%);
line-height: $control-bar-height;
}
.vjs-volume-level:before {
.vjs-volume-level::before {
content: ''; /* Remove Circle From Progress Bar */
}
@ -95,7 +98,7 @@ $slider-bg-color: lighten($primary-background-color, 33%);
.vjs-control-bar,
.vjs-big-play-button,
.vjs-menu-button .vjs-menu-content {
.vjs-settings-dialog {
background-color: rgba($primary-background-color, 0.5);
}
@ -110,8 +113,13 @@ $slider-bg-color: lighten($primary-background-color, 33%);
}
.vjs-play-progress {
&::before:hover {
top: -0.372em;
&::before {
top: -0.3em;
&:hover {
top: -0.372em;
}
}
.vjs-time-tooltip {
@ -141,8 +149,11 @@ $slider-bg-color: lighten($primary-background-color, 33%);
.vjs-mute-control,
.vjs-volume-control,
.vjs-resolution-control,
.vjs-fullscreen-control
.vjs-fullscreen-control,
.vjs-peertube-link,
.vjs-settings
{
color: $primary-foreground-color !important;
opacity: $primary-foreground-opacity;
transition: opacity .1s;
@ -155,6 +166,7 @@ $slider-bg-color: lighten($primary-background-color, 33%);
.vjs-duration,
.vjs-peertube {
color: $primary-foreground-color;
opacity: $primary-foreground-opacity;
}
.vjs-progress-control {
@ -172,6 +184,7 @@ $slider-bg-color: lighten($primary-background-color, 33%);
.vjs-play-control {
@include disable-outline;
cursor: pointer;
font-size: $font-size;
padding: 0 17px;
margin-right: 5px;
@ -291,7 +304,7 @@ $slider-bg-color: lighten($primary-background-color, 33%);
.vjs-volume-control {
width: 30px;
margin: 0;
margin: 0 5px 0 0;
}
.vjs-volume-bar {
@ -348,6 +361,16 @@ $slider-bg-color: lighten($primary-background-color, 33%);
}
}
.vjs-peertube-link {
@include disable-outline;
@include disable-default-a-behaviour;
text-decoration: none;
line-height: $control-bar-height;
font-weight: $font-semibold;
padding: 0 5px;
}
.vjs-fullscreen-control {
@include disable-outline;
@ -371,19 +394,6 @@ $slider-bg-color: lighten($primary-background-color, 33%);
font-weight: $font-semibold;
width: 50px;
// Thanks: https://github.com/kmoskwiak/videojs-resolution-switcher/pull/92/files
.vjs-resolution-button-label {
line-height: $control-bar-height;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
box-sizing: inherit;
text-align: center;
}
.vjs-resolution-button {
@include disable-outline;
}
@ -451,6 +461,35 @@ $slider-bg-color: lighten($primary-background-color, 33%);
}
}
// Play/pause animations
.vjs-has-started .vjs-play-control {
&.vjs-playing {
animation: remove-pause-button 0.25s ease;
}
&.vjs-paused {
animation: add-play-button 0.25s ease;
}
@keyframes remove-pause-button {
0% {
transform: rotate(90deg);
}
100% {
transform: rotate(0deg);
}
}
@keyframes add-play-button {
0% {
transform: rotate(-90deg);
}
100% {
transform: rotate(0deg);
}
}
}
// Thanks: https://projects.lukehaas.me/css-loaders/
.vjs-loading-spinner {
left: 50%;
@ -463,11 +502,11 @@ $slider-bg-color: lighten($primary-background-color, 33%);
overflow: hidden;
visibility: hidden;
&:before {
&::before {
animation: none !important;
}
&:after {
&::after {
border-radius: 50%;
width: 6em;
height: 6em;
@ -520,3 +559,169 @@ $slider-bg-color: lighten($primary-background-color, 33%);
display: block;
}
}
/* Sass for videojs-settings-menu */
.video-js {
.vjs-settings {
@include disable-outline;
cursor: pointer;
width: 37px;
.vjs-icon-placeholder {
display: inline-block;
width: 17px;
height: 17px;
vertical-align: middle;
background: url('../assets/player/images/settings.svg') no-repeat;
background-size: contain;
&::before {
content: '';
}
}
}
.vjs-settings-sub-menu-title {
width: 4em;
text-transform: initial;
}
.vjs-settings-dialog {
position: absolute;
right: .5em;
bottom: 3.5em;
color: $primary-foreground-color;
opacity: $primary-foreground-opacity;
margin: 0 auto;
font-size: $font-size !important;
width: auto;
overflow: hidden;
transition: width $setting-transition-duration $setting-transition-easing, height $setting-transition-duration $setting-transition-easing;
.vjs-settings-sub-menu-value,
.vjs-settings-sub-menu-title {
display: table-cell;
padding: 0 5px;
}
.vjs-settings-sub-menu-title {
text-align: left;
font-weight: $font-semibold;
}
.vjs-settings-sub-menu-value {
width: 100%;
text-align: right;
}
.vjs-settings-panel {
position: absolute;
bottom: 0;
right: 0;
overflow-y: auto;
overflow-x: hidden;
border-radius: 1px;
}
.vjs-settings-panel-child {
display: flex;
align-items: flex-end;
white-space: nowrap;
&:focus,
&:active {
outline: none;
}
> .vjs-menu {
flex: 1;
min-width: 200px;
}
> .vjs-menu,
> .vjs-settings-sub-menu {
transition: all $setting-transition-duration $setting-transition-easing;
.vjs-menu-item {
&:first-child {
margin-top: 5px;
}
&:last-child {
margin-bottom: 5px;
}
}
li {
font-size: 1em;
text-transform: initial;
&:hover {
cursor: pointer;
}
}
}
> .vjs-menu {
.vjs-menu-item {
padding: 8px 16px;
}
.vjs-settings-sub-menu-value::after {
@include chevron-right(9px, 2px);
margin-left: 5px;
}
}
> .vjs-settings-sub-menu {
width: 80px;
.vjs-menu-item {
outline: 0;
font-weight: $font-semibold;
padding: 5px 8px;
text-align: right;
&.vjs-back-button {
background-color: inherit;
padding: 8px 8px 13px 8px;
margin-bottom: 5px;
border-bottom: 1px solid grey;
&::before {
@include chevron-left(9px, 2px);
margin-right: 5px;
}
}
&.vjs-selected {
background-color: inherit;
color: inherit;
position: relative;
&::before {
@include icon(15px);
position: absolute;
left: 8px;
content: ' ';
margin-top: 1px;
background-image: url('../assets/player/images/tick.svg');
}
}
}
}
}
}
}

View File

@ -14,8 +14,6 @@ html, body {
margin: 0;
}
.video-js.vjs-peertube-skin {
width: 100%;
height: 100%;
@ -25,22 +23,6 @@ html, body {
background-size: 100% auto;
}
.vjs-peertube-link {
@include disable-outline;
color: #fff;
text-decoration: none;
font-size: $font-size;
line-height: $control-bar-height;
transition: all .4s;
font-weight: $font-semibold;
padding-right: 5px;
}
.vjs-peertube-link:hover {
text-shadow: 0 0 1em #fff;
}
@media screen and (max-width: 350px) {
.vjs-play-control {
padding: 0 5px !important;

View File

@ -1,10 +1,9 @@
import './embed.scss'
import * as videojs from 'video.js'
import 'videojs-hotkeys'
import '../../assets/player/peertube-videojs-plugin'
import 'videojs-dock/dist/videojs-dock.es.js'
import { VideoDetails } from '../../../../shared'
import { getVideojsOptions } from '../../assets/player/peertube-player'
function getVideoUrl (id: string) {
return window.location.origin + '/api/v1/videos/' + id
@ -20,9 +19,10 @@ const videoId = urlParts[urlParts.length - 1]
loadVideoInfo(videoId)
.then(videoInfo => {
const videoElement = document.getElementById('video-container') as HTMLVideoElement
const previewUrl = window.location.origin + videoInfo.previewPath
videoElement.poster = previewUrl
const videoContainerId = 'video-container'
const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
videoElement.poster = window.location.origin + videoInfo.previewPath
let autoplay = false
@ -33,45 +33,17 @@ loadVideoInfo(videoId)
console.error('Cannot get params from URL.', err)
}
const videojsOptions = {
controls: true,
const videojsOptions = getVideojsOptions({
autoplay,
inactivityTimeout: 500,
plugins: {
peertube: {
videoFiles: videoInfo.files,
playerElement: videoElement,
videoViewUrl: getVideoUrl(videoId) + '/views',
videoDuration: videoInfo.duration
},
hotkeys: {
enableVolumeScroll: false
}
},
controlBar: {
children: [
'playToggle',
'currentTimeDisplay',
'timeDivider',
'durationDisplay',
'liveDisplay',
'flexibleWidthSpacer',
'progressControl',
'webTorrentButton',
'muteToggle',
'volumeControl',
'resolutionMenuButton',
'peerTubeLinkButton',
'fullscreenToggle'
]
}
}
videojs('video-container', videojsOptions, function () {
inactivityTimeout: 1500,
videoViewUrl: getVideoUrl(videoId) + '/views',
playerElement: videoElement,
videoFiles: videoInfo.files,
videoDuration: videoInfo.duration,
enableHotkeys: true,
peertubeLink: true
})
videojs(videoContainerId, videojsOptions, function () {
const player = this
player.dock({