1218 lines
64 KiB
ActionScript
1218 lines
64 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.*;
|
|
import flash.net.*;
|
|
import flash.utils.ByteArray;
|
|
import flash.utils.Timer;
|
|
import flash.utils.getTimer;
|
|
|
|
import org.mangui.hls.HLS;
|
|
import org.mangui.hls.HLSSettings;
|
|
import org.mangui.hls.constant.HLSLoaderTypes;
|
|
import org.mangui.hls.constant.HLSTypes;
|
|
import org.mangui.hls.controller.AudioTrackController;
|
|
import org.mangui.hls.controller.LevelController;
|
|
import org.mangui.hls.demux.DemuxHelper;
|
|
import org.mangui.hls.demux.Demuxer;
|
|
import org.mangui.hls.demux.ID3Tag;
|
|
import org.mangui.hls.event.HLSError;
|
|
import org.mangui.hls.event.HLSEvent;
|
|
import org.mangui.hls.event.HLSLoadMetrics;
|
|
import org.mangui.hls.flv.FLVTag;
|
|
import org.mangui.hls.model.AudioTrack;
|
|
import org.mangui.hls.model.Fragment;
|
|
import org.mangui.hls.model.FragmentData;
|
|
import org.mangui.hls.model.Level;
|
|
import org.mangui.hls.stream.StreamBuffer;
|
|
import org.mangui.hls.utils.AES;
|
|
|
|
import com.hurlant.crypto.symmetric.DESKey;
|
|
import com.hurlant.crypto.symmetric.ECBMode;
|
|
import com.hurlant.util.Base64;
|
|
import ckaction.act.des;
|
|
|
|
CONFIG::LOGGING {
|
|
import org.mangui.hls.utils.Log;
|
|
import org.mangui.hls.utils.Hex;
|
|
}
|
|
/** Class that fetches fragments. **/
|
|
public class FragmentLoader {
|
|
/** Reference to the HLS controller. **/
|
|
private var _hls : HLS;
|
|
/** reference to auto level manager */
|
|
private var _levelController : LevelController;
|
|
/** reference to audio track controller */
|
|
private var _audioTrackController : AudioTrackController;
|
|
/** has manifest just being reloaded **/
|
|
private var _manifestJustLoaded : Boolean;
|
|
/** last loaded level. **/
|
|
private var _levelLastLoaded : int;
|
|
/** next level (-1 if not defined yet) **/
|
|
private var _levelNext : int = -1;
|
|
/** Reference to the manifest levels. **/
|
|
private var _levels : Vector.<Level>;
|
|
/** Util for loading the fragment. **/
|
|
private var _fragstreamloader : URLStream;
|
|
/** Util for loading the key. **/
|
|
private var _keystreamloader : URLStream;
|
|
/** key map **/
|
|
private var _keymap : Object;
|
|
/** Did the stream switch quality levels. **/
|
|
private var _switchLevel : Boolean;
|
|
/** Did a discontinuity occurs in the stream **/
|
|
private var _hasDiscontinuity : Boolean;
|
|
/** boolean to track whether PTS analysis is ongoing or not */
|
|
private var _ptsAnalyzing : Boolean = false;
|
|
/** Timer used to monitor/schedule fragment download. **/
|
|
private var _timer : Timer;
|
|
/** requested seek position **/
|
|
private var _seekPosition : Number;
|
|
/** first fragment loaded ? **/
|
|
private var _fragmentFirstLoaded : Boolean;
|
|
/* demux instance */
|
|
private var _demux : Demuxer;
|
|
/* stream buffer instance **/
|
|
private var _streamBuffer : StreamBuffer;
|
|
/* key error/reload */
|
|
private var _keyLoadErrorDate : Number;
|
|
private var _keyRetryTimeout : Number;
|
|
private var _keyRetryCount : int;
|
|
private var _keyLoadStatus : int;
|
|
/* fragment error/reload */
|
|
private var _fragLoadErrorDate : Number;
|
|
private var _fragRetryTimeout : Number;
|
|
private var _fragRetryCount : int;
|
|
private var _fragLoadStatus : int;
|
|
private var _fragSkipping : Boolean;
|
|
private var _fragSkipCount : int;
|
|
/** reference to previous/current fragment */
|
|
private var _fragPrevious : Fragment;
|
|
private var _fragCurrent : Fragment;
|
|
/* loading state variable */
|
|
private var _loadingState : int;
|
|
/* loading metrics */
|
|
private var _metrics : HLSLoadMetrics;
|
|
private static const LOADING_STOPPED : int = -1;
|
|
private static const LOADING_IDLE : int = 0;
|
|
private static const LOADING_IN_PROGRESS : int = 1;
|
|
private static const LOADING_WAITING_LEVEL_UPDATE : int = 2;
|
|
private static const LOADING_STALLED : int = 3;
|
|
private static const LOADING_FRAGMENT_IO_ERROR : int = 4;
|
|
private static const LOADING_KEY_IO_ERROR : int = 5;
|
|
private static const LOADING_COMPLETED : int = 6;
|
|
|
|
/** Create the loader. **/
|
|
public function FragmentLoader(hls : HLS, audioTrackController : AudioTrackController, levelController : LevelController, streamBuffer : StreamBuffer) : void {
|
|
_hls = hls;
|
|
_levelController = levelController;
|
|
_audioTrackController = audioTrackController;
|
|
_streamBuffer = streamBuffer;
|
|
_hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
|
_hls.addEventListener(HLSEvent.LEVEL_LOADED, _levelLoadedHandler);
|
|
_hls.addEventListener(HLSEvent.LEVEL_LOADING_ABORTED, _levelLoadingAbortedHandler);
|
|
_timer = new Timer(20, 0);
|
|
_timer.addEventListener(TimerEvent.TIMER, _checkLoading);
|
|
_loadingState = LOADING_STOPPED;
|
|
_manifestJustLoaded = false;
|
|
_keymap = new Object();
|
|
};
|
|
|
|
public function dispose() : void {
|
|
stop();
|
|
_timer.removeEventListener(TimerEvent.TIMER, _checkLoading);
|
|
_hls.removeEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
|
_hls.removeEventListener(HLSEvent.LEVEL_LOADED, _levelLoadedHandler);
|
|
_hls.removeEventListener(HLSEvent.LEVEL_LOADING_ABORTED, _levelLoadingAbortedHandler);
|
|
_loadingState = LOADING_STOPPED;
|
|
_keymap = new Object();
|
|
}
|
|
|
|
public function get audioExpected() : Boolean {
|
|
if (_demux) {
|
|
return _demux.audioExpected;
|
|
} else {
|
|
// always return true in case demux is not yet initialized
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public function get videoExpected() : Boolean {
|
|
if (_demux) {
|
|
return _demux.videoExpected;
|
|
} else {
|
|
// always return true in case demux is not yet initialized
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/** fragment loading Timer **/
|
|
private function _checkLoading(e : Event) : void {
|
|
switch(_loadingState) {
|
|
// nothing to load, stop fragment loader.
|
|
case LOADING_STOPPED:
|
|
stop();
|
|
break;
|
|
// nothing to load until level is retrieved
|
|
case LOADING_WAITING_LEVEL_UPDATE:
|
|
break;
|
|
// loading already in progress
|
|
case LOADING_IN_PROGRESS:
|
|
/* only monitor fragment loading rate if in auto mode, AND
|
|
we are not loading the first segment AND
|
|
current level is not the lowest level */
|
|
if(_hls.autoLevel && !_manifestJustLoaded && _fragCurrent.level) {
|
|
// monitor fragment load progress after half of expected fragment duration,to stabilize bitrate
|
|
var requestDelay : int = getTimer() - _metrics.loading_request_time;
|
|
var fragDuration : Number = _fragCurrent.duration;
|
|
if(requestDelay > 500*fragDuration) {
|
|
var loaded : int = _fragCurrent.data.bytesLoaded;
|
|
var expected : int = fragDuration*_levels[_fragCurrent.level].bitrate/8;
|
|
if(expected < loaded) {
|
|
expected = loaded;
|
|
}
|
|
var loadRate : int = loaded*1000/requestDelay; // byte/s
|
|
var fragLoadedDelay : Number =(expected-loaded)/loadRate;
|
|
var fragLevel0LoadedDelay : Number = fragDuration*_levels[0].bitrate/(8*loadRate); //bps/Bps
|
|
var bufferLen : Number = _hls.stream.bufferLength;
|
|
// CONFIG::LOGGING {
|
|
// Log.info("bufferLen/fragDuration/fragLoadedDelay/fragLevel0LoadedDelay:" + bufferLen.toFixed(1) + "/" + fragDuration.toFixed(1) + "/" + fragLoadedDelay.toFixed(1) + "/" + fragLevel0LoadedDelay.toFixed(1));
|
|
// }
|
|
/* if we have less than 2 frag duration in buffer and if frag loaded delay is greater than buffer len
|
|
... and also bigger than duration needed to load fragment at next level ...*/
|
|
if(bufferLen < 2*fragDuration && fragLoadedDelay > bufferLen && fragLoadedDelay > fragLevel0LoadedDelay) {
|
|
// try to abort fragment loading ...
|
|
// try to flush last fragment seamlessly
|
|
if(_streamBuffer.flushLastFragment(_fragCurrent.level,_fragCurrent.seqnum)) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("_checkLoading : loading too slow, abort fragment loading");
|
|
Log.warn("fragLoadedDelay/bufferLen/fragLevel0LoadedDelay:" + fragLoadedDelay.toFixed(1) + "/" + bufferLen.toFixed(1) + "/" + fragLevel0LoadedDelay.toFixed(1));
|
|
}
|
|
//abort fragment loading
|
|
_stop_load();
|
|
// fill loadMetrics to please LevelController that will adjust bw for next fragment
|
|
// fill theoritical value, assuming bw will remain as it is
|
|
_metrics.size = expected;
|
|
_metrics.duration = 1000*fragDuration;
|
|
_metrics.loading_end_time = _metrics.parsing_end_time = _metrics.loading_request_time + 1000*expected/loadRate;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_LOAD_EMERGENCY_ABORTED, _metrics));
|
|
_levelNext = _levelController.getnextlevel(_fragCurrent.level, bufferLen);
|
|
// ensure that we really switch down to avoid looping here.
|
|
// _fragCurrent.level is gt 0 in that case, no need to Math.max(0,_levelNext)
|
|
_levelNext = Math.min(_levelNext, _fragCurrent.level-1);
|
|
// switch back to IDLE state to request new fragment at lowest level
|
|
_loadingState = LOADING_IDLE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
// no loading in progress, try to load first/next fragment
|
|
case LOADING_IDLE:
|
|
var level : int;
|
|
// check if first fragment after seek has been already loaded
|
|
if (_fragmentFirstLoaded == false) {
|
|
// select level for first fragment load
|
|
if(_levelNext != -1) {
|
|
level = _levelNext;
|
|
} else if (_hls.autoLevel) {
|
|
if (_manifestJustLoaded) {
|
|
level = _hls.startLevel;
|
|
} else {
|
|
if(_hls.stream.bufferLength) {
|
|
// if buffer not empty, select level from heuristics
|
|
level = _levelController.getnextlevel(_hls.loadLevel, _hls.stream.bufferLength);
|
|
} else {
|
|
// if buffer empty, retrieve seek level
|
|
level = _hls.seekLevel;
|
|
}
|
|
}
|
|
} else {
|
|
level = _hls.manualLevel;
|
|
}
|
|
if (level != _hls.loadLevel) {
|
|
_demux = null;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_SWITCH, level));
|
|
}
|
|
_switchLevel = true;
|
|
|
|
// check if we received playlist for choosen level. if live playlist, ensure that new playlist has been refreshed
|
|
// to avoid loading outdated fragments
|
|
if ((_levels[level].fragments.length == 0) || (_hls.type == HLSTypes.LIVE && _levelLastLoaded != level)) {
|
|
// playlist not yet received
|
|
CONFIG::LOGGING {
|
|
Log.debug("_checkLoading : playlist not received for level:" + level);
|
|
}
|
|
_loadingState = LOADING_WAITING_LEVEL_UPDATE;
|
|
_levelNext = level;
|
|
} else {
|
|
// just after seek, load first fragment
|
|
_loadingState = _loadfirstfragment(_seekPosition, level);
|
|
}
|
|
|
|
/* first fragment already loaded
|
|
* check if we need to load next fragment, do it only if buffer is NOT full
|
|
*/
|
|
} else if (HLSSettings.maxBufferLength == 0 || _hls.stream.bufferLength < HLSSettings.maxBufferLength) {
|
|
// select level for next fragment load
|
|
if(_levelNext != -1) {
|
|
level = _levelNext;
|
|
} else if (_hls.autoLevel && _levels.length > 1 ) {
|
|
// select level from heuristics (current level / last fragment duration / buffer length)
|
|
level = _levelController.getnextlevel(_hls.loadLevel, _hls.stream.bufferLength);
|
|
} else if (_hls.autoLevel && _levels.length == 1 ) {
|
|
level = 0;
|
|
} else {
|
|
level = _hls.manualLevel;
|
|
}
|
|
// notify in case level switch occurs
|
|
if (level != _hls.loadLevel) {
|
|
_switchLevel = true;
|
|
_demux = null;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_SWITCH, level));
|
|
}
|
|
// check if we received playlist for choosen level. if live playlist, ensure that new playlist has been refreshed
|
|
if ((_levels[level].fragments.length == 0) || (_hls.type == HLSTypes.LIVE && _levelLastLoaded != level)) {
|
|
// playlist not yet received
|
|
CONFIG::LOGGING {
|
|
Log.debug("_checkLoading : playlist not received for level:" + level);
|
|
}
|
|
_loadingState = LOADING_WAITING_LEVEL_UPDATE;
|
|
_levelNext = level;
|
|
} else {
|
|
_loadingState = _loadnextfragment(level, _fragPrevious);
|
|
}
|
|
}
|
|
break;
|
|
case LOADING_STALLED:
|
|
/* next consecutive fragment not found:
|
|
it could happen on live playlist :
|
|
- if bandwidth available is lower than lowest quality needed bandwidth
|
|
- after long pause
|
|
*/
|
|
CONFIG::LOGGING {
|
|
Log.warn("loading stalled:stop fragment loading");
|
|
}
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LIVE_LOADING_STALLED));
|
|
stop();
|
|
break;
|
|
// if key loading failed
|
|
case LOADING_KEY_IO_ERROR:
|
|
// compare current date and next retry date.
|
|
if (getTimer() >= _keyLoadErrorDate) {
|
|
/* try to reload the key ...
|
|
calling _loadfragment will also reload key */
|
|
_loadfragment(_fragCurrent);
|
|
_loadingState = LOADING_IN_PROGRESS;
|
|
}
|
|
break;
|
|
// if fragment loading failed
|
|
case LOADING_FRAGMENT_IO_ERROR:
|
|
// compare current date and next retry date.
|
|
if (getTimer() >= _fragLoadErrorDate) {
|
|
/* try to reload fragment ... */
|
|
_loadfragment(_fragCurrent);
|
|
_loadingState = LOADING_IN_PROGRESS;
|
|
}
|
|
break;
|
|
case LOADING_COMPLETED:
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LAST_VOD_FRAGMENT_LOADED));
|
|
// stop fragment loader, as no other fragments can be loaded
|
|
stop();
|
|
break;
|
|
default:
|
|
CONFIG::LOGGING {
|
|
Log.error("invalid loading state:" + _loadingState);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
public function seek(position : Number) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("FragmentLoader:seek(" + position.toFixed(2) + ")");
|
|
}
|
|
// reset IO Error when seeking
|
|
_fragRetryCount = _keyRetryCount = 0;
|
|
_fragRetryTimeout = _keyRetryTimeout = 1000;
|
|
_loadingState = LOADING_IDLE;
|
|
_seekPosition = position;
|
|
_fragmentFirstLoaded = false;
|
|
_fragPrevious = null;
|
|
_fragSkipping = false;
|
|
_fragSkipCount = 0;
|
|
_levelNext = -1;
|
|
_timer.start();
|
|
}
|
|
|
|
public function seekFromLastFrag(lastFrag : Fragment) : void {
|
|
CONFIG::LOGGING {
|
|
Log.info("FragmentLoader:seekFromLastFrag(level:" + lastFrag.level + ",SN:" + lastFrag.seqnum + ",PTS:" + lastFrag.data.pts_start +")");
|
|
}
|
|
// reset IO Error when seeking
|
|
_fragRetryCount = _keyRetryCount = 0;
|
|
_fragRetryTimeout = _keyRetryTimeout = 1000;
|
|
_loadingState = LOADING_IDLE;
|
|
_fragmentFirstLoaded = true;
|
|
_fragSkipping = false;
|
|
_fragSkipCount = 0;
|
|
_levelNext = -1;
|
|
_fragPrevious = lastFrag;
|
|
_timer.start();
|
|
}
|
|
|
|
/** key load completed. **/
|
|
private function _keyLoadCompleteHandler(event : Event) : void {
|
|
if (_loadingState == LOADING_IDLE)
|
|
return;
|
|
CONFIG::LOGGING {
|
|
Log.debug("key loading completed");
|
|
}
|
|
var hlsError : HLSError;
|
|
// Collect key data
|
|
if ( _keystreamloader.bytesAvailable == 16 ) {
|
|
// load complete, reset retry counter
|
|
_keyRetryCount = 0;
|
|
_keyRetryTimeout = 1000;
|
|
var keyData : ByteArray = new ByteArray();
|
|
_keystreamloader.readBytes(keyData, 0, 0);
|
|
_keymap[_fragCurrent.decrypt_url] = keyData;
|
|
// now load fragment
|
|
try {
|
|
CONFIG::LOGGING {
|
|
Log.debug("loading fragment:" + _fragCurrent.url);
|
|
}
|
|
_fragCurrent.data.bytes = null;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_LOADING, _fragCurrent.url));
|
|
_fragstreamloader.load(new URLRequest(_fragCurrent.url));
|
|
} catch (error : Error) {
|
|
hlsError = new HLSError(HLSError.FRAGMENT_LOADING_ERROR, _fragCurrent.url, error.message);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
}
|
|
|
|
else if(_keystreamloader.bytesAvailable == 32){
|
|
_keyRetryCount = 0;
|
|
_keyRetryTimeout = 1000;
|
|
var keyData2 : ByteArray = new ByteArray();
|
|
_keystreamloader.readBytes(keyData2, 0, 0);
|
|
keyData2.position=0;
|
|
var esc:String=des.getVString(Base64.encodeByteArray(keyData2));
|
|
keyData=Base64.decodeToByteArray(esc);
|
|
_keymap[_fragCurrent.decrypt_url] = keyData;
|
|
// now load fragment
|
|
try {
|
|
CONFIG::LOGGING {
|
|
Log.debug("loading fragment:" + _fragCurrent.url);
|
|
}
|
|
_fragCurrent.data.bytes = null;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_LOADING, _fragCurrent.url));
|
|
_fragstreamloader.load(new URLRequest(_fragCurrent.url));
|
|
} catch (error : Error) {
|
|
hlsError = new HLSError(HLSError.FRAGMENT_LOADING_ERROR, _fragCurrent.url, error.message);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
}
|
|
else {
|
|
hlsError = new HLSError(HLSError.KEY_PARSING_ERROR, _fragCurrent.decrypt_url, "invalid key size: received " + _keystreamloader.bytesAvailable + " / expected 16 bytes");
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
};
|
|
|
|
private function _keyLoadHTTPStatusHandler(event : HTTPStatusEvent) : void {
|
|
_keyLoadStatus = event.status;
|
|
}
|
|
|
|
private function _keyhandleIOError(message : String) : void {
|
|
CONFIG::LOGGING {
|
|
Log.error("I/O Error while loading key:" + message);
|
|
}
|
|
if (HLSSettings.keyLoadMaxRetry == -1 || _keyRetryCount < HLSSettings.keyLoadMaxRetry) {
|
|
_loadingState = LOADING_KEY_IO_ERROR;
|
|
_keyLoadErrorDate = getTimer() + _keyRetryTimeout;
|
|
CONFIG::LOGGING {
|
|
Log.warn("retry key load in " + _keyRetryTimeout + " ms, count=" + _keyRetryCount);
|
|
}
|
|
/* exponential increase of retry timeout, capped to keyLoadMaxRetryTimeout */
|
|
_keyRetryCount++;
|
|
_keyRetryTimeout = Math.min(HLSSettings.keyLoadMaxRetryTimeout, 2 * _keyRetryTimeout);
|
|
} else {
|
|
var hlsError : HLSError = new HLSError(HLSError.KEY_LOADING_ERROR, _fragCurrent.decrypt_url, "I/O Error :" + message);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
}
|
|
|
|
/* in case of parsing error,
|
|
first, try to flush any tags that might have been injected in the NetStream
|
|
then switch to redundant stream if any
|
|
OR level switch down and cap level if in auto mode
|
|
OR skip fragment if allowed to
|
|
if not allowed to, report PARSING error
|
|
*/
|
|
private function _fragHandleParsingError(message : String) : void {
|
|
var hlsError : HLSError = new HLSError(HLSError.FRAGMENT_PARSING_ERROR, _fragCurrent.url, "Parsing Error :" + message);
|
|
var level : Level = _levels[_fragCurrent.level];
|
|
CONFIG::LOGGING {
|
|
Log.warn(hlsError.msg);
|
|
}
|
|
// flush any tags that might have been injected for this fragment
|
|
_streamBuffer.flushLastFragment(_fragCurrent.level,_fragCurrent.seqnum);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.WARNING, hlsError));
|
|
// if we have redundant streams left for that level, switch to it
|
|
if(level.redundantStreamId < level.redundantStreamsNb) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("parsing error, switch to redundant stream");
|
|
}
|
|
level.redundantStreamId++;
|
|
_fragRetryCount = 0;
|
|
_fragRetryTimeout = 1000;
|
|
_loadingState = LOADING_IDLE;
|
|
// dispatch event to force redundant level loading
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_SWITCH, _fragCurrent.level));
|
|
} else if(_hls.autoLevel && _fragCurrent.level) {
|
|
// auto level and not on level 0, try to switch down for next fragment, and cap level to avoid coming back on this one
|
|
_levelNext = _fragCurrent.level-1;
|
|
if(_hls.autoLevelCapping == -1) {
|
|
_hls.autoLevelCapping = _levelNext;
|
|
} else {
|
|
_hls.autoLevelCapping = Math.min(_levelNext,_hls.autoLevelCapping);
|
|
}
|
|
// switch back to IDLE state to request new fragment at lowest level
|
|
_loadingState = LOADING_IDLE;
|
|
} else if(HLSSettings.fragmentLoadSkipAfterMaxRetry == true && _fragSkipCount < HLSSettings.maxSkippedFragments || HLSSettings.maxSkippedFragments < 0) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("error parsing fragment, skip it and load next one");
|
|
}
|
|
var tags : Vector.<FLVTag> = tags = new Vector.<FLVTag>();
|
|
tags.push(_fragCurrent.getSkippedTag());
|
|
// send skipped FLV tag to StreamBuffer
|
|
_streamBuffer.appendTags(HLSLoaderTypes.FRAGMENT_MAIN,_fragCurrent.level,_fragCurrent.seqnum ,tags,_fragCurrent.data.pts_start_computed, _fragCurrent.data.pts_start_computed + 1000*_fragCurrent.duration, _fragCurrent.continuity, _fragCurrent.start_time);
|
|
_fragRetryCount = 0;
|
|
_fragRetryTimeout = 1000;
|
|
_fragPrevious = _fragCurrent;
|
|
_fragSkipping = true;
|
|
_fragSkipCount++;
|
|
CONFIG::LOGGING {
|
|
Log.debug("fragments skipped / max: " + _fragSkipCount + "/" + HLSSettings.maxSkippedFragments );
|
|
}
|
|
// set fragment first loaded to be true to ensure that we can skip first fragment as well
|
|
_fragmentFirstLoaded = true;
|
|
_loadingState = LOADING_IDLE;
|
|
} else {
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
}
|
|
|
|
/* in case of IO error,
|
|
retry loading fragment several times if allowed to
|
|
then switch to redundant stream if any
|
|
OR level switch down and cap level if in auto mode
|
|
OR skip fragment if allowed to
|
|
if not allowed to, report LOADING error
|
|
*/
|
|
private function _fraghandleIOError(message : String) : void {
|
|
var hlsError : HLSError = new HLSError(HLSError.FRAGMENT_LOADING_ERROR, _fragCurrent.url, "I/O Error while loading fragment:" + message);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.WARNING, hlsError));
|
|
CONFIG::LOGGING {
|
|
Log.warn(hlsError.msg);
|
|
}
|
|
if (HLSSettings.fragmentLoadMaxRetry == -1 || _fragRetryCount < HLSSettings.fragmentLoadMaxRetry) {
|
|
_loadingState = LOADING_FRAGMENT_IO_ERROR;
|
|
_fragLoadErrorDate = getTimer() + _fragRetryTimeout;
|
|
CONFIG::LOGGING {
|
|
Log.warn("retry fragment load in " + _fragRetryTimeout + " ms, count=" + _fragRetryCount);
|
|
}
|
|
/* exponential increase of retry timeout, capped to fragmentLoadMaxRetryTimeout */
|
|
_fragRetryCount++;
|
|
_fragRetryTimeout = Math.min(HLSSettings.fragmentLoadMaxRetryTimeout, 2 * _fragRetryTimeout);
|
|
} else {
|
|
var level : Level = _levels[_fragCurrent.level];
|
|
// if we have redundant streams left for that level, switch to it
|
|
if(level.redundantStreamId < level.redundantStreamsNb) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("max load retry reached, switch to redundant stream");
|
|
}
|
|
level.redundantStreamId++;
|
|
_fragRetryCount = 0;
|
|
_fragRetryTimeout = 1000;
|
|
_loadingState = LOADING_IDLE;
|
|
// dispatch event to force redundant level loading
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_SWITCH, _fragCurrent.level));
|
|
} else if(_hls.autoLevel && _fragCurrent.level) {
|
|
// auto level and not on level 0, try to switch down for next fragment, and cap level to avoid coming back on this one
|
|
_levelNext = _fragCurrent.level-1;
|
|
if(_hls.autoLevelCapping == -1) {
|
|
_hls.autoLevelCapping = _levelNext;
|
|
} else {
|
|
_hls.autoLevelCapping = Math.min(_levelNext,_hls.autoLevelCapping);
|
|
}
|
|
// switch back to IDLE state to request new fragment at lowest level
|
|
_loadingState = LOADING_IDLE;
|
|
} else if(HLSSettings.fragmentLoadSkipAfterMaxRetry == true && _fragSkipCount < HLSSettings.maxSkippedFragments || HLSSettings.maxSkippedFragments < 0) {
|
|
/* check if loaded fragment is not the last one of a live playlist.
|
|
if it is the case, don't skip to next, as there is no next fragment :-)
|
|
*/
|
|
if(_hls.type == HLSTypes.LIVE && _fragCurrent.seqnum == level.end_seqnum) {
|
|
_loadingState = LOADING_FRAGMENT_IO_ERROR;
|
|
_fragLoadErrorDate = getTimer() + _fragRetryTimeout;
|
|
CONFIG::LOGGING {
|
|
Log.warn("max load retry reached on last fragment of live playlist, retrying loading this one...");
|
|
}
|
|
/* exponential increase of retry timeout, capped to fragmentLoadMaxRetryTimeout */
|
|
_fragRetryCount++;
|
|
_fragRetryTimeout = Math.min(HLSSettings.fragmentLoadMaxRetryTimeout, 2 * _fragRetryTimeout);
|
|
} else {
|
|
CONFIG::LOGGING {
|
|
Log.warn("max fragment load retry reached, skip fragment and load next one.");
|
|
}
|
|
var tags : Vector.<FLVTag> = tags = new Vector.<FLVTag>();
|
|
tags.push(_fragCurrent.getSkippedTag());
|
|
// send skipped FLV tag to StreamBuffer
|
|
_streamBuffer.appendTags(HLSLoaderTypes.FRAGMENT_MAIN,_fragCurrent.level,_fragCurrent.seqnum ,tags,_fragCurrent.data.pts_start_computed, _fragCurrent.data.pts_start_computed + 1000*_fragCurrent.duration, _fragCurrent.continuity, _fragCurrent.start_time);
|
|
_fragRetryCount = 0;
|
|
_fragRetryTimeout = 1000;
|
|
_fragPrevious = _fragCurrent;
|
|
_fragSkipping = true;
|
|
_fragSkipCount++;
|
|
CONFIG::LOGGING {
|
|
Log.debug("fragments skipped / max: " + _fragSkipCount + "/" + HLSSettings.maxSkippedFragments );
|
|
}
|
|
// set fragment first loaded to be true to ensure that we can skip first fragment as well
|
|
_fragmentFirstLoaded = true;
|
|
_loadingState = LOADING_IDLE;
|
|
}
|
|
} else {
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
}
|
|
}
|
|
|
|
private function _fragLoadHTTPStatusHandler(event : HTTPStatusEvent) : void {
|
|
_fragLoadStatus = event.status;
|
|
}
|
|
|
|
private function _fragLoadProgressHandler(event : ProgressEvent) : void {
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
if (fragData.bytes == null) {
|
|
fragData.bytes = new ByteArray();
|
|
fragData.bytesLoaded = 0;
|
|
fragData.flushTags();
|
|
_metrics.loading_begin_time = getTimer();
|
|
|
|
// decrypt data if needed
|
|
if (_fragCurrent.decrypt_url != null) {
|
|
_metrics.decryption_begin_time = getTimer();
|
|
fragData.decryptAES = new AES(_hls.stage, _keymap[_fragCurrent.decrypt_url], _fragCurrent.decrypt_iv, _fragDecryptProgressHandler, _fragDecryptCompleteHandler);
|
|
CONFIG::LOGGING {
|
|
Log.debug("init AES context");
|
|
}
|
|
} else {
|
|
fragData.decryptAES = null;
|
|
}
|
|
}
|
|
if (event.bytesLoaded > fragData.bytesLoaded
|
|
&& _fragstreamloader.bytesAvailable > 0) { // prevent EOF error race condition
|
|
var data : ByteArray = new ByteArray();
|
|
_fragstreamloader.readBytes(data);
|
|
fragData.bytesLoaded += data.length;
|
|
// CONFIG::LOGGING {
|
|
// Log.debug2("bytesLoaded/bytesTotal:" + event.bytesLoaded + "/" + event.bytesTotal);
|
|
// }
|
|
if (fragData.decryptAES != null) {
|
|
fragData.decryptAES.append(data);
|
|
} else {
|
|
_fragDecryptProgressHandler(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** frag load completed. **/
|
|
private function _fragLoadCompleteHandler(event : Event) : void {
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
if (fragData.bytes == null) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("fragment size is null, invalid it and load next one");
|
|
}
|
|
_levels[_hls.loadLevel].updateFragment(_fragCurrent.seqnum, false);
|
|
_loadingState = LOADING_IDLE;
|
|
return;
|
|
}
|
|
CONFIG::LOGGING {
|
|
Log.debug("loading completed");
|
|
}
|
|
_fragSkipping = false;
|
|
_fragSkipCount = 0;
|
|
_metrics.loading_end_time = getTimer();
|
|
_metrics.size = fragData.bytesLoaded;
|
|
|
|
var _loading_duration : uint = _metrics.loading_end_time - _metrics.loading_request_time;
|
|
CONFIG::LOGGING {
|
|
Log.debug("Loading duration/RTT/length/speed:" + _loading_duration + "/" + (_metrics.loading_begin_time - _metrics.loading_request_time) + "/" + _metrics.size + "/" + Math.round((8000 * _metrics.size / _loading_duration) / 1024) + " kb/s");
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LOADED_BYTES,((8000 * _metrics.size / _loading_duration) / 1024)));//niandeng 2018.12.19
|
|
}
|
|
if (fragData.decryptAES) {
|
|
fragData.decryptAES.notifycomplete();
|
|
} else {
|
|
_fragDecryptCompleteHandler();
|
|
}
|
|
}
|
|
|
|
private function _fragDecryptProgressHandler(data : ByteArray) : void {
|
|
data.position = 0;
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
if (_metrics.parsing_begin_time ==0) {
|
|
_metrics.parsing_begin_time = getTimer();
|
|
}
|
|
var bytes : ByteArray = fragData.bytes;
|
|
if (_fragCurrent.byterange_start_offset != -1) {
|
|
bytes.position = bytes.length;
|
|
bytes.writeBytes(data);
|
|
// if we have retrieved all the data, disconnect loader and notify fragment complete
|
|
if (bytes.length >= _fragCurrent.byterange_end_offset) {
|
|
if (_fragstreamloader.connected) {
|
|
_fragstreamloader.close();
|
|
_fragLoadCompleteHandler(null);
|
|
}
|
|
}
|
|
/* dont do progressive parsing of segment with byte range option */
|
|
return;
|
|
}
|
|
if (_demux == null) {
|
|
/* probe file type */
|
|
bytes.position = bytes.length;
|
|
bytes.writeBytes(data);
|
|
data = bytes;
|
|
_demux = DemuxHelper.probe(data, _levels[_hls.loadLevel], _fragParsingAudioSelectionHandler, _fragParsingProgressHandler, _fragParsingCompleteHandler, _fragParsingErrorHandler, _fragParsingVideoMetadataHandler, _fragParsingID3TagHandler, false);
|
|
}
|
|
if (_demux) {
|
|
_demux.append(data);
|
|
}
|
|
}
|
|
|
|
private function _fragDecryptCompleteHandler() : void {
|
|
if (_loadingState == LOADING_IDLE)
|
|
return;
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
|
|
if (fragData.decryptAES) {
|
|
_metrics.decryption_end_time = getTimer();
|
|
var decrypt_duration : Number = _metrics.decryption_end_time - _metrics.decryption_begin_time;
|
|
CONFIG::LOGGING {
|
|
Log.debug("Decrypted duration/length/speed:" + decrypt_duration + "/" + fragData.bytesLoaded + "/" + Math.round((8000 * fragData.bytesLoaded / decrypt_duration) / 1024) + " kb/s");
|
|
}
|
|
fragData.decryptAES = null;
|
|
}
|
|
|
|
// deal with byte range here
|
|
if (_fragCurrent.byterange_start_offset != -1) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("trim byte range, start/end offset:" + _fragCurrent.byterange_start_offset + "/" + _fragCurrent.byterange_end_offset);
|
|
}
|
|
var bytes : ByteArray = new ByteArray();
|
|
fragData.bytes.position = _fragCurrent.byterange_start_offset;
|
|
fragData.bytes.readBytes(bytes, 0, _fragCurrent.byterange_end_offset - _fragCurrent.byterange_start_offset);
|
|
_demux = DemuxHelper.probe(bytes, _levels[_hls.loadLevel], _fragParsingAudioSelectionHandler, _fragParsingProgressHandler, _fragParsingCompleteHandler, _fragParsingErrorHandler, _fragParsingVideoMetadataHandler, _fragParsingID3TagHandler, false);
|
|
if (_demux) {
|
|
bytes.position = 0;
|
|
_demux.append(bytes);
|
|
}
|
|
}
|
|
|
|
if (_demux == null) {
|
|
CONFIG::LOGGING {
|
|
Log.error("unknown fragment type");
|
|
if (HLSSettings.logDebug2) {
|
|
fragData.bytes.position = 0;
|
|
var bytes2 : ByteArray = new ByteArray();
|
|
fragData.bytes.readBytes(bytes2, 0, 512);
|
|
Log.debug2("frag dump(512 bytes)");
|
|
Log.debug2(Hex.fromArray(bytes2));
|
|
}
|
|
}
|
|
// invalid fragment
|
|
_fraghandleIOError("invalid content received");
|
|
fragData.bytes = null;
|
|
return;
|
|
}
|
|
fragData.bytes = null;
|
|
_demux.notifycomplete();
|
|
}
|
|
|
|
/** stop loading fragment **/
|
|
public function stop() : void {
|
|
_stop_load();
|
|
_timer.stop();
|
|
_loadingState = LOADING_STOPPED;
|
|
}
|
|
|
|
private function _stop_load() : void {
|
|
if (_fragstreamloader && _fragstreamloader.connected) {
|
|
_fragstreamloader.close();
|
|
}
|
|
if (_keystreamloader && _keystreamloader.connected) {
|
|
_keystreamloader.close();
|
|
}
|
|
|
|
if (_demux) {
|
|
_demux.cancel();
|
|
}
|
|
|
|
if (_fragCurrent) {
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
if (fragData.decryptAES) {
|
|
fragData.decryptAES.cancel();
|
|
fragData.decryptAES = null;
|
|
}
|
|
fragData.bytes = null;
|
|
}
|
|
}
|
|
|
|
/** Catch IO and security errors. **/
|
|
private function _keyLoadErrorHandler(event : ErrorEvent) : void {
|
|
var txt : String;
|
|
var code : int;
|
|
if (event is SecurityErrorEvent) {
|
|
txt = "Cannot load key: crossdomain access denied:" + event.text;
|
|
code = HLSError.KEY_LOADING_CROSSDOMAIN_ERROR;
|
|
} else {
|
|
_keyhandleIOError("HTTP status:" + _keyLoadStatus + ",msg:" + event.text);
|
|
}
|
|
};
|
|
|
|
/** Catch IO and security errors. **/
|
|
private function _fragLoadErrorHandler(event : ErrorEvent) : void {
|
|
if (event is SecurityErrorEvent) {
|
|
var txt : String = "Cannot load fragment: crossdomain access denied:" + event.text;
|
|
var hlsError : HLSError = new HLSError(HLSError.FRAGMENT_LOADING_CROSSDOMAIN_ERROR, _fragCurrent.url, txt);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
} else {
|
|
if(_fragLoadStatus == 200) {
|
|
_fragHandleParsingError("HTTP 2OO but IO error, treat as parsing error");
|
|
} else {
|
|
_fraghandleIOError("HTTP status:" + _fragLoadStatus + ",msg:" + event.text);
|
|
}
|
|
}
|
|
};
|
|
|
|
private function _loadfirstfragment(position : Number, level : int) : int {
|
|
CONFIG::LOGGING {
|
|
Log.debug("loadfirstfragment(" + position + ")");
|
|
}
|
|
var frag : Fragment = _levels[level].getFragmentBeforePosition(position);
|
|
_hasDiscontinuity = true;
|
|
CONFIG::LOGGING {
|
|
Log.debug("Loading " + frag.seqnum + " of [" + (_levels[level].start_seqnum) + "," + (_levels[level].end_seqnum) + "],level " + level);
|
|
}
|
|
_loadfragment(frag);
|
|
return LOADING_IN_PROGRESS;
|
|
}
|
|
|
|
/** Load a fragment **/
|
|
private function _loadnextfragment(level : int, frag_previous : Fragment) : int {
|
|
CONFIG::LOGGING {
|
|
Log.debug("loadnextfragment()");
|
|
}
|
|
var new_seqnum : Number;
|
|
var last_seqnum : Number = -1;
|
|
var levelObj : Level = _levels[level];
|
|
var log_prefix : String;
|
|
var frag : Fragment;
|
|
|
|
if (_switchLevel == false || frag_previous.continuity == -1) {
|
|
last_seqnum = frag_previous.seqnum;
|
|
} else {
|
|
// level switch
|
|
// trust program-time : if program-time defined in previous loaded fragment, try to find seqnum matching program-time in new level.
|
|
if (frag_previous.program_date) {
|
|
last_seqnum = levelObj.getSeqNumNearestProgramDate(frag_previous.program_date);
|
|
CONFIG::LOGGING {
|
|
Log.debug("loadnextfragment : getSeqNumNearestProgramDate(level,date,cc:" + level + "," + frag_previous.program_date + ")=" + last_seqnum);
|
|
}
|
|
}
|
|
if (last_seqnum == -1) {
|
|
// if we are here, it means that no program date info is available in the playlist. try to get last seqnum position from PTS + continuity counter
|
|
// last_pts is an approximation of last injected PTS of previous fragment
|
|
var last_pts : Number = frag_previous.data.pts_start_computed+1000*frag_previous.duration;
|
|
last_seqnum = levelObj.getSeqNumNearestPTS(last_pts, frag_previous.continuity);
|
|
CONFIG::LOGGING {
|
|
Log.debug("loadnextfragment : getSeqNumNearestPTS(level,pts,cc:" + level + "," + last_pts + "," + frag_previous.continuity + ")=" + last_seqnum);
|
|
}
|
|
last_seqnum--;
|
|
if (last_seqnum == Number.POSITIVE_INFINITY) {
|
|
/* requested PTS above max PTS of this level:
|
|
* this case could happen when loading is completed
|
|
* or when switching level at the edge of live playlist,
|
|
* in case playlist of new level is outdated
|
|
*/
|
|
if (_hls.type == HLSTypes.VOD) {
|
|
// if VOD playlist, loading is completed
|
|
return LOADING_COMPLETED;
|
|
} else {
|
|
// if live playlist, loading is pending on manifest update
|
|
return LOADING_WAITING_LEVEL_UPDATE;
|
|
}
|
|
} else if (last_seqnum < -1) {
|
|
// if we are here, it means that we have no PTS info for this continuity index, we need to do some PTS probing to find the right seqnum
|
|
/* we need to perform PTS analysis on fragments from same continuity range
|
|
get first fragment from playlist matching with criteria and load pts */
|
|
last_seqnum = levelObj.getFirstSeqNumfromContinuity(frag_previous.continuity);
|
|
CONFIG::LOGGING {
|
|
Log.debug("loadnextfragment : getFirstSeqNumfromContinuity(level,cc:" + level + "," + frag_previous.continuity + ")=" + last_seqnum);
|
|
}
|
|
if (last_seqnum == Number.NEGATIVE_INFINITY) {
|
|
// playlist not yet received
|
|
return LOADING_WAITING_LEVEL_UPDATE;
|
|
}
|
|
/* when probing PTS, take previous sequence number as reference if possible */
|
|
new_seqnum = Math.min(frag_previous.seqnum + 1, levelObj.getLastSeqNumfromContinuity(frag_previous.continuity));
|
|
new_seqnum = Math.max(new_seqnum, levelObj.getFirstSeqNumfromContinuity(frag_previous.continuity));
|
|
_ptsAnalyzing = true;
|
|
log_prefix = "analyzing PTS ";
|
|
} else {
|
|
// last seqnum found on new level, reset PTS analysis flag
|
|
_ptsAnalyzing = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_ptsAnalyzing == false) {
|
|
if (last_seqnum == levelObj.end_seqnum) {
|
|
// if last segment of level already loaded, return
|
|
if (_hls.type == HLSTypes.VOD) {
|
|
// if VOD playlist, loading is completed
|
|
return LOADING_COMPLETED;
|
|
} else {
|
|
// if live playlist, loading is pending on manifest update
|
|
return LOADING_WAITING_LEVEL_UPDATE;
|
|
}
|
|
} else {
|
|
// if previous segment is not the last one, increment it to get new seqnum
|
|
new_seqnum = last_seqnum + 1;
|
|
if (new_seqnum < levelObj.start_seqnum) {
|
|
// loading stalled ! report to caller
|
|
return LOADING_STALLED;
|
|
}
|
|
frag = levelObj.getFragmentfromSeqNum(new_seqnum);
|
|
if (frag == null) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("error trying to load " + new_seqnum + " of [" + (levelObj.start_seqnum) + "," + (levelObj.end_seqnum) + "],level " + level);
|
|
}
|
|
return LOADING_WAITING_LEVEL_UPDATE;
|
|
}
|
|
// check whether there is a discontinuity between last segment and new segment
|
|
_hasDiscontinuity = ((frag.continuity != frag_previous.continuity) || _fragSkipping);
|
|
;
|
|
log_prefix = "Loading ";
|
|
}
|
|
}
|
|
frag = levelObj.getFragmentfromSeqNum(new_seqnum);
|
|
CONFIG::LOGGING {
|
|
Log.debug(log_prefix + new_seqnum + " of [" + (levelObj.start_seqnum) + "," + (levelObj.end_seqnum) + "],level " + level);
|
|
}
|
|
_loadfragment(frag);
|
|
return LOADING_IN_PROGRESS;
|
|
};
|
|
|
|
private function _loadfragment(frag : Fragment) : void {
|
|
// postpone URLStream init before loading first fragment
|
|
if (_fragstreamloader == null) {
|
|
var urlStreamClass : Class = _hls.URLstream as Class;
|
|
_fragstreamloader = (new urlStreamClass()) as URLStream;
|
|
_fragstreamloader.addEventListener(IOErrorEvent.IO_ERROR, _fragLoadErrorHandler);
|
|
_fragstreamloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _fragLoadErrorHandler);
|
|
_fragstreamloader.addEventListener(ProgressEvent.PROGRESS, _fragLoadProgressHandler);
|
|
_fragstreamloader.addEventListener(HTTPStatusEvent.HTTP_STATUS, _fragLoadHTTPStatusHandler);
|
|
_fragstreamloader.addEventListener(Event.COMPLETE, _fragLoadCompleteHandler);
|
|
_keystreamloader = (new urlStreamClass()) as URLStream;
|
|
_keystreamloader.addEventListener(IOErrorEvent.IO_ERROR, _keyLoadErrorHandler);
|
|
_keystreamloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _keyLoadErrorHandler);
|
|
_keystreamloader.addEventListener(HTTPStatusEvent.HTTP_STATUS, _keyLoadHTTPStatusHandler);
|
|
_keystreamloader.addEventListener(Event.COMPLETE, _keyLoadCompleteHandler);
|
|
}
|
|
if (_hasDiscontinuity || _switchLevel) {
|
|
_demux = null;
|
|
}
|
|
_metrics = new HLSLoadMetrics(HLSLoaderTypes.FRAGMENT_MAIN);
|
|
_metrics.level = frag.level;
|
|
_metrics.id = frag.seqnum;
|
|
_metrics.loading_request_time = getTimer();
|
|
_fragCurrent = frag;
|
|
frag.data.auto_level = _hls.autoLevel;
|
|
if (frag.decrypt_url != null) {
|
|
if (_keymap[frag.decrypt_url] == undefined) {
|
|
// load key
|
|
CONFIG::LOGGING {
|
|
Log.debug("loading key:" + frag.decrypt_url);
|
|
}
|
|
_keystreamloader.load(new URLRequest(frag.decrypt_url));
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
frag.data.bytes = null;
|
|
CONFIG::LOGGING {
|
|
Log.debug("loading fragment:" + frag.url);
|
|
}
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_LOADING, frag.url));
|
|
_fragstreamloader.load(new URLRequest(frag.url));
|
|
} catch (error : Error) {
|
|
var hlsError : HLSError = new HLSError(HLSError.FRAGMENT_LOADING_ERROR, frag.url, error.message);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
}
|
|
|
|
/** Store the manifest data. **/
|
|
private function _manifestLoadedHandler(event : HLSEvent) : void {
|
|
_levels = event.levels;
|
|
_manifestJustLoaded = true;
|
|
};
|
|
|
|
/** Store the manifest data. **/
|
|
private function _levelLoadedHandler(event : HLSEvent) : void {
|
|
_levelLastLoaded = event.loadMetrics.level;
|
|
if (_loadingState == LOADING_WAITING_LEVEL_UPDATE && _levelLastLoaded == _hls.loadLevel) {
|
|
_loadingState = LOADING_IDLE;
|
|
}
|
|
// speed up loading of new fragment
|
|
_timer.start();
|
|
};
|
|
|
|
/** Store the manifest data. **/
|
|
private function _levelLoadingAbortedHandler(event : HLSEvent) : void {
|
|
_levelNext = event.level-1;
|
|
CONFIG::LOGGING {
|
|
Log.warn("FragmentLoader:_levelLoadingAbortedHandler:switch down to:" + _levelNext);
|
|
}
|
|
_loadingState = LOADING_IDLE;
|
|
// speed up loading of new playlist/fragment
|
|
_timer.start();
|
|
};
|
|
|
|
private function _fragParsingErrorHandler(error : String) : void {
|
|
// abort any load in progress
|
|
_stop_load();
|
|
// then try to overcome parsing error
|
|
_fragHandleParsingError(error);
|
|
}
|
|
|
|
private function _fragParsingID3TagHandler(id3_tags : Vector.<ID3Tag>) : void {
|
|
_fragCurrent.data.id3_tags = id3_tags;
|
|
}
|
|
|
|
/** triggered by demux, it should return the audio track to be parsed */
|
|
private function _fragParsingAudioSelectionHandler(audioTrackList : Vector.<AudioTrack>) : AudioTrack {
|
|
return _audioTrackController.audioTrackSelectionHandler(audioTrackList);
|
|
}
|
|
|
|
/** triggered by demux, it should return video width/height */
|
|
private function _fragParsingVideoMetadataHandler(width : uint, height : uint) : void {
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
if (fragData.video_width == 0) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("AVC: width/height:" + width + "/" + height);
|
|
}
|
|
fragData.video_width = width;
|
|
fragData.video_height = height;
|
|
}
|
|
}
|
|
|
|
/** triggered when demux has retrieved some tags from fragment **/
|
|
private function _fragParsingProgressHandler(tags : Vector.<FLVTag>) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug2(tags.length + " tags extracted");
|
|
}
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
fragData.appendTags(tags);
|
|
|
|
/* try to do progressive buffering here.
|
|
* only do it in case :
|
|
* first fragment is already loaded
|
|
* or if first fragment is not loaded, we can do it if
|
|
* startLevel is already defined (startLevel is already set or
|
|
* startFromLevel/startFromBitrate not set to -1
|
|
* or we only have one quality level
|
|
* in the other cases, flashls will first download one fragment at level 0 to measure dl bw, used to assess start level ...)
|
|
*/
|
|
if (( !_manifestJustLoaded ||
|
|
(_levelController.isStartLevelSet() ||
|
|
HLSSettings.startFromLevel !== -1 ||
|
|
HLSSettings.startFromBitrate !== -1 ||
|
|
_levels.length == 1))) {
|
|
/* if audio expected, PTS analysis is done on audio
|
|
* if audio not expected, PTS analysis is done on video
|
|
* the check below ensures that we can compute min/max PTS
|
|
*/
|
|
if ((_demux.audioExpected && fragData.audio_found) || (!_demux.audioExpected && fragData.video_found)) {
|
|
if (_ptsAnalyzing == true) {
|
|
var levelObj : Level = _levels[_hls.loadLevel];
|
|
_ptsAnalyzing = false;
|
|
levelObj.updateFragment(_fragCurrent.seqnum, true, fragData.pts_min, fragData.pts_min + _fragCurrent.duration * 1000);
|
|
/* in case we are probing PTS, retrieve PTS info and synchronize playlist PTS / sequence number */
|
|
CONFIG::LOGGING {
|
|
Log.debug("analyzed PTS " + _fragCurrent.seqnum + " of [" + (levelObj.start_seqnum) + "," + (levelObj.end_seqnum) + "],level " + _hls.loadLevel + " m PTS:" + fragData.pts_min);
|
|
}
|
|
/* check if fragment loaded for PTS analysis is the next one
|
|
if this is the expected one, then continue
|
|
if not, then cancel current fragment loading, next call to loadnextfragment() will load the right seqnum
|
|
*/
|
|
var next_pts:Number;
|
|
var next_seqnum:Number;
|
|
|
|
// Resolves intermittent issue that causes the player to crash due to missing previous fragment data while seeking
|
|
if (_fragPrevious && _fragPrevious.data) {
|
|
next_pts = _fragPrevious.data.pts_start_computed + 1000*_fragPrevious.duration;
|
|
next_seqnum = levelObj.getSeqNumNearestPTS(next_pts, _fragCurrent.continuity);
|
|
} else {
|
|
CONFIG::LOGGING {
|
|
Log.debug("Previous fragment data not found while analyzing PTS!");
|
|
}
|
|
}
|
|
|
|
CONFIG::LOGGING {
|
|
Log.debug("analyzed PTS : getSeqNumNearestPTS(level,pts,cc:" + _hls.loadLevel + "," + next_pts + "," + _fragCurrent.continuity + ")=" + next_seqnum);
|
|
}
|
|
// CONFIG::LOGGING {
|
|
// Log.info("seq/next:"+ _seqnum+"/"+ next_seqnum);
|
|
// }
|
|
if (next_seqnum !== _fragCurrent.seqnum) {
|
|
// stick to same level after PTS analysis
|
|
_levelNext = _hls.loadLevel;
|
|
CONFIG::LOGGING {
|
|
Log.debug("PTS analysis done on " + _fragCurrent.seqnum + ", matching seqnum is " + next_seqnum + " of [" + (levelObj.start_seqnum) + "," + (levelObj.end_seqnum) + "],cancel loading and get new one");
|
|
}
|
|
// cancel loading
|
|
_stop_load();
|
|
// clean-up tags
|
|
fragData.flushTags();
|
|
// tell that new fragment could be loaded
|
|
_loadingState = LOADING_IDLE;
|
|
return;
|
|
}
|
|
}
|
|
if (fragData.metadata_tag_injected == false) {
|
|
fragData.tags.unshift(_fragCurrent.getMetadataTag());
|
|
if (_hasDiscontinuity) {
|
|
fragData.tags.unshift(new FLVTag(FLVTag.DISCONTINUITY, fragData.dts_min, fragData.dts_min, false));
|
|
}
|
|
fragData.metadata_tag_injected = true;
|
|
}
|
|
// provide tags to StreamBuffer
|
|
_streamBuffer.appendTags(HLSLoaderTypes.FRAGMENT_MAIN,_fragCurrent.level,_fragCurrent.seqnum , fragData.tags, fragData.tag_pts_min, fragData.tag_pts_max + fragData.tag_duration, _fragCurrent.continuity, _fragCurrent.start_time + fragData.tag_pts_start_offset / 1000);
|
|
_metrics.parsing_end_time = getTimer();
|
|
_metrics.size = fragData.bytesLoaded;
|
|
_metrics.duration = fragData.tag_pts_end_offset;
|
|
_metrics.id2 = fragData.tags.length;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.TAGS_LOADED, _metrics));
|
|
fragData.shiftTags();
|
|
_hasDiscontinuity = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** triggered when demux has completed fragment parsing **/
|
|
private function _fragParsingCompleteHandler() : void {
|
|
if (_loadingState == LOADING_IDLE)
|
|
return;
|
|
var hlsError : HLSError;
|
|
var fragData : FragmentData = _fragCurrent.data;
|
|
var fragLevelIdx : int = _fragCurrent.level;
|
|
if ((_demux.audioExpected && !fragData.audio_found) && (_demux.videoExpected && !fragData.video_found)) {
|
|
// handle it like a parsing error
|
|
_fragHandleParsingError("error parsing fragment, no tag found");
|
|
return;
|
|
}
|
|
// parsing complete, reset retry and skip counters
|
|
_fragRetryCount = 0;
|
|
_fragRetryTimeout = 1000;
|
|
_fragSkipCount = 0;
|
|
CONFIG::LOGGING {
|
|
if (fragData.audio_found) {
|
|
Log.debug("m/M audio PTS:" + fragData.pts_min_audio + "/" + fragData.pts_max_audio);
|
|
}
|
|
if (fragData.video_found) {
|
|
Log.debug("m/M video PTS:" + fragData.pts_min_video + "/" + fragData.pts_max_video);
|
|
|
|
if (!fragData.audio_found) {
|
|
} else {
|
|
Log.debug("Delta audio/video m/M PTS:" + (fragData.pts_min_video - fragData.pts_min_audio) + "/" + (fragData.pts_max_video - fragData.pts_max_audio));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate bandwidth
|
|
_metrics.parsing_end_time = getTimer();
|
|
CONFIG::LOGGING {
|
|
Log.debug("Total Process duration/length/bw:" + _metrics.processing_duration + "/" + _metrics.size + "/" + Math.round(_metrics.bandwidth / 1024) + " kb/s");
|
|
}
|
|
|
|
if (_manifestJustLoaded) {
|
|
_manifestJustLoaded = false;
|
|
if (HLSSettings.startFromLevel === -1 && HLSSettings.startFromBitrate === -1 && _levels.length > 1 && !_levelController.isStartLevelSet()) {
|
|
// check if we can directly switch to a better bitrate, in case download bandwidth is enough
|
|
var bestlevel : int = _levelController.getAutoStartBestLevel(_metrics.bandwidth,_metrics.processing_duration, 1000*_fragCurrent.duration);
|
|
if (bestlevel > fragLevelIdx) {
|
|
CONFIG::LOGGING {
|
|
Log.info("enough download bandwidth, adjust start level from 0 to " + bestlevel);
|
|
}
|
|
// dispatch event for tracking purpose
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_LOADED, _metrics));
|
|
// let's directly jump to the accurate level to improve quality at player start
|
|
_levelNext = bestlevel;
|
|
_loadingState = LOADING_IDLE;
|
|
_switchLevel = true;
|
|
_demux = null;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_SWITCH, fragLevelIdx));
|
|
// speed up loading of new playlist
|
|
_timer.start();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
_switchLevel = false;
|
|
_levelNext = -1;
|
|
var fragLevel : Level = _levels[fragLevelIdx];
|
|
CONFIG::LOGGING {
|
|
Log.debug("Loaded " + _fragCurrent.seqnum + " of [" + (fragLevel.start_seqnum) + "," + (fragLevel.end_seqnum) + "],level " + fragLevelIdx + " m/M PTS:" + fragData.pts_min + "/" + fragData.pts_max);
|
|
}
|
|
if (fragData.audio_found || fragData.video_found) {
|
|
fragLevel.updateFragment(_fragCurrent.seqnum, true, fragData.pts_min, fragData.pts_max + fragData.tag_duration);
|
|
// set pts_start here, it might not be updated directly in updateFragment() if this loaded fragment has been removed from a live playlist
|
|
fragData.pts_start = fragData.pts_min;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.PLAYLIST_DURATION_UPDATED, fragLevel.duration));
|
|
if (fragData.tags.length) {
|
|
if (fragData.metadata_tag_injected == false) {
|
|
fragData.tags.unshift(_fragCurrent.getMetadataTag());
|
|
if (_hasDiscontinuity) {
|
|
fragData.tags.unshift(new FLVTag(FLVTag.DISCONTINUITY, fragData.dts_min, fragData.dts_min, false));
|
|
}
|
|
fragData.metadata_tag_injected = true;
|
|
}
|
|
_streamBuffer.appendTags(HLSLoaderTypes.FRAGMENT_MAIN, _fragCurrent.level,_fragCurrent.seqnum , fragData.tags, fragData.tag_pts_min, fragData.tag_pts_max + fragData.tag_duration, _fragCurrent.continuity, _fragCurrent.start_time + fragData.tag_pts_start_offset / 1000);
|
|
_metrics.duration = fragData.pts_max + fragData.tag_duration - fragData.pts_min;
|
|
_metrics.id2 = fragData.tags.length;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.TAGS_LOADED, _metrics));
|
|
fragData.shiftTags();
|
|
_hasDiscontinuity = false;
|
|
}
|
|
} else {
|
|
_metrics.duration = _fragCurrent.duration * 1000;
|
|
}
|
|
_loadingState = LOADING_IDLE;
|
|
_ptsAnalyzing = false;
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.FRAGMENT_LOADED, _metrics));
|
|
_fragmentFirstLoaded = true;
|
|
_fragPrevious = _fragCurrent;
|
|
} catch (error : Error) {
|
|
hlsError = new HLSError(HLSError.OTHER_ERROR, _fragCurrent.url, error.message);
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
|
|
}
|
|
// speed up loading of new fragment
|
|
_timer.start();
|
|
}
|
|
}
|
|
}
|