diff options
7 files changed, 229 insertions, 60 deletions
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 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> |
2 | <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"> | 2 | <svg |
3 | <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> | 3 | xmlns:dc="http://purl.org/dc/elements/1.1/" |
4 | </svg> \ No newline at end of file | 4 | xmlns:cc="http://creativecommons.org/ns#" |
5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||
6 | xmlns:svg="http://www.w3.org/2000/svg" | ||
7 | xmlns="http://www.w3.org/2000/svg" | ||
8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||
9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||
10 | inkscape:version="1.0 (4035a4fb49, 2020-05-01)" | ||
11 | sodipodi:docname="next.svg" | ||
12 | id="svg4" | ||
13 | version="1.1" | ||
14 | viewBox="0 0 12 12" | ||
15 | height="8" | ||
16 | width="8"> | ||
17 | <metadata | ||
18 | id="metadata10"> | ||
19 | <rdf:RDF> | ||
20 | <cc:Work | ||
21 | rdf:about=""> | ||
22 | <dc:format>image/svg+xml</dc:format> | ||
23 | <dc:type | ||
24 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||
25 | <dc:title></dc:title> | ||
26 | </cc:Work> | ||
27 | </rdf:RDF> | ||
28 | </metadata> | ||
29 | <defs | ||
30 | id="defs8" /> | ||
31 | <sodipodi:namedview | ||
32 | inkscape:current-layer="svg4" | ||
33 | inkscape:window-maximized="1" | ||
34 | inkscape:window-y="0" | ||
35 | inkscape:window-x="0" | ||
36 | inkscape:cy="-2.5620165" | ||
37 | inkscape:cx="-7.4038126" | ||
38 | inkscape:zoom="29.791667" | ||
39 | fit-margin-bottom="0" | ||
40 | fit-margin-right="0" | ||
41 | fit-margin-left="0" | ||
42 | fit-margin-top="0" | ||
43 | showgrid="false" | ||
44 | id="namedview6" | ||
45 | inkscape:window-height="1037" | ||
46 | inkscape:window-width="1916" | ||
47 | inkscape:pageshadow="2" | ||
48 | inkscape:pageopacity="0" | ||
49 | guidetolerance="10" | ||
50 | gridtolerance="10" | ||
51 | objecttolerance="10" | ||
52 | borderopacity="1" | ||
53 | bordercolor="#666666" | ||
54 | pagecolor="#ffffff" /> | ||
55 | <path | ||
56 | id="path2" | ||
57 | d="M 0,12 8.5,6 0,0 Z M 10,0 v 12 h 2 V 0 Z" | ||
58 | fill="#ffffff" /> | ||
59 | </svg> | ||
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' | |||
6 | import './upnext/upnext-plugin' | 6 | import './upnext/upnext-plugin' |
7 | import './bezels/bezels-plugin' | 7 | import './bezels/bezels-plugin' |
8 | import './peertube-plugin' | 8 | import './peertube-plugin' |
9 | import './videojs-components/next-video-button' | 9 | import './videojs-components/next-previous-video-button' |
10 | import './videojs-components/p2p-info-button' | 10 | import './videojs-components/p2p-info-button' |
11 | import './videojs-components/peertube-link-button' | 11 | import './videojs-components/peertube-link-button' |
12 | import './videojs-components/peertube-load-progress-bar' | 12 | import './videojs-components/peertube-load-progress-bar' |
@@ -27,6 +27,7 @@ import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder | |||
27 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | 27 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' |
28 | import { getStoredP2PEnabled } from './peertube-player-local-storage' | 28 | import { getStoredP2PEnabled } from './peertube-player-local-storage' |
29 | import { | 29 | import { |
30 | NextPreviousVideoButtonOptions, | ||
30 | P2PMediaLoaderPluginOptions, | 31 | P2PMediaLoaderPluginOptions, |
31 | PlaylistPluginOptions, | 32 | PlaylistPluginOptions, |
32 | UserWatching, | 33 | UserWatching, |
@@ -77,7 +78,12 @@ export interface CommonOptions extends CustomizationOptions { | |||
77 | onPlayerElementChange: (element: HTMLVideoElement) => void | 78 | onPlayerElementChange: (element: HTMLVideoElement) => void |
78 | 79 | ||
79 | autoplay: boolean | 80 | autoplay: boolean |
80 | nextVideo?: Function | 81 | |
82 | nextVideo?: () => void | ||
83 | hasNextVideo?: () => boolean | ||
84 | |||
85 | previousVideo?: () => void | ||
86 | hasPreviousVideo?: () => boolean | ||
81 | 87 | ||
82 | playlist?: PlaylistPluginOptions | 88 | playlist?: PlaylistPluginOptions |
83 | 89 | ||
@@ -259,7 +265,12 @@ export class PeertubePlayerManager { | |||
259 | captions: commonOptions.captions, | 265 | captions: commonOptions.captions, |
260 | peertubeLink: commonOptions.peertubeLink, | 266 | peertubeLink: commonOptions.peertubeLink, |
261 | theaterButton: commonOptions.theaterButton, | 267 | theaterButton: commonOptions.theaterButton, |
262 | nextVideo: commonOptions.nextVideo | 268 | |
269 | nextVideo: commonOptions.nextVideo, | ||
270 | hasNextVideo: commonOptions.hasNextVideo, | ||
271 | |||
272 | previousVideo: commonOptions.previousVideo, | ||
273 | hasPreviousVideo: commonOptions.hasPreviousVideo | ||
263 | }) as any // FIXME: typings | 274 | }) as any // FIXME: typings |
264 | } | 275 | } |
265 | } | 276 | } |
@@ -360,9 +371,14 @@ export class PeertubePlayerManager { | |||
360 | 371 | ||
361 | private static getControlBarChildren (mode: PlayerMode, options: { | 372 | private static getControlBarChildren (mode: PlayerMode, options: { |
362 | peertubeLink: boolean | 373 | peertubeLink: boolean |
363 | theaterButton: boolean, | 374 | theaterButton: boolean |
364 | captions: boolean, | 375 | captions: boolean |
376 | |||
365 | nextVideo?: Function | 377 | nextVideo?: Function |
378 | hasNextVideo?: () => boolean | ||
379 | |||
380 | previousVideo?: Function | ||
381 | hasPreviousVideo?: () => boolean | ||
366 | }) { | 382 | }) { |
367 | const settingEntries = [] | 383 | const settingEntries = [] |
368 | const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' | 384 | const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' |
@@ -372,15 +388,39 @@ export class PeertubePlayerManager { | |||
372 | if (options.captions === true) settingEntries.push('captionsButton') | 388 | if (options.captions === true) settingEntries.push('captionsButton') |
373 | settingEntries.push('resolutionMenuButton') | 389 | settingEntries.push('resolutionMenuButton') |
374 | 390 | ||
375 | const children = { | 391 | const children = {} |
376 | 'playToggle': {} | 392 | |
393 | if (options.previousVideo) { | ||
394 | const buttonOptions: NextPreviousVideoButtonOptions = { | ||
395 | type: 'previous', | ||
396 | handler: options.previousVideo, | ||
397 | isDisabled: () => { | ||
398 | if (!options.hasPreviousVideo) return false | ||
399 | |||
400 | return !options.hasPreviousVideo() | ||
401 | } | ||
402 | } | ||
403 | |||
404 | Object.assign(children, { | ||
405 | 'previousVideoButton': buttonOptions | ||
406 | }) | ||
377 | } | 407 | } |
378 | 408 | ||
409 | Object.assign(children, { playToggle: {} }) | ||
410 | |||
379 | if (options.nextVideo) { | 411 | if (options.nextVideo) { |
380 | Object.assign(children, { | 412 | const buttonOptions: NextPreviousVideoButtonOptions = { |
381 | 'nextVideoButton': { | 413 | type: 'next', |
382 | handler: options.nextVideo | 414 | handler: options.nextVideo, |
415 | isDisabled: () => { | ||
416 | if (!options.hasNextVideo) return false | ||
417 | |||
418 | return !options.hasNextVideo() | ||
383 | } | 419 | } |
420 | } | ||
421 | |||
422 | Object.assign(children, { | ||
423 | 'nextVideoButton': buttonOptions | ||
384 | }) | 424 | }) |
385 | } | 425 | } |
386 | 426 | ||
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 = { | |||
118 | onItemClicked: (element: VideoPlaylistElement) => void | 118 | onItemClicked: (element: VideoPlaylistElement) => void |
119 | } | 119 | } |
120 | 120 | ||
121 | type NextPreviousVideoButtonOptions = { | ||
122 | type: 'next' | 'previous' | ||
123 | handler: Function | ||
124 | isDisabled: () => boolean | ||
125 | } | ||
126 | |||
121 | type WebtorrentPluginOptions = { | 127 | type WebtorrentPluginOptions = { |
122 | playerElement: HTMLVideoElement | 128 | playerElement: HTMLVideoElement |
123 | 129 | ||
@@ -194,6 +200,7 @@ type PlaylistItemOptions = { | |||
194 | export { | 200 | export { |
195 | PlayerNetworkInfo, | 201 | PlayerNetworkInfo, |
196 | PlaylistItemOptions, | 202 | PlaylistItemOptions, |
203 | NextPreviousVideoButtonOptions, | ||
197 | ResolutionUpdateData, | 204 | ResolutionUpdateData, |
198 | AutoResolutionUpdateData, | 205 | AutoResolutionUpdateData, |
199 | PlaylistPluginOptions, | 206 | 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { NextPreviousVideoButtonOptions } from '../peertube-videojs-typings' | ||
3 | |||
4 | const Button = videojs.getComponent('Button') | ||
5 | |||
6 | class NextPreviousVideoButton extends Button { | ||
7 | private readonly nextPreviousVideoButtonOptions: NextPreviousVideoButtonOptions | ||
8 | |||
9 | constructor (player: videojs.Player, options?: NextPreviousVideoButtonOptions) { | ||
10 | super(player, options as any) | ||
11 | |||
12 | this.nextPreviousVideoButtonOptions = options | ||
13 | |||
14 | this.update() | ||
15 | } | ||
16 | |||
17 | createEl () { | ||
18 | const type = (this.options_ as NextPreviousVideoButtonOptions).type | ||
19 | |||
20 | const button = videojs.dom.createEl('button', { | ||
21 | className: 'vjs-' + type + '-video' | ||
22 | }) as HTMLButtonElement | ||
23 | const nextIcon = videojs.dom.createEl('span', { | ||
24 | className: 'icon icon-' + type | ||
25 | }) | ||
26 | button.appendChild(nextIcon) | ||
27 | |||
28 | if (type === 'next') { | ||
29 | button.title = this.player_.localize('Next video') | ||
30 | } else { | ||
31 | button.title = this.player_.localize('Previous video') | ||
32 | } | ||
33 | |||
34 | return button | ||
35 | } | ||
36 | |||
37 | handleClick () { | ||
38 | this.nextPreviousVideoButtonOptions.handler() | ||
39 | } | ||
40 | |||
41 | update () { | ||
42 | const disabled = this.nextPreviousVideoButtonOptions.isDisabled() | ||
43 | |||
44 | if (disabled) this.addClass('vjs-disabled') | ||
45 | else this.removeClass('vjs-disabled') | ||
46 | } | ||
47 | } | ||
48 | |||
49 | videojs.registerComponent('NextVideoButton', NextPreviousVideoButton) | ||
50 | 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | |||
3 | const Button = videojs.getComponent('Button') | ||
4 | |||
5 | export interface NextVideoButtonOptions extends videojs.ComponentOptions { | ||
6 | handler: Function | ||
7 | } | ||
8 | |||
9 | class NextVideoButton extends Button { | ||
10 | private readonly nextVideoButtonOptions: NextVideoButtonOptions | ||
11 | |||
12 | constructor (player: videojs.Player, options?: NextVideoButtonOptions) { | ||
13 | super(player, options) | ||
14 | |||
15 | this.nextVideoButtonOptions = options | ||
16 | } | ||
17 | |||
18 | createEl () { | ||
19 | const button = videojs.dom.createEl('button', { | ||
20 | className: 'vjs-next-video' | ||
21 | }) as HTMLButtonElement | ||
22 | const nextIcon = videojs.dom.createEl('span', { | ||
23 | className: 'icon icon-next' | ||
24 | }) | ||
25 | button.appendChild(nextIcon) | ||
26 | |||
27 | button.title = this.player_.localize('Next video') | ||
28 | |||
29 | return button | ||
30 | } | ||
31 | |||
32 | handleClick () { | ||
33 | this.nextVideoButtonOptions.handler() | ||
34 | } | ||
35 | } | ||
36 | |||
37 | 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 { | |||
147 | box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2); | 147 | box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2); |
148 | text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); | 148 | text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); |
149 | 149 | ||
150 | > button:first-child { | ||
151 | margin-left: 1em; | ||
152 | } | ||
153 | |||
150 | .vjs-progress-control, | 154 | .vjs-progress-control, |
151 | .vjs-play-control, | 155 | .vjs-play-control, |
152 | .vjs-playback-rate, | 156 | .vjs-playback-rate, |
@@ -230,7 +234,6 @@ body { | |||
230 | 234 | ||
231 | cursor: pointer; | 235 | cursor: pointer; |
232 | font-size: $font-size; | 236 | font-size: $font-size; |
233 | margin-left: 1em; | ||
234 | width: 3em; | 237 | width: 3em; |
235 | } | 238 | } |
236 | 239 | ||
@@ -301,24 +304,32 @@ body { | |||
301 | } | 304 | } |
302 | } | 305 | } |
303 | 306 | ||
304 | .vjs-next-video { | 307 | .vjs-next-video, |
308 | .vjs-previous-video { | ||
305 | line-height: $control-bar-height; | 309 | line-height: $control-bar-height; |
306 | text-align: right; | 310 | text-align: right; |
307 | 311 | ||
308 | .icon { | 312 | .icon { |
309 | &.icon-next { | 313 | &.icon-next, |
314 | &.icon-previous { | ||
310 | mask-image: url('#{$assets-path}/player/images/next.svg'); | 315 | mask-image: url('#{$assets-path}/player/images/next.svg'); |
311 | -webkit-mask-image: url('#{$assets-path}/player/images/next.svg'); | 316 | -webkit-mask-image: url('#{$assets-path}/player/images/next.svg'); |
312 | background-color: white; | 317 | background-color: white; |
313 | mask-size: cover; | 318 | mask-size: cover; |
314 | -webkit-mask-size: cover; | 319 | -webkit-mask-size: cover; |
315 | transform: scale(2.2); | 320 | width: 11px; |
321 | height: 11px; | ||
322 | margin-top: -2px; | ||
323 | display: inline-block; | ||
324 | } | ||
325 | |||
326 | &.icon-previous { | ||
327 | transform: rotate(180deg); | ||
316 | } | 328 | } |
317 | } | 329 | } |
318 | } | 330 | } |
319 | 331 | ||
320 | .vjs-peertube, | 332 | .vjs-peertube { |
321 | .vjs-next-video { | ||
322 | .icon { | 333 | .icon { |
323 | display: inline-block; | 334 | display: inline-block; |
324 | width: 15px; | 335 | width: 15px; |
@@ -650,3 +661,13 @@ body { | |||
650 | display: block; | 661 | display: block; |
651 | } | 662 | } |
652 | } | 663 | } |
664 | |||
665 | .vjs-no-next-in-playlist { | ||
666 | .vjs-next-video { | ||
667 | cursor: default; | ||
668 | |||
669 | .icon { | ||
670 | background-color: rgba(255, 255, 255, 0.5); | ||
671 | } | ||
672 | } | ||
673 | } | ||
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 { | |||
309 | cancelText: peertubeTranslate('Cancel', translations), | 309 | cancelText: peertubeTranslate('Cancel', translations), |
310 | suspendedText: peertubeTranslate('Autoplay is suspended', translations), | 310 | suspendedText: peertubeTranslate('Autoplay is suspended', translations), |
311 | getTitle: () => this.nextVideoTitle(), | 311 | getTitle: () => this.nextVideoTitle(), |
312 | next: () => this.autoplayNext(), | 312 | next: () => this.playNextVideo(), |
313 | condition: () => !!this.getNextPlaylistElement(), | 313 | condition: () => !!this.getNextPlaylistElement(), |
314 | suspended: () => false | 314 | suspended: () => false |
315 | }) | 315 | }) |
316 | } | 316 | } |
317 | 317 | ||
318 | private async autoplayNext () { | 318 | private async playNextVideo () { |
319 | const next = this.getNextPlaylistElement() | 319 | const next = this.getNextPlaylistElement() |
320 | if (!next) { | 320 | if (!next) { |
321 | console.log('Next element not found in playlist.') | 321 | console.log('Next element not found in playlist.') |
@@ -327,6 +327,18 @@ export class PeerTubeEmbed { | |||
327 | return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) | 327 | return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) |
328 | } | 328 | } |
329 | 329 | ||
330 | private async playPreviousVideo () { | ||
331 | const previous = this.getPreviousPlaylistElement() | ||
332 | if (!previous) { | ||
333 | console.log('Previous element not found in playlist.') | ||
334 | return | ||
335 | } | ||
336 | |||
337 | this.currentPlaylistElement = previous | ||
338 | |||
339 | return this.loadVideoAndBuildPlayer(this.currentPlaylistElement.video.uuid) | ||
340 | } | ||
341 | |||
330 | private async loadVideoAndBuildPlayer (uuid: string) { | 342 | private async loadVideoAndBuildPlayer (uuid: string) { |
331 | const res = await this.loadVideo(uuid) | 343 | const res = await this.loadVideo(uuid) |
332 | if (res === undefined) return | 344 | if (res === undefined) return |
@@ -357,6 +369,22 @@ export class PeerTubeEmbed { | |||
357 | return next | 369 | return next |
358 | } | 370 | } |
359 | 371 | ||
372 | private getPreviousPlaylistElement (position?: number): VideoPlaylistElement { | ||
373 | if (!position) position = this.currentPlaylistElement.position -1 | ||
374 | |||
375 | if (position < 1) { | ||
376 | return undefined | ||
377 | } | ||
378 | |||
379 | const prev = this.playlistElements.find(e => e.position === position) | ||
380 | |||
381 | if (!prev || !prev.video) { | ||
382 | return this.getNextPlaylistElement(position - 1) | ||
383 | } | ||
384 | |||
385 | return prev | ||
386 | } | ||
387 | |||
360 | private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) { | 388 | private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise<Response>) { |
361 | let alreadyHadPlayer = false | 389 | let alreadyHadPlayer = false |
362 | 390 | ||
@@ -418,7 +446,12 @@ export class PeerTubeEmbed { | |||
418 | stopTime: this.stopTime, | 446 | stopTime: this.stopTime, |
419 | subtitle: this.subtitle, | 447 | subtitle: this.subtitle, |
420 | 448 | ||
421 | nextVideo: () => this.autoplayNext(), | 449 | nextVideo: this.playlist ? () => this.playNextVideo() : undefined, |
450 | hasNextVideo: this.playlist ? () => !!this.getNextPlaylistElement() : undefined, | ||
451 | |||
452 | previousVideo: this.playlist ? () => this.playPreviousVideo() : undefined, | ||
453 | hasPreviousVideo: this.playlist ? () => !!this.getPreviousPlaylistElement() : undefined, | ||
454 | |||
422 | playlist: playlistPlugin, | 455 | playlist: playlistPlugin, |
423 | 456 | ||
424 | videoCaptions, | 457 | videoCaptions, |