diff options
author | Chocobozzz <me@florianbigard.com> | 2021-09-06 16:08:59 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-09-10 15:06:57 +0200 |
commit | e367da949bb97c3db8c2f9a28ea09eef93abb2f5 (patch) | |
tree | 49efec8135840525eedeee45fd914b77542534f9 /client/src/assets/player/p2p-media-loader | |
parent | 4d3e611dd2764d1d5d0a7e777312631e1e7005d4 (diff) | |
download | PeerTube-e367da949bb97c3db8c2f9a28ea09eef93abb2f5.tar.gz PeerTube-e367da949bb97c3db8c2f9a28ea09eef93abb2f5.tar.zst PeerTube-e367da949bb97c3db8c2f9a28ea09eef93abb2f5.zip |
Cleanup player quality change
Diffstat (limited to 'client/src/assets/player/p2p-media-loader')
-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 |
2 files changed, 42 insertions, 277 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 | ||
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 | ||