aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/package.json4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts2
-rw-r--r--client/src/app/+videos/video-list/videos-list-common-page.component.ts3
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts6
-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.ts5
-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/manager-options/control-bar-options-builder.ts21
-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/root-helpers/logger.ts8
-rw-r--r--client/src/sass/player/control-bar.scss21
-rw-r--r--client/src/standalone/videos/shared/player-manager-options.ts4
-rw-r--r--client/yarn.lock18
20 files changed, 232 insertions, 54 deletions
diff --git a/client/package.json b/client/package.json
index a53f19e55..5e962550c 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/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 94853423b..fa0417832 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -295,6 +295,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
295 subtitle: queryParams.subtitle, 295 subtitle: queryParams.subtitle,
296 296
297 playerMode: queryParams.mode, 297 playerMode: queryParams.mode,
298 playbackRate: queryParams.playbackRate,
298 peertubeLink: false 299 peertubeLink: false
299 } 300 }
300 301
@@ -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
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/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/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..413ec2b80 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 = () => {
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/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts
index 27f366732..df1e8eabe 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
@@ -90,7 +87,23 @@ export class ControlBarOptionsBuilder {
90 } 87 }
91 } 88 }
92 89
90 private getTimeControls () {
91 if (this.options.isLive) {
92 return {
93 peerTubeLiveDisplay: {}
94 }
95 }
96
97 return {
98 currentTimeDisplay: {},
99 timeDivider: {},
100 durationDisplay: {}
101 }
102 }
103
93 private getProgressControl () { 104 private getProgressControl () {
105 if (this.options.isLive) return {}
106
94 const loadProgressBar = this.mode === 'webtorrent' 107 const loadProgressBar = this.mode === 'webtorrent'
95 ? 'peerTubeLoadProgressBar' 108 ? 'peerTubeLoadProgressBar'
96 : 'loadProgressBar' 109 : '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/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/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/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"