diff options
Diffstat (limited to 'client')
27 files changed, 353 insertions, 126 deletions
diff --git a/client/package.json b/client/package.json index 115a4a199..6f88d4fb9 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -52,8 +52,8 @@ | |||
52 | "@ngx-loading-bar/core": "^6.0.0", | 52 | "@ngx-loading-bar/core": "^6.0.0", |
53 | "@ngx-loading-bar/http-client": "^6.0.0", | 53 | "@ngx-loading-bar/http-client": "^6.0.0", |
54 | "@ngx-loading-bar/router": "^6.0.0", | 54 | "@ngx-loading-bar/router": "^6.0.0", |
55 | "@peertube/p2p-media-loader-core": "^1.0.13", | 55 | "@peertube/p2p-media-loader-core": "^1.0.14", |
56 | "@peertube/p2p-media-loader-hlsjs": "^1.0.13", | 56 | "@peertube/p2p-media-loader-hlsjs": "^1.0.14", |
57 | "@peertube/videojs-contextmenu": "^5.5.0", | 57 | "@peertube/videojs-contextmenu": "^5.5.0", |
58 | "@peertube/xliffmerge": "^2.0.3", | 58 | "@peertube/xliffmerge": "^2.0.3", |
59 | "@popperjs/core": "^2.11.5", | 59 | "@popperjs/core": "^2.11.5", |
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 @@ | |||
1 | import { environment } from 'src/environments/environment' | ||
1 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' | 4 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' |
@@ -7,7 +8,7 @@ import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-valid | |||
7 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' | 8 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' |
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 9 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 10 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { PluginsManager } from '@root-helpers/plugins-manager' | 11 | import { getExternalAuthHref } from '@shared/core-utils' |
11 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | 12 | import { 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 | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { Notifier } from '@app/core/notification/notifier.service' | 7 | import { Notifier } from '@app/core/notification/notifier.service' |
8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' | 8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage, PluginsManager } from '@root-helpers/index' |
9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' | 9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { RestExtractor } from '../rest/rest-extractor.service' |
12 | import { ServerService } from '../server' | ||
12 | import { AuthStatus } from './auth-status.model' | 13 | import { AuthStatus } from './auth-status.model' |
13 | import { AuthUser } from './auth-user.model' | 14 | import { 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 @@ | |||
1 | import { HotkeysService } from 'angular2-hotkeys' | 1 | import { HotkeysService } from 'angular2-hotkeys' |
2 | import * as debug from 'debug' | 2 | import * as debug from 'debug' |
3 | import { switchMap } from 'rxjs/operators' | 3 | import { switchMap } from 'rxjs/operators' |
4 | import { environment } from 'src/environments/environment' | ||
4 | import { ViewportScroller } from '@angular/common' | 5 | import { ViewportScroller } from '@angular/common' |
5 | import { Component, OnInit, ViewChild } from '@angular/core' | 6 | import { Component, OnInit, ViewChild } from '@angular/core' |
6 | import { Router } from '@angular/router' | 7 | import { 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 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' | 2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' |
3 | import { debounceTime, switchMap } from 'rxjs/operators' | 3 | import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' |
4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' | 4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { | 6 | import { |
@@ -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' | |||
11 | import './shared/control-bar/peertube-link-button' | 11 | import './shared/control-bar/peertube-link-button' |
12 | import './shared/control-bar/peertube-load-progress-bar' | 12 | import './shared/control-bar/peertube-load-progress-bar' |
13 | import './shared/control-bar/theater-button' | 13 | import './shared/control-bar/theater-button' |
14 | import './shared/control-bar/peertube-live-display' | ||
14 | import './shared/settings/resolution-menu-button' | 15 | import './shared/settings/resolution-menu-button' |
15 | import './shared/settings/resolution-menu-item' | 16 | import './shared/settings/resolution-menu-item' |
16 | import './shared/settings/settings-dialog' | 17 | import './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 @@ | |||
1 | export * from './next-previous-video-button' | 1 | export * from './next-previous-video-button' |
2 | export * from './p2p-info-button' | 2 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | ||
4 | export * from './peertube-load-progress-bar' | 5 | export * from './peertube-load-progress-bar' |
5 | export * from './theater-button' | 6 | export * 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PeerTubeLinkButtonOptions } from '../../types' | ||
3 | |||
4 | const ClickableComponent = videojs.getComponent('ClickableComponent') | ||
5 | |||
6 | class 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 | |||
93 | videojs.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 | ||
5 | const Plugin = videojs.getPlugin('plugin') | 5 | const Plugin = videojs.getPlugin('plugin') |
6 | 6 | ||
7 | export type HotkeysOptions = { | ||
8 | isLive: boolean | ||
9 | } | ||
10 | |||
7 | class PeerTubeHotkeysPlugin extends Plugin { | 11 | class 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 | ||
34 | export interface CommonOptions extends CustomizationOptions { | 36 | export 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' | |||
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' |
4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' |
6 | import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin' | ||
6 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' | 7 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' |
7 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' | 8 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' |
8 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' | 9 | import { 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' | |||
3 | import { firstValueFrom, ReplaySubject } from 'rxjs' | 3 | import { firstValueFrom, ReplaySubject } from 'rxjs' |
4 | import { first, shareReplay } from 'rxjs/operators' | 4 | import { first, shareReplay } from 'rxjs/operators' |
5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' | 5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' |
6 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' | 6 | import { getExternalAuthHref, getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' |
7 | import { | 7 | import { |
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' |
22 | import { environment } from '../environments/environment' | 21 | import { 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 { | |||
294 | body .p-datepicker table { | 294 | body .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 | } |
298 | body .p-datepicker table th { | 299 | body .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), |
diff --git a/client/yarn.lock b/client/yarn.lock index b680bfdfb..6a5456283 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -1789,10 +1789,10 @@ | |||
1789 | read-package-json-fast "^2.0.3" | 1789 | read-package-json-fast "^2.0.3" |
1790 | which "^2.0.2" | 1790 | which "^2.0.2" |
1791 | 1791 | ||
1792 | "@peertube/p2p-media-loader-core@^1.0.13", "@peertube/p2p-media-loader-core@^1.0.8": | 1792 | "@peertube/p2p-media-loader-core@^1.0.14": |
1793 | version "1.0.13" | 1793 | version "1.0.14" |
1794 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.13.tgz#36744a291b69c001b2562c1a93017979f8534ff8" | 1794 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.14.tgz#b4442dd343d6b30a51502e1240275eb98ef2c788" |
1795 | integrity sha512-ArSAaeuxwwBAG0Xd3Gj0TzKObLfJFYzHz9+fREvmUf+GZQEG6qGwWmrdVWL6xjPiEuo6LdFeCOnHSQzAbj/ptg== | 1795 | integrity sha512-tjQv1CNziNY+zYzcL1h4q6AA2WuBUZnBIeVyjWR/EsO1EEC1VMdvPsL02cqYLz9yvIxgycjeTsWCm6XDqNgXRw== |
1796 | dependencies: | 1796 | dependencies: |
1797 | bittorrent-tracker "^9.19.0" | 1797 | bittorrent-tracker "^9.19.0" |
1798 | debug "^4.3.4" | 1798 | debug "^4.3.4" |
@@ -1800,12 +1800,12 @@ | |||
1800 | sha.js "^2.4.11" | 1800 | sha.js "^2.4.11" |
1801 | simple-peer "^9.11.1" | 1801 | simple-peer "^9.11.1" |
1802 | 1802 | ||
1803 | "@peertube/p2p-media-loader-hlsjs@^1.0.13": | 1803 | "@peertube/p2p-media-loader-hlsjs@^1.0.14": |
1804 | version "1.0.13" | 1804 | version "1.0.14" |
1805 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.13.tgz#5305e2008041d01850802544d1c49298f79dd67a" | 1805 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.14.tgz#829629a57608b0e30f4b50bc98578e6bee9f8b9b" |
1806 | integrity sha512-2BO2oaRsSHEhLkgi2iw1r4n1Yqq1EnyoOgOZccPDqjmHUsZSV/wNrno8WYr6LsleudrHA26Imu57hVD1jDx7lg== | 1806 | integrity sha512-ySUVgUvAFXCE5E94xxjfywQ8xzk3jy9UGVkgi5Oqq+QeY7uG+o7CZ+LsQ/RjXgWBD70tEnyyfADHtL+9FCnwyQ== |
1807 | dependencies: | 1807 | dependencies: |
1808 | "@peertube/p2p-media-loader-core" "^1.0.8" | 1808 | "@peertube/p2p-media-loader-core" "^1.0.14" |
1809 | debug "^4.3.4" | 1809 | debug "^4.3.4" |
1810 | events "^3.3.0" | 1810 | events "^3.3.0" |
1811 | m3u8-parser "^4.7.1" | 1811 | m3u8-parser "^4.7.1" |