Skip to content

Commit

Permalink
Add delivery directive for next SN and Part 0 to live playlist reload…
Browse files Browse the repository at this point in the history
… requests when block reload is available
  • Loading branch information
Rob Walch committed Sep 22, 2020
1 parent 75f42df commit 1b5ab5d
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 84 deletions.
4 changes: 3 additions & 1 deletion demo/chart/timeline-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ export class TimelineChart {
const { targetduration, totalduration, url } = details;
const { datasets } = this.chart.data;
// eslint-disable-next-line no-restricted-properties
const levelDataSet = arrayFind(datasets, dataset => dataset.url && dataset.url.toString() === url);
const deliveryDirectivePattern = /[?&]_HLS_(?:msn|part|skip)=[^?&]+/g;
const levelDataSet = arrayFind(datasets, dataset =>
dataset.url?.toString().replace(deliveryDirectivePattern, '') === url.replace(deliveryDirectivePattern, ''));
if (!levelDataSet) {
return;
}
Expand Down
4 changes: 2 additions & 2 deletions demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1468,10 +1468,10 @@ function hideAllTabs () {
$('.demo-tab').hide();
}

function toggleTab (btn) {
function toggleTab (btn, dontHideOpenTabs) {
const tabElId = $(btn).data('tab');
// eslint-disable-next-line no-restricted-globals
const modifierPressed = window.event && (window.event.metaKey || window.event.shiftKey);
const modifierPressed = dontHideOpenTabs || window.event && (window.event.metaKey || window.event.shiftKey);
if (!modifierPressed) {
hideAllTabs();
}
Expand Down
55 changes: 33 additions & 22 deletions src/controller/audio-track-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../types/events';
import { NetworkComponentAPI } from '../types/component-api';
import Hls from '../hls';
import { HlsUrlParameters } from '../types/level';

/**
* @class AudioTrackController
Expand Down Expand Up @@ -142,12 +143,22 @@ class AudioTrackController implements NetworkComponentAPI {
// if current playlist is a live playlist, arm a timer to reload it
if (details.live) {
details.reloaded(curDetails);
const reloadInterval = computeReloadInterval(details, data.stats);
logger.log(`[audio-track-controller]: live audio track ${details.updated ? 'REFRESHED' : 'MISSED'}, reload in ${Math.round(reloadInterval)} ms`);
// Stop reloading if the timer was cleared
if (this.canLoad) {
this.timer = self.setTimeout(() => this._updateTrack(this._trackId), reloadInterval);
if (curDetails) {
logger.log(`[audio-track-controller]: live audio track ${id} ${details.updated ? 'REFRESHED' : 'MISSED'}`);
}
// TODO: Do not use LL-HLS delivery directives if playlist "endSN" is stale
if (details?.canBlockReload && details.endSN && details.updated) {
// Load track with LL-HLS delivery directives
// TODO: LL-HLS Specify latest partial segment
// TODO: LL-HLS enable skip parameter for delta playlists independent of canBlockReload
this._updateTrack(new HlsUrlParameters(details.endSN + 1, 0, false));
return;
}
const reloadInterval = computeReloadInterval(details, data.stats);
logger.log(`[audio-track-controller]: reload live audio track ${id} in ${Math.round(reloadInterval)} ms`);
this.timer = self.setTimeout(() => {
this._updateTrack();
}, reloadInterval);
} else {
// playlist is not live and timer is scheduled: cancel it
this.clearTimer();
Expand All @@ -164,7 +175,7 @@ class AudioTrackController implements NetworkComponentAPI {
public startLoad (): void {
this.canLoad = true;
if (this.timer === null) {
this._updateTrack(this._trackId);
this._updateTrack();
}
}

Expand Down Expand Up @@ -248,7 +259,6 @@ class AudioTrackController implements NetworkComponentAPI {
private _setAudioTrack (newId: number): void {
// noop on same audio track id as already set
if (this._trackId === newId && this.tracks[this._trackId].details) {
logger.debug('[audio-track-controller]: Same id as current audio-track passed, and track details available -> no-op');
return;
}

Expand All @@ -268,7 +278,8 @@ class AudioTrackController implements NetworkComponentAPI {

const { url, type, id } = audioTrack;
this.hls.trigger(Events.AUDIO_TRACK_SWITCHING, { id, type, url });
this._loadTrackDetailsIfNeeded(audioTrack);
// TODO: LL-HLS use RENDITION-REPORT if available
this._updateTrack();
}

private _selectInitialAudioTrack (): void {
Expand Down Expand Up @@ -334,27 +345,27 @@ class AudioTrackController implements NetworkComponentAPI {
return !!url && (!details || details.live);
}

private _loadTrackDetailsIfNeeded (audioTrack: MediaPlaylist): void {
if (this._needsTrackLoading(audioTrack)) {
const { url, id } = audioTrack;
// track not retrieved yet, or live playlist we need to (re)load it
logger.log(`[audio-track-controller]: loading audio-track playlist for id: ${id}`);
this.hls.trigger(Events.AUDIO_TRACK_LOADING, { url, id });
}
}

private _updateTrack (newId: number): void {
private _updateTrack (hlsUrlParameters?: HlsUrlParameters): void {
const newId: number = this._trackId;
// check if level idx is valid
if (newId < 0 || newId >= this.tracks.length) {
return;
}

// stopping live reloading timer if any
this.clearTimer();
this._trackId = newId;
logger.log(`[audio-track-controller]: trying to update audio-track ${newId}`);
logger.log(`[audio-track-controller]: updated audio-track index ${newId}`);
const audioTrack = this.tracks[newId];
this._loadTrackDetailsIfNeeded(audioTrack);
if (this.canLoad && this._needsTrackLoading(audioTrack)) {
const { url, id, details } = audioTrack;
// track not retrieved yet, or live playlist we need to (re)load it
logger.log(`[audio-track-controller]: loading audio-track playlist for id: ${id}`);
this.clearTimer();
this.hls.trigger(Events.AUDIO_TRACK_LOADING, {
url,
id,
deliveryDirectives: hlsUrlParameters || null
});
}
}

private _handleLoadError (): void {
Expand Down
59 changes: 45 additions & 14 deletions src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
FragLoadedData,
ErrorData
} from '../types/events';
import { Level, LevelParsed } from '../types/level';
import { HlsUrlParameters, Level, LevelParsed } from '../types/level';
import { Events } from '../events';
import { logger } from '../utils/logger';
import { ErrorTypes, ErrorDetails } from '../errors';
Expand Down Expand Up @@ -165,7 +165,7 @@ export default class LevelController implements NetworkComponentAPI {
for (let i = 0; i < levels.length; i++) {
if (levels[i].bitrate === bitrateStart) {
this._firstLevel = i;
logger.log(`[level-controller]: manifest loaded,${levels.length} level(s) found, first bitrate:${bitrateStart}`);
logger.log(`[level-controller]: manifest loaded, ${levels.length} level(s) found, first bitrate: ${bitrateStart}`);
break;
}
}
Expand Down Expand Up @@ -216,7 +216,8 @@ export default class LevelController implements NetworkComponentAPI {
// stopping live reloading timer if any
this.clearTimer();
if (this.currentLevelIndex !== newLevel) {
logger.log(`[level-controller]: switching to level ${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
Expand All @@ -228,6 +229,7 @@ export default class LevelController implements NetworkComponentAPI {
// 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.loadLevel();
}
} else {
Expand Down Expand Up @@ -297,6 +299,7 @@ export default class LevelController implements NetworkComponentAPI {

let levelError = false;
let fragmentError = false;
let levelSwitch = true;
let levelIndex;

// try to recover not fatal errors
Expand All @@ -311,6 +314,11 @@ export default class LevelController implements NetworkComponentAPI {
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
// Do not perform level switch if an error occurred using delivery directives
// Attempt to reload level without directives first
if (data.context.deliveryDirectives) {
levelSwitch = false;
}
levelIndex = data.context.level;
levelError = true;
break;
Expand All @@ -321,7 +329,7 @@ export default class LevelController implements NetworkComponentAPI {
}

if (levelIndex !== undefined) {
this.recoverLevel(data, levelIndex, levelError, fragmentError);
this.recoverLevel(data, levelIndex, levelError, fragmentError, levelSwitch);
}
}

Expand All @@ -330,7 +338,7 @@ export default class LevelController implements NetworkComponentAPI {
* If redundant stream is not available, emergency switch down if ABR mode is enabled.
*/
// FIXME Find a better abstraction where fragment/level retry management is well decoupled
private recoverLevel (errorEvent: ErrorData, levelIndex: number, levelError: boolean, fragmentError: boolean): void {
private recoverLevel (errorEvent: ErrorData, levelIndex: number, levelError: boolean, fragmentError: boolean, levelSwitch: boolean): void {
// TODO: Handle levels not set rather than throwing (see other parts of this module throwing the same error)
if (!this._levels) {
throw new Error('Levels are not set');
Expand Down Expand Up @@ -366,7 +374,7 @@ export default class LevelController implements NetworkComponentAPI {

// Try any redundant streams if available for both errors: level and fragment
// If level.loadError reaches redundantLevels it means that we tried them all, no hope => let's switch down
if (levelError || fragmentError) {
if (levelSwitch && (levelError || fragmentError)) {
redundantLevels = level.url.length;

if (redundantLevels > 1 && level.loadError < redundantLevels) {
Expand Down Expand Up @@ -428,8 +436,19 @@ export default class LevelController implements NetworkComponentAPI {
// if current playlist is a live playlist, arm a timer to reload it
if (details.live) {
details.reloaded(curDetails);
if (curDetails) {
logger.log(`[level-controller]: live level ${level} ${details.updated ? 'REFRESHED' : 'MISSED'}`);
}
// TODO: Do not use LL-HLS delivery directives if playlist "endSN" is stale
if (details.canBlockReload && details.endSN && details.updated) {
// Load level with LL-HLS delivery directives
// TODO: LL-HLS Specify latest partial segment
// TODO: LL-HLS enable skip parameter for delta playlists independent of canBlockReload
this.loadLevel(new HlsUrlParameters(details.endSN + 1, 0, false));
return;
}
const reloadInterval = computeReloadInterval(details, data.stats);
logger.log(`[level-controller]: live playlist ${details.updated ? 'REFRESHED' : 'MISSED'}, reload in ${Math.round(reloadInterval)} ms`);
logger.log(`[level-controller]: reload live level ${level} in ${Math.round(reloadInterval)} ms`);
this.timer = self.setTimeout(() => this.loadLevel(), reloadInterval);
} else {
this.clearTimer();
Expand Down Expand Up @@ -459,9 +478,7 @@ export default class LevelController implements NetworkComponentAPI {
}
}

private loadLevel () {
logger.log(`[level-controller]: call to loadLevel (canLoad ${this.canLoad})`);

private loadLevel (hlsUrlParameters?: HlsUrlParameters) {
if (this.currentLevelIndex !== null && this.canLoad) {
if (!this._levels) {
throw new Error('Levels are not set');
Expand All @@ -471,14 +488,28 @@ export default class LevelController implements NetworkComponentAPI {
if (typeof levelObject === 'object' && levelObject.url.length > 0) {
const level = this.currentLevelIndex;
const id = levelObject.urlId;
const url = levelObject.url[id];
let url = levelObject.url[id];
if (hlsUrlParameters) {
try {
url = hlsUrlParameters.addDirectives(url);
} catch (error) {
logger.warn(`[level-controller] Could not construct new URL with HLS Delivery Directives: ${error}`);
}
}

logger.log(`[level-controller]: Attempt loading level index ${level} with URL-id ${id}`);
logger.log(`[level-controller]: Attempt loading level index ${level}${
hlsUrlParameters ? ' at sn ' + hlsUrlParameters.msn : ''
} with URL-id ${id}`);

// console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
// console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level);

this.hls.trigger(Events.LEVEL_LOADING, { url, level, id });
this.clearTimer();
this.hls.trigger(Events.LEVEL_LOADING, {
url,
level,
id,
deliveryDirectives: hlsUrlParameters || null
});
}
}
}
Expand Down
32 changes: 25 additions & 7 deletions src/controller/subtitle-track-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { computeReloadInterval } from './level-helper';
import { clearCurrentCues } from '../utils/texttrack-utils';
import { MediaPlaylist } from '../types/media-playlist';
import { TrackLoadedData, ManifestLoadedData, MediaAttachedData, SubtitleTracksUpdatedData } from '../types/events';
import { ComponentAPI } from '../types/component-api';
import { NetworkComponentAPI } from '../types/component-api';
import Hls from '../hls';
import { HlsUrlParameters } from '../types/level';

class SubtitleTrackController implements ComponentAPI {
class SubtitleTrackController implements NetworkComponentAPI {
private hls: Hls;
private tracks: MediaPlaylist[];
private trackId: number = -1;
Expand Down Expand Up @@ -133,8 +134,19 @@ class SubtitleTrackController implements ComponentAPI {

if (details.live && !this.stopped) {
details.reloaded(curDetails);
if (curDetails) {
logger.log(`[subtitle-track-controller]: live subtitle track ${id} ${details.updated ? 'REFRESHED' : 'MISSED'}`);
}
// TODO: Do not use LL-HLS delivery directives if playlist "endSN" is stale
if (details?.canBlockReload && details.endSN && details.updated) {
// Load track with LL-HLS delivery directives
// TODO: LL-HLS Specify latest partial segment
// TODO: LL-HLS enable skip parameter for delta playlists independent of canBlockReload
this._loadCurrentTrack(new HlsUrlParameters(details.endSN + 1, 0, false));
return;
}
const reloadInterval = computeReloadInterval(details, data.stats);
logger.log(`[subtitle-track-controller]: live subtitle track ${details.updated ? 'REFRESHED' : 'MISSED'}, reload in ${Math.round(reloadInterval)} ms`);
logger.log(`[subtitle-track-controller]: reload live subtitle track ${id} in ${Math.round(reloadInterval)} ms`);
this.timer = self.setTimeout(() => {
this._loadCurrentTrack();
}, reloadInterval);
Expand Down Expand Up @@ -178,14 +190,18 @@ class SubtitleTrackController implements ComponentAPI {
}
}

private _loadCurrentTrack (): void {
const { trackId, tracks, hls } = this;
private _loadCurrentTrack (hlsUrlParameters?: HlsUrlParameters): void {
const { trackId, tracks } = this;
const currentTrack = tracks[trackId];
if (this.stopped || trackId < 0 || !currentTrack || (currentTrack.details && !currentTrack.details.live)) {
return;
}
logger.log(`[subtitle-track-controller]: Loading subtitle track ${trackId}`);
hls.trigger(Events.SUBTITLE_TRACK_LOADING, { url: currentTrack.url, id: trackId });
this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, {
url: currentTrack.url,
id: trackId,
deliveryDirectives: hlsUrlParameters || null
});
}

/**
Expand Down Expand Up @@ -223,13 +239,15 @@ class SubtitleTrackController implements ComponentAPI {
*/
private _setSubtitleTrackInternal (newId: number): void {
const { hls, tracks } = this;
if (!Number.isFinite(newId) || newId < -1 || newId >= tracks.length) {
if (this.trackId === newId && this.tracks[this.trackId].details ||
newId < -1 || newId >= tracks.length) {
return;
}

this.trackId = 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._loadCurrentTrack();
}

Expand Down
Loading

0 comments on commit 1b5ab5d

Please sign in to comment.