423 lines
19 KiB
ActionScript
423 lines
19 KiB
ActionScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
package org.mangui.hls.loader {
|
|
import flash.events.ErrorEvent;
|
|
import flash.events.Event;
|
|
import flash.events.IOErrorEvent;
|
|
import flash.events.ProgressEvent;
|
|
import flash.events.SecurityErrorEvent;
|
|
import flash.net.URLLoader;
|
|
import flash.net.URLRequest;
|
|
import flash.utils.clearTimeout;
|
|
import flash.utils.getTimer;
|
|
import flash.utils.setTimeout;
|
|
|
|
import org.mangui.hls.constant.HLSLoaderTypes;
|
|
import org.mangui.hls.constant.HLSPlayStates;
|
|
import org.mangui.hls.constant.HLSTypes;
|
|
import org.mangui.hls.event.HLSError;
|
|
import org.mangui.hls.event.HLSEvent;
|
|
import org.mangui.hls.event.HLSLoadMetrics;
|
|
import org.mangui.hls.HLS;
|
|
import org.mangui.hls.HLSSettings;
|
|
import org.mangui.hls.model.Fragment;
|
|
import org.mangui.hls.model.Level;
|
|
import org.mangui.hls.playlist.AltAudioTrack;
|
|
import org.mangui.hls.playlist.DataUri;
|
|
import org.mangui.hls.playlist.Manifest;
|
|
|
|
CONFIG::LOGGING {
|
|
import org.mangui.hls.utils.Log;
|
|
}
|
|
public class LevelLoader {
|
|
/** Reference to the hls framework controller. **/
|
|
private var _hls : HLS;
|
|
/** levels vector. **/
|
|
private var _levels : Vector.<Level>;
|
|
/** Object that fetches the manifest. **/
|
|
private var _urlloader : URLLoader;
|
|
/** Link to the M3U8 file. **/
|
|
private var _url : String;
|
|
/** are all playlists filled ? **/
|
|
private var _canStart : Boolean;
|
|
/** Timeout ID for reloading live playlists. **/
|
|
private var _timeoutID : uint;
|
|
/** Streaming type (live, ondemand). **/
|
|
private var _type : String;
|
|
/** last reload manifest time **/
|
|
private var _reloadPlaylistTimer : uint;
|
|
/** current load level **/
|
|
private var _loadLevel : int;
|
|
/** reference to manifest being loaded **/
|
|
private var _manifestLoading : Manifest;
|
|
/** is this loader closed **/
|
|
private var _closed : Boolean = false;
|
|
/* playlist retry timeout */
|
|
private var _retryTimeout : Number;
|
|
private var _retryCount : int;
|
|
private var _redundantRetryCount:int;
|
|
/* alt audio tracks */
|
|
private var _altAudioTracks : Vector.<AltAudioTrack>;
|
|
/* manifest load metrics */
|
|
private var _metrics : HLSLoadMetrics;
|
|
|
|
/** Setup the loader. **/
|
|
public function LevelLoader(hls : HLS) {
|
|
_hls = hls;
|
|
_hls.addEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
|
|
_hls.addEventListener(HLSEvent.LEVEL_SWITCH, _levelSwitchHandler);
|
|
_levels = new Vector.<Level>();
|
|
}
|
|
|
|
public function dispose() : void {
|
|
_close();
|
|
if(_urlloader) {
|
|
_urlloader.removeEventListener(Event.COMPLETE, _loadCompleteHandler);
|
|
_urlloader.removeEventListener(ProgressEvent.PROGRESS, _loadProgressHandler);
|
|
_urlloader.removeEventListener(IOErrorEvent.IO_ERROR, _errorHandler);
|
|
_urlloader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, _errorHandler);
|
|
_urlloader = null;
|
|
}
|
|
_hls.removeEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
|
|
_hls.removeEventListener(HLSEvent.LEVEL_SWITCH, _levelSwitchHandler);
|
|
}
|
|
|
|
/** Loading failed; return errors. **/
|
|
private function _errorHandler(event : ErrorEvent) : void {
|
|
var txt : String = "Cannot load M3U8: " + event.text;
|
|
var code : int = HLSError.MANIFEST_LOADING_IO_ERROR;
|
|
if (event is SecurityErrorEvent) {
|
|
code = HLSError.MANIFEST_LOADING_CROSSDOMAIN_ERROR;
|
|
txt = "Cannot load M3U8: crossdomain access denied:" + event.text;
|
|
} else if (event is IOErrorEvent && (HLSSettings.manifestLoadMaxRetry == -1 || _retryCount < HLSSettings.manifestLoadMaxRetry)) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("I/O Error while trying to load Playlist, retry in " + _retryTimeout + " ms");
|
|
}
|
|
if(_levels.length) {
|
|
_timeoutID = setTimeout(_loadActiveLevelPlaylist, _retryTimeout);
|
|
} else {
|
|
_timeoutID = setTimeout(_loadManifest, _retryTimeout);
|
|
}
|
|
/* exponential increase of retry timeout, capped to manifestLoadMaxRetryTimeout */
|
|
_retryTimeout = Math.min(HLSSettings.manifestLoadMaxRetryTimeout, 2 * _retryTimeout);
|
|
_retryCount++;
|
|
dispatchHLSEvent(HLSEvent.WARNING,code, txt);
|
|
return;
|
|
} else {
|
|
// if we have redundant streams left for that level, switch to it, otherwise retry primary stream
|
|
if(_loadLevel < _levels.length && _levels[_loadLevel].redundantStreamsNb>0 ) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("max load retry reached, switch to redundant stream");
|
|
}
|
|
// try next redundant stream
|
|
if (_levels[_loadLevel].redundantStreamId < _levels[_loadLevel].redundantStreamsNb) {
|
|
_levels[_loadLevel].redundantStreamId++;
|
|
}
|
|
// retry primary stream if the last redundant stream has failed and HLSSettings.manifestRedundantLoadmaxRetry allows
|
|
else if ((HLSSettings.manifestRedundantLoadmaxRetry > 0 && _redundantRetryCount < HLSSettings.manifestRedundantLoadmaxRetry ) || HLSSettings.manifestRedundantLoadmaxRetry==-1 ) {
|
|
_redundantRetryCount++;
|
|
_levels[_loadLevel].redundantStreamId = 0;
|
|
}
|
|
else {
|
|
dispatchHLSEvent(HLSEvent.ERROR,code, txt);
|
|
return;
|
|
}
|
|
_timeoutID = setTimeout(_loadActiveLevelPlaylist, 0);
|
|
_retryTimeout = 1000;
|
|
_retryCount = 0;
|
|
dispatchHLSEvent(HLSEvent.WARNING,code, txt);
|
|
return;
|
|
} else {
|
|
// if level > 0 and in autolevel, and switch down on level error is activated, trigger LEVEL_LOADING_ABORTED
|
|
if(_loadLevel && _hls.autoLevel && HLSSettings.switchDownOnLevelError) {
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_LOADING_ABORTED, _loadLevel));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
dispatchHLSEvent(HLSEvent.ERROR,code, txt);
|
|
}
|
|
|
|
private function dispatchHLSEvent(event : String,code:int, txt:String):void {
|
|
var hlsError : HLSError = new HLSError(code, _url, txt);
|
|
_hls.dispatchEvent(new HLSEvent(event, hlsError));
|
|
}
|
|
|
|
/** Return the current manifest. **/
|
|
public function get levels() : Vector.<Level> {
|
|
return _levels;
|
|
}
|
|
|
|
/** Return the stream type. **/
|
|
public function get type() : String {
|
|
return _type;
|
|
}
|
|
|
|
public function get altAudioTracks() : Vector.<AltAudioTrack> {
|
|
return _altAudioTracks;
|
|
}
|
|
|
|
/** Load the manifest file. **/
|
|
public function load(url : String) : void {
|
|
if(!_urlloader) {
|
|
//_urlloader = new URLLoader();
|
|
var urlLoaderClass : Class = _hls.URLloader as Class;
|
|
_urlloader = (new urlLoaderClass()) as URLLoader;
|
|
_urlloader.addEventListener(Event.COMPLETE, _loadCompleteHandler);
|
|
_urlloader.addEventListener(ProgressEvent.PROGRESS, _loadProgressHandler);
|
|
_urlloader.addEventListener(IOErrorEvent.IO_ERROR, _errorHandler);
|
|
_urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _errorHandler);
|
|
}
|
|
_close();
|
|
_closed = false;
|
|
_url = url;
|
|
_levels = new Vector.<Level>();
|
|
_canStart = false;
|
|
_reloadPlaylistTimer = getTimer();
|
|
_retryTimeout = 1000;
|
|
_retryCount = 0;
|
|
_redundantRetryCount = 0;
|
|
_altAudioTracks = null;
|
|
_loadManifest();
|
|
}
|
|
|
|
/** loading progress handler, use to determine loading latency **/
|
|
private function _loadProgressHandler(event : Event) : void {
|
|
if(_metrics.loading_begin_time == 0) {
|
|
_metrics.loading_begin_time = getTimer();
|
|
}
|
|
}
|
|
|
|
|
|
/** Manifest loaded; check and parse it **/
|
|
private function _loadCompleteHandler(event : Event) : void {
|
|
_metrics.loading_end_time = getTimer();
|
|
// successful loading, reset retry counter
|
|
_retryTimeout = 1000;
|
|
_retryCount = 0;
|
|
_parseManifest(String(_urlloader.data));
|
|
}
|
|
|
|
/** parse a playlist **/
|
|
private function _parseLevelPlaylist(string : String, url : String, level : int, metrics : HLSLoadMetrics) : void {
|
|
var frags : Vector.<Fragment>;
|
|
if (string != null) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("level " + level + " playlist:\n" + string);
|
|
}
|
|
frags = Manifest.getFragments(string, url, level);
|
|
}
|
|
|
|
if(frags && frags.length) {
|
|
// successful loading, reset retry counter
|
|
_retryTimeout = 1000;
|
|
_retryCount = 0;
|
|
// set fragment and update sequence number range
|
|
_levels[level].updateFragments(frags);
|
|
_levels[level].targetduration = Manifest.getTargetDuration(string);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYLIST_DURATION_UPDATED, _levels[level].duration));
|
|
} else {
|
|
if(HLSSettings.manifestLoadMaxRetry == -1 || _retryCount < HLSSettings.manifestLoadMaxRetry) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("empty level Playlist, retry in " + _retryTimeout + " ms");
|
|
}
|
|
_timeoutID = setTimeout(_loadActiveLevelPlaylist, _retryTimeout);
|
|
/* exponential increase of retry timeout, capped to manifestLoadMaxRetryTimeout */
|
|
_retryTimeout = Math.min(HLSSettings.manifestLoadMaxRetryTimeout, 2 * _retryTimeout);
|
|
_retryCount++;
|
|
return;
|
|
} else {
|
|
var hlsError : HLSError = new HLSError(HLSError.MANIFEST_LOADING_IO_ERROR, _url, "no fragments in playlist");
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
return;
|
|
}
|
|
}
|
|
// Check whether the stream is live or not finished yet
|
|
if (Manifest.hasEndlist(string)) {
|
|
setType(HLSTypes.VOD);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_ENDLIST, level));
|
|
} else {
|
|
setType(HLSTypes.LIVE);
|
|
/* in order to determine playlist reload timer,
|
|
check playback position against playlist duration.
|
|
if we are less than one frag from the edge of a live playlist, reload playlist
|
|
twice quicker to discover new fragments quicker and avoid potential buffer starvation.
|
|
*/
|
|
var _reloadInterval : Number = _levels[level].averageduration;
|
|
if((_levels[level].duration - _hls.position) < _reloadInterval) {
|
|
_reloadInterval /= 2;
|
|
}
|
|
// keep at least 1s between requests, in case last one was really slow
|
|
var timeout : int = Math.max(1000,_reloadPlaylistTimer + 1000*_reloadInterval - getTimer());
|
|
CONFIG::LOGGING {
|
|
Log.debug("Level " + level + " Live Playlist parsing finished: reload in " + timeout + " ms");
|
|
}
|
|
_timeoutID = setTimeout(_loadActiveLevelPlaylist, timeout);
|
|
}
|
|
if (!_canStart) {
|
|
_canStart = ((type == HLSTypes.VOD) && _levels[level].fragments.length > 0) || (_levels[level].fragments.length >= HLSSettings.initialLiveManifestSize);
|
|
CONFIG::LOGGING {
|
|
if(!_canStart && (type == HLSTypes.LIVE)){
|
|
Log.warn("Can not start playback of level, reason: not enough segments " +
|
|
_levels[level].fragments.length + "<" + HLSSettings.initialLiveManifestSize);
|
|
}
|
|
}
|
|
if (_canStart) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("First level filled with necessary amount of fragments, notify event");
|
|
}
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST_LOADED, _levels, _metrics));
|
|
}
|
|
}
|
|
metrics.id = _levels[level].start_seqnum;
|
|
metrics.id2 = _levels[level].end_seqnum;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_LOADED, metrics));
|
|
_manifestLoading = null;
|
|
}
|
|
|
|
/** Parse First Level Playlist **/
|
|
private function _parseManifest(string : String) : void {
|
|
var errorTxt : String = null;
|
|
// Check for M3U8 playlist or manifest.
|
|
if (string.indexOf(Manifest.HEADER) == 0) {
|
|
// 1 level playlist, create unique level and parse playlist
|
|
if (string.indexOf(Manifest.FRAGMENT) > 0) {
|
|
var level : Level = new Level();
|
|
level.urls = new Vector.<String>();
|
|
level.urls.push(_url);
|
|
_levels.push(level);
|
|
_metrics.parsing_end_time = getTimer();
|
|
CONFIG::LOGGING {
|
|
Log.debug("1 Level Playlist, load it");
|
|
}
|
|
_parseLevelPlaylist(string, _url, 0,_metrics);
|
|
_loadLevel = 0;
|
|
_metrics.type = HLSLoaderTypes.LEVEL_MAIN;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST_PARSED, _levels));
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_LOADING, 0));
|
|
} else if (string.indexOf(Manifest.LEVEL) > 0) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("adaptive playlist:\n" + string);
|
|
}
|
|
// adaptative playlist, extract levels from playlist, get them and parse them
|
|
_levels = Manifest.extractLevels(string, _url);
|
|
if (_levels.length) {
|
|
_metrics.parsing_end_time = getTimer();
|
|
_loadLevel = -1;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST_PARSED, _levels));
|
|
if (string.indexOf(Manifest.ALTERNATE_AUDIO) > 0) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("alternate audio level found");
|
|
}
|
|
// parse alternate audio tracks
|
|
_altAudioTracks = Manifest.extractAltAudioTracks(string, _url);
|
|
CONFIG::LOGGING {
|
|
if (_altAudioTracks.length > 0) {
|
|
Log.debug(_altAudioTracks.length + " alternate audio tracks found");
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
errorTxt = "No level found in Manifest";
|
|
}
|
|
} else {
|
|
// manifest start with correct header, but it does not contain any fragment or level info ...
|
|
errorTxt = "empty Manifest";
|
|
}
|
|
} else {
|
|
errorTxt = "Manifest is not a valid M3U8 file";
|
|
}
|
|
if(errorTxt) {
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, new HLSError(HLSError.MANIFEST_PARSING_ERROR, _url, errorTxt)));
|
|
}
|
|
}
|
|
|
|
/** load/reload manifest **/
|
|
private function _loadManifest() : void {
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.MANIFEST_LOADING, _url));
|
|
_metrics = new HLSLoadMetrics(HLSLoaderTypes.MANIFEST);
|
|
_metrics.loading_request_time = getTimer();
|
|
if (DataUri.isDataUri(_url)) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("Identified manifest <" + _url + "> as a data URI.");
|
|
}
|
|
_metrics.loading_begin_time = getTimer();
|
|
var data : String = new DataUri(_url).extractData();
|
|
_metrics.loading_end_time = getTimer();
|
|
_parseManifest(data || "");
|
|
} else {
|
|
_urlloader.load(new URLRequest(_url));
|
|
}
|
|
}
|
|
|
|
/** load/reload active M3U8 playlist **/
|
|
private function _loadActiveLevelPlaylist() : void {
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
_reloadPlaylistTimer = getTimer();
|
|
// load active M3U8 playlist only
|
|
_manifestLoading = new Manifest();
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_LOADING, _loadLevel));
|
|
_manifestLoading.loadPlaylist(_hls,_levels[_loadLevel].url, _parseLevelPlaylist, _errorHandler, _loadLevel, _type, HLSSettings.flushLiveURLCache);
|
|
}
|
|
|
|
/** When level switch occurs, assess the need of (re)loading new level playlist **/
|
|
private function _levelSwitchHandler(event : HLSEvent) : void {
|
|
if (_loadLevel != event.level || _levels[_loadLevel].fragments.length == 0) {
|
|
_loadLevel = event.level;
|
|
CONFIG::LOGGING {
|
|
Log.debug("switch to level " + _loadLevel);
|
|
}
|
|
if (type == HLSTypes.LIVE || _levels[_loadLevel].fragments.length == 0) {
|
|
_closed = false;
|
|
CONFIG::LOGGING {
|
|
Log.debug("(re)load Playlist");
|
|
}
|
|
if(_manifestLoading) {
|
|
_manifestLoading.close();
|
|
_manifestLoading = null;
|
|
}
|
|
clearTimeout(_timeoutID);
|
|
_timeoutID = setTimeout(_loadActiveLevelPlaylist, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function _close() : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("cancel any manifest load in progress");
|
|
}
|
|
_closed = true;
|
|
clearTimeout(_timeoutID);
|
|
try {
|
|
_urlloader.close();
|
|
if (_manifestLoading) {
|
|
_manifestLoading.close();
|
|
}
|
|
} catch(e : Error) {
|
|
}
|
|
}
|
|
|
|
/** When the framework idles out, stop reloading manifest **/
|
|
private function _stateHandler(event : HLSEvent) : void {
|
|
if (event.state == HLSPlayStates.IDLE) {
|
|
_close();
|
|
}
|
|
}
|
|
|
|
private function setType(value: String):void{
|
|
if(value != _type){
|
|
_type = value;
|
|
|
|
CONFIG::LOGGING {
|
|
Log.debug("Stream type did change to " + value);
|
|
}
|
|
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.STREAM_TYPE_DID_CHANGE, _type));
|
|
}
|
|
}
|
|
}
|
|
}
|