diff --git a/client/src/assets/player/images/next.svg b/client/src/assets/player/images/next.svg
index af42dd270..0441f93c8 100644
--- a/client/src/assets/player/images/next.svg
+++ b/client/src/assets/player/images/next.svg
@@ -1,4 +1,59 @@
-
-
\ No newline at end of file
+
+
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index dcfa3a593..c71b43415 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -6,7 +6,7 @@ import './upnext/end-card'
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'
@@ -27,6 +27,7 @@ import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder
import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
import { getStoredP2PEnabled } from './peertube-player-local-storage'
import {
+ NextPreviousVideoButtonOptions,
P2PMediaLoaderPluginOptions,
PlaylistPluginOptions,
UserWatching,
@@ -77,7 +78,12 @@ export interface CommonOptions extends CustomizationOptions {
onPlayerElementChange: (element: HTMLVideoElement) => void
autoplay: boolean
- nextVideo?: Function
+
+ nextVideo?: () => void
+ hasNextVideo?: () => boolean
+
+ previousVideo?: () => void
+ hasPreviousVideo?: () => boolean
playlist?: PlaylistPluginOptions
@@ -259,7 +265,12 @@ export class PeertubePlayerManager {
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
}
}
@@ -360,9 +371,14 @@ export class PeertubePlayerManager {
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'
@@ -372,15 +388,39 @@ export class PeertubePlayerManager {
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
})
}
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index b72c4b0f9..a359b8595 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -118,6 +118,12 @@ type PlaylistPluginOptions = {
onItemClicked: (element: VideoPlaylistElement) => void
}
+type NextPreviousVideoButtonOptions = {
+ type: 'next' | 'previous'
+ handler: Function
+ isDisabled: () => boolean
+}
+
type WebtorrentPluginOptions = {
playerElement: HTMLVideoElement
@@ -194,6 +200,7 @@ type PlaylistItemOptions = {
export {
PlayerNetworkInfo,
PlaylistItemOptions,
+ NextPreviousVideoButtonOptions,
ResolutionUpdateData,
AutoResolutionUpdateData,
PlaylistPluginOptions,
diff --git a/client/src/assets/player/videojs-components/next-previous-video-button.ts b/client/src/assets/player/videojs-components/next-previous-video-button.ts
new file mode 100644
index 000000000..fe17ce2ce
--- /dev/null
+++ b/client/src/assets/player/videojs-components/next-previous-video-button.ts
@@ -0,0 +1,50 @@
+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)
diff --git a/client/src/assets/player/videojs-components/next-video-button.ts b/client/src/assets/player/videojs-components/next-video-button.ts
deleted file mode 100644
index 22b32f06b..000000000
--- a/client/src/assets/player/videojs-components/next-video-button.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-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)
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 2c22239a0..994936f81 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -147,6 +147,10 @@ body {
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,
@@ -230,7 +234,6 @@ body {
cursor: pointer;
font-size: $font-size;
- margin-left: 1em;
width: 3em;
}
@@ -301,24 +304,32 @@ body {
}
}
- .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;
@@ -650,3 +661,13 @@ body {
display: block;
}
}
+
+.vjs-no-next-in-playlist {
+ .vjs-next-video {
+ cursor: default;
+
+ .icon {
+ background-color: rgba(255, 255, 255, 0.5);
+ }
+ }
+}
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 17b0ee9ef..786d749a4 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -309,13 +309,13 @@ export class PeerTubeEmbed {
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.')
@@ -327,6 +327,18 @@ export class PeerTubeEmbed {
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
@@ -357,6 +369,22 @@ export class PeerTubeEmbed {
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) {
let alreadyHadPlayer = false
@@ -418,7 +446,12 @@ export class PeerTubeEmbed {
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,