diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/package.json | 1 | ||||
-rw-r--r-- | client/src/assets/player/p2p-media-loader/hls-plugin.ts | 311 | ||||
-rw-r--r-- | client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | 8 | ||||
-rw-r--r-- | client/src/assets/player/peertube-player-manager.ts | 2 | ||||
-rw-r--r-- | client/src/assets/player/peertube-plugin.ts | 38 | ||||
-rw-r--r-- | client/src/assets/player/peertube-resolutions-plugin.ts | 88 | ||||
-rw-r--r-- | client/src/assets/player/peertube-videojs-typings.ts | 38 | ||||
-rw-r--r-- | client/src/assets/player/videojs-components/resolution-menu-button.ts | 41 | ||||
-rw-r--r-- | client/src/assets/player/videojs-components/resolution-menu-item.ts | 48 | ||||
-rw-r--r-- | client/src/assets/player/videojs-components/settings-menu-item.ts | 2 | ||||
-rw-r--r-- | client/src/assets/player/webtorrent/webtorrent-plugin.ts | 111 | ||||
-rw-r--r-- | client/src/standalone/videos/embed-api.ts | 74 | ||||
-rw-r--r-- | client/src/standalone/videos/test-embed.ts | 2 | ||||
-rw-r--r-- | client/yarn.lock | 8 |
14 files changed, 236 insertions, 536 deletions
diff --git a/client/package.json b/client/package.json index ffe7df4cd..afa0efe13 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -128,7 +128,6 @@ | |||
128 | "typescript": "~4.3.4", | 128 | "typescript": "~4.3.4", |
129 | "video.js": "^7", | 129 | "video.js": "^7", |
130 | "videojs-contextmenu-pt": "^5.4.1", | 130 | "videojs-contextmenu-pt": "^5.4.1", |
131 | "videojs-contrib-quality-levels": "^2.0.9", | ||
132 | "videojs-dock": "^2.0.2", | 131 | "videojs-dock": "^2.0.2", |
133 | "videojs-hotkeys": "^0.2.27", | 132 | "videojs-hotkeys": "^0.2.27", |
134 | "videostream": "~3.2.1", | 133 | "videostream": "~3.2.1", |
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 | ||
4 | import Hlsjs, { ErrorData, HlsConfig, Level, ManifestLoadedData } from 'hls.js' | 4 | import Hlsjs, { ErrorData, HlsConfig, Level, LevelSwitchingData, ManifestParsedData } from 'hls.js' |
5 | import videojs from 'video.js' | 5 | import videojs from 'video.js' |
6 | import { HlsjsConfigHandlerOptions, QualityLevelRepresentation, QualityLevels, VideoJSTechHLS } from '../peertube-videojs-typings' | 6 | import { HlsjsConfigHandlerOptions, PeerTubeResolution, VideoJSTechHLS } from '../peertube-videojs-typings' |
7 | 7 | ||
8 | type ErrorCounts = { | 8 | type 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 @@ | |||
1 | import 'videojs-hotkeys/videojs.hotkeys' | 1 | import 'videojs-hotkeys/videojs.hotkeys' |
2 | import 'videojs-dock' | 2 | import 'videojs-dock' |
3 | import 'videojs-contextmenu-pt' | 3 | import 'videojs-contextmenu-pt' |
4 | import 'videojs-contrib-quality-levels' | ||
5 | import './upnext/end-card' | 4 | import './upnext/end-card' |
6 | import './upnext/upnext-plugin' | 5 | import './upnext/upnext-plugin' |
7 | import './stats/stats-card' | 6 | import './stats/stats-card' |
8 | import './stats/stats-plugin' | 7 | import './stats/stats-plugin' |
9 | import './bezels/bezels-plugin' | 8 | import './bezels/bezels-plugin' |
10 | import './peertube-plugin' | 9 | import './peertube-plugin' |
10 | import './peertube-resolutions-plugin' | ||
11 | import './videojs-components/next-previous-video-button' | 11 | import './videojs-components/next-previous-video-button' |
12 | import './videojs-components/p2p-info-button' | 12 | import './videojs-components/p2p-info-button' |
13 | import './videojs-components/peertube-link-button' | 13 | import './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 @@ | |||
1 | import './videojs-components/settings-menu-button' | ||
2 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
3 | import { timeToInt } from '@shared/core-utils' | 2 | import { timeToInt } from '@shared/core-utils' |
4 | import { | 3 | import { |
@@ -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' |
13 | import { PeerTubePluginOptions, ResolutionUpdateData, UserWatching, VideoJSCaption } from './peertube-videojs-typings' | 12 | import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings' |
14 | import { isMobile } from './utils' | 13 | import { isMobile } from './utils' |
15 | 14 | ||
16 | const Plugin = videojs.getPlugin('plugin') | 15 | const 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 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PeerTubeResolution } from './peertube-videojs-typings' | ||
3 | |||
4 | const Plugin = videojs.getPlugin('plugin') | ||
5 | |||
6 | class 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 | |||
87 | videojs.registerPlugin('peertubeResolutions', PeerTubeResolutionsPlugin) | ||
88 | export { 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 | |||
4 | import { HlsConfig, Level } from 'hls.js' | 1 | import { HlsConfig, Level } from 'hls.js' |
5 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
6 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 3 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
@@ -8,11 +5,12 @@ import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin | |||
8 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' | 5 | import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' |
9 | import { PlayerMode } from './peertube-player-manager' | 6 | import { PlayerMode } from './peertube-player-manager' |
10 | import { PeerTubePlugin } from './peertube-plugin' | 7 | import { PeerTubePlugin } from './peertube-plugin' |
8 | import { PeerTubeResolutionsPlugin } from './peertube-resolutions-plugin' | ||
11 | import { PlaylistPlugin } from './playlist/playlist-plugin' | 9 | import { PlaylistPlugin } from './playlist/playlist-plugin' |
12 | import { EndCardOptions } from './upnext/end-card' | ||
13 | import { StatsCardOptions } from './stats/stats-card' | 10 | import { StatsCardOptions } from './stats/stats-card' |
14 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | ||
15 | import { StatsForNerdsPlugin } from './stats/stats-plugin' | 11 | import { StatsForNerdsPlugin } from './stats/stats-plugin' |
12 | import { EndCardOptions } from './upnext/end-card' | ||
13 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | ||
16 | 14 | ||
17 | declare module 'video.js' { | 15 | declare 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 | ||
72 | type QualityLevelRepresentation = { | 69 | type 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 | |||
85 | type QualityLevels = QualityLevelRepresentation[] & { | ||
86 | selectedIndex: number | ||
87 | selectedIndex_: number | ||
88 | |||
89 | addQualityLevel (representation: QualityLevelRepresentation): void | ||
90 | } | 79 | } |
91 | 80 | ||
92 | type VideoJSCaption = { | 81 | type VideoJSCaption = { |
@@ -131,7 +120,7 @@ type PlaylistPluginOptions = { | |||
131 | 120 | ||
132 | type NextPreviousVideoButtonOptions = { | 121 | type 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 = { | |||
214 | type PlaylistItemOptions = { | 203 | type PlaylistItemOptions = { |
215 | element: VideoPlaylistElement | 204 | element: VideoPlaylistElement |
216 | 205 | ||
217 | onClicked: Function | 206 | onClicked: () => void |
218 | } | 207 | } |
219 | 208 | ||
220 | export { | 209 | export { |
@@ -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 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | |||
3 | import { LoadedQualityData } from '../peertube-videojs-typings' | ||
4 | import { ResolutionMenuItem } from './resolution-menu-item' | 2 | import { ResolutionMenuItem } from './resolution-menu-item' |
5 | 3 | ||
6 | const Menu = videojs.getComponent('Menu') | 4 | const 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 @@ | |||
1 | import videojs from 'video.js' | 1 | import videojs from 'video.js' |
2 | import { AutoResolutionUpdateData, ResolutionUpdateData } from '../peertube-videojs-typings' | ||
3 | 2 | ||
4 | const MenuItem = videojs.getComponent('MenuItem') | 3 | const MenuItem = videojs.getComponent('MenuItem') |
5 | 4 | ||
6 | export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions { | 5 | export 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 | ||
12 | class ResolutionMenuItem extends MenuItem { | 9 | class 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..1a1cd7f1a 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' |
12 | import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' | 12 | import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
13 | import { getRtcConfig, isIOS, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' | 13 | import { getRtcConfig, isIOS, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
14 | import { PeertubeChunkStore } from './peertube-chunk-store' | 14 | import { PeertubeChunkStore } from './peertube-chunk-store' |
15 | import { renderVideo } from './video-renderer' | 15 | import { renderVideo } from './video-renderer' |
@@ -175,11 +175,10 @@ 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 | updateEngineResolution (resolutionId: number, delay = 0) { |
183 | // Remember player state | 182 | // Remember player state |
184 | const currentTime = this.player.currentTime() | 183 | const currentTime = this.player.currentTime() |
185 | const isPaused = this.player.paused() | 184 | const isPaused = this.player.paused() |
@@ -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 () { |
@@ -244,6 +236,22 @@ class WebTorrentPlugin extends Plugin { | |||
244 | return this.currentVideoFile | 236 | return this.currentVideoFile |
245 | } | 237 | } |
246 | 238 | ||
239 | changeQuality (id: number) { | ||
240 | if (id === -1) { | ||
241 | if (this.autoResolutionPossible === true) { | ||
242 | this.autoResolution = true | ||
243 | |||
244 | this.selectAppropriateResolution(false) | ||
245 | } | ||
246 | |||
247 | return | ||
248 | } | ||
249 | |||
250 | this.autoResolution = false | ||
251 | this.updateEngineResolution(id) | ||
252 | this.selectAppropriateResolution(false) | ||
253 | } | ||
254 | |||
247 | private addTorrent ( | 255 | private addTorrent ( |
248 | magnetOrTorrentUrl: string, | 256 | magnetOrTorrentUrl: string, |
249 | previousVideoFile: VideoFile, | 257 | previousVideoFile: VideoFile, |
@@ -466,7 +474,7 @@ class WebTorrentPlugin extends Plugin { | |||
466 | } | 474 | } |
467 | 475 | ||
468 | if (changeResolution === true) { | 476 | if (changeResolution === true) { |
469 | this.updateResolution(file.resolution.id, changeResolutionDelay) | 477 | this.updateEngineResolution(file.resolution.id, changeResolutionDelay) |
470 | 478 | ||
471 | // Wait some seconds in observation of our new resolution | 479 | // Wait some seconds in observation of our new resolution |
472 | this.isAutoResolutionObservation = true | 480 | this.isAutoResolutionObservation = true |
@@ -516,7 +524,7 @@ class WebTorrentPlugin extends Plugin { | |||
516 | private fallbackToHttp (options: PlayOptions, done?: (err?: Error) => void) { | 524 | private fallbackToHttp (options: PlayOptions, done?: (err?: Error) => void) { |
517 | const paused = this.player.paused() | 525 | const paused = this.player.paused() |
518 | 526 | ||
519 | this.disableAutoResolution(true) | 527 | this.disableAutoResolution() |
520 | 528 | ||
521 | this.flushVideoFile(this.currentVideoFile, true) | 529 | this.flushVideoFile(this.currentVideoFile, true) |
522 | this.torrent = null | 530 | this.torrent = null |
@@ -528,7 +536,7 @@ class WebTorrentPlugin extends Plugin { | |||
528 | this.player.src = this.savePlayerSrcFunction | 536 | this.player.src = this.savePlayerSrcFunction |
529 | this.player.src(httpUrl) | 537 | this.player.src(httpUrl) |
530 | 538 | ||
531 | this.changeQuality() | 539 | this.selectAppropriateResolution(true) |
532 | 540 | ||
533 | // We changed the source, so reinit captions | 541 | // We changed the source, so reinit captions |
534 | this.player.trigger('sourcechange') | 542 | this.player.trigger('sourcechange') |
@@ -601,32 +609,22 @@ class WebTorrentPlugin extends Plugin { | |||
601 | } | 609 | } |
602 | 610 | ||
603 | private buildQualities () { | 611 | private buildQualities () { |
604 | const qualityLevelsPayload = [] | 612 | const resolutions: PeerTubeResolution[] = this.videoFiles.map(file => ({ |
605 | 613 | id: file.resolution.id, | |
606 | for (const file of this.videoFiles) { | 614 | label: this.buildQualityLabel(file), |
607 | const representation = { | 615 | height: file.resolution.id, |
608 | id: file.resolution.id, | 616 | selected: false, |
609 | label: this.buildQualityLabel(file), | 617 | selectCallback: () => this.changeQuality(file.resolution.id) |
610 | height: file.resolution.id, | 618 | })) |
611 | _enabled: true | 619 | |
612 | } | 620 | resolutions.push({ |
613 | 621 | id: -1, | |
614 | this.player.qualityLevels().addQualityLevel(representation) | 622 | label: this.player.localize('Auto'), |
615 | 623 | selected: true, | |
616 | qualityLevelsPayload.push({ | 624 | selectCallback: () => this.changeQuality(-1) |
617 | id: representation.id, | 625 | }) |
618 | label: representation.label, | ||
619 | selected: false | ||
620 | }) | ||
621 | } | ||
622 | 626 | ||
623 | const payload: LoadedQualityData = { | 627 | 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 | } | 628 | } |
631 | 629 | ||
632 | private buildQualityLabel (file: VideoFile) { | 630 | private buildQualityLabel (file: VideoFile) { |
@@ -639,29 +637,16 @@ class WebTorrentPlugin extends Plugin { | |||
639 | return label | 637 | return label |
640 | } | 638 | } |
641 | 639 | ||
642 | private qualitySwitchCallback (id: number) { | 640 | private selectAppropriateResolution (byEngine: boolean) { |
643 | if (id === -1) { | 641 | const resolution = this.autoResolution |
644 | if (this.autoResolutionPossible === true) this.enableAutoResolution() | 642 | ? -1 |
645 | return | 643 | : this.getCurrentResolutionId() |
646 | } | ||
647 | |||
648 | this.disableAutoResolution() | ||
649 | this.updateResolution(id) | ||
650 | } | ||
651 | |||
652 | private changeQuality () { | ||
653 | const resolutionId = this.currentVideoFile.resolution.id as number | ||
654 | const qualityLevels = this.player.qualityLevels() | ||
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 | ||
diff --git a/client/src/standalone/videos/embed-api.ts b/client/src/standalone/videos/embed-api.ts index b5c9da431..a28aeeaef 100644 --- a/client/src/standalone/videos/embed-api.ts +++ b/client/src/standalone/videos/embed-api.ts | |||
@@ -64,19 +64,12 @@ export class PeerTubeEmbedApi { | |||
64 | if (this.isWebtorrent()) { | 64 | if (this.isWebtorrent()) { |
65 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionPossible() === false) return | 65 | if (resolutionId === -1 && this.embed.player.webtorrent().isAutoResolutionPossible() === false) return |
66 | 66 | ||
67 | // Auto resolution | 67 | this.embed.player.webtorrent().changeQuality(resolutionId) |
68 | if (resolutionId === -1) { | ||
69 | this.embed.player.webtorrent().enableAutoResolution() | ||
70 | return | ||
71 | } | ||
72 | |||
73 | this.embed.player.webtorrent().disableAutoResolution() | ||
74 | this.embed.player.webtorrent().updateResolution(resolutionId) | ||
75 | 68 | ||
76 | return | 69 | return |
77 | } | 70 | } |
78 | 71 | ||
79 | this.embed.player.p2pMediaLoader().getHLSJS().nextLevel = resolutionId | 72 | this.embed.player.p2pMediaLoader().getHLSJS().currentLevel = resolutionId |
80 | } | 73 | } |
81 | 74 | ||
82 | private getCaptions (): PeerTubeTextTrack[] { | 75 | private getCaptions (): PeerTubeTextTrack[] { |
@@ -139,15 +132,10 @@ export class PeerTubeEmbedApi { | |||
139 | }) | 132 | }) |
140 | 133 | ||
141 | // PeerTube specific capabilities | 134 | // PeerTube specific capabilities |
142 | if (this.isWebtorrent()) { | 135 | this.embed.player.peertubeResolutions().on('resolutionsAdded', () => this.loadResolutions()) |
143 | this.embed.player.webtorrent().on('autoResolutionUpdate', () => this.loadWebTorrentResolutions()) | 136 | this.embed.player.peertubeResolutions().on('resolutionChanged', () => this.loadResolutions()) |
144 | this.embed.player.webtorrent().on('videoFileUpdate', () => this.loadWebTorrentResolutions()) | ||
145 | 137 | ||
146 | this.loadWebTorrentResolutions() | 138 | this.loadResolutions() |
147 | } else { | ||
148 | this.embed.player.p2pMediaLoader().on('resolutionChange', () => this.loadP2PMediaLoaderResolutions()) | ||
149 | this.embed.player.p2pMediaLoader().on('resolutionsLoaded', () => this.loadP2PMediaLoaderResolutions()) | ||
150 | } | ||
151 | 139 | ||
152 | this.embed.player.on('volumechange', () => { | 140 | this.embed.player.on('volumechange', () => { |
153 | this.channel.notify({ | 141 | this.channel.notify({ |
@@ -157,49 +145,15 @@ export class PeerTubeEmbedApi { | |||
157 | }) | 145 | }) |
158 | } | 146 | } |
159 | 147 | ||
160 | private loadWebTorrentResolutions () { | 148 | private loadResolutions () { |
161 | this.resolutions = [] | 149 | this.resolutions = this.embed.player.peertubeResolutions().getResolutions() |
162 | 150 | .map(r => ({ | |
163 | const currentResolutionId = this.embed.player.webtorrent().getCurrentResolutionId() | 151 | id: r.id, |
164 | 152 | label: r.label, | |
165 | for (const videoFile of this.embed.player.webtorrent().videoFiles) { | 153 | active: r.selected, |
166 | let label = videoFile.resolution.label | 154 | width: r.width, |
167 | if (videoFile.fps && videoFile.fps >= 50) { | 155 | height: r.height |
168 | label += videoFile.fps | 156 | })) |
169 | } | ||
170 | |||
171 | this.resolutions.push({ | ||
172 | id: videoFile.resolution.id, | ||
173 | label, | ||
174 | src: videoFile.magnetUri, | ||
175 | active: videoFile.resolution.id === currentResolutionId, | ||
176 | height: videoFile.resolution.id | ||
177 | }) | ||
178 | } | ||
179 | |||
180 | this.channel.notify({ | ||
181 | method: 'resolutionUpdate', | ||
182 | params: this.resolutions | ||
183 | }) | ||
184 | } | ||
185 | |||
186 | private loadP2PMediaLoaderResolutions () { | ||
187 | this.resolutions = [] | ||
188 | |||
189 | const qualityLevels = this.embed.player.qualityLevels() | ||
190 | const currentResolutionId = this.embed.player.qualityLevels().selectedIndex | ||
191 | |||
192 | for (let i = 0; i < qualityLevels.length; i++) { | ||
193 | const level = qualityLevels[i] | ||
194 | |||
195 | this.resolutions.push({ | ||
196 | id: level.id, | ||
197 | label: level.height + 'p', | ||
198 | active: level.id === currentResolutionId, | ||
199 | width: level.width, | ||
200 | height: level.height | ||
201 | }) | ||
202 | } | ||
203 | 157 | ||
204 | this.channel.notify({ | 158 | this.channel.notify({ |
205 | method: 'resolutionUpdate', | 159 | method: 'resolutionUpdate', |
diff --git a/client/src/standalone/videos/test-embed.ts b/client/src/standalone/videos/test-embed.ts index a28a83cc1..18c338a2d 100644 --- a/client/src/standalone/videos/test-embed.ts +++ b/client/src/standalone/videos/test-embed.ts | |||
@@ -86,8 +86,6 @@ window.addEventListener('load', async () => { | |||
86 | captionEl.innerHTML = '' | 86 | captionEl.innerHTML = '' |
87 | 87 | ||
88 | captions.forEach(c => { | 88 | captions.forEach(c => { |
89 | console.log(c) | ||
90 | |||
91 | if (c.mode === 'showing') { | 89 | if (c.mode === 'showing') { |
92 | const itemEl = document.createElement('strong') | 90 | const itemEl = document.createElement('strong') |
93 | itemEl.innerText = `${c.label} (active)` | 91 | itemEl.innerText = `${c.label} (active)` |
diff --git a/client/yarn.lock b/client/yarn.lock index 8f41b3102..508b8ad28 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -12618,14 +12618,6 @@ videojs-contextmenu-pt@^5.4.1: | |||
12618 | global "^4.4.0" | 12618 | global "^4.4.0" |
12619 | video.js "^7.6.0" | 12619 | video.js "^7.6.0" |
12620 | 12620 | ||
12621 | videojs-contrib-quality-levels@^2.0.9: | ||
12622 | version "2.1.0" | ||
12623 | resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.1.0.tgz#046e9e21ed01043f512b83a1916001d552457083" | ||
12624 | integrity sha512-dqGQGbL9AFhucxki7Zh0c3kIhH0PAPcHEh6jUdRyaFCVeOuqnJrOYs/3wNtsokDdBdRf2Du2annpu4Z2XaSZRg== | ||
12625 | dependencies: | ||
12626 | global "^4.3.2" | ||
12627 | video.js "^6 || ^7" | ||
12628 | |||
12629 | videojs-dock@^2.0.2: | 12621 | videojs-dock@^2.0.2: |
12630 | version "2.2.0" | 12622 | version "2.2.0" |
12631 | resolved "https://registry.yarnpkg.com/videojs-dock/-/videojs-dock-2.2.0.tgz#57e4f942da1b8e930c4387fed85942473bc40829" | 12623 | resolved "https://registry.yarnpkg.com/videojs-dock/-/videojs-dock-2.2.0.tgz#57e4f942da1b8e930c4387fed85942473bc40829" |