-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="24px" height="24px" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <path fill="white" d="M 12,24 20.5,18 12,12 V 24 z M 22,12 v 12 h 2 V 12 h -2 z"></path>
-</svg>
\ No newline at end of file
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ sodipodi:docname="next.svg"
+ id="svg4"
+ version="1.1"
+ viewBox="0 0 12 12"
+ height="8"
+ width="8">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ inkscape:current-layer="svg4"
+ inkscape:window-maximized="1"
+ inkscape:window-y="0"
+ inkscape:window-x="0"
+ inkscape:cy="-2.5620165"
+ inkscape:cx="-7.4038126"
+ inkscape:zoom="29.791667"
+ fit-margin-bottom="0"
+ fit-margin-right="0"
+ fit-margin-left="0"
+ fit-margin-top="0"
+ showgrid="false"
+ id="namedview6"
+ inkscape:window-height="1037"
+ inkscape:window-width="1916"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#ffffff" />
+ <path
+ id="path2"
+ d="M 0,12 8.5,6 0,0 Z M 10,0 v 12 h 2 V 0 Z"
+ fill="#ffffff" />
+</svg>
import './upnext/upnext-plugin'
import './bezels/bezels-plugin'
import './peertube-plugin'
-import './videojs-components/next-video-button'
+import './videojs-components/next-previous-video-button'
import './videojs-components/p2p-info-button'
import './videojs-components/peertube-link-button'
import './videojs-components/peertube-load-progress-bar'
import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
import { getStoredP2PEnabled } from './peertube-player-local-storage'
import {
+ NextPreviousVideoButtonOptions,
P2PMediaLoaderPluginOptions,
PlaylistPluginOptions,
UserWatching,
onPlayerElementChange: (element: HTMLVideoElement) => void
autoplay: boolean
- nextVideo?: Function
+
+ nextVideo?: () => void
+ hasNextVideo?: () => boolean
+
+ previousVideo?: () => void
+ hasPreviousVideo?: () => boolean
playlist?: PlaylistPluginOptions
captions: commonOptions.captions,
peertubeLink: commonOptions.peertubeLink,
theaterButton: commonOptions.theaterButton,
- nextVideo: commonOptions.nextVideo
+
+ nextVideo: commonOptions.nextVideo,
+ hasNextVideo: commonOptions.hasNextVideo,
+
+ previousVideo: commonOptions.previousVideo,
+ hasPreviousVideo: commonOptions.hasPreviousVideo
}) as any // FIXME: typings
}
}
private static getControlBarChildren (mode: PlayerMode, options: {
peertubeLink: boolean
- theaterButton: boolean,
- captions: boolean,
+ theaterButton: boolean
+ captions: boolean
+
nextVideo?: Function
+ hasNextVideo?: () => boolean
+
+ previousVideo?: Function
+ hasPreviousVideo?: () => boolean
}) {
const settingEntries = []
const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
if (options.captions === true) settingEntries.push('captionsButton')
settingEntries.push('resolutionMenuButton')
- const children = {
- 'playToggle': {}
+ const children = {}
+
+ if (options.previousVideo) {
+ const buttonOptions: NextPreviousVideoButtonOptions = {
+ type: 'previous',
+ handler: options.previousVideo,
+ isDisabled: () => {
+ if (!options.hasPreviousVideo) return false
+
+ return !options.hasPreviousVideo()
+ }
+ }
+
+ Object.assign(children, {
+ 'previousVideoButton': buttonOptions
+ })
}
+ Object.assign(children, { playToggle: {} })
+
if (options.nextVideo) {
- Object.assign(children, {
- 'nextVideoButton': {
- handler: options.nextVideo
+ const buttonOptions: NextPreviousVideoButtonOptions = {
+ type: 'next',
+ handler: options.nextVideo,
+ isDisabled: () => {
+ if (!options.hasNextVideo) return false
+
+ return !options.hasNextVideo()
}
+ }
+
+ Object.assign(children, {
+ 'nextVideoButton': buttonOptions
})
}
onItemClicked: (element: VideoPlaylistElement) => void
}
+type NextPreviousVideoButtonOptions = {
+ type: 'next' | 'previous'
+ handler: Function
+ isDisabled: () => boolean
+}
+
type WebtorrentPluginOptions = {
playerElement: HTMLVideoElement
export {
PlayerNetworkInfo,
PlaylistItemOptions,
+ NextPreviousVideoButtonOptions,
ResolutionUpdateData,
AutoResolutionUpdateData,
PlaylistPluginOptions,
--- /dev/null
+import videojs from 'video.js'
+import { NextPreviousVideoButtonOptions } from '../peertube-videojs-typings'
+
+const Button = videojs.getComponent('Button')
+
+class NextPreviousVideoButton extends Button {
+ private readonly nextPreviousVideoButtonOptions: NextPreviousVideoButtonOptions
+
+ constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions) {
+ super(player, options as any)
+
+ this.nextPreviousVideoButtonOptions = options
+
+ this.update()
+ }
+
+ createEl () {
+ const type = (this.options_ as NextPreviousVideoButtonOptions).type
+
+ const button = videojs.dom.createEl('button', {
+ className: 'vjs-' + type + '-video'
+ }) as HTMLButtonElement
+ const nextIcon = videojs.dom.createEl('span', {
+ className: 'icon icon-' + type
+ })
+ button.appendChild(nextIcon)
+
+ if (type === 'next') {
+ button.title = this.player_.localize('Next video')
+ } else {
+ button.title = this.player_.localize('Previous video')
+ }
+
+ return button
+ }
+
+ handleClick () {
+ this.nextPreviousVideoButtonOptions.handler()
+ }
+
+ update () {
+ const disabled = this.nextPreviousVideoButtonOptions.isDisabled()
+
+ if (disabled) this.addClass('vjs-disabled')
+ else this.removeClass('vjs-disabled')
+ }
+}
+
+videojs.registerComponent('NextVideoButton', NextPreviousVideoButton)
+videojs.registerComponent('PreviousVideoButton', NextPreviousVideoButton)
+++ /dev/null
-import videojs from 'video.js'
-
-const Button = videojs.getComponent('Button')
-
-export interface NextVideoButtonOptions extends videojs.ComponentOptions {
- handler: Function
-}
-
-class NextVideoButton extends Button {
- private readonly nextVideoButtonOptions: NextVideoButtonOptions
-
- constructor (player: videojs.Player, options?: NextVideoButtonOptions) {
- super(player, options)
-
- this.nextVideoButtonOptions = options
- }
-
- createEl () {
- const button = videojs.dom.createEl('button', {
- className: 'vjs-next-video'
- }) as HTMLButtonElement
- const nextIcon = videojs.dom.createEl('span', {
- className: 'icon icon-next'
- })
- button.appendChild(nextIcon)
-
- button.title = this.player_.localize('Next video')
-
- return button
- }
-
- handleClick () {
- this.nextVideoButtonOptions.handler()
- }
-}
-
-videojs.registerComponent('NextVideoButton', NextVideoButton)
box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2);
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
+ > button:first-child {
+ margin-left: 1em;
+ }
+
.vjs-progress-control,
.vjs-play-control,
.vjs-playback-rate,
cursor: pointer;
font-size: $font-size;
- margin-left: 1em;
width: 3em;
}
}
}
- .vjs-next-video {
+ .vjs-next-video,
+ .vjs-previous-video {
line-height: $control-bar-height;
text-align: right;
.icon {
- &.icon-next {
+ &.icon-next,
+ &.icon-previous {
mask-image: url('#{$assets-path}/player/images/next.svg');
-webkit-mask-image: url('#{$assets-path}/player/images/next.svg');
background-color: white;
mask-size: cover;
-webkit-mask-size: cover;
- transform: scale(2.2);
+ width: 11px;
+ height: 11px;
+ margin-top: -2px;
+ display: inline-block;
+ }
+
+ &.icon-previous {
+ transform: rotate(180deg);
}
}
}
- .vjs-peertube,
- .vjs-next-video {
+ .vjs-peertube {
.icon {
display: inline-block;
width: 15px;
display: block;
}
}
+
+.vjs-no-next-in-playlist {
+ .vjs-next-video {
+ cursor: default;
+
+ .icon {
+ background-color: rgba(255, 255, 255, 0.5);
+ }
+ }
+}
cancelText: peertubeTranslate('Cancel', translations),
suspendedText: peertubeTranslate('Autoplay is suspended', translations),
getTitle: () => this.nextVideoTitle(),
- next: () => this.autoplayNext(),
+ next: () => this.playNextVideo(),
condition: () => !!this.getNextPlaylistElement(),
suspended: () => false
})
}
- private async autoplayNext () {
+ private async playNextVideo () {
const next = this.getNextPlaylistElement()
if (!next) {
console.log('Next element not found in playlist.')
return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
}
+ private async playPreviousVideo () {
+ const previous = this.getPreviousPlaylistElement()
+ if (!previous) {
+ console.log('Previous element not found in playlist.')
+ return
+ }
+
+ this.currentPlaylistElement = previous
+
+ return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid)
+ }
+
private async loadVideoAndBuildPlayer (uuid: string) {
const res = await this.loadVideo(uuid)
if (res === undefined) return
return next
}
+ private getPreviousPlaylistElement (position?: number): VideoPlaylistElement {
+ if (!position) position = this.currentPlaylistElement.position -1
+
+ if (position < 1) {
+ return undefined
+ }
+
+ const prev = this.playlistElements.find(e => e.position === position)
+
+ if (!prev || !prev.video) {
+ return this.getNextPlaylistElement(position - 1)
+ }
+
+ return prev
+ }
+
private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) {
let alreadyHadPlayer = false
stopTime: this.stopTime,
subtitle: this.subtitle,
- nextVideo: () => this.autoplayNext(),
+ nextVideo: this.playlist ? () => this.playNextVideo() : undefined,
+ hasNextVideo: this.playlist ? () => !!this.getNextPlaylistElement() : undefined,
+
+ previousVideo: this.playlist ? () => this.playPreviousVideo() : undefined,
+ hasPreviousVideo: this.playlist ? () => !!this.getPreviousPlaylistElement() : undefined,
+
playlist: playlistPlugin,
videoCaptions,