diff options
-rw-r--r-- | client/src/app/videos/+video-watch/video-watch.component.ts | 33 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player-manager.ts | 1 | ||||
-rw-r--r-- | client/src/assets/player/upnext/upnext-plugin.ts | 169 | ||||
-rw-r--r-- | client/src/sass/player/index.scss | 3 | ||||
-rw-r--r-- | client/src/sass/player/upnext.scss | 108 |
5 files changed, 305 insertions, 9 deletions
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 3a7629cc6..50854c592 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -36,7 +36,6 @@ import { getStoredTheater } from '../../../assets/player/peertube-player-local-s | |||
36 | import { PluginService } from '@app/core/plugins/plugin.service' | 36 | import { PluginService } from '@app/core/plugins/plugin.service' |
37 | import { HooksService } from '@app/core/plugins/hooks.service' | 37 | import { HooksService } from '@app/core/plugins/hooks.service' |
38 | import { PlatformLocation } from '@angular/common' | 38 | import { PlatformLocation } from '@angular/common' |
39 | import { randomInt } from '@shared/core-utils/miscs/miscs' | ||
40 | import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component' | 39 | import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component' |
41 | import { scrollToTop } from '@app/shared/misc/utils' | 40 | import { scrollToTop } from '@app/shared/misc/utils' |
42 | 41 | ||
@@ -79,6 +78,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
79 | tooltipSaveToPlaylist = '' | 78 | tooltipSaveToPlaylist = '' |
80 | 79 | ||
81 | private nextVideoUuid = '' | 80 | private nextVideoUuid = '' |
81 | private nextVideoTitle = '' | ||
82 | private currentTime: number | 82 | private currentTime: number |
83 | private paramsSub: Subscription | 83 | private paramsSub: Subscription |
84 | private queryParamsSub: Subscription | 84 | private queryParamsSub: Subscription |
@@ -247,8 +247,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
247 | 247 | ||
248 | onRecommendations (videos: Video[]) { | 248 | onRecommendations (videos: Video[]) { |
249 | if (videos.length > 0) { | 249 | if (videos.length > 0) { |
250 | // Pick a random video until the recommendations are improved | 250 | // The recommended videos's first element should be the next video |
251 | this.nextVideoUuid = videos[randomInt(0,videos.length - 1)].uuid | 251 | const video = videos[0] |
252 | this.nextVideoUuid = video.uuid | ||
253 | this.nextVideoTitle = video.name | ||
252 | } | 254 | } |
253 | } | 255 | } |
254 | 256 | ||
@@ -468,11 +470,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
468 | this.currentTime = Math.floor(this.player.currentTime()) | 470 | this.currentTime = Math.floor(this.player.currentTime()) |
469 | }) | 471 | }) |
470 | 472 | ||
471 | this.player.one('ended', () => { | 473 | /** |
472 | if (this.playlist) { | 474 | * replaces this.player.one('ended') |
473 | if (this.isPlaylistAutoPlayEnabled()) this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) | 475 | * define 'condition(next)' to return true to wait, false to stop |
474 | } else if (this.isAutoPlayEnabled()) { | 476 | */ |
475 | this.zone.run(() => this.autoplayNext()) | 477 | this.player.upnext({ |
478 | timeout: 1000000, | ||
479 | headText: this.i18n('Up Next'), | ||
480 | cancelText: this.i18n('Cancel'), | ||
481 | getTitle: () => this.nextVideoTitle, | ||
482 | next: () => this.zone.run(() => this.autoplayNext()), | ||
483 | condition: () => { | ||
484 | if (this.playlist) { | ||
485 | if (this.isPlaylistAutoPlayEnabled()) { | ||
486 | // upnext will not trigger, and instead the next video will play immediately | ||
487 | this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) | ||
488 | } | ||
489 | } else if (this.isAutoPlayEnabled()) { | ||
490 | return true // upnext will trigger | ||
491 | } | ||
492 | return false // upnext will not trigger, and instead leave the video stopping | ||
476 | } | 493 | } |
477 | }) | 494 | }) |
478 | 495 | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index d10fb7a4a..2f4e0ac1a 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -5,6 +5,7 @@ import 'videojs-hotkeys' | |||
5 | import 'videojs-dock' | 5 | import 'videojs-dock' |
6 | import 'videojs-contextmenu-ui' | 6 | import 'videojs-contextmenu-ui' |
7 | import 'videojs-contrib-quality-levels' | 7 | import 'videojs-contrib-quality-levels' |
8 | import './upnext/upnext-plugin' | ||
8 | import './peertube-plugin' | 9 | import './peertube-plugin' |
9 | import './videojs-components/peertube-link-button' | 10 | import './videojs-components/peertube-link-button' |
10 | import './videojs-components/resolution-menu-button' | 11 | import './videojs-components/resolution-menu-button' |
diff --git a/client/src/assets/player/upnext/upnext-plugin.ts b/client/src/assets/player/upnext/upnext-plugin.ts new file mode 100644 index 000000000..1f0705481 --- /dev/null +++ b/client/src/assets/player/upnext/upnext-plugin.ts | |||
@@ -0,0 +1,169 @@ | |||
1 | // @ts-ignore | ||
2 | import * as videojs from 'video.js' | ||
3 | import { VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
4 | |||
5 | function getMainTemplate (options: any) { | ||
6 | return ` | ||
7 | <div class="vjs-upnext-top"> | ||
8 | <span class="vjs-upnext-headtext">${options.headText}</span> | ||
9 | <div class="vjs-upnext-title"></div> | ||
10 | </div> | ||
11 | <div class="vjs-upnext-autoplay-icon"> | ||
12 | <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%"> | ||
13 | <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle> | ||
14 | <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle> | ||
15 | <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg> | ||
16 | </div> | ||
17 | <span class="vjs-upnext-bottom"> | ||
18 | <span class="vjs-upnext-cancel"> | ||
19 | <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button> | ||
20 | </span> | ||
21 | </span> | ||
22 | ` | ||
23 | } | ||
24 | |||
25 | // @ts-ignore-start | ||
26 | const Component = videojs.getComponent('Component') | ||
27 | class EndCard extends Component { | ||
28 | options_: any | ||
29 | getTitle: Function | ||
30 | next: Function | ||
31 | condition: Function | ||
32 | dashOffsetTotal = 586 | ||
33 | dashOffsetStart = 293 | ||
34 | interval = 50 | ||
35 | upNextEvents = new videojs.EventTarget() | ||
36 | chunkSize: number | ||
37 | |||
38 | container: HTMLElement | ||
39 | title: HTMLElement | ||
40 | autoplayRing: HTMLElement | ||
41 | cancelButton: HTMLElement | ||
42 | nextButton: HTMLElement | ||
43 | |||
44 | constructor (player: videojs.Player, options: any) { | ||
45 | super(player, options) | ||
46 | this.options_ = options | ||
47 | |||
48 | this.getTitle = this.options_.getTitle | ||
49 | this.next = this.options_.next | ||
50 | this.condition = this.options_.condition | ||
51 | |||
52 | this.chunkSize = (this.dashOffsetTotal - this.dashOffsetStart) / (this.options_.timeout / this.interval) | ||
53 | |||
54 | player.on('ended', (_: any) => { | ||
55 | if (!this.condition()) return | ||
56 | |||
57 | player.addClass('vjs-upnext--showing') | ||
58 | this.showCard((canceled: boolean) => { | ||
59 | player.removeClass('vjs-upnext--showing') | ||
60 | this.container.style.display = 'none' | ||
61 | if (!canceled) { | ||
62 | this.next() | ||
63 | } | ||
64 | }) | ||
65 | }) | ||
66 | |||
67 | player.on('playing', () => { | ||
68 | this.upNextEvents.trigger('playing') | ||
69 | }) | ||
70 | } | ||
71 | |||
72 | createEl () { | ||
73 | const container = super.createEl('div', { | ||
74 | className: 'vjs-upnext-content', | ||
75 | innerHTML: getMainTemplate(this.options_) | ||
76 | }) | ||
77 | |||
78 | this.container = container | ||
79 | container.style.display = 'none' | ||
80 | |||
81 | this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0] | ||
82 | this.title = container.getElementsByClassName('vjs-upnext-title')[0] | ||
83 | this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0] | ||
84 | this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0] | ||
85 | |||
86 | this.cancelButton.onclick = () => { | ||
87 | this.upNextEvents.trigger('cancel') | ||
88 | } | ||
89 | |||
90 | this.nextButton.onclick = () => { | ||
91 | this.upNextEvents.trigger('next') | ||
92 | } | ||
93 | |||
94 | return container | ||
95 | } | ||
96 | |||
97 | showCard (cb: Function) { | ||
98 | let timeout: any | ||
99 | let start: number | ||
100 | let now: number | ||
101 | let newOffset: number | ||
102 | |||
103 | this.autoplayRing.setAttribute('stroke-dasharray', this.dashOffsetStart) | ||
104 | this.autoplayRing.setAttribute('stroke-dashoffset', -this.dashOffsetStart) | ||
105 | |||
106 | this.title.innerHTML = this.getTitle() | ||
107 | |||
108 | this.upNextEvents.one('cancel', () => { | ||
109 | clearTimeout(timeout) | ||
110 | cb(true) | ||
111 | }) | ||
112 | |||
113 | this.upNextEvents.one('playing', () => { | ||
114 | clearTimeout(timeout) | ||
115 | cb(true) | ||
116 | }) | ||
117 | |||
118 | this.upNextEvents.one('next', () => { | ||
119 | clearTimeout(timeout) | ||
120 | cb(false) | ||
121 | }) | ||
122 | |||
123 | const update = () => { | ||
124 | now = this.options_.timeout - (new Date().getTime() - start) | ||
125 | |||
126 | if (now <= 0) { | ||
127 | clearTimeout(timeout) | ||
128 | cb(false) | ||
129 | } else { | ||
130 | newOffset = Math.max(-this.dashOffsetTotal, this.autoplayRing.getAttribute('stroke-dashoffset') - this.chunkSize) | ||
131 | this.autoplayRing.setAttribute('stroke-dashoffset', newOffset) | ||
132 | timeout = setTimeout(update.bind(this), this.interval) | ||
133 | } | ||
134 | |||
135 | } | ||
136 | |||
137 | this.container.style.display = 'block' | ||
138 | start = new Date().getTime() | ||
139 | timeout = setTimeout(update.bind(this), this.interval) | ||
140 | } | ||
141 | } | ||
142 | // @ts-ignore-end | ||
143 | |||
144 | videojs.registerComponent('EndCard', EndCard) | ||
145 | |||
146 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
147 | class UpNextPlugin extends Plugin { | ||
148 | constructor (player: videojs.Player, options: any = {}) { | ||
149 | const settings = { | ||
150 | next: options.next, | ||
151 | getTitle: options.getTitle, | ||
152 | timeout: options.timeout || 5000, | ||
153 | cancelText: options.cancelText || 'Cancel', | ||
154 | headText: options.headText || 'Up Next', | ||
155 | condition: options.condition | ||
156 | } | ||
157 | |||
158 | super(player, settings) | ||
159 | |||
160 | this.player.ready(() => { | ||
161 | player.addClass('vjs-upnext') | ||
162 | }) | ||
163 | |||
164 | player.addChild('EndCard', settings) | ||
165 | } | ||
166 | } | ||
167 | |||
168 | videojs.registerPlugin('upnext', UpNextPlugin) | ||
169 | export { UpNextPlugin } | ||
diff --git a/client/src/sass/player/index.scss b/client/src/sass/player/index.scss index e4a315d1f..886a76536 100644 --- a/client/src/sass/player/index.scss +++ b/client/src/sass/player/index.scss | |||
@@ -2,4 +2,5 @@ | |||
2 | @import './mobile'; | 2 | @import './mobile'; |
3 | @import './context-menu'; | 3 | @import './context-menu'; |
4 | @import './settings-menu'; | 4 | @import './settings-menu'; |
5 | @import './spinner'; \ No newline at end of file | 5 | @import './spinner'; |
6 | @import './upnext'; \ No newline at end of file | ||
diff --git a/client/src/sass/player/upnext.scss b/client/src/sass/player/upnext.scss new file mode 100644 index 000000000..ecce22aa8 --- /dev/null +++ b/client/src/sass/player/upnext.scss | |||
@@ -0,0 +1,108 @@ | |||
1 | $browser-context: 16; | ||
2 | |||
3 | @function em($pixels, $context: $browser-context) { | ||
4 | @return #{$pixels/$context}em; | ||
5 | } | ||
6 | |||
7 | @mixin transition($string: $transition--default) { | ||
8 | transition: $string; | ||
9 | } | ||
10 | |||
11 | .video-js { | ||
12 | |||
13 | .vjs-upnext-content { | ||
14 | font-size: 1.8em; | ||
15 | pointer-events: auto; | ||
16 | position: absolute; | ||
17 | top: 0; | ||
18 | bottom: 0; | ||
19 | background: rgba(0,0,0,0.6); | ||
20 | width: 100%; | ||
21 | |||
22 | @include transition(opacity 0.1s); | ||
23 | } | ||
24 | |||
25 | .vjs-upnext-top { | ||
26 | width: 100%; | ||
27 | position: absolute; | ||
28 | margin-left: auto; | ||
29 | margin-right: auto; | ||
30 | bottom: 50%; | ||
31 | margin-bottom: 60px; | ||
32 | } | ||
33 | |||
34 | .vjs-upnext-bottom { | ||
35 | width: 100%; | ||
36 | position: absolute; | ||
37 | margin-left: auto; | ||
38 | margin-right: auto; | ||
39 | top: 50%; | ||
40 | margin-top: 52px; | ||
41 | } | ||
42 | |||
43 | .vjs-upnext-cancel { | ||
44 | display: block; | ||
45 | float: none; | ||
46 | text-align: center; | ||
47 | } | ||
48 | |||
49 | .vjs-upnext-headtext { | ||
50 | display: block; | ||
51 | font-size: 14px; | ||
52 | text-align: center; | ||
53 | padding-bottom: 7px; | ||
54 | } | ||
55 | |||
56 | .vjs-upnext-title { | ||
57 | display: block; | ||
58 | padding: 10px 10px 2px; | ||
59 | text-align: center; | ||
60 | font-size: 22px; | ||
61 | font-weight: 600; | ||
62 | overflow: hidden; | ||
63 | white-space: nowrap; | ||
64 | word-wrap: normal; | ||
65 | text-overflow: ellipsis; | ||
66 | } | ||
67 | |||
68 | .vjs-upnext-cancel-button { | ||
69 | cursor: pointer; | ||
70 | display: inline-block; | ||
71 | float: none; | ||
72 | padding: 10px !important; | ||
73 | font-size: 16px !important; | ||
74 | border: none; | ||
75 | } | ||
76 | |||
77 | .vjs-upnext-cancel-button, | ||
78 | .vjs-upnext-cancel-button:focus { | ||
79 | outline: 0; | ||
80 | } | ||
81 | |||
82 | .vjs-upnext-cancel-button:hover { | ||
83 | background-color: rgba(255,255,255,0.25); | ||
84 | border-radius: 2px; | ||
85 | } | ||
86 | |||
87 | &.vjs-no-flex .vjs-upnext-content { | ||
88 | padding-bottom: 1em; | ||
89 | } | ||
90 | |||
91 | .vjs-upnext-autoplay-icon { | ||
92 | position: absolute; | ||
93 | top: 50%; | ||
94 | left: 50%; | ||
95 | width: 98px; | ||
96 | height: 98px; | ||
97 | margin: -49px 0 0 -49px; | ||
98 | transition: stroke-dasharray 0.1s cubic-bezier(0.4,0,1,1); | ||
99 | cursor: pointer; | ||
100 | } | ||
101 | |||
102 | } | ||
103 | |||
104 | .video-js.vjs-upnext--showing { | ||
105 | .vjs-control-bar { | ||
106 | z-index: 1; | ||
107 | } | ||
108 | } | ||