Skip to content

Commit

Permalink
Merge pull request #3125 from video-dev/feature/ll-hls-rendition-repo…
Browse files Browse the repository at this point in the history
…rt-switching

LL-HLS RENDITION-REPORT playlist variant switching
  • Loading branch information
robwalch committed Oct 19, 2020
2 parents 07dfd1b + 7b23412 commit 12d5548
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 63 deletions.
6 changes: 3 additions & 3 deletions docs/API.md
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
20 changes: 10 additions & 10 deletions src/controller/audio-track-controller.ts
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions src/controller/base-playlist-controller.ts
Expand Up @@ -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 {
Expand Down
66 changes: 32 additions & 34 deletions src/controller/level-controller.ts
Expand Up @@ -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);
}
}

Expand Down
35 changes: 23 additions & 12 deletions 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[];
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/types/events.ts
Expand Up @@ -155,6 +155,8 @@ export interface SubtitleTracksUpdatedData {
}

export interface SubtitleTrackSwitchData {
url?: string
type?: MediaPlaylistType | 'main'
id: number
}

Expand Down
4 changes: 4 additions & 0 deletions src/types/level.ts
Expand Up @@ -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] || '';
}
}
8 changes: 7 additions & 1 deletion src/types/media-playlist.ts
Expand Up @@ -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 {
Expand Down
31 changes: 28 additions & 3 deletions tests/unit/controller/subtitle-track-controller.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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 () {
Expand All @@ -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 () {
Expand Down

0 comments on commit 12d5548

Please sign in to comment.