]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
add 'up next' screen on autoplay
authorRigel Kent <sendmemail@rigelk.eu>
Tue, 17 Dec 2019 15:49:33 +0000 (16:49 +0100)
committerRigel Kent <sendmemail@rigelk.eu>
Tue, 17 Dec 2019 15:49:33 +0000 (16:49 +0100)
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/assets/player/peertube-player-manager.ts
client/src/assets/player/upnext/upnext-plugin.ts [new file with mode: 0644]
client/src/sass/player/index.scss
client/src/sass/player/upnext.scss [new file with mode: 0644]

index 3a7629cc64585ee6260b59b771ae5a48e81a92d8..50854c5924bed93d90f390ef67f029bb1100028b 100644 (file)
@@ -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
         }
       })
 
index d10fb7a4a9363fe85017e00b09de2ceb0824a0c7..2f4e0ac1a03ca30c9e77ac855dffc054cee61e76 100644 (file)
@@ -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'
diff --git a/client/src/assets/player/upnext/upnext-plugin.ts b/client/src/assets/player/upnext/upnext-plugin.ts
new file mode 100644 (file)
index 0000000..1f07054
--- /dev/null
@@ -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 }
index e4a315d1f84fe426022b8687f33f70d2c6cc0cc6..886a7653600aa41120e060fa178617e075faa28a 100644 (file)
@@ -2,4 +2,5 @@
 @import './mobile';
 @import './context-menu';
 @import './settings-menu';
-@import './spinner';
\ No newline at end of file
+@import './spinner';
+@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 (file)
index 0000000..ecce22a
--- /dev/null
@@ -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;
+  }
+}