mirror of https://github.com/Chocobozzz/PeerTube
add 'up next' screen on autoplay
parent
d6ed9ccc81
commit
3bcb4fd741
|
@ -36,7 +36,6 @@ import { getStoredTheater } from '../../../assets/player/peertube-player-local-s
|
|||
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'
|
||||
|
||||
|
@ -79,6 +78,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
tooltipSaveToPlaylist = ''
|
||||
|
||||
private nextVideoUuid = ''
|
||||
private nextVideoTitle = ''
|
||||
private currentTime: number
|
||||
private paramsSub: Subscription
|
||||
private queryParamsSub: Subscription
|
||||
|
@ -247,8 +247,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -468,11 +470,26 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'videojs-hotkeys'
|
|||
import 'videojs-dock'
|
||||
import 'videojs-contextmenu-ui'
|
||||
import 'videojs-contrib-quality-levels'
|
||||
import './upnext/upnext-plugin'
|
||||
import './peertube-plugin'
|
||||
import './videojs-components/peertube-link-button'
|
||||
import './videojs-components/resolution-menu-button'
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
// @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 }
|
|
@ -2,4 +2,5 @@
|
|||
@import './mobile';
|
||||
@import './context-menu';
|
||||
@import './settings-menu';
|
||||
@import './spinner';
|
||||
@import './spinner';
|
||||
@import './upnext';
|
|
@ -0,0 +1,108 @@
|
|||
$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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue