diff options
author | Chocobozzz <me@florianbigard.com> | 2018-04-05 16:15:51 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-04-05 16:15:51 +0200 |
commit | a8462c8e3a61f4f7314fe18c0c10cc2946c254d1 (patch) | |
tree | b5330cf0a8c313277f83263724c2a70b2f246c0f | |
parent | 3d3441d6c7a5646388ab0a77acad57fdb63b9d32 (diff) | |
download | PeerTube-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.ts | 18 | ||||
-rw-r--r-- | client/src/assets/player/peertube-videojs-plugin.ts | 192 | ||||
-rw-r--r-- | client/src/assets/player/resolution-menu-button.ts | 17 | ||||
-rw-r--r-- | client/src/assets/player/resolution-menu-item.ts | 21 | ||||
-rw-r--r-- | client/src/assets/player/settings-menu-item.ts | 19 | ||||
-rw-r--r-- | client/src/assets/player/utils.ts | 18 | ||||
-rw-r--r-- | client/src/sass/video-js-custom.scss | 5 | ||||
-rw-r--r-- | server/models/video/video.ts | 4 |
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' |
9 | import { Account } from '../../../../../shared/models/actors' | 8 | import { Account } from '../../../../../shared/models/actors' |
10 | import { VideoConstant } from '../../../../../shared/models/videos/video.model' | 9 | import { 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' | |||
4 | import { renderVideo } from './video-renderer' | 4 | import { renderVideo } from './video-renderer' |
5 | import './settings-menu-button' | 5 | import './settings-menu-button' |
6 | import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 6 | import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' |
7 | import { getStoredMute, getStoredVolume, saveMuteInStore, saveVolumeInStore } from './utils' | 7 | import { |
8 | getAverageBandwidth, | ||
9 | getStoredMute, | ||
10 | getStoredVolume, | ||
11 | saveAverageBandwidth, | ||
12 | saveMuteInStore, | ||
13 | saveVolumeInStore | ||
14 | } from './utils' | ||
15 | import minBy from 'lodash-es/minBy' | ||
16 | import maxBy from 'lodash-es/maxBy' | ||
8 | 17 | ||
9 | const webtorrent = new WebTorrent({ | 18 | const webtorrent = new WebTorrent({ |
10 | tracker: { | 19 | tracker: { |
@@ -25,16 +34,31 @@ const webtorrent = new WebTorrent({ | |||
25 | const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') | 34 | const Plugin: VideoJSComponentInterface = videojsUntyped.getPlugin('plugin') |
26 | class PeerTubePlugin extends Plugin { | 35 | class 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 | } |
67 | MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) | 80 | MenuButton.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 | } |
29 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | 50 | MenuItem.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 | ||
39 | function 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 | |||
39 | function saveVolumeInStore (value: number) { | 51 | function 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 | ||
59 | function saveAverageBandwidth (value: number) { | ||
60 | return setLocalStorage('average-bandwidth', value.toString()) | ||
61 | } | ||
62 | |||
47 | export { | 63 | export { |
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' |
29 | import { VideoPrivacy, VideoResolution } from '../../../shared' | 29 | import { VideoPrivacy, VideoResolution } from '../../../shared' |
30 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 30 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
31 | import { Video, VideoDetails } from '../../../shared/models/videos' | 31 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
32 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 32 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
33 | import { activityPubCollection } from '../../helpers/activitypub' | 33 | import { activityPubCollection } from '../../helpers/activitypub' |
34 | import { | 34 | import { |
@@ -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 |