aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html6
-rw-r--r--client/src/app/+login/login.component.ts5
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts40
-rw-r--r--client/src/app/+videos/video-list/videos-list-common-page.component.ts3
-rw-r--r--client/src/app/core/auth/auth.service.ts45
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts6
-rw-r--r--client/src/app/menu/menu.component.ts8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-list.component.ts62
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts4
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.service.ts16
-rw-r--r--client/src/assets/player/peertube-player-manager.ts7
-rw-r--r--client/src/assets/player/shared/control-bar/index.ts1
-rw-r--r--client/src/assets/player/shared/control-bar/peertube-live-display.ts93
-rw-r--r--client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts76
-rw-r--r--client/src/assets/player/shared/manager-options/control-bar-options-builder.ts25
-rw-r--r--client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts4
-rw-r--r--client/src/assets/player/shared/stats/stats-card.ts4
-rw-r--r--client/src/assets/player/types/manager-options.ts2
-rw-r--r--client/src/assets/player/types/peertube-videojs-typings.ts3
-rw-r--r--client/src/root-helpers/logger.ts8
-rw-r--r--client/src/root-helpers/plugins-manager.ts11
-rw-r--r--client/src/sass/player/control-bar.scss21
-rw-r--r--client/src/sass/primeng-custom.scss1
-rw-r--r--client/src/standalone/videos/shared/player-manager-options.ts4
25 files changed, 342 insertions, 115 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index 43f1438e0..174f5d29c 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -44,9 +44,13 @@
44 44
45 <div class="peertube-select-container"> 45 <div class="peertube-select-container">
46 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> 46 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
47 <option i18n value="publishedAt">Recently added videos</option>
48 <option i18n value="originallyPublishedAt">Original publication date</option>
49 <option i18n value="name">Name</option>
47 <option i18n value="hot">Hot videos</option> 50 <option i18n value="hot">Hot videos</option>
48 <option i18n value="most-viewed">Most viewed videos</option> 51 <option i18n value="most-viewed">Recent views</option>
49 <option i18n value="most-liked">Most liked videos</option> 52 <option i18n value="most-liked">Most liked videos</option>
53 <option i18n value="views">Global views</option>
50 </select> 54 </select>
51 </div> 55 </div>
52 56
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index c1705807f..5f6aa842e 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -1,3 +1,4 @@
1import { environment } from 'src/environments/environment'
1import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' 2import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' 4import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core'
@@ -7,7 +8,7 @@ import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-valid
7import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' 8import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms'
8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' 9import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 10import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
10import { PluginsManager } from '@root-helpers/plugins-manager' 11import { getExternalAuthHref } from '@shared/core-utils'
11import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 12import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
12 13
13@Component({ 14@Component({
@@ -119,7 +120,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
119 } 120 }
120 121
121 getAuthHref (auth: RegisteredExternalAuthConfig) { 122 getAuthHref (auth: RegisteredExternalAuthConfig) {
122 return PluginsManager.getExternalAuthHref(auth) 123 return getExternalAuthHref(environment.apiUrl, auth)
123 } 124 }
124 125
125 login () { 126 login () {
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 94853423b..84548de97 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -133,8 +133,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
133 this.loadRouteParams() 133 this.loadRouteParams()
134 this.loadRouteQuery() 134 this.loadRouteQuery()
135 135
136 this.initHotkeys()
137
138 this.theaterEnabled = getStoredTheater() 136 this.theaterEnabled = getStoredTheater()
139 137
140 this.hooks.runAction('action:video-watch.init', 'video-watch') 138 this.hooks.runAction('action:video-watch.init', 'video-watch')
@@ -295,6 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
295 subtitle: queryParams.subtitle, 293 subtitle: queryParams.subtitle,
296 294
297 playerMode: queryParams.mode, 295 playerMode: queryParams.mode,
296 playbackRate: queryParams.playbackRate,
298 peertubeLink: false 297 peertubeLink: false
299 } 298 }
300 299
@@ -406,6 +405,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
406 if (res === false) return this.location.back() 405 if (res === false) return this.location.back()
407 } 406 }
408 407
408 this.buildHotkeysHelp(video)
409
409 this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) 410 this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay })
410 .catch(err => logger.error('Cannot build the player', err)) 411 .catch(err => logger.error('Cannot build the player', err))
411 412
@@ -657,6 +658,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
657 muted: urlOptions.muted, 658 muted: urlOptions.muted,
658 loop: urlOptions.loop, 659 loop: urlOptions.loop,
659 subtitle: urlOptions.subtitle, 660 subtitle: urlOptions.subtitle,
661 playbackRate: urlOptions.playbackRate,
660 662
661 peertubeLink: urlOptions.peertubeLink, 663 peertubeLink: urlOptions.peertubeLink,
662 664
@@ -785,33 +787,43 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
785 this.video.viewers = newViewers 787 this.video.viewers = newViewers
786 } 788 }
787 789
788 private initHotkeys () { 790 private buildHotkeysHelp (video: Video) {
791 if (this.hotkeys.length !== 0) {
792 this.hotkeysService.remove(this.hotkeys)
793 }
794
789 this.hotkeys = [ 795 this.hotkeys = [
790 // These hotkeys are managed by the player 796 // These hotkeys are managed by the player
791 new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), 797 new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`),
792 new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), 798 new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`),
793 new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), 799 new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`),
794 800
795 new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`),
796
797 new Hotkey('up', e => e, undefined, $localize`Increase the volume`), 801 new Hotkey('up', e => e, undefined, $localize`Increase the volume`),
798 new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), 802 new Hotkey('down', e => e, undefined, $localize`Decrease the volume`),
799 803
800 new Hotkey('right', e => e, undefined, $localize`Seek the video forward`),
801 new Hotkey('left', e => e, undefined, $localize`Seek the video backward`),
802
803 new Hotkey('>', e => e, undefined, $localize`Increase playback rate`),
804 new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`),
805
806 new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`),
807 new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`),
808
809 new Hotkey('t', e => { 804 new Hotkey('t', e => {
810 this.theaterEnabled = !this.theaterEnabled 805 this.theaterEnabled = !this.theaterEnabled
811 return false 806 return false
812 }, undefined, $localize`Toggle theater mode`) 807 }, undefined, $localize`Toggle theater mode`)
813 ] 808 ]
814 809
810 if (!video.isLive) {
811 this.hotkeys = this.hotkeys.concat([
812 // These hotkeys are also managed by the player but only for VOD
813
814 new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`),
815
816 new Hotkey('right', e => e, undefined, $localize`Seek the video forward`),
817 new Hotkey('left', e => e, undefined, $localize`Seek the video backward`),
818
819 new Hotkey('>', e => e, undefined, $localize`Increase playback rate`),
820 new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`),
821
822 new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`),
823 new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`)
824 ])
825 }
826
815 if (this.isUserLoggedIn()) { 827 if (this.isUserLoggedIn()) {
816 this.hotkeys = this.hotkeys.concat([ 828 this.hotkeys = this.hotkeys.concat([
817 new Hotkey('shift+s', () => { 829 new Hotkey('shift+s', () => {
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
index c8fa8ef30..bafe30fd7 100644
--- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts
+++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts
@@ -177,6 +177,9 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable
177 case 'best': 177 case 'best':
178 return '-hot' 178 return '-hot'
179 179
180 case 'name':
181 return 'name'
182
180 default: 183 default:
181 return '-' + algorithm as VideoSortField 184 return '-' + algorithm as VideoSortField
182 } 185 }
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 4de28e51e..ed7eabb76 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -5,10 +5,11 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { Router } from '@angular/router' 6import { Router } from '@angular/router'
7import { Notifier } from '@app/core/notification/notifier.service' 7import { Notifier } from '@app/core/notification/notifier.service'
8import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' 8import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage, PluginsManager } from '@root-helpers/index'
9import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' 9import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11import { RestExtractor } from '../rest/rest-extractor.service' 11import { RestExtractor } from '../rest/rest-extractor.service'
12import { ServerService } from '../server'
12import { AuthStatus } from './auth-status.model' 13import { AuthStatus } from './auth-status.model'
13import { AuthUser } from './auth-user.model' 14import { AuthUser } from './auth-user.model'
14 15
@@ -44,6 +45,7 @@ export class AuthService {
44 private refreshingTokenObservable: Observable<any> 45 private refreshingTokenObservable: Observable<any>
45 46
46 constructor ( 47 constructor (
48 private serverService: ServerService,
47 private http: HttpClient, 49 private http: HttpClient,
48 private notifier: Notifier, 50 private notifier: Notifier,
49 private hotkeysService: HotkeysService, 51 private hotkeysService: HotkeysService,
@@ -213,25 +215,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
213 const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') 215 const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
214 216
215 this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) 217 this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
216 .pipe( 218 .pipe(
217 map(res => this.handleRefreshToken(res)), 219 map(res => this.handleRefreshToken(res)),
218 tap(() => { 220 tap(() => {
219 this.refreshingTokenObservable = null 221 this.refreshingTokenObservable = null
220 }), 222 }),
221 catchError(err => { 223 catchError(err => {
222 this.refreshingTokenObservable = null 224 this.refreshingTokenObservable = null
223 225
224 logger.error(err) 226 logger.error(err)
225 logger.info('Cannot refresh token -> logout...') 227 logger.info('Cannot refresh token -> logout...')
226 this.logout() 228 this.logout()
227 this.router.navigate([ '/login' ]) 229
228 230 const externalLoginUrl = PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverService.getHTMLConfig())
229 return observableThrowError(() => ({ 231 if (externalLoginUrl) window.location.href = externalLoginUrl
230 error: $localize`You need to reconnect.` 232 else this.router.navigate([ '/login' ])
231 })) 233
232 }), 234 return observableThrowError(() => ({
233 share() 235 error: $localize`You need to reconnect.`
234 ) 236 }))
237 }),
238 share()
239 )
235 240
236 return this.refreshingTokenObservable 241 return this.refreshingTokenObservable
237 } 242 }
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts
index de3f2bfff..daed7f178 100644
--- a/client/src/app/core/rest/rest-extractor.service.ts
+++ b/client/src/app/core/rest/rest-extractor.service.ts
@@ -87,7 +87,11 @@ export class RestExtractor {
87 87
88 if (err.status !== undefined) { 88 if (err.status !== undefined) {
89 const errorMessage = this.buildServerErrorMessage(err) 89 const errorMessage = this.buildServerErrorMessage(err)
90 logger.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) 90
91 const message = `Backend returned code ${err.status}, errorMessage is: ${errorMessage}`
92
93 if (err.status === HttpStatusCode.NOT_FOUND_404) logger.clientError(message)
94 else logger.error(message)
91 95
92 return errorMessage 96 return errorMessage
93 } 97 }
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index 63f01df92..568cb98bb 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -1,6 +1,7 @@
1import { HotkeysService } from 'angular2-hotkeys' 1import { HotkeysService } from 'angular2-hotkeys'
2import * as debug from 'debug' 2import * as debug from 'debug'
3import { switchMap } from 'rxjs/operators' 3import { switchMap } from 'rxjs/operators'
4import { environment } from 'src/environments/environment'
4import { ViewportScroller } from '@angular/common' 5import { ViewportScroller } from '@angular/common'
5import { Component, OnInit, ViewChild } from '@angular/core' 6import { Component, OnInit, ViewChild } from '@angular/core'
6import { Router } from '@angular/router' 7import { Router } from '@angular/router'
@@ -131,12 +132,7 @@ export class MenuComponent implements OnInit {
131 } 132 }
132 133
133 getExternalLoginHref () { 134 getExternalLoginHref () {
134 if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined 135 return PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverConfig)
135
136 const externalAuths = this.serverConfig.plugin.registeredExternalAuths
137 if (externalAuths.length !== 1) return undefined
138
139 return PluginsManager.getExternalAuthHref(externalAuths[0])
140 } 136 }
141 137
142 isRegistrationAllowed () { 138 isRegistrationAllowed () {
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index 85c63c173..706227e66 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -314,6 +314,6 @@ export class VideoMiniatureComponent implements OnInit {
314 this.cd.markForCheck() 314 this.cd.markForCheck()
315 }) 315 })
316 316
317 this.videoPlaylistService.runPlaylistCheck(this.video.id) 317 this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id)
318 } 318 }
319} 319}
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
index d5cdd958e..a423377de 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
@@ -1,6 +1,6 @@
1import * as debug from 'debug' 1import * as debug from 'debug'
2import { fromEvent, Observable, Subject, Subscription } from 'rxjs' 2import { fromEvent, Observable, Subject, Subscription } from 'rxjs'
3import { debounceTime, switchMap } from 'rxjs/operators' 3import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators'
4import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' 4import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'
5import { ActivatedRoute } from '@angular/router' 5import { ActivatedRoute } from '@angular/router'
6import { 6import {
@@ -111,6 +111,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
111 111
112 private lastQueryLength: number 112 private lastQueryLength: number
113 113
114 private videoRequests = new Subject<{ reset: boolean, obs: Observable<ResultList<Video>> }>()
115
114 constructor ( 116 constructor (
115 private notifier: Notifier, 117 private notifier: Notifier,
116 private authService: AuthService, 118 private authService: AuthService,
@@ -124,6 +126,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
124 } 126 }
125 127
126 ngOnInit () { 128 ngOnInit () {
129 this.subscribeToVideoRequests()
130
127 const hiddenFilters = this.hideScopeFilter 131 const hiddenFilters = this.hideScopeFilter
128 ? [ 'scope' ] 132 ? [ 'scope' ]
129 : [] 133 : []
@@ -228,30 +232,12 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
228 } 232 }
229 233
230 loadMoreVideos (reset = false) { 234 loadMoreVideos (reset = false) {
231 if (reset) this.hasDoneFirstQuery = false 235 if (reset) {
232 236 this.hasDoneFirstQuery = false
233 this.getVideosObservableFunction(this.pagination, this.filters) 237 this.videos = []
234 .subscribe({ 238 }
235 next: ({ data }) => {
236 this.hasDoneFirstQuery = true
237 this.lastQueryLength = data.length
238
239 if (reset) this.videos = []
240 this.videos = this.videos.concat(data)
241
242 if (this.groupByDate) this.buildGroupedDateLabels()
243
244 this.onDataSubject.next(data)
245 this.videosLoaded.emit(this.videos)
246 },
247
248 error: err => {
249 const message = $localize`Cannot load more videos. Try again later.`
250 239
251 logger.error(message, err) 240 this.videoRequests.next({ reset, obs: this.getVideosObservableFunction(this.pagination, this.filters) })
252 this.notifier.error(message)
253 }
254 })
255 } 241 }
256 242
257 reloadVideos () { 243 reloadVideos () {
@@ -423,4 +409,32 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy {
423 this.onFiltersChanged(true) 409 this.onFiltersChanged(true)
424 }) 410 })
425 } 411 }
412
413 private subscribeToVideoRequests () {
414 this.videoRequests
415 .pipe(concatMap(({ reset, obs }) => obs.pipe(map(({ data }) => ({ data, reset })))))
416 .subscribe({
417 next: ({ data, reset }) => {
418 console.log(data[0].name)
419
420 this.hasDoneFirstQuery = true
421 this.lastQueryLength = data.length
422
423 if (reset) this.videos = []
424 this.videos = this.videos.concat(data)
425
426 if (this.groupByDate) this.buildGroupedDateLabels()
427
428 this.onDataSubject.next(data)
429 this.videosLoaded.emit(this.videos)
430 },
431
432 error: err => {
433 const message = $localize`Cannot load more videos. Try again later.`
434
435 logger.error(message, err)
436 this.notifier.error(message)
437 }
438 })
439 }
426} 440}
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
index 2fc39fc75..f802416a4 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
@@ -81,7 +81,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
81 .subscribe(result => { 81 .subscribe(result => {
82 this.playlistsData = result.data 82 this.playlistsData = result.data
83 83
84 this.videoPlaylistService.runPlaylistCheck(this.video.id) 84 this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id)
85 }) 85 })
86 86
87 this.videoPlaylistSearchChanged 87 this.videoPlaylistSearchChanged
@@ -129,7 +129,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
129 .subscribe(playlistsResult => { 129 .subscribe(playlistsResult => {
130 this.playlistsData = playlistsResult.data 130 this.playlistsData = playlistsResult.data
131 131
132 this.videoPlaylistService.runPlaylistCheck(this.video.id) 132 this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id)
133 }) 133 })
134 } 134 }
135 135
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
index 330a51f91..bc9fb0d74 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
@@ -206,7 +206,15 @@ export class VideoPlaylistService {
206 stopTimestamp: body.stopTimestamp 206 stopTimestamp: body.stopTimestamp
207 }) 207 })
208 208
209 this.runPlaylistCheck(body.videoId) 209 this.runVideoExistsInPlaylistCheck(body.videoId)
210
211 if (this.myAccountPlaylistCache) {
212 const playlist = this.myAccountPlaylistCache.data.find(p => p.id === playlistId)
213 if (!playlist) return
214
215 const otherPlaylists = this.myAccountPlaylistCache.data.filter(p => p !== playlist)
216 this.myAccountPlaylistCache.data = [ playlist, ...otherPlaylists ]
217 }
210 }), 218 }),
211 catchError(err => this.restExtractor.handleError(err)) 219 catchError(err => this.restExtractor.handleError(err))
212 ) 220 )
@@ -225,7 +233,7 @@ export class VideoPlaylistService {
225 elem.stopTimestamp = body.stopTimestamp 233 elem.stopTimestamp = body.stopTimestamp
226 } 234 }
227 235
228 this.runPlaylistCheck(videoId) 236 this.runVideoExistsInPlaylistCheck(videoId)
229 }), 237 }),
230 catchError(err => this.restExtractor.handleError(err)) 238 catchError(err => this.restExtractor.handleError(err))
231 ) 239 )
@@ -242,7 +250,7 @@ export class VideoPlaylistService {
242 .filter(e => e.playlistElementId !== playlistElementId) 250 .filter(e => e.playlistElementId !== playlistElementId)
243 } 251 }
244 252
245 this.runPlaylistCheck(videoId) 253 this.runVideoExistsInPlaylistCheck(videoId)
246 }), 254 }),
247 catchError(err => this.restExtractor.handleError(err)) 255 catchError(err => this.restExtractor.handleError(err))
248 ) 256 )
@@ -296,7 +304,7 @@ export class VideoPlaylistService {
296 return obs 304 return obs
297 } 305 }
298 306
299 runPlaylistCheck (videoId: number) { 307 runVideoExistsInPlaylistCheck (videoId: number) {
300 debugLogger('Running playlist check.') 308 debugLogger('Running playlist check.')
301 309
302 if (this.videoExistsCache[videoId]) { 310 if (this.videoExistsCache[videoId]) {
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 56310c4e9..2781850b9 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -11,6 +11,7 @@ import './shared/control-bar/p2p-info-button'
11import './shared/control-bar/peertube-link-button' 11import './shared/control-bar/peertube-link-button'
12import './shared/control-bar/peertube-load-progress-bar' 12import './shared/control-bar/peertube-load-progress-bar'
13import './shared/control-bar/theater-button' 13import './shared/control-bar/theater-button'
14import './shared/control-bar/peertube-live-display'
14import './shared/settings/resolution-menu-button' 15import './shared/settings/resolution-menu-button'
15import './shared/settings/resolution-menu-item' 16import './shared/settings/resolution-menu-item'
16import './shared/settings/settings-dialog' 17import './shared/settings/settings-dialog'
@@ -96,6 +97,10 @@ export class PeertubePlayerManager {
96 videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { 97 videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
97 const player = this 98 const player = this
98 99
100 if (!isNaN(+options.common.playbackRate)) {
101 player.playbackRate(+options.common.playbackRate)
102 }
103
99 let alreadyFallback = false 104 let alreadyFallback = false
100 105
101 const handleError = () => { 106 const handleError = () => {
@@ -118,7 +123,7 @@ export class PeertubePlayerManager {
118 self.addContextMenu(videojsOptionsBuilder, player, options.common) 123 self.addContextMenu(videojsOptionsBuilder, player, options.common)
119 124
120 if (isMobile()) player.peertubeMobile() 125 if (isMobile()) player.peertubeMobile()
121 if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin() 126 if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive })
122 if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') 127 if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden')
123 128
124 player.bezels() 129 player.bezels()
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts
index db5b8938d..e71e90713 100644
--- a/client/src/assets/player/shared/control-bar/index.ts
+++ b/client/src/assets/player/shared/control-bar/index.ts
@@ -1,5 +1,6 @@
1export * from './next-previous-video-button' 1export * from './next-previous-video-button'
2export * from './p2p-info-button' 2export * from './p2p-info-button'
3export * from './peertube-link-button' 3export * from './peertube-link-button'
4export * from './peertube-live-display'
4export * from './peertube-load-progress-bar' 5export * from './peertube-load-progress-bar'
5export * from './theater-button' 6export * from './theater-button'
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts
new file mode 100644
index 000000000..649eb0b00
--- /dev/null
+++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts
@@ -0,0 +1,93 @@
1import videojs from 'video.js'
2import { PeerTubeLinkButtonOptions } from '../../types'
3
4const ClickableComponent = videojs.getComponent('ClickableComponent')
5
6class PeerTubeLiveDisplay extends ClickableComponent {
7 private interval: any
8
9 private contentEl_: any
10
11 constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) {
12 super(player, options as any)
13
14 this.interval = this.setInterval(() => this.updateClass(), 1000)
15
16 this.show()
17 this.updateSync(true)
18 }
19
20 dispose () {
21 if (this.interval) {
22 this.clearInterval(this.interval)
23 this.interval = undefined
24 }
25
26 this.contentEl_ = null
27
28 super.dispose()
29 }
30
31 createEl () {
32 const el = super.createEl('div', {
33 className: 'vjs-live-control vjs-control'
34 })
35
36 this.contentEl_ = videojs.dom.createEl('div', {
37 className: 'vjs-live-display'
38 }, {
39 'aria-live': 'off'
40 })
41
42 this.contentEl_.appendChild(videojs.dom.createEl('span', {
43 className: 'vjs-control-text',
44 textContent: `${this.localize('Stream Type')}\u00a0`
45 }))
46
47 this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE')))
48
49 el.appendChild(this.contentEl_)
50 return el
51 }
52
53 handleClick () {
54 const hlsjs = this.getHLSJS()
55 if (!hlsjs) return
56
57 this.player().currentTime(hlsjs.liveSyncPosition)
58 this.player().play()
59 this.updateSync(true)
60 }
61
62 private updateClass () {
63 const hlsjs = this.getHLSJS()
64 if (!hlsjs) return
65
66 // Not loaded yet
67 if (this.player().currentTime() === 0) return
68
69 const isSync = Math.abs(this.player().currentTime() - hlsjs.liveSyncPosition) < 10
70 this.updateSync(isSync)
71 }
72
73 private updateSync (isSync: boolean) {
74 if (isSync) {
75 this.addClass('synced-with-live-edge')
76 this.removeAttribute('title')
77 this.disable()
78 } else {
79 this.removeClass('synced-with-live-edge')
80 this.setAttribute('title', this.localize('Go back to the live'))
81 this.enable()
82 }
83 }
84
85 private getHLSJS () {
86 const p2pMediaLoader = this.player()?.p2pMediaLoader
87 if (!p2pMediaLoader) return undefined
88
89 return p2pMediaLoader().getHLSJS()
90 }
91}
92
93videojs.registerComponent('PeerTubeLiveDisplay', PeerTubeLiveDisplay)
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
index ec1e1038b..f5b4b3919 100644
--- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
+++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts
@@ -4,6 +4,10 @@ type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardE
4 4
5const Plugin = videojs.getPlugin('plugin') 5const Plugin = videojs.getPlugin('plugin')
6 6
7export type HotkeysOptions = {
8 isLive: boolean
9}
10
7class PeerTubeHotkeysPlugin extends Plugin { 11class PeerTubeHotkeysPlugin extends Plugin {
8 private static readonly VOLUME_STEP = 0.1 12 private static readonly VOLUME_STEP = 0.1
9 private static readonly SEEK_STEP = 5 13 private static readonly SEEK_STEP = 5
@@ -12,9 +16,13 @@ class PeerTubeHotkeysPlugin extends Plugin {
12 16
13 private readonly handlers: KeyHandler[] 17 private readonly handlers: KeyHandler[]
14 18
15 constructor (player: videojs.Player, options: videojs.PlayerOptions) { 19 private readonly isLive: boolean
20
21 constructor (player: videojs.Player, options: videojs.PlayerOptions & HotkeysOptions) {
16 super(player, options) 22 super(player, options)
17 23
24 this.isLive = options.isLive
25
18 this.handlers = this.buildHandlers() 26 this.handlers = this.buildHandlers()
19 27
20 this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) 28 this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event)
@@ -68,28 +76,6 @@ class PeerTubeHotkeysPlugin extends Plugin {
68 } 76 }
69 }, 77 },
70 78
71 // Rewind
72 {
73 accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'),
74 cb: e => {
75 e.preventDefault()
76
77 const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP)
78 this.player.currentTime(target)
79 }
80 },
81
82 // Forward
83 {
84 accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'),
85 cb: e => {
86 e.preventDefault()
87
88 const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP)
89 this.player.currentTime(target)
90 }
91 },
92
93 // Fullscreen 79 // Fullscreen
94 { 80 {
95 // f key or Ctrl + Enter 81 // f key or Ctrl + Enter
@@ -116,6 +102,8 @@ class PeerTubeHotkeysPlugin extends Plugin {
116 { 102 {
117 accept: e => e.key === '>', 103 accept: e => e.key === '>',
118 cb: () => { 104 cb: () => {
105 if (this.isLive) return
106
119 const target = Math.min(this.player.playbackRate() + 0.1, 5) 107 const target = Math.min(this.player.playbackRate() + 0.1, 5)
120 108
121 this.player.playbackRate(parseFloat(target.toFixed(2))) 109 this.player.playbackRate(parseFloat(target.toFixed(2)))
@@ -126,6 +114,8 @@ class PeerTubeHotkeysPlugin extends Plugin {
126 { 114 {
127 accept: e => e.key === '<', 115 accept: e => e.key === '<',
128 cb: () => { 116 cb: () => {
117 if (this.isLive) return
118
129 const target = Math.max(this.player.playbackRate() - 0.1, 0.10) 119 const target = Math.max(this.player.playbackRate() - 0.1, 0.10)
130 120
131 this.player.playbackRate(parseFloat(target.toFixed(2))) 121 this.player.playbackRate(parseFloat(target.toFixed(2)))
@@ -136,6 +126,8 @@ class PeerTubeHotkeysPlugin extends Plugin {
136 { 126 {
137 accept: e => e.key === ',', 127 accept: e => e.key === ',',
138 cb: () => { 128 cb: () => {
129 if (this.isLive) return
130
139 this.player.pause() 131 this.player.pause()
140 132
141 // Calculate movement distance (assuming 30 fps) 133 // Calculate movement distance (assuming 30 fps)
@@ -148,6 +140,8 @@ class PeerTubeHotkeysPlugin extends Plugin {
148 { 140 {
149 accept: e => e.key === '.', 141 accept: e => e.key === '.',
150 cb: () => { 142 cb: () => {
143 if (this.isLive) return
144
151 this.player.pause() 145 this.player.pause()
152 146
153 // Calculate movement distance (assuming 30 fps) 147 // Calculate movement distance (assuming 30 fps)
@@ -157,11 +151,47 @@ class PeerTubeHotkeysPlugin extends Plugin {
157 } 151 }
158 ] 152 ]
159 153
154 if (this.isLive) return handlers
155
156 return handlers.concat(this.buildVODHandlers())
157 }
158
159 private buildVODHandlers () {
160 const handlers: KeyHandler[] = [
161 // Rewind
162 {
163 accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'),
164 cb: e => {
165 if (this.isLive) return
166
167 e.preventDefault()
168
169 const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP)
170 this.player.currentTime(target)
171 }
172 },
173
174 // Forward
175 {
176 accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'),
177 cb: e => {
178 if (this.isLive) return
179
180 e.preventDefault()
181
182 const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP)
183 this.player.currentTime(target)
184 }
185 }
186 ]
187
160 // 0-9 key handlers 188 // 0-9 key handlers
161 for (let i = 0; i < 10; i++) { 189 for (let i = 0; i < 10; i++) {
162 handlers.push({ 190 handlers.push({
163 accept: e => this.isNakedOrShift(e, i + ''), 191 accept: e => this.isNakedOrShift(e, i + ''),
164 cb: e => { 192 cb: e => {
193 if (this.isLive) return
194
165 e.preventDefault() 195 e.preventDefault()
166 196
167 this.player.currentTime(this.player.duration() * i * 0.1) 197 this.player.currentTime(this.player.duration() * i * 0.1)
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts
index 27f366732..26f923e92 100644
--- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts
@@ -30,10 +30,7 @@ export class ControlBarOptionsBuilder {
30 } 30 }
31 31
32 Object.assign(children, { 32 Object.assign(children, {
33 currentTimeDisplay: {}, 33 ...this.getTimeControls(),
34 timeDivider: {},
35 durationDisplay: {},
36 liveDisplay: {},
37 34
38 flexibleWidthSpacer: {}, 35 flexibleWidthSpacer: {},
39 36
@@ -74,7 +71,9 @@ export class ControlBarOptionsBuilder {
74 private getSettingsButton () { 71 private getSettingsButton () {
75 const settingEntries: string[] = [] 72 const settingEntries: string[] = []
76 73
77 settingEntries.push('playbackRateMenuButton') 74 if (!this.options.isLive) {
75 settingEntries.push('playbackRateMenuButton')
76 }
78 77
79 if (this.options.captions === true) settingEntries.push('captionsButton') 78 if (this.options.captions === true) settingEntries.push('captionsButton')
80 79
@@ -90,7 +89,23 @@ export class ControlBarOptionsBuilder {
90 } 89 }
91 } 90 }
92 91
92 private getTimeControls () {
93 if (this.options.isLive) {
94 return {
95 peerTubeLiveDisplay: {}
96 }
97 }
98
99 return {
100 currentTimeDisplay: {},
101 timeDivider: {},
102 durationDisplay: {}
103 }
104 }
105
93 private getProgressControl () { 106 private getProgressControl () {
107 if (this.options.isLive) return {}
108
94 const loadProgressBar = this.mode === 'webtorrent' 109 const loadProgressBar = this.mode === 'webtorrent'
95 ? 'peerTubeLoadProgressBar' 110 ? 'peerTubeLoadProgressBar'
96 : 'loadProgressBar' 111 : 'loadProgressBar'
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
index a14beb347..7f7d90ab9 100644
--- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts
@@ -281,8 +281,8 @@ class Html5Hlsjs {
281 if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 281 if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1
282 else this.errorCounts[data.type] = 1 282 else this.errorCounts[data.type] = 1
283 283
284 if (data.fatal) logger.warn(error.message) 284 if (data.fatal) logger.error(error.message, { currentTime: this.player.currentTime(), data })
285 else logger.error(error.message, { data }) 285 else logger.warn(error.message)
286 286
287 if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { 287 if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) {
288 error.code = 2 288 error.code = 2
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts
index f23ae48be..471a5e46c 100644
--- a/client/src/assets/player/shared/stats/stats-card.ts
+++ b/client/src/assets/player/shared/stats/stats-card.ts
@@ -182,7 +182,7 @@ class StatsCard extends Component {
182 let colorSpace = 'unknown' 182 let colorSpace = 'unknown'
183 let codecs = 'unknown' 183 let codecs = 'unknown'
184 184
185 if (metadata?.streams[0]) { 185 if (metadata?.streams?.[0]) {
186 const stream = metadata.streams[0] 186 const stream = metadata.streams[0]
187 187
188 colorSpace = stream['color_space'] !== 'unknown' 188 colorSpace = stream['color_space'] !== 'unknown'
@@ -193,7 +193,7 @@ class StatsCard extends Component {
193 } 193 }
194 194
195 const resolution = videoFile?.resolution.label + videoFile?.fps 195 const resolution = videoFile?.resolution.label + videoFile?.fps
196 const buffer = this.timeRangesToString(this.player().buffered()) 196 const buffer = this.timeRangesToString(this.player_.buffered())
197 const progress = this.player_.webtorrent().getTorrent()?.progress 197 const progress = this.player_.webtorrent().getTorrent()?.progress
198 198
199 return { 199 return {
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts
index 3057a5adb..3fbcec29c 100644
--- a/client/src/assets/player/types/manager-options.ts
+++ b/client/src/assets/player/types/manager-options.ts
@@ -29,6 +29,8 @@ export interface CustomizationOptions {
29 resume?: string 29 resume?: string
30 30
31 peertubeLink: boolean 31 peertubeLink: boolean
32
33 playbackRate?: number | string
32} 34}
33 35
34export interface CommonOptions extends CustomizationOptions { 36export interface CommonOptions extends CustomizationOptions {
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts
index c60154f3b..5674f78cb 100644
--- a/client/src/assets/player/types/peertube-videojs-typings.ts
+++ b/client/src/assets/player/types/peertube-videojs-typings.ts
@@ -3,6 +3,7 @@ import videojs from 'video.js'
3import { Engine } from '@peertube/p2p-media-loader-hlsjs' 3import { Engine } from '@peertube/p2p-media-loader-hlsjs'
4import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' 4import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
5import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' 5import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
6import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin'
6import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' 7import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin'
7import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' 8import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin'
8import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' 9import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager'
@@ -44,7 +45,7 @@ declare module 'video.js' {
44 45
45 bezels (): void 46 bezels (): void
46 peertubeMobile (): void 47 peertubeMobile (): void
47 peerTubeHotkeysPlugin (): void 48 peerTubeHotkeysPlugin (options?: HotkeysOptions): void
48 49
49 stats (options?: StatsCardOptions): StatsForNerdsPlugin 50 stats (options?: StatsCardOptions): StatsForNerdsPlugin
50 51
diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts
index d1fdf73aa..618be62cd 100644
--- a/client/src/root-helpers/logger.ts
+++ b/client/src/root-helpers/logger.ts
@@ -27,6 +27,10 @@ class Logger {
27 warn (message: LoggerMessage, meta?: LoggerMeta) { 27 warn (message: LoggerMessage, meta?: LoggerMeta) {
28 this.runHooks('warn', message, meta) 28 this.runHooks('warn', message, meta)
29 29
30 this.clientWarn(message, meta)
31 }
32
33 clientWarn (message: LoggerMessage, meta?: LoggerMeta) {
30 if (meta) console.warn(message, meta) 34 if (meta) console.warn(message, meta)
31 else console.warn(message) 35 else console.warn(message)
32 } 36 }
@@ -34,6 +38,10 @@ class Logger {
34 error (message: LoggerMessage, meta?: LoggerMeta) { 38 error (message: LoggerMessage, meta?: LoggerMeta) {
35 this.runHooks('error', message, meta) 39 this.runHooks('error', message, meta)
36 40
41 this.clientError(message, meta)
42 }
43
44 clientError (message: LoggerMessage, meta?: LoggerMeta) {
37 if (meta) console.error(message, meta) 45 if (meta) console.error(message, meta)
38 else console.error(message) 46 else console.error(message)
39 } 47 }
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts
index 6c64e2b01..e5b06a94c 100644
--- a/client/src/root-helpers/plugins-manager.ts
+++ b/client/src/root-helpers/plugins-manager.ts
@@ -3,7 +3,7 @@ import * as debug from 'debug'
3import { firstValueFrom, ReplaySubject } from 'rxjs' 3import { firstValueFrom, ReplaySubject } from 'rxjs'
4import { first, shareReplay } from 'rxjs/operators' 4import { first, shareReplay } from 'rxjs/operators'
5import { RegisterClientHelpers } from 'src/types/register-client-option.model' 5import { RegisterClientHelpers } from 'src/types/register-client-option.model'
6import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' 6import { getExternalAuthHref, getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
7import { 7import {
8 ClientHookName, 8 ClientHookName,
9 clientHookObject, 9 clientHookObject,
@@ -16,7 +16,6 @@ import {
16 RegisterClientRouteOptions, 16 RegisterClientRouteOptions,
17 RegisterClientSettingsScriptOptions, 17 RegisterClientSettingsScriptOptions,
18 RegisterClientVideoFieldOptions, 18 RegisterClientVideoFieldOptions,
19 RegisteredExternalAuthConfig,
20 ServerConfigPlugin 19 ServerConfigPlugin
21} from '@shared/models' 20} from '@shared/models'
22import { environment } from '../environments/environment' 21import { environment } from '../environments/environment'
@@ -94,9 +93,13 @@ class PluginsManager {
94 return isTheme ? '/themes' : '/plugins' 93 return isTheme ? '/themes' : '/plugins'
95 } 94 }
96 95
97 static getExternalAuthHref (auth: RegisteredExternalAuthConfig) { 96 static getDefaultLoginHref (apiUrl: string, serverConfig: HTMLServerConfig) {
98 return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` 97 if (!serverConfig || serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined
99 98
99 const externalAuths = serverConfig.plugin.registeredExternalAuths
100 if (externalAuths.length !== 1) return undefined
101
102 return getExternalAuthHref(apiUrl, externalAuths[0])
100 } 103 }
101 104
102 loadPluginsList (config: HTMLServerConfig) { 105 loadPluginsList (config: HTMLServerConfig) {
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss
index 0082378e4..96b3adf66 100644
--- a/client/src/sass/player/control-bar.scss
+++ b/client/src/sass/player/control-bar.scss
@@ -153,8 +153,25 @@
153 } 153 }
154 154
155 .vjs-live-control { 155 .vjs-live-control {
156 line-height: $control-bar-height; 156 padding: 5px 7px;
157 min-width: 4em; 157 border-radius: 3px;
158 height: fit-content;
159 margin: auto 10px;
160 font-weight: bold;
161 max-width: fit-content;
162 opacity: 1 !important;
163 line-height: normal;
164 position: relative;
165 top: -1px;
166
167 &.synced-with-live-edge {
168 background: #d7281c;
169 }
170
171 &:not(.synced-with-live-edge) {
172 cursor: pointer;
173 background: #80807f;
174 }
158 } 175 }
159 176
160 .vjs-peertube { 177 .vjs-peertube {
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index 88f6efb6a..ee66a9db3 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -294,6 +294,7 @@ body .p-datepicker .p-datepicker-header .p-datepicker-title select:focus {
294body .p-datepicker table { 294body .p-datepicker table {
295 font-size: 14px; 295 font-size: 14px;
296 margin: 0.857em 0 0 0; 296 margin: 0.857em 0 0 0;
297 table-layout: fixed;
297} 298}
298body .p-datepicker table th { 299body .p-datepicker table th {
299 padding: 0.5em; 300 padding: 0.5em;
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts
index b0bdb2dd9..f09c86d14 100644
--- a/client/src/standalone/videos/shared/player-manager-options.ts
+++ b/client/src/standalone/videos/shared/player-manager-options.ts
@@ -38,6 +38,7 @@ export class PlayerManagerOptions {
38 private enableApi = false 38 private enableApi = false
39 private startTime: number | string = 0 39 private startTime: number | string = 0
40 private stopTime: number | string 40 private stopTime: number | string
41 private playbackRate: number | string
41 42
42 private title: boolean 43 private title: boolean
43 private warningTitle: boolean 44 private warningTitle: boolean
@@ -130,6 +131,7 @@ export class PlayerManagerOptions {
130 this.subtitle = getParamString(params, 'subtitle') 131 this.subtitle = getParamString(params, 'subtitle')
131 this.startTime = getParamString(params, 'start') 132 this.startTime = getParamString(params, 'start')
132 this.stopTime = getParamString(params, 'stop') 133 this.stopTime = getParamString(params, 'stop')
134 this.playbackRate = getParamString(params, 'playbackRate')
133 135
134 this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') 136 this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor')
135 this.foregroundColor = getParamString(params, 'foregroundColor') 137 this.foregroundColor = getParamString(params, 'foregroundColor')
@@ -210,6 +212,8 @@ export class PlayerManagerOptions {
210 ? playlistTracker.getCurrentElement().stopTimestamp 212 ? playlistTracker.getCurrentElement().stopTimestamp
211 : this.stopTime, 213 : this.stopTime,
212 214
215 playbackRate: this.playbackRate,
216
213 videoCaptions, 217 videoCaptions,
214 inactivityTimeout: 2500, 218 inactivityTimeout: 2500,
215 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), 219 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),