aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-04-05 16:15:51 +0200
committerChocobozzz <me@florianbigard.com>2018-04-05 16:15:51 +0200
commita8462c8e3a61f4f7314fe18c0c10cc2946c254d1 (patch)
treeb5330cf0a8c313277f83263724c2a70b2f246c0f
parent3d3441d6c7a5646388ab0a77acad57fdb63b9d32 (diff)
downloadPeerTube-a8462c8e3a61f4f7314fe18c0c10cc2946c254d1.tar.gz
PeerTube-a8462c8e3a61f4f7314fe18c0c10cc2946c254d1.tar.zst
PeerTube-a8462c8e3a61f4f7314fe18c0c10cc2946c254d1.zip
Automatic resolution according to user bandwidth V1
-rw-r--r--client/src/app/shared/video/video-details.model.ts18
-rw-r--r--client/src/assets/player/peertube-videojs-plugin.ts192
-rw-r--r--client/src/assets/player/resolution-menu-button.ts17
-rw-r--r--client/src/assets/player/resolution-menu-item.ts21
-rw-r--r--client/src/assets/player/settings-menu-item.ts19
-rw-r--r--client/src/assets/player/utils.ts18
-rw-r--r--client/src/sass/video-js-custom.scss5
-rw-r--r--server/models/video/video.ts4
8 files changed, 226 insertions, 68 deletions
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index 9fc326beb..a1f7207a2 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -3,8 +3,7 @@ import {
3 VideoChannel, 3 VideoChannel,
4 VideoDetails as VideoDetailsServerModel, 4 VideoDetails as VideoDetailsServerModel,
5 VideoFile, 5 VideoFile,
6 VideoPrivacy, 6 VideoPrivacy
7 VideoResolution
8} from '../../../../../shared' 7} from '../../../../../shared'
9import { Account } from '../../../../../shared/models/actors' 8import { Account } from '../../../../../shared/models/actors'
10import { VideoConstant } from '../../../../../shared/models/videos/video.model' 9import { VideoConstant } from '../../../../../shared/models/videos/video.model'
@@ -39,21 +38,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
39 this.buildLikeAndDislikePercents() 38 this.buildLikeAndDislikePercents()
40 } 39 }
41 40
42 getAppropriateMagnetUri (actualDownloadSpeed = 0) {
43 if (this.files === undefined || this.files.length === 0) return ''
44 if (this.files.length === 1) return this.files[0].magnetUri
45
46 // Find first video that is good for our download speed (remember they are sorted)
47 let betterResolutionFile = this.files.find(f => actualDownloadSpeed > (f.size / this.duration))
48
49 // If the download speed is too bad, return the lowest resolution we have
50 if (betterResolutionFile === undefined) {
51 betterResolutionFile = this.files.find(f => f.resolution.id === VideoResolution.H_240P)
52 }
53
54 return betterResolutionFile.magnetUri
55 }
56
57 isRemovableBy (user: AuthUser) { 41 isRemovableBy (user: AuthUser) {
58 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) 42 return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
59 } 43 }
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts
index 10c31cc0f..91a3993a3 100644
--- a/client/src/assets/player/peertube-videojs-plugin.ts
+++ b/client/src/assets/player/peertube-videojs-plugin.ts
@@ -4,7 +4,16 @@ import { VideoFile } from '../../../../shared/models/videos/video.model'
4import { renderVideo } from './video-renderer' 4import { renderVideo } from './video-renderer'
5import './settings-menu-button' 5import './settings-menu-button'
6import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 6import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
7import { getStoredMute, getStoredVolume, saveMuteInStore, saveVolumeInStore } from './utils' 7import {
8 getAverageBandwidth,
9 getStoredMute,
10 getStoredVolume,
11 saveAverageBandwidth,
12 saveMuteInStore,
13 saveVolumeInStore
14} from './utils'
15import minBy from 'lodash-es/minBy'
16import maxBy from 'lodash-es/maxBy'
8 17
9const webtorrent = new WebTorrent({ 18const webtorrent = new WebTorrent({
10 tracker: { 19 tracker: {
@@ -25,16 +34,31 @@ const webtorrent = new WebTorrent({
25const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') 34const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin')
26class PeerTubePlugin extends Plugin { 35class PeerTubePlugin extends Plugin {
27 private readonly playerElement: HTMLVideoElement 36 private readonly playerElement: HTMLVideoElement
37
28 private readonly autoplay: boolean = false 38 private readonly autoplay: boolean = false
29 private readonly savePlayerSrcFunction: Function 39 private readonly savePlayerSrcFunction: Function
40 private readonly videoFiles: VideoFile[]
41 private readonly videoViewUrl: string
42 private readonly videoDuration: number
43 private readonly CONSTANTS = {
44 INFO_SCHEDULER: 1000, // Don't change this
45 AUTO_QUALITY_SCHEDULER: 3000, // Check quality every 3 seconds
46 AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it
47 AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds before potentially changing the definition
48 AUTO_QUALITY_UPPER_RESOLUTION_DELAY: 5000, // Buffer upper resolution during 5 seconds
49 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
50 }
51
30 private player: any 52 private player: any
31 private currentVideoFile: VideoFile 53 private currentVideoFile: VideoFile
32 private videoFiles: VideoFile[]
33 private torrent: WebTorrent.Torrent 54 private torrent: WebTorrent.Torrent
34 private videoViewUrl: string
35 private videoDuration: number
36 private videoViewInterval 55 private videoViewInterval
37 private torrentInfoInterval 56 private torrentInfoInterval
57 private autoQualityInterval
58 private autoResolution = true
59 private isAutoResolutionObservation = false
60
61 private downloadSpeeds: number[] = []
38 62
39 constructor (player: videojs.Player, options: PeertubePluginOptions) { 63 constructor (player: videojs.Player, options: PeertubePluginOptions) {
40 super(player, options) 64 super(player, options)
@@ -64,6 +88,11 @@ class PeerTubePlugin extends Plugin {
64 this.initializePlayer() 88 this.initializePlayer()
65 this.runTorrentInfoScheduler() 89 this.runTorrentInfoScheduler()
66 this.runViewAdd() 90 this.runViewAdd()
91
92 this.player.one('play', () => {
93 // Don't run immediately scheduler, wait some seconds the TCP connections are maid
94 setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
95 })
67 }) 96 })
68 97
69 this.player.on('volumechange', () => { 98 this.player.on('volumechange', () => {
@@ -75,6 +104,7 @@ class PeerTubePlugin extends Plugin {
75 dispose () { 104 dispose () {
76 clearInterval(this.videoViewInterval) 105 clearInterval(this.videoViewInterval)
77 clearInterval(this.torrentInfoInterval) 106 clearInterval(this.torrentInfoInterval)
107 clearInterval(this.autoQualityInterval)
78 108
79 // Don't need to destroy renderer, video player will be destroyed 109 // Don't need to destroy renderer, video player will be destroyed
80 this.flushVideoFile(this.currentVideoFile, false) 110 this.flushVideoFile(this.currentVideoFile, false)
@@ -88,14 +118,17 @@ class PeerTubePlugin extends Plugin {
88 return this.currentVideoFile ? this.currentVideoFile.resolution.label : '' 118 return this.currentVideoFile ? this.currentVideoFile.resolution.label : ''
89 } 119 }
90 120
91 updateVideoFile (videoFile?: VideoFile, done?: () => void) { 121 updateVideoFile (videoFile?: VideoFile, delay = 0, done?: () => void) {
92 if (done === undefined) { 122 if (done === undefined) {
93 done = () => { /* empty */ } 123 done = () => { /* empty */ }
94 } 124 }
95 125
96 // Pick the first one 126 // Automatically choose the adapted video file
97 if (videoFile === undefined) { 127 if (videoFile === undefined) {
98 videoFile = this.videoFiles[0] 128 const savedAverageBandwidth = getAverageBandwidth()
129 videoFile = savedAverageBandwidth
130 ? this.getAppropriateFile(savedAverageBandwidth)
131 : this.videoFiles[0]
99 } 132 }
100 133
101 // Don't add the same video file once again 134 // Don't add the same video file once again
@@ -112,7 +145,7 @@ class PeerTubePlugin extends Plugin {
112 const previousVideoFile = this.currentVideoFile 145 const previousVideoFile = this.currentVideoFile
113 this.currentVideoFile = videoFile 146 this.currentVideoFile = videoFile
114 147
115 this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, () => { 148 this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, delay, () => {
116 this.player.playbackRate(oldPlaybackRate) 149 this.player.playbackRate(oldPlaybackRate)
117 return done() 150 return done()
118 }) 151 })
@@ -120,29 +153,39 @@ class PeerTubePlugin extends Plugin {
120 this.trigger('videoFileUpdate') 153 this.trigger('videoFileUpdate')
121 } 154 }
122 155
123 addTorrent (magnetOrTorrentUrl: string, previousVideoFile: VideoFile, done: Function) { 156 addTorrent (magnetOrTorrentUrl: string, previousVideoFile: VideoFile, delay = 0, done: Function) {
124 console.log('Adding ' + magnetOrTorrentUrl + '.') 157 console.log('Adding ' + magnetOrTorrentUrl + '.')
125 158
159 const oldTorrent = this.torrent
126 this.torrent = webtorrent.add(magnetOrTorrentUrl, torrent => { 160 this.torrent = webtorrent.add(magnetOrTorrentUrl, torrent => {
127 console.log('Added ' + magnetOrTorrentUrl + '.') 161 console.log('Added ' + magnetOrTorrentUrl + '.')
128 162
129 this.flushVideoFile(previousVideoFile) 163 // Pause the old torrent
164 if (oldTorrent) {
165 oldTorrent.pause()
166 // Pause does not remove actual peers (in particular the webseed peer)
167 oldTorrent.removePeer(oldTorrent['ws'])
168 }
130 169
131 const options = { autoplay: true, controls: true } 170 setTimeout(() => {
132 renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => { 171 this.flushVideoFile(previousVideoFile)
133 this.renderer = renderer
134 172
135 if (err) return this.fallbackToHttp(done) 173 const options = { autoplay: true, controls: true }
174 renderVideo(torrent.files[0], this.playerElement, options,(err, renderer) => {
175 this.renderer = renderer
136 176
137 if (!this.player.paused()) { 177 if (err) return this.fallbackToHttp(done)
138 const playPromise = this.player.play()
139 if (playPromise !== undefined) return playPromise.then(done)
140 178
141 return done() 179 if (!this.player.paused()) {
142 } 180 const playPromise = this.player.play()
181 if (playPromise !== undefined) return playPromise.then(done)
143 182
144 return done() 183 return done()
145 }) 184 }
185
186 return done()
187 })
188 }, delay)
146 }) 189 })
147 190
148 this.torrent.on('error', err => this.handleError(err)) 191 this.torrent.on('error', err => this.handleError(err))
@@ -160,14 +203,14 @@ class PeerTubePlugin extends Plugin {
160 // Magnet hash is not up to date with the torrent file, add directly the torrent file 203 // Magnet hash is not up to date with the torrent file, add directly the torrent file
161 if (err.message.indexOf('incorrect info hash') !== -1) { 204 if (err.message.indexOf('incorrect info hash') !== -1) {
162 console.error('Incorrect info hash detected, falling back to torrent file.') 205 console.error('Incorrect info hash detected, falling back to torrent file.')
163 return this.addTorrent(this.torrent['xs'], previousVideoFile, done) 206 return this.addTorrent(this.torrent['xs'], previousVideoFile, 0, done)
164 } 207 }
165 208
166 return this.handleError(err) 209 return this.handleError(err)
167 }) 210 })
168 } 211 }
169 212
170 updateResolution (resolutionId: number) { 213 updateResolution (resolutionId: number, delay = 0) {
171 // Remember player state 214 // Remember player state
172 const currentTime = this.player.currentTime() 215 const currentTime = this.player.currentTime()
173 const isPaused = this.player.paused() 216 const isPaused = this.player.paused()
@@ -181,7 +224,7 @@ class PeerTubePlugin extends Plugin {
181 } 224 }
182 225
183 const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId) 226 const newVideoFile = this.videoFiles.find(f => f.resolution.id === resolutionId)
184 this.updateVideoFile(newVideoFile, () => { 227 this.updateVideoFile(newVideoFile, delay, () => {
185 this.player.currentTime(currentTime) 228 this.player.currentTime(currentTime)
186 this.player.handleTechSeeked_() 229 this.player.handleTechSeeked_()
187 }) 230 })
@@ -196,14 +239,58 @@ class PeerTubePlugin extends Plugin {
196 } 239 }
197 } 240 }
198 241
199 setVideoFiles (files: VideoFile[], videoViewUrl: string, videoDuration: number) { 242 isAutoResolutionOn () {
200 this.videoViewUrl = videoViewUrl 243 return this.autoResolution
201 this.videoDuration = videoDuration 244 }
202 this.videoFiles = files
203 245
204 // Re run view add for the new video 246 enableAutoResolution () {
205 this.runViewAdd() 247 this.autoResolution = true
206 this.updateVideoFile(undefined, () => this.player.play()) 248 this.trigger('autoResolutionUpdate')
249 }
250
251 disableAutoResolution () {
252 this.autoResolution = false
253 this.trigger('autoResolutionUpdate')
254 }
255
256 private getAppropriateFile (averageDownloadSpeed?: number): VideoFile {
257 if (this.videoFiles === undefined || this.videoFiles.length === 0) return undefined
258 if (this.videoFiles.length === 1) return this.videoFiles[0]
259 if (this.torrent && this.torrent.progress === 1) return this.currentVideoFile
260
261 if (!averageDownloadSpeed) averageDownloadSpeed = this.getActualDownloadSpeed()
262
263 // Filter videos we can play according to our bandwidth
264 const filteredFiles = this.videoFiles.filter(f => {
265 const fileBitrate = (f.size / this.videoDuration)
266 let threshold = fileBitrate
267
268 // If this is for a higher resolution, or an initial load -> add a upper margin
269 if (!this.currentVideoFile || f.resolution.id > this.currentVideoFile.resolution.id) {
270 threshold += ((fileBitrate * this.CONSTANTS.AUTO_QUALITY_THRESHOLD_PERCENT) / 100)
271 }
272
273 return averageDownloadSpeed > threshold
274 })
275
276 // If the download speed is too bad, return the lowest resolution we have
277 if (filteredFiles.length === 0) return minBy(this.videoFiles, 'resolution.id')
278
279 return maxBy(filteredFiles, 'resolution.id')
280 }
281
282 private getActualDownloadSpeed () {
283 const start = Math.max(this.downloadSpeeds.length - this.CONSTANTS.BANDWIDTH_AVERAGE_NUMBER_OF_VALUES, 0)
284 const lastDownloadSpeeds = this.downloadSpeeds.slice(start, this.downloadSpeeds.length)
285 if (lastDownloadSpeeds.length === 0) return -1
286
287 const sum = lastDownloadSpeeds.reduce((a, b) => a + b)
288 const averageBandwidth = Math.round(sum / lastDownloadSpeeds.length)
289
290 // Save the average bandwidth for future use
291 saveAverageBandwidth(averageBandwidth)
292
293 return averageBandwidth
207 } 294 }
208 295
209 private initializePlayer () { 296 private initializePlayer () {
@@ -213,17 +300,51 @@ class PeerTubePlugin extends Plugin {
213 300
214 if (this.autoplay === true) { 301 if (this.autoplay === true) {
215 this.player.posterImage.hide() 302 this.player.posterImage.hide()
216 this.updateVideoFile(undefined, () => this.player.play()) 303 this.updateVideoFile(undefined, 0, () => this.player.play())
217 } else { 304 } else {
218 // Proxy first play 305 // Proxy first play
219 const oldPlay = this.player.play.bind(this.player) 306 const oldPlay = this.player.play.bind(this.player)
220 this.player.play = () => { 307 this.player.play = () => {
221 this.updateVideoFile(undefined, () => oldPlay) 308 this.updateVideoFile(undefined, 0, () => oldPlay)
222 this.player.play = oldPlay 309 this.player.play = oldPlay
223 } 310 }
224 } 311 }
225 } 312 }
226 313
314 private runAutoQualityScheduler () {
315 this.autoQualityInterval = setInterval(() => {
316 if (this.torrent === undefined) return
317 if (this.isAutoResolutionOn() === false) return
318 if (this.isAutoResolutionObservation === true) return
319
320 const file = this.getAppropriateFile()
321 let changeResolution = false
322 let changeResolutionDelay = 0
323
324 // Lower resolution
325 if (this.isPlayerWaiting() && file.resolution.id < this.currentVideoFile.resolution.id) {
326 console.log('Downgrading automatically the resolution to: %s', file.resolution.label)
327 changeResolution = true
328 } else if (file.resolution.id > this.currentVideoFile.resolution.id) { // Greater resolution
329 console.log('Upgrading automatically the resolution to: %s', file.resolution.label)
330 changeResolution = true
331 changeResolutionDelay = this.CONSTANTS.AUTO_QUALITY_UPPER_RESOLUTION_DELAY
332 }
333
334 if (changeResolution === true) {
335 this.updateResolution(file.resolution.id, changeResolutionDelay)
336
337 // Wait some seconds in observation of our new resolution
338 this.isAutoResolutionObservation = true
339 setTimeout(() => this.isAutoResolutionObservation = false, this.CONSTANTS.AUTO_QUALITY_OBSERVATION_TIME)
340 }
341 }, this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
342 }
343
344 private isPlayerWaiting () {
345 return this.player.hasClass('vjs-waiting')
346 }
347
227 private runTorrentInfoScheduler () { 348 private runTorrentInfoScheduler () {
228 this.torrentInfoInterval = setInterval(() => { 349 this.torrentInfoInterval = setInterval(() => {
229 // Not initialized yet 350 // Not initialized yet
@@ -232,12 +353,15 @@ class PeerTubePlugin extends Plugin {
232 // Http fallback 353 // Http fallback
233 if (this.torrent === null) return this.trigger('torrentInfo', false) 354 if (this.torrent === null) return this.trigger('torrentInfo', false)
234 355
356 // webtorrent.downloadSpeed because we need to take into account the potential old torrent too
357 if (webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(webtorrent.downloadSpeed)
358
235 return this.trigger('torrentInfo', { 359 return this.trigger('torrentInfo', {
236 downloadSpeed: this.torrent.downloadSpeed, 360 downloadSpeed: this.torrent.downloadSpeed,
237 numPeers: this.torrent.numPeers, 361 numPeers: this.torrent.numPeers,
238 uploadSpeed: this.torrent.uploadSpeed 362 uploadSpeed: this.torrent.uploadSpeed
239 }) 363 })
240 }, 1000) 364 }, this.CONSTANTS.INFO_SCHEDULER)
241 } 365 }
242 366
243 private runViewAdd () { 367 private runViewAdd () {
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts
index 712e71192..2efc8de69 100644
--- a/client/src/assets/player/resolution-menu-button.ts
+++ b/client/src/assets/player/resolution-menu-button.ts
@@ -22,7 +22,7 @@ class ResolutionMenuButton extends MenuButton {
22 22
23 this.labelEl_ = videojsUntyped.dom.createEl('div', { 23 this.labelEl_ = videojsUntyped.dom.createEl('div', {
24 className: 'vjs-resolution-value', 24 className: 'vjs-resolution-value',
25 innerHTML: this.player_.peertube().getCurrentResolutionLabel() 25 innerHTML: this.buildLabelHTML()
26 }) 26 })
27 27
28 el.appendChild(this.labelEl_) 28 el.appendChild(this.labelEl_)
@@ -47,13 +47,22 @@ class ResolutionMenuButton extends MenuButton {
47 ) 47 )
48 } 48 }
49 49
50 menu.addChild(new ResolutionMenuItem(
51 this.player_,
52 {
53 id: -1,
54 label: 'Auto',
55 src: null
56 }
57 ))
58
50 return menu 59 return menu
51 } 60 }
52 61
53 updateLabel () { 62 updateLabel () {
54 if (!this.labelEl_) return 63 if (!this.labelEl_) return
55 64
56 this.labelEl_.innerHTML = this.player_.peertube().getCurrentResolutionLabel() 65 this.labelEl_.innerHTML = this.buildLabelHTML()
57 } 66 }
58 67
59 buildCSSClass () { 68 buildCSSClass () {
@@ -63,5 +72,9 @@ class ResolutionMenuButton extends MenuButton {
63 buildWrapperCSSClass () { 72 buildWrapperCSSClass () {
64 return 'vjs-resolution-control ' + super.buildWrapperCSSClass() 73 return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
65 } 74 }
75
76 private buildLabelHTML () {
77 return this.player_.peertube().getCurrentResolutionLabel()
78 }
66} 79}
67MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) 80MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts
index 8ad834c59..4b1ed0642 100644
--- a/client/src/assets/player/resolution-menu-item.ts
+++ b/client/src/assets/player/resolution-menu-item.ts
@@ -14,17 +14,38 @@ class ResolutionMenuItem extends MenuItem {
14 this.id = options.id 14 this.id = options.id
15 15
16 player.peertube().on('videoFileUpdate', () => this.updateSelection()) 16 player.peertube().on('videoFileUpdate', () => this.updateSelection())
17 player.peertube().on('autoResolutionUpdate', () => this.updateSelection())
17 } 18 }
18 19
19 handleClick (event) { 20 handleClick (event) {
20 super.handleClick(event) 21 super.handleClick(event)
21 22
23 // Auto resolution
24 if (this.id === -1) {
25 this.player_.peertube().enableAutoResolution()
26 return
27 }
28
29 this.player_.peertube().disableAutoResolution()
22 this.player_.peertube().updateResolution(this.id) 30 this.player_.peertube().updateResolution(this.id)
23 } 31 }
24 32
25 updateSelection () { 33 updateSelection () {
34 if (this.player_.peertube().isAutoResolutionOn()) {
35 this.selected(this.id === -1)
36 return
37 }
38
26 this.selected(this.player_.peertube().getCurrentResolutionId() === this.id) 39 this.selected(this.player_.peertube().getCurrentResolutionId() === this.id)
27 } 40 }
41
42 getLabel () {
43 if (this.id === -1) {
44 return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>'
45 }
46
47 return this.label
48 }
28} 49}
29MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) 50MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
30 51
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/settings-menu-item.ts
index e979ae088..048c88533 100644
--- a/client/src/assets/player/settings-menu-item.ts
+++ b/client/src/assets/player/settings-menu-item.ts
@@ -241,21 +241,14 @@ class SettingsMenuItem extends MenuItem {
241 continue 241 continue
242 } 242 }
243 243
244 switch (subMenu) { 244 if (subMenuItem.hasClass('vjs-selected')) {
245 case 'SubtitlesButton': 245 // Prefer to use the function
246 case 'CaptionsButton': 246 if (typeof subMenuItem.getLabel === 'function') {
247 // subtitlesButton entering default check twice and overwriting 247 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel()
248 // selected label in main manu
249 if (subMenuItem.hasClass('vjs-selected')) {
250 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
251 }
252 break 248 break
249 }
253 250
254 default: 251 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
255 // Set submenu value based on what item is selected
256 if (subMenuItem.options_.selected || subMenuItem.hasClass('vjs-selected')) {
257 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label
258 }
259 } 252 }
260 } 253 }
261 } 254 }
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index 7a99dba1a..f5407ef60 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -36,6 +36,18 @@ function getStoredMute () {
36 return undefined 36 return undefined
37} 37}
38 38
39function getAverageBandwidth () {
40 const value = getLocalStorage('average-bandwidth')
41 if (value !== null && value !== undefined) {
42 const valueNumber = parseInt(value, 10)
43 if (isNaN(valueNumber)) return undefined
44
45 return valueNumber
46 }
47
48 return undefined
49}
50
39function saveVolumeInStore (value: number) { 51function saveVolumeInStore (value: number) {
40 return setLocalStorage('volume', value.toString()) 52 return setLocalStorage('volume', value.toString())
41} 53}
@@ -44,10 +56,16 @@ function saveMuteInStore (value: boolean) {
44 return setLocalStorage('mute', value.toString()) 56 return setLocalStorage('mute', value.toString())
45} 57}
46 58
59function saveAverageBandwidth (value: number) {
60 return setLocalStorage('average-bandwidth', value.toString())
61}
62
47export { 63export {
48 toTitleCase, 64 toTitleCase,
49 getStoredVolume, 65 getStoredVolume,
50 saveVolumeInStore, 66 saveVolumeInStore,
67 saveAverageBandwidth,
68 getAverageBandwidth,
51 saveMuteInStore, 69 saveMuteInStore,
52 getStoredMute, 70 getStoredMute,
53 bytes 71 bytes
diff --git a/client/src/sass/video-js-custom.scss b/client/src/sass/video-js-custom.scss
index efaf3e3cb..768b7895f 100644
--- a/client/src/sass/video-js-custom.scss
+++ b/client/src/sass/video-js-custom.scss
@@ -641,6 +641,11 @@ $setting-transition-easing: ease-out;
641 .vjs-settings-sub-menu-value { 641 .vjs-settings-sub-menu-value {
642 width: 100%; 642 width: 100%;
643 text-align: right; 643 text-align: right;
644
645 small {
646 font-size: 0.85em;
647 opacity: 0.8;
648 }
644 } 649 }
645 650
646 .vjs-settings-panel { 651 .vjs-settings-panel {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 2a1226f6d..8b58b393b 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -28,7 +28,7 @@ import {
28} from 'sequelize-typescript' 28} from 'sequelize-typescript'
29import { VideoPrivacy, VideoResolution } from '../../../shared' 29import { VideoPrivacy, VideoResolution } from '../../../shared'
30import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 30import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
31import { Video, VideoDetails } from '../../../shared/models/videos' 31import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
32import { VideoFilter } from '../../../shared/models/videos/video-query.type' 32import { VideoFilter } from '../../../shared/models/videos/video-query.type'
33import { activityPubCollection } from '../../helpers/activitypub' 33import { activityPubCollection } from '../../helpers/activitypub'
34import { 34import {
@@ -1020,7 +1020,7 @@ export class VideoModel extends Model<VideoModel> {
1020 size: videoFile.size, 1020 size: videoFile.size,
1021 torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), 1021 torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
1022 fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp) 1022 fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp)
1023 } 1023 } as VideoFile
1024 }) 1024 })
1025 .sort((a, b) => { 1025 .sort((a, b) => {
1026 if (a.resolution.id < b.resolution.id) return 1 1026 if (a.resolution.id < b.resolution.id) return 1