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 /client/src/assets/player/peertube-videojs-plugin.ts | |
parent | 3d3441d6c7a5646388ab0a77acad57fdb63b9d32 (diff) | |
download | PeerTube-a8462c8e3a61f4f7314fe18c0c10cc2946c254d1.tar.gz PeerTube-a8462c8e3a61f4f7314fe18c0c10cc2946c254d1.tar.zst PeerTube-a8462c8e3a61f4f7314fe18c0c10cc2946c254d1.zip |
Automatic resolution according to user bandwidth V1
Diffstat (limited to 'client/src/assets/player/peertube-videojs-plugin.ts')
-rw-r--r-- | client/src/assets/player/peertube-videojs-plugin.ts | 192 |
1 files changed, 158 insertions, 34 deletions
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 () { |