import { PluginService } from '@app/core/plugins/plugin.service'
import { HooksService } from '@app/core/plugins/hooks.service'
import { PlatformLocation } from '@angular/common'
-import { randomInt } from '@shared/core-utils/miscs/miscs'
import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component'
import { scrollToTop } from '@app/shared/misc/utils'
tooltipSaveToPlaylist = ''
private nextVideoUuid = ''
+ private nextVideoTitle = ''
private currentTime: number
private paramsSub: Subscription
private queryParamsSub: Subscription
onRecommendations (videos: Video[]) {
if (videos.length > 0) {
- // Pick a random video until the recommendations are improved
- this.nextVideoUuid = videos[randomInt(0,videos.length - 1)].uuid
+ // The recommended videos's first element should be the next video
+ const video = videos[0]
+ this.nextVideoUuid = video.uuid
+ this.nextVideoTitle = video.name
}
}
this.currentTime = Math.floor(this.player.currentTime())
})
- this.player.one('ended', () => {
- if (this.playlist) {
- if (this.isPlaylistAutoPlayEnabled()) this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
- } else if (this.isAutoPlayEnabled()) {
- this.zone.run(() => this.autoplayNext())
+ /**
+ * replaces this.player.one('ended')
+ * define 'condition(next)' to return true to wait, false to stop
+ */
+ this.player.upnext({
+ timeout: 1000000,
+ headText: this.i18n('Up Next'),
+ cancelText: this.i18n('Cancel'),
+ getTitle: () => this.nextVideoTitle,
+ next: () => this.zone.run(() => this.autoplayNext()),
+ condition: () => {
+ if (this.playlist) {
+ if (this.isPlaylistAutoPlayEnabled()) {
+ // upnext will not trigger, and instead the next video will play immediately
+ this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
+ }
+ } else if (this.isAutoPlayEnabled()) {
+ return true // upnext will trigger
+ }
+ return false // upnext will not trigger, and instead leave the video stopping
}
})
--- /dev/null
+// @ts-ignore
+import * as videojs from 'video.js'
+import { VideoJSComponentInterface } from '../peertube-videojs-typings'
+
+function getMainTemplate (options: any) {
+ return `
+ <div class="vjs-upnext-top">
+ <span class="vjs-upnext-headtext">${options.headText}</span>
+ <div class="vjs-upnext-title"></div>
+ </div>
+ <div class="vjs-upnext-autoplay-icon">
+ <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%">
+ <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle>
+ <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>
+ <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg>
+ </div>
+ <span class="vjs-upnext-bottom">
+ <span class="vjs-upnext-cancel">
+ <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button>
+ </span>
+ </span>
+ `
+}
+
+// @ts-ignore-start
+const Component = videojs.getComponent('Component')
+class EndCard extends Component {
+ options_: any
+ getTitle: Function
+ next: Function
+ condition: Function
+ dashOffsetTotal = 586
+ dashOffsetStart = 293
+ interval = 50
+ upNextEvents = new videojs.EventTarget()
+ chunkSize: number
+
+ container: HTMLElement
+ title: HTMLElement
+ autoplayRing: HTMLElement
+ cancelButton: HTMLElement
+ nextButton: HTMLElement
+
+ constructor (player: videojs.Player, options: any) {
+ super(player, options)
+ this.options_ = options
+
+ this.getTitle = this.options_.getTitle
+ this.next = this.options_.next
+ this.condition = this.options_.condition
+
+ this.chunkSize = (this.dashOffsetTotal - this.dashOffsetStart) / (this.options_.timeout / this.interval)
+
+ player.on('ended', (_: any) => {
+ if (!this.condition()) return
+
+ player.addClass('vjs-upnext--showing')
+ this.showCard((canceled: boolean) => {
+ player.removeClass('vjs-upnext--showing')
+ this.container.style.display = 'none'
+ if (!canceled) {
+ this.next()
+ }
+ })
+ })
+
+ player.on('playing', () => {
+ this.upNextEvents.trigger('playing')
+ })
+ }
+
+ createEl () {
+ const container = super.createEl('div', {
+ className: 'vjs-upnext-content',
+ innerHTML: getMainTemplate(this.options_)
+ })
+
+ this.container = container
+ container.style.display = 'none'
+
+ this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0]
+ this.title = container.getElementsByClassName('vjs-upnext-title')[0]
+ this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0]
+ this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0]
+
+ this.cancelButton.onclick = () => {
+ this.upNextEvents.trigger('cancel')
+ }
+
+ this.nextButton.onclick = () => {
+ this.upNextEvents.trigger('next')
+ }
+
+ return container
+ }
+
+ showCard (cb: Function) {
+ let timeout: any
+ let start: number
+ let now: number
+ let newOffset: number
+
+ this.autoplayRing.setAttribute('stroke-dasharray', this.dashOffsetStart)
+ this.autoplayRing.setAttribute('stroke-dashoffset', -this.dashOffsetStart)
+
+ this.title.innerHTML = this.getTitle()
+
+ this.upNextEvents.one('cancel', () => {
+ clearTimeout(timeout)
+ cb(true)
+ })
+
+ this.upNextEvents.one('playing', () => {
+ clearTimeout(timeout)
+ cb(true)
+ })
+
+ this.upNextEvents.one('next', () => {
+ clearTimeout(timeout)
+ cb(false)
+ })
+
+ const update = () => {
+ now = this.options_.timeout - (new Date().getTime() - start)
+
+ if (now <= 0) {
+ clearTimeout(timeout)
+ cb(false)
+ } else {
+ newOffset = Math.max(-this.dashOffsetTotal, this.autoplayRing.getAttribute('stroke-dashoffset') - this.chunkSize)
+ this.autoplayRing.setAttribute('stroke-dashoffset', newOffset)
+ timeout = setTimeout(update.bind(this), this.interval)
+ }
+
+ }
+
+ this.container.style.display = 'block'
+ start = new Date().getTime()
+ timeout = setTimeout(update.bind(this), this.interval)
+ }
+}
+// @ts-ignore-end
+
+videojs.registerComponent('EndCard', EndCard)
+
+const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
+class UpNextPlugin extends Plugin {
+ constructor (player: videojs.Player, options: any = {}) {
+ const settings = {
+ next: options.next,
+ getTitle: options.getTitle,
+ timeout: options.timeout || 5000,
+ cancelText: options.cancelText || 'Cancel',
+ headText: options.headText || 'Up Next',
+ condition: options.condition
+ }
+
+ super(player, settings)
+
+ this.player.ready(() => {
+ player.addClass('vjs-upnext')
+ })
+
+ player.addChild('EndCard', settings)
+ }
+}
+
+videojs.registerPlugin('upnext', UpNextPlugin)
+export { UpNextPlugin }
--- /dev/null
+$browser-context: 16;
+
+@function em($pixels, $context: $browser-context) {
+ @return #{$pixels/$context}em;
+}
+
+@mixin transition($string: $transition--default) {
+ transition: $string;
+}
+
+.video-js {
+
+ .vjs-upnext-content {
+ font-size: 1.8em;
+ pointer-events: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ background: rgba(0,0,0,0.6);
+ width: 100%;
+
+ @include transition(opacity 0.1s);
+ }
+
+ .vjs-upnext-top {
+ width: 100%;
+ position: absolute;
+ margin-left: auto;
+ margin-right: auto;
+ bottom: 50%;
+ margin-bottom: 60px;
+ }
+
+ .vjs-upnext-bottom {
+ width: 100%;
+ position: absolute;
+ margin-left: auto;
+ margin-right: auto;
+ top: 50%;
+ margin-top: 52px;
+ }
+
+ .vjs-upnext-cancel {
+ display: block;
+ float: none;
+ text-align: center;
+ }
+
+ .vjs-upnext-headtext {
+ display: block;
+ font-size: 14px;
+ text-align: center;
+ padding-bottom: 7px;
+ }
+
+ .vjs-upnext-title {
+ display: block;
+ padding: 10px 10px 2px;
+ text-align: center;
+ font-size: 22px;
+ font-weight: 600;
+ overflow: hidden;
+ white-space: nowrap;
+ word-wrap: normal;
+ text-overflow: ellipsis;
+ }
+
+ .vjs-upnext-cancel-button {
+ cursor: pointer;
+ display: inline-block;
+ float: none;
+ padding: 10px !important;
+ font-size: 16px !important;
+ border: none;
+ }
+
+ .vjs-upnext-cancel-button,
+ .vjs-upnext-cancel-button:focus {
+ outline: 0;
+ }
+
+ .vjs-upnext-cancel-button:hover {
+ background-color: rgba(255,255,255,0.25);
+ border-radius: 2px;
+ }
+
+ &.vjs-no-flex .vjs-upnext-content {
+ padding-bottom: 1em;
+ }
+
+ .vjs-upnext-autoplay-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 98px;
+ height: 98px;
+ margin: -49px 0 0 -49px;
+ transition: stroke-dasharray 0.1s cubic-bezier(0.4,0,1,1);
+ cursor: pointer;
+ }
+
+}
+
+.video-js.vjs-upnext--showing {
+ .vjs-control-bar {
+ z-index: 1;
+ }
+}