aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets/player')
-rw-r--r--client/src/assets/player/p2p-media-loader/hls-plugin.ts311
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts8
-rw-r--r--client/src/assets/player/peertube-player-manager.ts2
-rw-r--r--client/src/assets/player/peertube-plugin.ts38
-rw-r--r--client/src/assets/player/peertube-resolutions-plugin.ts88
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts38
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts41
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts48
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts2
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts91
10 files changed, 212 insertions, 455 deletions
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
index a1b07aea6..17b9aba97 100644
--- a/client/src/assets/player/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
@@ -1,9 +1,9 @@
1// Thanks https://github.com/streamroot/videojs-hlsjs-plugin 1// Thanks https://github.com/streamroot/videojs-hlsjs-plugin
2// We duplicated this plugin to choose the hls.js version we want, because streamroot only provide a bundled file 2// We duplicated this plugin to choose the hls.js version we want, because streamroot only provide a bundled file
3 3
4import Hlsjs, { ErrorData, HlsConfig, Level, ManifestLoadedData } from 'hls.js' 4import Hlsjs, { ErrorData, HlsConfig, Level, LevelSwitchingData, ManifestParsedData } from 'hls.js'
5import videojs from 'video.js' 5import videojs from 'video.js'
6import { HlsjsConfigHandlerOptions, QualityLevelRepresentation, QualityLevels, VideoJSTechHLS } from '../peertube-videojs-typings' 6import { HlsjsConfigHandlerOptions, PeerTubeResolution, VideoJSTechHLS } from '../peertube-videojs-typings'
7 7
8type ErrorCounts = { 8type ErrorCounts = {
9 [ type: string ]: number 9 [ type: string ]: number
@@ -102,15 +102,10 @@ class Html5Hlsjs {
102 private dvrDuration: number = null 102 private dvrDuration: number = null
103 private edgeMargin: number = null 103 private edgeMargin: number = null
104 104
105 private handlers: { [ id in 'play' | 'playing' | 'textTracksChange' | 'audioTracksChange' ]: EventListener } = { 105 private handlers: { [ id in 'play' ]: EventListener } = {
106 play: null, 106 play: null
107 playing: null,
108 textTracksChange: null,
109 audioTracksChange: null
110 } 107 }
111 108
112 private uiTextTrackHandled = false
113
114 constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) { 109 constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) {
115 this.vjs = vjs 110 this.vjs = vjs
116 this.source = source 111 this.source = source
@@ -177,10 +172,6 @@ class Html5Hlsjs {
177 // See comment for `initialize` method. 172 // See comment for `initialize` method.
178 dispose () { 173 dispose () {
179 this.videoElement.removeEventListener('play', this.handlers.play) 174 this.videoElement.removeEventListener('play', this.handlers.play)
180 this.videoElement.removeEventListener('playing', this.handlers.playing)
181
182 this.player.textTracks().removeEventListener('change', this.handlers.textTracksChange)
183 this.uiTextTrackHandled = false
184 175
185 this.hls.destroy() 176 this.hls.destroy()
186 } 177 }
@@ -281,11 +272,7 @@ class Html5Hlsjs {
281 } 272 }
282 } 273 }
283 274
284 private switchQuality (qualityId: number) { 275 private buildLevelLabel (level: Level) {
285 this.hls.currentLevel = qualityId
286 }
287
288 private _levelLabel (level: Level) {
289 if (this.player.srOptions_.levelLabelHandler) { 276 if (this.player.srOptions_.levelLabelHandler) {
290 return this.player.srOptions_.levelLabelHandler(level as any) 277 return this.player.srOptions_.levelLabelHandler(level as any)
291 } 278 }
@@ -294,167 +281,37 @@ class Html5Hlsjs {
294 if (level.width) return Math.round(level.width * 9 / 16) + 'p' 281 if (level.width) return Math.round(level.width * 9 / 16) + 'p'
295 if (level.bitrate) return (level.bitrate / 1000) + 'kbps' 282 if (level.bitrate) return (level.bitrate / 1000) + 'kbps'
296 283
297 return 0 284 return '0'
298 }
299
300 private _relayQualityChange (qualityLevels: QualityLevels) {
301 // Determine if it is "Auto" (all tracks enabled)
302 let isAuto = true
303
304 for (let i = 0; i < qualityLevels.length; i++) {
305 if (!qualityLevels[i]._enabled) {
306 isAuto = false
307 break
308 }
309 }
310
311 // Interact with ME
312 if (isAuto) {
313 this.hls.currentLevel = -1
314 return
315 }
316
317 // Find ID of highest enabled track
318 let selectedTrack: number
319
320 for (selectedTrack = qualityLevels.length - 1; selectedTrack >= 0; selectedTrack--) {
321 if (qualityLevels[selectedTrack]._enabled) {
322 break
323 }
324 }
325
326 this.hls.currentLevel = selectedTrack
327 }
328
329 private _handleQualityLevels () {
330 if (!this.metadata) return
331
332 const qualityLevels = this.player.qualityLevels?.()
333 if (!qualityLevels) return
334
335 for (let i = 0; i < this.metadata.levels.length; i++) {
336 const details = this.metadata.levels[i]
337 const representation: QualityLevelRepresentation = {
338 id: i,
339 width: details.width,
340 height: details.height,
341 bandwidth: details.bitrate,
342 bitrate: details.bitrate,
343 _enabled: true
344 }
345
346 const self = this
347 representation.enabled = function (this: QualityLevels, level: number, toggle?: boolean) {
348 // Brightcove switcher works TextTracks-style (enable tracks that it wants to ABR on)
349 if (typeof toggle === 'boolean') {
350 this[level]._enabled = toggle
351 self._relayQualityChange(this)
352 }
353
354 return this[level]._enabled
355 }
356
357 qualityLevels.addQualityLevel(representation)
358 }
359 } 285 }
360 286
361 private _notifyVideoQualities () { 287 private _notifyVideoQualities () {
362 if (!this.metadata) return 288 if (!this.metadata) return
363 const cleanTracklist = []
364 289
365 if (this.metadata.levels.length > 1) { 290 const resolutions: PeerTubeResolution[] = []
366 const autoLevel = {
367 id: -1,
368 label: 'auto',
369 selected: this.hls.manualLevel === -1
370 }
371 cleanTracklist.push(autoLevel)
372 }
373 291
374 this.metadata.levels.forEach((level, index) => { 292 this.metadata.levels.forEach((level, index) => {
375 // Don't write in level (shared reference with Hls.js) 293 resolutions.push({
376 const quality = {
377 id: index, 294 id: index,
378 selected: index === this.hls.manualLevel, 295 height: level.height,
379 label: this._levelLabel(level) 296 width: level.width,
380 } 297 bitrate: level.bitrate,
381 298 label: this.buildLevelLabel(level),
382 cleanTracklist.push(quality) 299 selected: level.id === this.hls.manualLevel,
300
301 selectCallback: () => {
302 this.hls.currentLevel = index
303 }
304 })
383 }) 305 })
384 306
385 const payload = { 307 resolutions.push({
386 qualityData: { video: cleanTracklist }, 308 id: -1,
387 qualitySwitchCallback: this.switchQuality.bind(this) 309 label: this.player.localize('Auto'),
388 } 310 selected: true,
389 311 selectCallback: () => this.hls.currentLevel = -1
390 this.tech.trigger('loadedqualitydata', payload) 312 })
391
392 // Self-de-register so we don't raise the payload multiple times
393 this.videoElement.removeEventListener('playing', this.handlers.playing)
394 }
395
396 private _updateSelectedAudioTrack () {
397 const playerAudioTracks = this.tech.audioTracks()
398 for (let j = 0; j < playerAudioTracks.length; j++) {
399 // FIXME: typings
400 if ((playerAudioTracks[j] as any).enabled) {
401 this.hls.audioTrack = j
402 break
403 }
404 }
405 }
406
407 private _onAudioTracks () {
408 const hlsAudioTracks = this.hls.audioTracks
409 const playerAudioTracks = this.tech.audioTracks()
410
411 if (hlsAudioTracks.length > 1 && playerAudioTracks.length === 0) {
412 // Add Hls.js audio tracks if not added yet
413 for (let i = 0; i < hlsAudioTracks.length; i++) {
414 playerAudioTracks.addTrack(new this.vjs.AudioTrack({
415 id: i.toString(),
416 kind: 'alternative',
417 label: hlsAudioTracks[i].name || hlsAudioTracks[i].lang,
418 language: hlsAudioTracks[i].lang,
419 enabled: i === this.hls.audioTrack
420 }))
421 }
422
423 // Handle audio track change event
424 this.handlers.audioTracksChange = this._updateSelectedAudioTrack.bind(this)
425 playerAudioTracks.addEventListener('change', this.handlers.audioTracksChange)
426 }
427 }
428
429 private _getTextTrackLabel (textTrack: TextTrack) {
430 // Label here is readable label and is optional (used in the UI so if it is there it should be different)
431 return textTrack.label ? textTrack.label : textTrack.language
432 }
433
434 private _isSameTextTrack (track1: TextTrack, track2: TextTrack) {
435 return this._getTextTrackLabel(track1) === this._getTextTrackLabel(track2) &&
436 track1.kind === track2.kind
437 }
438
439 private _updateSelectedTextTrack () {
440 const playerTextTracks = this.player.textTracks()
441 let activeTrack: TextTrack = null
442
443 for (let j = 0; j < playerTextTracks.length; j++) {
444 if (playerTextTracks[j].mode === 'showing') {
445 activeTrack = playerTextTracks[j]
446 break
447 }
448 }
449 313
450 const hlsjsTracks = this.videoElement.textTracks 314 this.player.peertubeResolutions().add(resolutions)
451 for (let k = 0; k < hlsjsTracks.length; k++) {
452 if (hlsjsTracks[k].kind === 'subtitles' || hlsjsTracks[k].kind === 'captions') {
453 hlsjsTracks[k].mode = activeTrack && this._isSameTextTrack(hlsjsTracks[k], activeTrack)
454 ? 'showing'
455 : 'disabled'
456 }
457 }
458 } 315 }
459 316
460 private _startLoad () { 317 private _startLoad () {
@@ -472,97 +329,10 @@ class Html5Hlsjs {
472 return result 329 return result
473 } 330 }
474 331
475 private _filterDisplayableTextTracks (textTracks: TextTrackList) { 332 private _onMetaData (_event: any, data: ManifestParsedData) {
476 const displayableTracks = []
477
478 // Filter out tracks that is displayable (captions or subtitles)
479 for (let idx = 0; idx < textTracks.length; idx++) {
480 if (textTracks[idx].kind === 'subtitles' || textTracks[idx].kind === 'captions') {
481 displayableTracks.push(textTracks[idx])
482 }
483 }
484
485 return displayableTracks
486 }
487
488 private _updateTextTrackList () {
489 const displayableTracks = this._filterDisplayableTextTracks(this.videoElement.textTracks)
490 const playerTextTracks = this.player.textTracks()
491
492 // Add stubs to make the caption switcher shows up
493 // Adding the Hls.js text track in will make us have double captions
494 for (let idx = 0; idx < displayableTracks.length; idx++) {
495 let isAdded = false
496
497 for (let jdx = 0; jdx < playerTextTracks.length; jdx++) {
498 if (this._isSameTextTrack(displayableTracks[idx], playerTextTracks[jdx])) {
499 isAdded = true
500 break
501 }
502 }
503
504 if (!isAdded) {
505 const hlsjsTextTrack = displayableTracks[idx]
506 this.player.addRemoteTextTrack({
507 kind: hlsjsTextTrack.kind as videojs.TextTrack.Kind,
508 label: this._getTextTrackLabel(hlsjsTextTrack),
509 language: hlsjsTextTrack.language,
510 srclang: hlsjsTextTrack.language
511 }, false)
512 }
513 }
514
515 // Handle UI switching
516 this._updateSelectedTextTrack()
517
518 if (!this.uiTextTrackHandled) {
519 this.handlers.textTracksChange = this._updateSelectedTextTrack.bind(this)
520 playerTextTracks.addEventListener('change', this.handlers.textTracksChange)
521
522 this.uiTextTrackHandled = true
523 }
524 }
525
526 private _onMetaData (_event: any, data: ManifestLoadedData) {
527 // This could arrive before 'loadedqualitydata' handlers is registered, remember it so we can raise it later 333 // This could arrive before 'loadedqualitydata' handlers is registered, remember it so we can raise it later
528 this.metadata = data as any 334 this.metadata = data
529 this._handleQualityLevels() 335 this._notifyVideoQualities()
530 }
531
532 private _createCueHandler (captionConfig: any) {
533 return {
534 newCue: (track: any, startTime: number, endTime: number, captionScreen: { rows: any[] }) => {
535 let row: any
536 let cue: VTTCue
537 let text: string
538 const VTTCue = (window as any).VTTCue || (window as any).TextTrackCue
539
540 for (let r = 0; r < captionScreen.rows.length; r++) {
541 row = captionScreen.rows[r]
542 text = ''
543
544 if (!row.isEmpty()) {
545 for (let c = 0; c < row.chars.length; c++) {
546 text += row.chars[c].ucharj
547 }
548
549 cue = new VTTCue(startTime, endTime, text.trim())
550
551 // typeof null === 'object'
552 if (captionConfig != null && typeof captionConfig === 'object') {
553 // Copy client overridden property into the cue object
554 const configKeys = Object.keys(captionConfig)
555
556 for (let k = 0; k < configKeys.length; k++) {
557 cue[configKeys[k]] = captionConfig[configKeys[k]]
558 }
559 }
560 track.addCue(cue)
561 if (endTime === startTime) track.addCue(new VTTCue(endTime + 5, ''))
562 }
563 }
564 }
565 }
566 } 336 }
567 337
568 private _initHlsjs () { 338 private _initHlsjs () {
@@ -577,11 +347,6 @@ class Html5Hlsjs {
577 this.hlsjsConfig.autoStartLoad = false 347 this.hlsjsConfig.autoStartLoad = false
578 } 348 }
579 349
580 const captionConfig = srOptions_?.captionConfig || techOptions.captionConfig
581 if (captionConfig) {
582 this.hlsjsConfig.cueHandler = this._createCueHandler(captionConfig)
583 }
584
585 // If the user explicitly sets autoStartLoad to false, we're not going to enter the if block above 350 // If the user explicitly sets autoStartLoad to false, we're not going to enter the if block above
586 // That's why we have a separate if block here to set the 'play' listener 351 // That's why we have a separate if block here to set the 'play' listener
587 if (this.hlsjsConfig.autoStartLoad === false) { 352 if (this.hlsjsConfig.autoStartLoad === false) {
@@ -589,17 +354,12 @@ class Html5Hlsjs {
589 this.videoElement.addEventListener('play', this.handlers.play) 354 this.videoElement.addEventListener('play', this.handlers.play)
590 } 355 }
591 356
592 // _notifyVideoQualities sometimes runs before the quality picker event handler is registered -> no video switcher
593 this.handlers.playing = this._notifyVideoQualities.bind(this)
594 this.videoElement.addEventListener('playing', this.handlers.playing)
595
596 this.hls = new Hlsjs(this.hlsjsConfig) 357 this.hls = new Hlsjs(this.hlsjsConfig)
597 358
598 this._executeHooksFor('beforeinitialize') 359 this._executeHooksFor('beforeinitialize')
599 360
600 this.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data)) 361 this.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data))
601 this.hls.on(Hlsjs.Events.AUDIO_TRACKS_UPDATED, () => this._onAudioTracks()) 362 this.hls.on(Hlsjs.Events.MANIFEST_PARSED, (event, data) => this._onMetaData(event, data))
602 this.hls.on(Hlsjs.Events.MANIFEST_PARSED, (event, data) => this._onMetaData(event, data as any)) // FIXME: typings
603 this.hls.on(Hlsjs.Events.LEVEL_LOADED, (event, data) => { 363 this.hls.on(Hlsjs.Events.LEVEL_LOADED, (event, data) => {
604 // The DVR plugin will auto seek to "live edge" on start up 364 // The DVR plugin will auto seek to "live edge" on start up
605 if (this.hlsjsConfig.liveSyncDuration) { 365 if (this.hlsjsConfig.liveSyncDuration) {
@@ -612,12 +372,25 @@ class Html5Hlsjs {
612 this.dvrDuration = data.details.totalduration 372 this.dvrDuration = data.details.totalduration
613 this._duration = this.isLive ? Infinity : data.details.totalduration 373 this._duration = this.isLive ? Infinity : data.details.totalduration
614 }) 374 })
375
615 this.hls.once(Hlsjs.Events.FRAG_LOADED, () => { 376 this.hls.once(Hlsjs.Events.FRAG_LOADED, () => {
616 // Emit custom 'loadedmetadata' event for parity with `videojs-contrib-hls` 377 // Emit custom 'loadedmetadata' event for parity with `videojs-contrib-hls`
617 // Ref: https://github.com/videojs/videojs-contrib-hls#loadedmetadata 378 // Ref: https://github.com/videojs/videojs-contrib-hls#loadedmetadata
618 this.tech.trigger('loadedmetadata') 379 this.tech.trigger('loadedmetadata')
619 }) 380 })
620 381
382 this.hls.on(Hlsjs.Events.LEVEL_SWITCHING, (_e, data: LevelSwitchingData) => {
383 const resolutionId = this.hls.autoLevelEnabled
384 ? -1
385 : data.level
386
387 const autoResolutionChosenId = this.hls.autoLevelEnabled
388 ? data.level
389 : -1
390
391 this.player.peertubeResolutions().select({ id: resolutionId, autoResolutionChosenId, byEngine: true })
392 })
393
621 this.hls.attachMedia(this.videoElement) 394 this.hls.attachMedia(this.videoElement)
622 395
623 this.hls.loadSource(this.source.src) 396 this.hls.loadSource(this.source.src)
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
index acd40636e..d917fda03 100644
--- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -116,14 +116,6 @@ class P2pMediaLoaderPlugin extends Plugin {
116 const options = this.player.tech(true).options_ as any 116 const options = this.player.tech(true).options_ as any
117 this.p2pEngine = options.hlsjsConfig.loader.getEngine() 117 this.p2pEngine = options.hlsjsConfig.loader.getEngine()
118 118
119 this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHING, (_: any, data: any) => {
120 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height })
121 })
122
123 this.hlsjs.on(Hlsjs.Events.MANIFEST_LOADED, (_: any, data: any) => {
124 this.trigger('resolutionsLoaded')
125 })
126
127 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { 119 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
128 console.error('Segment error.', segment, err) 120 console.error('Segment error.', segment, err)
129 121
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index f3c21fc4c..230d6298b 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -1,13 +1,13 @@
1import 'videojs-hotkeys/videojs.hotkeys' 1import 'videojs-hotkeys/videojs.hotkeys'
2import 'videojs-dock' 2import 'videojs-dock'
3import 'videojs-contextmenu-pt' 3import 'videojs-contextmenu-pt'
4import 'videojs-contrib-quality-levels'
5import './upnext/end-card' 4import './upnext/end-card'
6import './upnext/upnext-plugin' 5import './upnext/upnext-plugin'
7import './stats/stats-card' 6import './stats/stats-card'
8import './stats/stats-plugin' 7import './stats/stats-plugin'
9import './bezels/bezels-plugin' 8import './bezels/bezels-plugin'
10import './peertube-plugin' 9import './peertube-plugin'
10import './peertube-resolutions-plugin'
11import './videojs-components/next-previous-video-button' 11import './videojs-components/next-previous-video-button'
12import './videojs-components/p2p-info-button' 12import './videojs-components/p2p-info-button'
13import './videojs-components/peertube-link-button' 13import './videojs-components/peertube-link-button'
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index b4841b235..9b4dc9bd5 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -1,4 +1,3 @@
1import './videojs-components/settings-menu-button'
2import videojs from 'video.js' 1import videojs from 'video.js'
3import { timeToInt } from '@shared/core-utils' 2import { timeToInt } from '@shared/core-utils'
4import { 3import {
@@ -10,7 +9,7 @@ import {
10 saveVideoWatchHistory, 9 saveVideoWatchHistory,
11 saveVolumeInStore 10 saveVolumeInStore
12} from './peertube-player-local-storage' 11} from './peertube-player-local-storage'
13import { PeerTubePluginOptions, ResolutionUpdateData, UserWatching, VideoJSCaption } from './peertube-videojs-typings' 12import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings'
14import { isMobile } from './utils' 13import { isMobile } from './utils'
15 14
16const Plugin = videojs.getPlugin('plugin') 15const Plugin = videojs.getPlugin('plugin')
@@ -27,7 +26,6 @@ class PeerTubePlugin extends Plugin {
27 26
28 private videoViewInterval: any 27 private videoViewInterval: any
29 private userWatchingVideoInterval: any 28 private userWatchingVideoInterval: any
30 private lastResolutionChange: ResolutionUpdateData
31 29
32 private isLive: boolean 30 private isLive: boolean
33 31
@@ -54,22 +52,6 @@ class PeerTubePlugin extends Plugin {
54 this.player.ready(() => { 52 this.player.ready(() => {
55 const playerOptions = this.player.options_ 53 const playerOptions = this.player.options_
56 54
57 if (options.mode === 'webtorrent') {
58 this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
59 this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
60 }
61
62 if (options.mode === 'p2p-media-loader') {
63 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
64 }
65
66 this.player.tech(true).on('loadedqualitydata', () => {
67 setTimeout(() => {
68 // Replay a resolution change, now we loaded all quality data
69 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
70 }, 0)
71 })
72
73 const volume = getStoredVolume() 55 const volume = getStoredVolume()
74 if (volume !== undefined) this.player.volume(volume) 56 if (volume !== undefined) this.player.volume(volume)
75 57
@@ -97,7 +79,7 @@ class PeerTubePlugin extends Plugin {
97 }) 79 })
98 } 80 }
99 81
100 this.player.textTracks().on('change', () => { 82 this.player.textTracks().addEventListener('change', () => {
101 const showing = this.player.textTracks().tracks_.find(t => { 83 const showing = this.player.textTracks().tracks_.find(t => {
102 return t.kind === 'captions' && t.mode === 'showing' 84 return t.kind === 'captions' && t.mode === 'showing'
103 }) 85 })
@@ -216,22 +198,6 @@ class PeerTubePlugin extends Plugin {
216 return fetch(url, { method: 'PUT', body, headers }) 198 return fetch(url, { method: 'PUT', body, headers })
217 } 199 }
218 200
219 private handleResolutionChange (data: ResolutionUpdateData) {
220 this.lastResolutionChange = data
221
222 const qualityLevels = this.player.qualityLevels()
223
224 for (let i = 0; i < qualityLevels.length; i++) {
225 if (qualityLevels[i].height === data.resolutionId) {
226 data.id = qualityLevels[i].id
227 break
228 }
229 }
230
231 console.log('Resolution changed.', data)
232 this.trigger('resolutionChange', data)
233 }
234
235 private listenControlBarMouse () { 201 private listenControlBarMouse () {
236 this.player.controlBar.on('mouseenter', () => { 202 this.player.controlBar.on('mouseenter', () => {
237 this.mouseInControlBar = true 203 this.mouseInControlBar = true
diff --git a/client/src/assets/player/peertube-resolutions-plugin.ts b/client/src/assets/player/peertube-resolutions-plugin.ts
new file mode 100644
index 000000000..cc36f18f3
--- /dev/null
+++ b/client/src/assets/player/peertube-resolutions-plugin.ts
@@ -0,0 +1,88 @@
1import videojs from 'video.js'
2import { PeerTubeResolution } from './peertube-videojs-typings'
3
4const Plugin = videojs.getPlugin('plugin')
5
6class PeerTubeResolutionsPlugin extends Plugin {
7 private currentSelection: PeerTubeResolution
8 private resolutions: PeerTubeResolution[] = []
9
10 private autoResolutionChosenId: number
11 private autoResolutionEnabled = true
12
13 add (resolutions: PeerTubeResolution[]) {
14 for (const r of resolutions) {
15 this.resolutions.push(r)
16 }
17
18 this.currentSelection = this.getSelected()
19
20 this.sort()
21 this.trigger('resolutionsAdded')
22 }
23
24 getResolutions () {
25 return this.resolutions
26 }
27
28 getSelected () {
29 return this.resolutions.find(r => r.selected)
30 }
31
32 getAutoResolutionChosen () {
33 return this.resolutions.find(r => r.id === this.autoResolutionChosenId)
34 }
35
36 select (options: {
37 id: number
38 byEngine: boolean
39 autoResolutionChosenId?: number
40 }) {
41 const { id, autoResolutionChosenId, byEngine } = options
42
43 if (this.currentSelection?.id === id && this.autoResolutionChosenId === autoResolutionChosenId) return
44
45 this.autoResolutionChosenId = autoResolutionChosenId
46
47 for (const r of this.resolutions) {
48 r.selected = r.id === id
49
50 if (r.selected) {
51 this.currentSelection = r
52
53 if (!byEngine) r.selectCallback()
54 }
55 }
56
57 this.trigger('resolutionChanged')
58 }
59
60 disableAutoResolution () {
61 this.autoResolutionEnabled = false
62 this.trigger('autoResolutionEnabledChanged')
63 }
64
65 enabledAutoResolution () {
66 this.autoResolutionEnabled = true
67 this.trigger('autoResolutionEnabledChanged')
68 }
69
70 isAutoResolutionEnabeld () {
71 return this.autoResolutionEnabled
72 }
73
74 private sort () {
75 this.resolutions.sort((a, b) => {
76 if (a.id === -1) return 1
77 if (b.id === -1) return -1
78
79 if (a.height > b.height) return -1
80 if (a.height === b.height) return 0
81 return 1
82 })
83 }
84
85}
86
87videojs.registerPlugin('peertubeResolutions', PeerTubeResolutionsPlugin)
88export { PeerTubeResolutionsPlugin }
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index 97828c802..bd6db4ffc 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -1,6 +1,3 @@
1// FIXME: lint
2/* eslint-disable @typescript-eslint/ban-types */
3
4import { HlsConfig, Level } from 'hls.js' 1import { HlsConfig, Level } from 'hls.js'
5import videojs from 'video.js' 2import videojs from 'video.js'
6import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' 3import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models'
@@ -8,11 +5,12 @@ import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin
8import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 5import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
9import { PlayerMode } from './peertube-player-manager' 6import { PlayerMode } from './peertube-player-manager'
10import { PeerTubePlugin } from './peertube-plugin' 7import { PeerTubePlugin } from './peertube-plugin'
8import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin'
11import { PlaylistPlugin } from './playlist/playlist-plugin' 9import { PlaylistPlugin } from './playlist/playlist-plugin'
12import { EndCardOptions } from './upnext/end-card'
13import { StatsCardOptions } from './stats/stats-card' 10import { StatsCardOptions } from './stats/stats-card'
14import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
15import { StatsForNerdsPlugin } from './stats/stats-plugin' 11import { StatsForNerdsPlugin } from './stats/stats-plugin'
12import { EndCardOptions } from './upnext/end-card'
13import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
16 14
17declare module 'video.js' { 15declare module 'video.js' {
18 16
@@ -37,16 +35,15 @@ declare module 'video.js' {
37 35
38 p2pMediaLoader (): P2pMediaLoaderPlugin 36 p2pMediaLoader (): P2pMediaLoaderPlugin
39 37
38 peertubeResolutions (): PeerTubeResolutionsPlugin
39
40 contextmenuUI (options: any): any 40 contextmenuUI (options: any): any
41 41
42 bezels (): void 42 bezels (): void
43 43
44 stats (options?: StatsCardOptions): StatsForNerdsPlugin 44 stats (options?: StatsCardOptions): StatsForNerdsPlugin
45 45
46 qualityLevels (): QualityLevels
47
48 textTracks (): TextTrackList & { 46 textTracks (): TextTrackList & {
49 on: Function
50 tracks_: (TextTrack & { id: string, label: string, src: string })[] 47 tracks_: (TextTrack & { id: string, label: string, src: string })[]
51 } 48 }
52 49
@@ -69,24 +66,16 @@ export interface HlsjsConfigHandlerOptions {
69 levelLabelHandler?: (level: Level) => string 66 levelLabelHandler?: (level: Level) => string
70} 67}
71 68
72type QualityLevelRepresentation = { 69type PeerTubeResolution = {
73 id: number 70 id: number
74 height: number
75 71
72 height?: number
76 label?: string 73 label?: string
77 width?: number 74 width?: number
78 bandwidth?: number
79 bitrate?: number 75 bitrate?: number
80 76
81 enabled?: Function 77 selected: boolean
82 _enabled: boolean 78 selectCallback: () => void
83}
84
85type QualityLevels = QualityLevelRepresentation[] & {
86 selectedIndex: number
87 selectedIndex_: number
88
89 addQualityLevel (representation: QualityLevelRepresentation): void
90} 79}
91 80
92type VideoJSCaption = { 81type VideoJSCaption = {
@@ -131,7 +120,7 @@ type PlaylistPluginOptions = {
131 120
132type NextPreviousVideoButtonOptions = { 121type NextPreviousVideoButtonOptions = {
133 type: 'next' | 'previous' 122 type: 'next' | 'previous'
134 handler: Function 123 handler: () => void
135 isDisabled: () => boolean 124 isDisabled: () => boolean
136} 125}
137 126
@@ -214,7 +203,7 @@ type PlayerNetworkInfo = {
214type PlaylistItemOptions = { 203type PlaylistItemOptions = {
215 element: VideoPlaylistElement 204 element: VideoPlaylistElement
216 205
217 onClicked: Function 206 onClicked: () => void
218} 207}
219 208
220export { 209export {
@@ -229,9 +218,8 @@ export {
229 PeerTubePluginOptions, 218 PeerTubePluginOptions,
230 WebtorrentPluginOptions, 219 WebtorrentPluginOptions,
231 P2PMediaLoaderPluginOptions, 220 P2PMediaLoaderPluginOptions,
221 PeerTubeResolution,
232 VideoJSPluginOptions, 222 VideoJSPluginOptions,
233 LoadedQualityData, 223 LoadedQualityData,
234 QualityLevelRepresentation, 224 PeerTubeLinkButtonOptions
235 PeerTubeLinkButtonOptions,
236 QualityLevels
237} 225}
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts
index 98e7f56fc..8bd5b4f03 100644
--- a/client/src/assets/player/videojs-components/resolution-menu-button.ts
+++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts
@@ -1,6 +1,4 @@
1import videojs from 'video.js' 1import videojs from 'video.js'
2
3import { LoadedQualityData } from '../peertube-videojs-typings'
4import { ResolutionMenuItem } from './resolution-menu-item' 2import { ResolutionMenuItem } from './resolution-menu-item'
5 3
6const Menu = videojs.getComponent('Menu') 4const Menu = videojs.getComponent('Menu')
@@ -13,9 +11,12 @@ class ResolutionMenuButton extends MenuButton {
13 11
14 this.controlText('Quality') 12 this.controlText('Quality')
15 13
16 player.tech(true).on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) 14 player.peertubeResolutions().on('resolutionsAdded', () => this.buildQualities())
17 15
18 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) 16 // For parent
17 player.peertubeResolutions().on('resolutionChanged', () => {
18 setTimeout(() => this.trigger('labelUpdated'))
19 })
19 } 20 }
20 21
21 createEl () { 22 createEl () {
@@ -58,20 +59,8 @@ class ResolutionMenuButton extends MenuButton {
58 }) 59 })
59 } 60 }
60 61
61 private buildQualities (data: LoadedQualityData) { 62 private buildQualities () {
62 // The automatic resolution item will need other labels 63 for (const d of this.player().peertubeResolutions().getResolutions()) {
63 const labels: { [ id: number ]: string } = {}
64
65 data.qualityData.video.sort((a, b) => {
66 if (a.id > b.id) return -1
67 if (a.id === b.id) return 0
68 return 1
69 })
70
71 for (const d of data.qualityData.video) {
72 // Skip auto resolution, we'll add it ourselves
73 if (d.id === -1) continue
74
75 const label = d.label === '0p' 64 const label = d.label === '0p'
76 ? this.player().localize('Audio-only') 65 ? this.player().localize('Audio-only')
77 : d.label 66 : d.label
@@ -81,25 +70,11 @@ class ResolutionMenuButton extends MenuButton {
81 { 70 {
82 id: d.id, 71 id: d.id,
83 label, 72 label,
84 selected: d.selected, 73 selected: d.selected
85 callback: data.qualitySwitchCallback
86 }) 74 })
87 ) 75 )
88
89 labels[d.id] = d.label
90 } 76 }
91 77
92 this.menu.addChild(new ResolutionMenuItem(
93 this.player_,
94 {
95 id: -1,
96 label: this.player_.localize('Auto'),
97 labels,
98 callback: data.qualitySwitchCallback,
99 selected: true // By default, in auto mode
100 }
101 ))
102
103 for (const m of this.menu.children()) { 78 for (const m of this.menu.children()) {
104 this.addClickListener(m) 79 this.addClickListener(m)
105 } 80 }
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts
index c1f502600..6047f52f7 100644
--- a/client/src/assets/player/videojs-components/resolution-menu-item.ts
+++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts
@@ -1,82 +1,72 @@
1import videojs from 'video.js' 1import videojs from 'video.js'
2import { AutoResolutionUpdateData, ResolutionUpdateData } from '../peertube-videojs-typings'
3 2
4const MenuItem = videojs.getComponent('MenuItem') 3const MenuItem = videojs.getComponent('MenuItem')
5 4
6export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions { 5export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions {
7 labels?: { [id: number]: string }
8 id: number 6 id: number
9 callback: (resolutionId: number, type: 'video') => void
10} 7}
11 8
12class ResolutionMenuItem extends MenuItem { 9class ResolutionMenuItem extends MenuItem {
13 private readonly resolutionId: number 10 private readonly resolutionId: number
14 private readonly label: string 11 private readonly label: string
15 // Only used for the automatic item
16 private readonly labels: { [id: number]: string }
17 private readonly callback: (resolutionId: number, type: 'video') => void
18 12
19 private autoResolutionPossible: boolean 13 private autoResolutionEnabled: boolean
20 private currentResolutionLabel: string 14 private autoResolutionChosen: string
21 15
22 constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) { 16 constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) {
23 options.selectable = true 17 options.selectable = true
24 18
25 super(player, options) 19 super(player, options)
26 20
27 this.autoResolutionPossible = true 21 this.autoResolutionEnabled = true
28 this.currentResolutionLabel = '' 22 this.autoResolutionChosen = ''
29 23
30 this.resolutionId = options.id 24 this.resolutionId = options.id
31 this.label = options.label 25 this.label = options.label
32 this.labels = options.labels
33 this.callback = options.callback
34 26
35 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) 27 player.peertubeResolutions().on('resolutionChanged', () => this.updateSelection())
36 28
37 // We only want to disable the "Auto" item 29 // We only want to disable the "Auto" item
38 if (this.resolutionId === -1) { 30 if (this.resolutionId === -1) {
39 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) 31 player.peertubeResolutions().on('autoResolutionEnabledChanged', () => this.updateAutoResolution())
40 } 32 }
41 } 33 }
42 34
43 handleClick (event: any) { 35 handleClick (event: any) {
44 // Auto button disabled? 36 // Auto button disabled?
45 if (this.autoResolutionPossible === false && this.resolutionId === -1) return 37 if (this.autoResolutionEnabled === false && this.resolutionId === -1) return
46 38
47 super.handleClick(event) 39 super.handleClick(event)
48 40
49 this.callback(this.resolutionId, 'video') 41 this.player().peertubeResolutions().select({ id: this.resolutionId, byEngine: false })
50 } 42 }
51 43
52 updateSelection (data: ResolutionUpdateData) { 44 updateSelection () {
53 if (this.resolutionId === -1) { 45 const selectedResolution = this.player().peertubeResolutions().getSelected()
54 this.currentResolutionLabel = this.labels[data.id]
55 }
56 46
57 // Automatic resolution only 47 if (this.resolutionId === -1) {
58 if (data.auto === true) { 48 this.autoResolutionChosen = this.player().peertubeResolutions().getAutoResolutionChosen()?.label
59 this.selected(this.resolutionId === -1)
60 return
61 } 49 }
62 50
63 this.selected(this.resolutionId === data.id) 51 this.selected(this.resolutionId === selectedResolution.id)
64 } 52 }
65 53
66 updateAutoResolution (data: AutoResolutionUpdateData) { 54 updateAutoResolution () {
55 const enabled = this.player().peertubeResolutions().isAutoResolutionEnabeld()
56
67 // Check if the auto resolution is enabled or not 57 // Check if the auto resolution is enabled or not
68 if (data.possible === false) { 58 if (enabled === false) {
69 this.addClass('disabled') 59 this.addClass('disabled')
70 } else { 60 } else {
71 this.removeClass('disabled') 61 this.removeClass('disabled')
72 } 62 }
73 63
74 this.autoResolutionPossible = data.possible 64 this.autoResolutionEnabled = enabled
75 } 65 }
76 66
77 getLabel () { 67 getLabel () {
78 if (this.resolutionId === -1) { 68 if (this.resolutionId === -1) {
79 return this.label + ' <small>' + this.currentResolutionLabel + '</small>' 69 return this.label + ' <small>' + this.autoResolutionChosen + '</small>'
80 } 70 }
81 71
82 return this.label 72 return this.label
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts
index 1871d41f8..48fed0fd9 100644
--- a/client/src/assets/player/videojs-components/settings-menu-item.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-item.ts
@@ -248,7 +248,7 @@ class SettingsMenuItem extends MenuItem {
248 } 248 }
249 249
250 build () { 250 build () {
251 this.subMenu.on('updateLabel', () => { 251 this.subMenu.on('labelUpdated', () => {
252 this.update() 252 this.update()
253 }) 253 })
254 this.subMenu.on('menuChanged', () => { 254 this.subMenu.on('menuChanged', () => {
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 0587ddee6..a464f02d5 100644
--- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -9,7 +9,7 @@ import {
9 getStoredVolume, 9 getStoredVolume,
10 saveAverageBandwidth 10 saveAverageBandwidth
11} from '../peertube-player-local-storage' 11} from '../peertube-player-local-storage'
12import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' 12import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings'
13import { getRtcConfig, isIOS, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' 13import { getRtcConfig, isIOS, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
14import { PeertubeChunkStore } from './peertube-chunk-store' 14import { PeertubeChunkStore } from './peertube-chunk-store'
15import { renderVideo } from './video-renderer' 15import { renderVideo } from './video-renderer'
@@ -175,8 +175,7 @@ class WebTorrentPlugin extends Plugin {
175 return done() 175 return done()
176 }) 176 })
177 177
178 this.changeQuality() 178 this.selectAppropriateResolution(true)
179 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
180 } 179 }
181 180
182 updateResolution (resolutionId: number, delay = 0) { 181 updateResolution (resolutionId: number, delay = 0) {
@@ -219,17 +218,10 @@ class WebTorrentPlugin extends Plugin {
219 } 218 }
220 } 219 }
221 220
222 enableAutoResolution () { 221 disableAutoResolution () {
223 this.autoResolution = true
224 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
225 }
226
227 disableAutoResolution (forbid = false) {
228 if (forbid === true) this.autoResolutionPossible = false
229
230 this.autoResolution = false 222 this.autoResolution = false
231 this.trigger('autoResolutionChange', { possible: this.autoResolutionPossible }) 223 this.autoResolutionPossible = false
232 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) 224 this.player.peertubeResolutions().disableAutoResolution()
233 } 225 }
234 226
235 isAutoResolutionPossible () { 227 isAutoResolutionPossible () {
@@ -516,7 +508,7 @@ class WebTorrentPlugin extends Plugin {
516 private fallbackToHttp (options: PlayOptions, done?: (err?: Error) => void) { 508 private fallbackToHttp (options: PlayOptions, done?: (err?: Error) => void) {
517 const paused = this.player.paused() 509 const paused = this.player.paused()
518 510
519 this.disableAutoResolution(true) 511 this.disableAutoResolution()
520 512
521 this.flushVideoFile(this.currentVideoFile, true) 513 this.flushVideoFile(this.currentVideoFile, true)
522 this.torrent = null 514 this.torrent = null
@@ -528,7 +520,7 @@ class WebTorrentPlugin extends Plugin {
528 this.player.src = this.savePlayerSrcFunction 520 this.player.src = this.savePlayerSrcFunction
529 this.player.src(httpUrl) 521 this.player.src(httpUrl)
530 522
531 this.changeQuality() 523 this.selectAppropriateResolution(true)
532 524
533 // We changed the source, so reinit captions 525 // We changed the source, so reinit captions
534 this.player.trigger('sourcechange') 526 this.player.trigger('sourcechange')
@@ -601,32 +593,22 @@ class WebTorrentPlugin extends Plugin {
601 } 593 }
602 594
603 private buildQualities () { 595 private buildQualities () {
604 const qualityLevelsPayload = [] 596 const resolutions: PeerTubeResolution[] = this.videoFiles.map(file => ({
605 597 id: file.resolution.id,
606 for (const file of this.videoFiles) { 598 label: this.buildQualityLabel(file),
607 const representation = { 599 height: file.resolution.id,
608 id: file.resolution.id, 600 selected: false,
609 label: this.buildQualityLabel(file), 601 selectCallback: () => this.qualitySwitchCallback(file.resolution.id)
610 height: file.resolution.id, 602 }))
611 _enabled: true 603
612 } 604 resolutions.push({
613 605 id: -1,
614 this.player.qualityLevels().addQualityLevel(representation) 606 label: this.player.localize('Auto'),
615 607 selected: true,
616 qualityLevelsPayload.push({ 608 selectCallback: () => this.qualitySwitchCallback(-1)
617 id: representation.id, 609 })
618 label: representation.label,
619 selected: false
620 })
621 }
622 610
623 const payload: LoadedQualityData = { 611 this.player.peertubeResolutions().add(resolutions)
624 qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d),
625 qualityData: {
626 video: qualityLevelsPayload
627 }
628 }
629 this.player.tech(true).trigger('loadedqualitydata', payload)
630 } 612 }
631 613
632 private buildQualityLabel (file: VideoFile) { 614 private buildQualityLabel (file: VideoFile) {
@@ -641,27 +623,30 @@ class WebTorrentPlugin extends Plugin {
641 623
642 private qualitySwitchCallback (id: number) { 624 private qualitySwitchCallback (id: number) {
643 if (id === -1) { 625 if (id === -1) {
644 if (this.autoResolutionPossible === true) this.enableAutoResolution() 626 if (this.autoResolutionPossible === true) {
627 this.autoResolution = true
628
629 this.selectAppropriateResolution(false)
630 }
631
645 return 632 return
646 } 633 }
647 634
648 this.disableAutoResolution() 635 this.autoResolution = false
649 this.updateResolution(id) 636 this.updateResolution(id)
637 this.selectAppropriateResolution(false)
650 } 638 }
651 639
652 private changeQuality () { 640 private selectAppropriateResolution (byEngine: boolean) {
653 const resolutionId = this.currentVideoFile.resolution.id as number 641 const resolution = this.autoResolution
654 const qualityLevels = this.player.qualityLevels() 642 ? -1
643 : this.getCurrentResolutionId()
655 644
656 if (resolutionId === -1) { 645 const autoResolutionChosen = this.autoResolution
657 qualityLevels.selectedIndex = -1 646 ? this.getCurrentResolutionId()
658 return 647 : undefined
659 }
660 648
661 for (let i = 0; i < qualityLevels.length; i++) { 649 this.player.peertubeResolutions().select({ id: resolution, autoResolutionChosenId: autoResolutionChosen, byEngine })
662 const q = qualityLevels[i]
663 if (q.height === resolutionId) qualityLevels.selectedIndex_ = i
664 }
665 } 650 }
666} 651}
667 652