diff --git a/cmd/mjpeg-player/hlsjs/loader/playlist-loader.js b/cmd/mjpeg-player/hlsjs/loader/playlist-loader.js index cd8c6c2b..5c57f542 100644 --- a/cmd/mjpeg-player/hlsjs/loader/playlist-loader.js +++ b/cmd/mjpeg-player/hlsjs/loader/playlist-loader.js @@ -1,71 +1,72 @@ -/** - * PlaylistLoader - delegate for media manifest/playlist loading tasks. Takes care of parsing media to internal data-models. - * - * Once loaded, dispatches events with parsed data-models of manifest/levels/audio/subtitle tracks. - * - * Uses loader(s) set in config to do actual internal loading of resource tasks. - * - * @module - * - */ +/* +AUTHOR + Trek Hopton -import Event from '../events'; -import EventHandler from '../event-handler'; -import { ErrorTypes, ErrorDetails } from '../errors'; -import { logger } from '../utils/logger'; -import { Loader, PlaylistContextType, PlaylistLoaderContext, PlaylistLevelType, LoaderCallbacks, LoaderResponse, LoaderStats, LoaderConfiguration } from '../types/loader'; -import MP4Demuxer from '../demux/mp4demuxer'; -import M3U8Parser from './m3u8-parser'; -import { AudioGroup } from '../types/media-playlist'; +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. +*/ + +import { PlaylistContextType, PlaylistLevelType } from '../types/loader.js'; +import Event from '../events.js'; +import EventHandler from '../event-handler.js'; +import M3U8Parser from './m3u8-parser.js'; const { performance } = window; -/** - * @constructor - */ class PlaylistLoader extends EventHandler { - private loaders: Partial>> = {}; - - /** - * @constructs - * @param {Hls} hls - */ - constructor (hls) { + constructor(hls) { super(hls, Event.MANIFEST_LOADING, Event.LEVEL_LOADING, Event.AUDIO_TRACK_LOADING, Event.SUBTITLE_TRACK_LOADING); + this.hls = hls; + this.loaders = {}; } /** - * @param {PlaylistContextType} type - * @returns {boolean} - */ - static canHaveQualityLevels (type: PlaylistContextType): boolean { + * @param {PlaylistContextType} type + * @returns {boolean} + */ + static canHaveQualityLevels(type) { return (type !== PlaylistContextType.AUDIO_TRACK && type !== PlaylistContextType.SUBTITLE_TRACK); } /** - * Map context.type to LevelType - * @param {PlaylistLoaderContext} context - * @returns {LevelType} - */ - static mapContextToLevelType (context: PlaylistLoaderContext): PlaylistLevelType { + * 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; + case PlaylistContextType.AUDIO_TRACK: + return PlaylistLevelType.AUDIO; + case PlaylistContextType.SUBTITLE_TRACK: + return PlaylistLevelType.SUBTITLE; + default: + return PlaylistLevelType.MAIN; } } - static getResponseUrl (response: LoaderResponse, context: PlaylistLoaderContext): string { + static getResponseUrl(response, context) { 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) @@ -82,7 +83,7 @@ class PlaylistLoader extends EventHandler { * @param {PlaylistLoaderContext} context * @returns {Loader} or other compatible configured overload */ - createInternalLoader (context: PlaylistLoaderContext): Loader { + createInternalLoader(context) { const config = this.hls.config; const PLoader = config.pLoader; const Loader = config.loader; @@ -98,37 +99,17 @@ class PlaylistLoader extends EventHandler { return loader; } - getInternalLoader (context: PlaylistLoaderContext): Loader | undefined { + getInternalLoader(context) { return this.loaders[context.type]; } - resetInternalLoader (contextType: PlaylistContextType) { + resetInternalLoader(contextType) { if (this.loaders[contextType]) { delete this.loaders[contextType]; } } - /** - * Call `destroy` on all internal loader instances mapped (one per context type) - */ - destroyInternalLoaders () { - for (let contextType in this.loaders) { - let loader = this.loaders[contextType]; - if (loader) { - loader.destroy(); - } - - this.resetInternalLoader(contextType as PlaylistContextType); - } - } - - destroy () { - this.destroyInternalLoaders(); - - super.destroy(); - } - - onManifestLoading (data: { url: string; }) { + onManifestLoading(data) { this.load({ url: data.url, type: PlaylistContextType.MANIFEST, @@ -138,7 +119,7 @@ class PlaylistLoader extends EventHandler { }); } - onLevelLoading (data: { url: string; level: number | null; id: number | null; }) { + onLevelLoading(data) { this.load({ url: data.url, type: PlaylistContextType.LEVEL, @@ -148,7 +129,7 @@ class PlaylistLoader extends EventHandler { }); } - onAudioTrackLoading (data: { url: string; id: number | null; }) { + onAudioTrackLoading(data) { this.load({ url: data.url, type: PlaylistContextType.AUDIO_TRACK, @@ -158,7 +139,7 @@ class PlaylistLoader extends EventHandler { }); } - onSubtitleTrackLoading (data: { url: string; id: number | null; }) { + onSubtitleTrackLoading(data) { this.load({ url: data.url, type: PlaylistContextType.SUBTITLE_TRACK, @@ -168,76 +149,72 @@ class PlaylistLoader extends EventHandler { }); } - load (context: PlaylistLoaderContext): boolean { + load(context) { const config = this.hls.config; - logger.debug(`Loading playlist of type ${context.type}, level: ${context.level}, id: ${context.id}`); - // 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 - logger.trace('playlist request ongoing'); return false; } else { - logger.warn(`aborting previous loader for type: ${context.type}`); + console.warn(`aborting previous loader for type: ${context.type}`); loader.abort(); } } - let maxRetry: number; - let timeout: number; - let retryDelay: number; - let maxRetryDelay: number; + let maxRetry; + let timeout; + let retryDelay; + let maxRetryDelay; // apply different configs for retries depending on // context (manifest, level, audio/subs playlist) switch (context.type) { - 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; + 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; } loader = this.createInternalLoader(context); - const loaderConfig: LoaderConfiguration = { + const loaderConfig = { timeout, maxRetry, retryDelay, maxRetryDelay }; - const loaderCallbacks: LoaderCallbacks = { + const loaderCallbacks = { onSuccess: this.loadsuccess.bind(this), onError: this.loaderror.bind(this), onTimeout: this.loadtimeout.bind(this) }; - logger.debug(`Calling internal loader delegate for URL: ${context.url}`); loader.load(context, loaderConfig, loaderCallbacks); return true; } - loadsuccess (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: unknown = null) { + loadsuccess(response, stats, context, networkDetails = null) { if (context.isSidxRequest) { this._handleSidxRequest(response, context); this._handlePlaylistLoaded(response, stats, context, networkDetails); @@ -252,11 +229,10 @@ class PlaylistLoader extends EventHandler { const string = response.data; stats.tload = performance.now(); - // stats.mtime = new Date(target.getResponseHeader('Last-Modified')); // Validate if it is an M3U8 at all if (string.indexOf('#EXTM3U') !== 0) { - this._handleManifestParsingError(response, context, 'no EXTM3U delimiter', networkDetails); + console.error("no EXTM3U delimiter"); return; } @@ -264,77 +240,21 @@ class PlaylistLoader extends EventHandler { if (string.indexOf('#EXTINF:') > 0 || string.indexOf('#EXT-X-TARGETDURATION:') > 0) { this._handleTrackOrLevelPlaylist(response, stats, context, networkDetails); } else { - this._handleMasterPlaylist(response, stats, context, networkDetails); - } - } - - loaderror (response: LoaderResponse, context: PlaylistLoaderContext, networkDetails = null) { - this._handleNetworkError(context, networkDetails, false, response); - } - - loadtimeout (stats: LoaderStats, context: PlaylistLoaderContext, networkDetails = null) { - this._handleNetworkError(context, networkDetails, true); - } - - // TODO(typescript-config): networkDetails can currently be a XHR or Fetch impl, - // but with custom loaders it could be generic investigate this further when config is typed - _handleMasterPlaylist (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: unknown) { - const hls = this.hls; - const string = response.data as string; - - const url = PlaylistLoader.getResponseUrl(response, context); - const levels = M3U8Parser.parseMasterPlaylist(string, url); - if (!levels.length) { - this._handleManifestParsingError(response, context, 'no level found in manifest', networkDetails); - return; + console.log("handling of master playlists is not implemented"); + // this._handleMasterPlaylist(response, stats, context, networkDetails); } - // multi level playlist, parse level info - const audioGroups: Array = levels.map(level => ({ - id: level.attrs.AUDIO, - codec: level.audioCodec - })); - - const audioTracks = M3U8Parser.parseMasterPlaylistMedia(string, url, 'AUDIO', audioGroups); - const subtitles = M3U8Parser.parseMasterPlaylistMedia(string, url, 'SUBTITLES'); - - if (audioTracks.length) { - // check if we have found an audio track embedded in main playlist (audio track without URI attribute) - let embeddedAudioFound = false; - audioTracks.forEach(audioTrack => { - if (!audioTrack.url) { - embeddedAudioFound = true; - } - }); - - // if no embedded audio track defined, but audio codec signaled in quality level, - // we need to signal this main audio track this could happen with playlists with - // alt audio rendition in which quality levels (main) - // contains both audio+video. but with mixed audio track not signaled - if (embeddedAudioFound === false && levels[0].audioCodec && !levels[0].attrs.AUDIO) { - logger.log('audio codec signaled in quality level, but no embedded audio track signaled, create one'); - audioTracks.unshift({ - type: 'main', - name: 'main', - default: false, - autoselect: false, - forced: false, - id: -1 - }); - } - } - - hls.trigger(Event.MANIFEST_LOADED, { - levels, - audioTracks, - subtitles, - url, - stats, - networkDetails - }); } - _handleTrackOrLevelPlaylist (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: unknown) { + loaderror(response, context, networkDetails = null) { + console.error("network error while loading", response); + } + + loadtimeout(stats, context, networkDetails = null) { + console.error("network timeout while loading", stats); + } + + _handleTrackOrLevelPlaylist(response, stats, context, networkDetails) { const hls = this.hls; const { id, level, type } = context; @@ -342,15 +262,15 @@ class PlaylistLoader extends EventHandler { const url = PlaylistLoader.getResponseUrl(response, context); // if the values are null, they will result in the else conditional - const levelUrlId = Number.isFinite(id as number) ? id as number : 0; - const levelId = Number.isFinite(level as number) ? level as number : levelUrlId; + const levelUrlId = Number.isFinite(id) ? id : 0; + const levelId = Number.isFinite(level) ? level : levelUrlId; const levelType = PlaylistLoader.mapContextToLevelType(context); - const levelDetails = M3U8Parser.parseLevelPlaylist(response.data as string, url, levelId, levelType, levelUrlId); + const levelDetails = M3U8Parser.parseLevelPlaylist(response.data, url, levelId, levelType, levelUrlId); // set stats on level structure // TODO(jstackhouse): why? mixing concerns, is it just treated as value bag? - (levelDetails as any).tload = stats.tload; + (levelDetails).tload = stats.tload; // We have done our first request (Manifest-type) and receive // not a master playlist but a chunk-list (track/level) @@ -374,124 +294,17 @@ class PlaylistLoader extends EventHandler { // save parsing time stats.tparsed = performance.now(); - // in case we need SIDX ranges - // return early after calling load for - // the SIDX box. - if (levelDetails.needSidxRanges) { - const sidxUrl = levelDetails.initSegment.url; - this.load({ - url: sidxUrl, - isSidxRequest: true, - type, - level, - levelDetails, - id, - rangeStart: 0, - rangeEnd: 2048, - responseType: 'arraybuffer' - }); - return; - } - // extend the context with the new levelDetails property context.levelDetails = levelDetails; this._handlePlaylistLoaded(response, stats, context, networkDetails); } - _handleSidxRequest (response: LoaderResponse, context: PlaylistLoaderContext) { - if (typeof response.data === 'string') { - throw new Error('sidx request must be made with responseType of array buffer'); - } - - const sidxInfo = MP4Demuxer.parseSegmentIndex(new Uint8Array(response.data)); - // if provided fragment does not contain sidx, early return - if (!sidxInfo) { - return; - } - const sidxReferences = sidxInfo.references; - const levelDetails = context.levelDetails; - sidxReferences.forEach((segmentRef, index) => { - const segRefInfo = segmentRef.info; - if (!levelDetails) { - return; - } - const frag = levelDetails.fragments[index]; - if (frag.byteRange.length === 0) { - frag.setByteRange(String(1 + segRefInfo.end - segRefInfo.start) + '@' + String(segRefInfo.start)); - } - }); - - if (levelDetails) { - levelDetails.initSegment.setByteRange(String(sidxInfo.moovEndOffset) + '@0'); - } - } - - _handleManifestParsingError (response: LoaderResponse, context: PlaylistLoaderContext, reason: string, networkDetails: unknown) { - this.hls.trigger(Event.ERROR, { - type: ErrorTypes.NETWORK_ERROR, - details: ErrorDetails.MANIFEST_PARSING_ERROR, - fatal: true, - url: response.url, - reason, - networkDetails - }); - } - - _handleNetworkError (context: PlaylistLoaderContext, networkDetails: unknown, timeout: boolean = false, response: LoaderResponse | null = null) { - logger.info(`A network error occured while loading a ${context.type}-type playlist`); - - let details; - let fatal; - - const loader = this.getInternalLoader(context); - - switch (context.type) { - case PlaylistContextType.MANIFEST: - details = (timeout ? ErrorDetails.MANIFEST_LOAD_TIMEOUT : ErrorDetails.MANIFEST_LOAD_ERROR); - fatal = true; - break; - case PlaylistContextType.LEVEL: - details = (timeout ? ErrorDetails.LEVEL_LOAD_TIMEOUT : ErrorDetails.LEVEL_LOAD_ERROR); - fatal = false; - break; - case PlaylistContextType.AUDIO_TRACK: - details = (timeout ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT : ErrorDetails.AUDIO_TRACK_LOAD_ERROR); - fatal = false; - break; - default: - // details = ...? - fatal = false; - } - - if (loader) { - loader.abort(); - this.resetInternalLoader(context.type); - } - - // TODO(typescript-events): when error events are handled, type this - let errorData: any = { - type: ErrorTypes.NETWORK_ERROR, - details, - fatal, - url: context.url, - loader, - context, - networkDetails - }; - - if (response) { - errorData.response = response; - } - - this.hls.trigger(Event.ERROR, errorData); - } - - _handlePlaylistLoaded (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: unknown) { + _handlePlaylistLoaded(response, stats, context, networkDetails) { const { type, level, id, levelDetails } = context; if (!levelDetails || !levelDetails.targetduration) { - this._handleManifestParsingError(response, context, 'invalid target duration', networkDetails); + console.error("manifest parsing error"); return; } @@ -506,22 +319,22 @@ class PlaylistLoader extends EventHandler { }); } else { switch (type) { - 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; + 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; } } }