2020-01-20 12:35:23 +03:00
|
|
|
/*
|
|
|
|
AUTHOR
|
|
|
|
Trek Hopton <trek@ausocean.org>
|
|
|
|
|
|
|
|
LICENSE
|
|
|
|
This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
|
|
|
|
|
|
|
|
It is free software: you can redistribute it and/or modify them
|
|
|
|
under the terms of the GNU General Public License as published by the
|
|
|
|
Free Software Foundation, either version 3 of the License, or (at your
|
|
|
|
option) any later version.
|
|
|
|
|
|
|
|
It is distributed in the hope that it will be useful, but WITHOUT
|
|
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
|
|
for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License in gpl.txt.
|
|
|
|
If not, see http://www.gnu.org/licenses.
|
|
|
|
|
|
|
|
For hls.js Copyright notice and license, see LICENSE file.
|
|
|
|
*/
|
2020-01-02 11:06:52 +03:00
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
import { PlaylistContextType, PlaylistLevelType } from '../types/loader.js';
|
|
|
|
import Event from '../events.js';
|
|
|
|
import EventHandler from '../event-handler.js';
|
|
|
|
import M3U8Parser from './m3u8-parser.js';
|
2020-01-02 11:06:52 +03:00
|
|
|
|
|
|
|
const { performance } = window;
|
|
|
|
|
|
|
|
class PlaylistLoader extends EventHandler {
|
2020-01-20 12:35:23 +03:00
|
|
|
constructor(hls) {
|
2020-01-02 11:06:52 +03:00
|
|
|
super(hls,
|
|
|
|
Event.MANIFEST_LOADING,
|
|
|
|
Event.LEVEL_LOADING,
|
|
|
|
Event.AUDIO_TRACK_LOADING,
|
|
|
|
Event.SUBTITLE_TRACK_LOADING);
|
2020-01-20 12:35:23 +03:00
|
|
|
this.hls = hls;
|
|
|
|
this.loaders = {};
|
2020-01-02 11:06:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-01-20 12:35:23 +03:00
|
|
|
* @param {PlaylistContextType} type
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
static canHaveQualityLevels(type) {
|
2020-01-02 11:06:52 +03:00
|
|
|
return (type !== PlaylistContextType.AUDIO_TRACK &&
|
|
|
|
type !== PlaylistContextType.SUBTITLE_TRACK);
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
onManifestLoading(data) {
|
|
|
|
this.load({
|
|
|
|
url: data.url,
|
|
|
|
type: PlaylistContextType.MANIFEST,
|
|
|
|
level: 0,
|
|
|
|
id: null,
|
|
|
|
responseType: 'text'
|
|
|
|
});
|
|
|
|
}
|
2020-01-02 11:06:52 +03:00
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
// param 1 -> data: { url: string; level: number | null; id: number | null; }
|
|
|
|
onLevelLoading(data) {
|
|
|
|
this.load({
|
|
|
|
url: data.url,
|
|
|
|
type: PlaylistContextType.LEVEL,
|
|
|
|
level: data.level,
|
|
|
|
id: data.id,
|
|
|
|
responseType: 'text'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onAudioTrackLoading(data) {
|
|
|
|
this.load({
|
|
|
|
url: data.url,
|
|
|
|
type: PlaylistContextType.AUDIO_TRACK,
|
|
|
|
level: null,
|
|
|
|
id: data.id,
|
|
|
|
responseType: 'text'
|
|
|
|
});
|
2020-01-02 11:06:52 +03:00
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
onSubtitleTrackLoading(data) {
|
|
|
|
this.load({
|
|
|
|
url: data.url,
|
|
|
|
type: PlaylistContextType.SUBTITLE_TRACK,
|
|
|
|
level: null,
|
|
|
|
id: data.id,
|
|
|
|
responseType: 'text'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
static getResponseUrl(response, context) {
|
2020-01-02 11:06:52 +03:00
|
|
|
let url = response.url;
|
|
|
|
// responseURL not supported on some browsers (it is used to detect URL redirection)
|
|
|
|
// data-uri mode also not supported (but no need to detect redirection)
|
|
|
|
if (url === undefined || url.indexOf('data:') === 0) {
|
|
|
|
// fallback to initial URL
|
|
|
|
url = context.url;
|
|
|
|
}
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
/**
|
|
|
|
* Map context.type to LevelType
|
|
|
|
* @param {PlaylistLoaderContext} context
|
|
|
|
* @returns {LevelType}
|
|
|
|
*/
|
|
|
|
static mapContextToLevelType(context) {
|
|
|
|
const { type } = context;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case PlaylistContextType.AUDIO_TRACK:
|
|
|
|
return PlaylistLevelType.AUDIO;
|
|
|
|
case PlaylistContextType.SUBTITLE_TRACK:
|
|
|
|
return PlaylistLevelType.SUBTITLE;
|
|
|
|
default:
|
|
|
|
return PlaylistLevelType.MAIN;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getInternalLoader(context) {
|
|
|
|
return this.loaders[context.type];
|
|
|
|
}
|
|
|
|
|
2020-01-02 11:06:52 +03:00
|
|
|
/**
|
|
|
|
* Returns defaults or configured loader-type overloads (pLoader and loader config params)
|
|
|
|
* Default loader is XHRLoader (see utils)
|
|
|
|
* @param {PlaylistLoaderContext} context
|
|
|
|
* @returns {Loader} or other compatible configured overload
|
|
|
|
*/
|
2020-01-20 12:35:23 +03:00
|
|
|
createInternalLoader(context) {
|
2020-01-02 11:06:52 +03:00
|
|
|
const config = this.hls.config;
|
|
|
|
const PLoader = config.pLoader;
|
|
|
|
const Loader = config.loader;
|
|
|
|
// TODO(typescript-config): Verify once config is typed that InternalLoader always returns a Loader
|
|
|
|
const InternalLoader = PLoader || Loader;
|
|
|
|
|
|
|
|
const loader = new InternalLoader(config);
|
|
|
|
|
|
|
|
// TODO - Do we really need to assign the instance or if the dep has been lost
|
|
|
|
context.loader = loader;
|
|
|
|
this.loaders[context.type] = loader;
|
|
|
|
|
|
|
|
return loader;
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
resetInternalLoader(contextType) {
|
2020-01-02 11:06:52 +03:00
|
|
|
if (this.loaders[contextType]) {
|
|
|
|
delete this.loaders[contextType];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
load(context) {
|
2020-01-02 11:06:52 +03:00
|
|
|
const config = this.hls.config;
|
|
|
|
|
|
|
|
// Check if a loader for this context already exists
|
|
|
|
let loader = this.getInternalLoader(context);
|
|
|
|
if (loader) {
|
|
|
|
const loaderContext = loader.context;
|
|
|
|
if (loaderContext && loaderContext.url === context.url) { // same URL can't overlap
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
loader.abort();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
let maxRetry;
|
|
|
|
let timeout;
|
|
|
|
let retryDelay;
|
|
|
|
let maxRetryDelay;
|
2020-01-02 11:06:52 +03:00
|
|
|
|
|
|
|
// apply different configs for retries depending on
|
|
|
|
// context (manifest, level, audio/subs playlist)
|
|
|
|
switch (context.type) {
|
2020-01-20 12:35:23 +03:00
|
|
|
case PlaylistContextType.MANIFEST:
|
|
|
|
maxRetry = config.manifestLoadingMaxRetry;
|
|
|
|
timeout = config.manifestLoadingTimeOut;
|
|
|
|
retryDelay = config.manifestLoadingRetryDelay;
|
|
|
|
maxRetryDelay = config.manifestLoadingMaxRetryTimeout;
|
|
|
|
break;
|
|
|
|
case PlaylistContextType.LEVEL:
|
|
|
|
// Disable internal loader retry logic, since we are managing retries in Level Controller
|
|
|
|
maxRetry = 0;
|
|
|
|
maxRetryDelay = 0;
|
|
|
|
retryDelay = 0;
|
|
|
|
timeout = config.levelLoadingTimeOut;
|
|
|
|
// TODO Introduce retry settings for audio-track and subtitle-track, it should not use level retry config
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
maxRetry = config.levelLoadingMaxRetry;
|
|
|
|
timeout = config.levelLoadingTimeOut;
|
|
|
|
retryDelay = config.levelLoadingRetryDelay;
|
|
|
|
maxRetryDelay = config.levelLoadingMaxRetryTimeout;
|
|
|
|
break;
|
2020-01-02 11:06:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
loader = this.createInternalLoader(context);
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
const loaderConfig = {
|
2020-01-02 11:06:52 +03:00
|
|
|
timeout,
|
|
|
|
maxRetry,
|
|
|
|
retryDelay,
|
|
|
|
maxRetryDelay
|
|
|
|
};
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
const loaderCallbacks = {
|
2020-01-02 11:06:52 +03:00
|
|
|
onSuccess: this.loadsuccess.bind(this),
|
|
|
|
onError: this.loaderror.bind(this),
|
|
|
|
onTimeout: this.loadtimeout.bind(this)
|
|
|
|
};
|
|
|
|
|
|
|
|
loader.load(context, loaderConfig, loaderCallbacks);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
loadsuccess(response, stats, context, networkDetails = null) {
|
2020-01-02 11:06:52 +03:00
|
|
|
if (context.isSidxRequest) {
|
|
|
|
this._handleSidxRequest(response, context);
|
|
|
|
this._handlePlaylistLoaded(response, stats, context, networkDetails);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.resetInternalLoader(context.type);
|
|
|
|
if (typeof response.data !== 'string') {
|
|
|
|
throw new Error('expected responseType of "text" for PlaylistLoader');
|
|
|
|
}
|
|
|
|
|
|
|
|
const string = response.data;
|
|
|
|
|
|
|
|
stats.tload = performance.now();
|
|
|
|
|
|
|
|
// Validate if it is an M3U8 at all
|
|
|
|
if (string.indexOf('#EXTM3U') !== 0) {
|
2020-01-20 12:35:23 +03:00
|
|
|
console.error("no EXTM3U delimiter");
|
2020-01-02 11:06:52 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if chunk-list or master. handle empty chunk list case (first EXTINF not signaled, but TARGETDURATION present)
|
|
|
|
if (string.indexOf('#EXTINF:') > 0 || string.indexOf('#EXT-X-TARGETDURATION:') > 0) {
|
|
|
|
this._handleTrackOrLevelPlaylist(response, stats, context, networkDetails);
|
|
|
|
} else {
|
2020-01-20 12:35:23 +03:00
|
|
|
console.log("handling of master playlists is not implemented");
|
|
|
|
// this._handleMasterPlaylist(response, stats, context, networkDetails);
|
2020-01-02 11:06:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
loaderror(response, context, networkDetails = null) {
|
|
|
|
console.error("network error while loading", response);
|
2020-01-02 11:06:52 +03:00
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
loadtimeout(stats, context, networkDetails = null) {
|
|
|
|
console.error("network timeout while loading", stats);
|
2020-01-02 11:06:52 +03:00
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
_handleTrackOrLevelPlaylist(response, stats, context, networkDetails) {
|
2020-01-02 11:06:52 +03:00
|
|
|
const hls = this.hls;
|
|
|
|
|
|
|
|
const { id, level, type } = context;
|
|
|
|
|
|
|
|
const url = PlaylistLoader.getResponseUrl(response, context);
|
|
|
|
|
|
|
|
// if the values are null, they will result in the else conditional
|
2020-01-20 12:35:23 +03:00
|
|
|
const levelUrlId = Number.isFinite(id) ? id : 0;
|
|
|
|
const levelId = Number.isFinite(level) ? level : levelUrlId;
|
2020-01-02 11:06:52 +03:00
|
|
|
|
|
|
|
const levelType = PlaylistLoader.mapContextToLevelType(context);
|
2020-01-20 12:35:23 +03:00
|
|
|
const levelDetails = M3U8Parser.parseLevelPlaylist(response.data, url, levelId, levelType, levelUrlId);
|
2020-01-02 11:06:52 +03:00
|
|
|
|
|
|
|
// set stats on level structure
|
|
|
|
// TODO(jstackhouse): why? mixing concerns, is it just treated as value bag?
|
2020-01-20 12:35:23 +03:00
|
|
|
(levelDetails).tload = stats.tload;
|
2020-01-02 11:06:52 +03:00
|
|
|
|
|
|
|
// We have done our first request (Manifest-type) and receive
|
|
|
|
// not a master playlist but a chunk-list (track/level)
|
|
|
|
// We fire the manifest-loaded event anyway with the parsed level-details
|
|
|
|
// by creating a single-level structure for it.
|
|
|
|
if (type === PlaylistContextType.MANIFEST) {
|
|
|
|
const singleLevel = {
|
|
|
|
url,
|
|
|
|
details: levelDetails
|
|
|
|
};
|
|
|
|
|
|
|
|
hls.trigger(Event.MANIFEST_LOADED, {
|
|
|
|
levels: [singleLevel],
|
|
|
|
audioTracks: [],
|
|
|
|
url,
|
|
|
|
stats,
|
|
|
|
networkDetails
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// save parsing time
|
|
|
|
stats.tparsed = performance.now();
|
|
|
|
|
|
|
|
// extend the context with the new levelDetails property
|
|
|
|
context.levelDetails = levelDetails;
|
|
|
|
|
|
|
|
this._handlePlaylistLoaded(response, stats, context, networkDetails);
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:35:23 +03:00
|
|
|
_handlePlaylistLoaded(response, stats, context, networkDetails) {
|
2020-01-02 11:06:52 +03:00
|
|
|
const { type, level, id, levelDetails } = context;
|
|
|
|
|
|
|
|
if (!levelDetails || !levelDetails.targetduration) {
|
2020-01-20 12:35:23 +03:00
|
|
|
console.error("manifest parsing error");
|
2020-01-02 11:06:52 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const canHaveLevels = PlaylistLoader.canHaveQualityLevels(context.type);
|
|
|
|
if (canHaveLevels) {
|
|
|
|
this.hls.trigger(Event.LEVEL_LOADED, {
|
|
|
|
details: levelDetails,
|
|
|
|
level: level || 0,
|
|
|
|
id: id || 0,
|
|
|
|
stats,
|
|
|
|
networkDetails
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
switch (type) {
|
2020-01-20 12:35:23 +03:00
|
|
|
case PlaylistContextType.AUDIO_TRACK:
|
|
|
|
this.hls.trigger(Event.AUDIO_TRACK_LOADED, {
|
|
|
|
details: levelDetails,
|
|
|
|
id,
|
|
|
|
stats,
|
|
|
|
networkDetails
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case PlaylistContextType.SUBTITLE_TRACK:
|
|
|
|
this.hls.trigger(Event.SUBTITLE_TRACK_LOADED, {
|
|
|
|
details: levelDetails,
|
|
|
|
id,
|
|
|
|
stats,
|
|
|
|
networkDetails
|
|
|
|
});
|
|
|
|
break;
|
2020-01-02 11:06:52 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default PlaylistLoader;
|