From 7b2341271398c17e1663274a5db0bb9d60255272 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Sat, 17 Oct 2020 12:51:06 -0400 Subject: [PATCH] Use RENDITION-REPORT details to make blocking reload requests when playlist switching --- docs/API.md | 6 +- src/controller/audio-track-controller.ts | 20 +++--- src/controller/base-playlist-controller.ts | 23 +++++++ src/controller/level-controller.ts | 66 +++++++++---------- src/controller/subtitle-track-controller.ts | 35 ++++++---- src/types/events.ts | 2 + src/types/level.ts | 4 ++ src/types/media-playlist.ts | 8 ++- .../controller/subtitle-track-controller.js | 31 ++++++++- 9 files changed, 132 insertions(+), 63 deletions(-) diff --git a/docs/API.md b/docs/API.md index bf39af72bfa..a90939eb1a4 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1354,7 +1354,7 @@ Full list of Events is available below: - `Hls.Events.MANIFEST_PARSED` - fired after manifest has been parsed - data: { levels : [ available quality levels ], firstLevel : index of first quality level appearing in Manifest, audioTracks, subtitleTracks, stats, audio: boolean, video: boolean, altAudio: boolean } - `Hls.Events.LEVEL_SWITCHING` - fired when a level switch is requested - - data: { `level` object (please see [below](#level) for more information) } + - data: { `level` and Level object properties (please see [below](#level) for more information) } - `Hls.Events.LEVEL_SWITCHED` - fired when a level switch is effective - data: { level : id of new level } - `Hls.Events.LEVEL_LOADING` - fired when a level playlist loading starts @@ -1370,7 +1370,7 @@ Full list of Events is available below: - `Hls.Events.AUDIO_TRACKS_UPDATED` - fired to notify that audio track lists has been updated - data: { audioTracks : audioTracks } - `Hls.Events.AUDIO_TRACK_SWITCHING` - fired when an audio track switching is requested - - data: { id : audio track id } + - data: { id : audio track id, type : playlist type ('AUDIO' | 'main'), url : audio track URL } - `Hls.Events.AUDIO_TRACK_SWITCHED` - fired when an audio track switch actually occurs - data: { id : audio track id } - `Hls.Events.AUDIO_TRACK_LOADING` - fired when an audio track loading starts @@ -1380,7 +1380,7 @@ Full list of Events is available below: - `Hls.Events.SUBTITLE_TRACKS_UPDATED` - fired to notify that subtitle track lists has been updated - data: { subtitleTracks : subtitleTracks } - `Hls.Events.SUBTITLE_TRACK_SWITCH` - fired when a subtitle track switch occurs - - data: { id : subtitle track id } + - data: { id : subtitle track id, type? : playlist type ('SUBTITLES' | 'CLOSED-CAPTIONS'), url? : subtitle track URL } - `Hls.Events.SUBTITLE_TRACK_LOADING` - fired when a subtitle track loading starts - data: { url : audio track URL, id : audio track id } - `Hls.Events.SUBTITLE_TRACK_LOADED` - fired when a subtitle track loading finishes diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index 71987f8e42b..7b3cb8cf0e2 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -240,29 +240,29 @@ class AudioTrackController extends BasePlaylistController { } private _setAudioTrack (newId: number): void { + const tracks = this.tracks; // noop on same audio track id as already set - if (this.trackId === newId && this.tracks[this.trackId].details) { + if (this.trackId === newId && tracks[newId]?.details) { return; } // check if level idx is valid - if (newId < 0 || newId >= this.tracks.length) { + if (newId < 0 || newId >= tracks.length) { logger.warn('[audio-track-controller]: Invalid id passed to audio-track controller'); return; } - const audioTrack = this.tracks[newId]; - - logger.log(`[audio-track-controller]: Now switching to audio-track index ${newId}`); - // stopping live reloading timer if any this.clearTimer(); - this.trackId = newId; - const { url, type, id } = audioTrack; + const lastTrack = tracks[this.trackId]; + const track = tracks[newId]; + logger.log(`[audio-track-controller]: Now switching to audio-track index ${newId}`); + this.trackId = newId; + const { url, type, id } = track; this.hls.trigger(Events.AUDIO_TRACK_SWITCHING, { id, type, url }); - // TODO: LL-HLS use RENDITION-REPORT if available - this.loadPlaylist(); + const hlsUrlParameters = this.switchParams(track.url, lastTrack?.details); + this.loadPlaylist(hlsUrlParameters); } private _selectInitialAudioTrack (): void { diff --git a/src/controller/base-playlist-controller.ts b/src/controller/base-playlist-controller.ts index abe6f26359b..e1d886d7652 100644 --- a/src/controller/base-playlist-controller.ts +++ b/src/controller/base-playlist-controller.ts @@ -39,6 +39,29 @@ export default class BasePlaylistController implements NetworkComponentAPI { this.clearTimer(); } + protected switchParams (playlistUri: string, previous?: LevelDetails): HlsUrlParameters | undefined { + const renditionReports = previous?.renditionReports; + if (renditionReports) { + for (let i = 0; i < renditionReports.length; i++) { + const attr = renditionReports[i]; + const uri = '' + attr.URI; + if (uri === playlistUri.substr(-uri.length)) { + const msn = parseInt(attr['LAST-MSN']); + let part = parseInt(attr['LAST-PART']); + if (previous && this.hls.config.lowLatencyMode) { + const currentGoal = Math.min(previous.age - previous.partTarget, previous.targetduration); + if (part !== undefined && currentGoal > previous.partTarget) { + part += 1; + } + } + if (Number.isFinite(msn)) { + return new HlsUrlParameters(msn, Number.isFinite(part) ? part : undefined, HlsSkip.No); + } + } + } + } + } + protected loadPlaylist (hlsUrlParameters?: HlsUrlParameters): void {} protected shouldLoadTrack (track: MediaPlaylist): boolean { diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index b6b15a58cc8..efedaa72042 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -192,40 +192,38 @@ export default class LevelController extends BasePlaylistController { set level (newLevel: number) { const levels = this._levels; - newLevel = Math.min(newLevel, levels.length - 1); - if (this.currentLevelIndex !== newLevel || !levels[newLevel]?.details) { - const hls = this.hls; - // check if level idx is valid - if (newLevel >= 0 && newLevel < levels.length) { - // stopping live reloading timer if any - this.clearTimer(); - if (this.currentLevelIndex !== newLevel) { - const lastLevel = this.currentLevelIndex; - logger.log(`[level-controller]: switching to level ${newLevel} from ${lastLevel}`); - this.currentLevelIndex = newLevel; - hls.trigger(Events.LEVEL_SWITCHING, Object.assign({}, levels[newLevel], { - level: newLevel - })); - } - const level = levels[newLevel]; - const levelDetails = level.details; - - // check if we need to load playlist for this level - if (!levelDetails || levelDetails.live) { - // level not retrieved yet, or live playlist we need to (re)load it - // TODO: LL-HLS use RENDITION-REPORT if available - this.loadPlaylist(); - } - } else { - // invalid level id given, trigger error - hls.trigger(Events.ERROR, { - type: ErrorTypes.OTHER_ERROR, - details: ErrorDetails.LEVEL_SWITCH_ERROR, - level: newLevel, - fatal: false, - reason: 'invalid level idx' - }); - } + if (this.currentLevelIndex === newLevel && levels[newLevel]?.details) { + return; + } + // check if level idx is valid + if (newLevel < 0 || newLevel >= levels.length) { + // invalid level id given, trigger error + this.hls.trigger(Events.ERROR, { + type: ErrorTypes.OTHER_ERROR, + details: ErrorDetails.LEVEL_SWITCH_ERROR, + level: newLevel, + fatal: false, + reason: 'invalid level idx' + }); + return; + } + // stopping live reloading timer if any + this.clearTimer(); + + const lastLevelIndex = this.currentLevelIndex; + const lastLevel = levels[lastLevelIndex]; + const level = levels[newLevel]; + logger.log(`[level-controller]: switching to level ${newLevel} from ${lastLevelIndex}`); + this.currentLevelIndex = newLevel; + this.hls.trigger(Events.LEVEL_SWITCHING, Object.assign({}, level, { + level: newLevel + })); + // check if we need to load playlist for this level + const levelDetails = level.details; + if (!levelDetails || levelDetails.live) { + // level not retrieved yet, or live playlist we need to (re)load it + const hlsUrlParameters = this.switchParams(level.uri, lastLevel?.details); + this.loadPlaylist(hlsUrlParameters); } } diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index 4f12ec6b667..173c000aaef 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -1,16 +1,16 @@ import { Events } from '../events'; import { logger } from '../utils/logger'; import { clearCurrentCues } from '../utils/texttrack-utils'; -import { MediaPlaylist } from '../types/media-playlist'; -import { +import BasePlaylistController from './base-playlist-controller'; +import { HlsUrlParameters } from '../types/level'; +import type Hls from '../hls'; +import type { TrackLoadedData, MediaAttachedData, SubtitleTracksUpdatedData, ManifestParsedData } from '../types/events'; -import BasePlaylistController from './base-playlist-controller'; -import Hls from '../hls'; -import { HlsUrlParameters } from '../types/level'; +import type { MediaPlaylist } from '../types/media-playlist'; class SubtitleTrackController extends BasePlaylistController { private tracks: MediaPlaylist[]; @@ -210,17 +210,28 @@ class SubtitleTrackController extends BasePlaylistController { * Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track. */ private _setSubtitleTrackInternal (newId: number): void { - const { hls, tracks } = this; - if (this.trackId === newId && this.tracks[this.trackId].details || - newId < -1 || newId >= tracks.length) { + const tracks = this.tracks; + // noop on same audio track id as already set or invalid + if ((this.trackId === newId && tracks[newId]?.details) || newId < -1 || newId >= tracks.length) { return; } - this.trackId = newId; + // stopping live reloading timer if any + this.clearTimer(); + + const lastTrack = tracks[this.trackId]; + const track = tracks[newId]; logger.log(`[subtitle-track-controller]: Switching to subtitle track ${newId}`); - hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { id: newId }); - // TODO: LL-HLS use RENDITION-REPORT if available - this.loadPlaylist(); + this.trackId = newId; + if (track) { + const { url, type, id } = track; + this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { id, type, url }); + const hlsUrlParameters = this.switchParams(track.url, lastTrack?.details); + this.loadPlaylist(hlsUrlParameters); + } else { + // switch to -1 + this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { id: newId }); + } } private _onTextTracksChanged (): void { diff --git a/src/types/events.ts b/src/types/events.ts index d43f8f898a7..ee8f946ed1d 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -155,6 +155,8 @@ export interface SubtitleTracksUpdatedData { } export interface SubtitleTrackSwitchData { + url?: string + type?: MediaPlaylistType | 'main' id: number } diff --git a/src/types/level.ts b/src/types/level.ts index 0c2b885b597..7d6f423334a 100644 --- a/src/types/level.ts +++ b/src/types/level.ts @@ -119,4 +119,8 @@ export class Level { get maxBitrate (): number { return Math.max(this.realBitrate, this.bitrate); } + + get uri (): string { + return this.url[this.urlId] || ''; + } } diff --git a/src/types/media-playlist.ts b/src/types/media-playlist.ts index 2657811e807..8a9b5bf7df0 100644 --- a/src/types/media-playlist.ts +++ b/src/types/media-playlist.ts @@ -5,7 +5,13 @@ export interface AudioGroup { codec?: string; } -export type MediaPlaylistType = 'AUDIO' | 'VIDEO' | 'SUBTITLES' | 'CLOSED-CAPTIONS'; +export type AudioPlaylistType = 'AUDIO'; + +export type MainPlaylistType = AudioPlaylistType | 'VIDEO'; + +export type SubtitlePlaylistType = 'SUBTITLES' | 'CLOSED-CAPTIONS'; + +export type MediaPlaylistType = MainPlaylistType | SubtitlePlaylistType; // audioTracks, captions and subtitles returned by `M3U8Parser.parseMasterPlaylistMedia` export interface MediaPlaylist extends LevelParsed { diff --git a/tests/unit/controller/subtitle-track-controller.js b/tests/unit/controller/subtitle-track-controller.js index 963abdb72fc..516b41d75da 100644 --- a/tests/unit/controller/subtitle-track-controller.js +++ b/tests/unit/controller/subtitle-track-controller.js @@ -18,7 +18,32 @@ describe('SubtitleTrackController', function () { videoElement = document.createElement('video'); subtitleTrackController = new SubtitleTrackController(hls); subtitleTrackController.media = videoElement; - subtitleTrackController.tracks = [{ id: 0, url: 'baz', details: { live: false } }, { id: 1, url: 'bar' }, { id: 2, details: { live: true }, url: 'foo' }]; + subtitleTrackController.tracks = [{ + id: 0, + groupId: 'default-text-group', + lang: 'en', + name: 'English', + type: 'SUBTITLES', + url: 'baz', + details: { live: false } + }, + { + id: 1, + groupId: 'default-text-group', + lang: 'en', + name: 'English', + type: 'SUBTITLES', + url: 'bar' + }, + { + id: 2, + groupId: 'default-text-group', + lang: 'en', + name: 'English', + type: 'SUBTITLES', + url: 'foo', + details: { live: true } + }]; const textTrack1 = videoElement.addTextTrack('subtitles', 'English', 'en'); const textTrack2 = videoElement.addTextTrack('subtitles', 'Swedish', 'se'); @@ -95,7 +120,7 @@ describe('SubtitleTrackController', function () { subtitleTrackController.subtitleTrack = 1; expect(triggerSpy).to.have.been.calledTwice; - expect(triggerSpy.firstCall).to.have.been.calledWith('hlsSubtitleTrackSwitch', { id: 1 }); + expect(triggerSpy.firstCall).to.have.been.calledWith('hlsSubtitleTrackSwitch', { id: 1, type: 'SUBTITLES', url: 'bar' }); }); it('should trigger SUBTITLE_TRACK_LOADING if the track has no details', function () { @@ -118,7 +143,7 @@ describe('SubtitleTrackController', function () { subtitleTrackController.subtitleTrack = 0; expect(triggerSpy).to.have.been.calledOnce; - expect(triggerSpy.firstCall).to.have.been.calledWith('hlsSubtitleTrackSwitch', { id: 0 }); + expect(triggerSpy.firstCall).to.have.been.calledWith('hlsSubtitleTrackSwitch', { id: 0, type: 'SUBTITLES', url: 'baz' }); }); it('should trigger SUBTITLE_TRACK_SWITCH if passed -1', function () {