aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts33
-rw-r--r--client/src/assets/player/peertube-player-manager.ts1
-rw-r--r--client/src/assets/player/upnext/upnext-plugin.ts169
-rw-r--r--client/src/sass/player/index.scss3
-rw-r--r--client/src/sass/player/upnext.scss108
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
36import { PluginService } from '@app/core/plugins/plugin.service' 36import { PluginService } from '@app/core/plugins/plugin.service'
37import { HooksService } from '@app/core/plugins/hooks.service' 37import { HooksService } from '@app/core/plugins/hooks.service'
38import { PlatformLocation } from '@angular/common' 38import { PlatformLocation } from '@angular/common'
39import { randomInt } from '@shared/core-utils/miscs/miscs'
40import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component' 39import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component'
41import { scrollToTop } from '@app/shared/misc/utils' 40import { 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'
5import 'videojs-dock' 5import 'videojs-dock'
6import 'videojs-contextmenu-ui' 6import 'videojs-contextmenu-ui'
7import 'videojs-contrib-quality-levels' 7import 'videojs-contrib-quality-levels'
8import './upnext/upnext-plugin'
8import './peertube-plugin' 9import './peertube-plugin'
9import './videojs-components/peertube-link-button' 10import './videojs-components/peertube-link-button'
10import './videojs-components/resolution-menu-button' 11import './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
2import * as videojs from 'video.js'
3import { VideoJSComponentInterface } from '../peertube-videojs-typings'
4
5function 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
26const Component = videojs.getComponent('Component')
27class 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
144videojs.registerComponent('EndCard', EndCard)
145
146const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
147class 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
168videojs.registerPlugin('upnext', UpNextPlugin)
169export { 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}