no commit message
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
/* 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.controller {
|
||||
import org.mangui.hls.playlist.AltAudioTrack;
|
||||
import org.mangui.hls.event.HLSEvent;
|
||||
import org.mangui.hls.model.AudioTrack;
|
||||
import org.mangui.hls.HLS;
|
||||
|
||||
CONFIG::LOGGING {
|
||||
import org.mangui.hls.utils.Log;
|
||||
}
|
||||
/*
|
||||
* class that handle audio tracks, consolidating tracks retrieved from Manifest and from Demux
|
||||
*/
|
||||
public class AudioTrackController {
|
||||
/** Reference to the HLS controller. **/
|
||||
private var _hls : HLS;
|
||||
/** list of audio tracks from demuxed fragments **/
|
||||
private var _audioTracksfromDemux : Vector.<AudioTrack>;
|
||||
/** list of audio tracks from Manifest, matching with current level **/
|
||||
private var _audioTracksfromManifest : Vector.<AudioTrack>;
|
||||
/** merged audio tracks list **/
|
||||
private var _audioTracks : Vector.<AudioTrack>;
|
||||
/** current audio track id **/
|
||||
private var _audioTrackId : int;
|
||||
|
||||
public function AudioTrackController(hls : HLS) {
|
||||
_hls = hls;
|
||||
_hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
||||
_hls.addEventListener(HLSEvent.LEVEL_LOADED, _levelLoadedHandler);
|
||||
|
||||
// Prevents crash when reading hls.audioTracks.length before audio tracks are initialized
|
||||
_audioTracks = new Vector.<AudioTrack>();
|
||||
}
|
||||
|
||||
public function dispose() : void {
|
||||
_hls.removeEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
||||
_hls.removeEventListener(HLSEvent.LEVEL_LOADED, _levelLoadedHandler);
|
||||
}
|
||||
|
||||
public function set audioTrack(num : int) : void {
|
||||
if (_audioTrackId != num) {
|
||||
_audioTrackId = num;
|
||||
var ev : HLSEvent = new HLSEvent(HLSEvent.AUDIO_TRACK_SWITCH);
|
||||
ev.audioTrack = _audioTrackId;
|
||||
_hls.dispatchEvent(ev);
|
||||
CONFIG::LOGGING {
|
||||
Log.info('Setting audio track to ' + num);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get audioTrack() : int {
|
||||
return _audioTrackId;
|
||||
}
|
||||
|
||||
public function get audioTracks() : Vector.<AudioTrack> {
|
||||
return _audioTracks;
|
||||
}
|
||||
|
||||
private function _manifestLoadedHandler(event : HLSEvent) : void {
|
||||
// reset audio tracks
|
||||
_audioTrackId = -1;
|
||||
_audioTracksfromDemux = new Vector.<AudioTrack>();
|
||||
_audioTracksfromManifest = new Vector.<AudioTrack>();
|
||||
_updateAudioTrackforLevel(_hls.loadLevel);
|
||||
_audioTracksMerge();
|
||||
};
|
||||
|
||||
/** Store the manifest data. **/
|
||||
private function _levelLoadedHandler(event : HLSEvent) : void {
|
||||
var level : int = event.loadMetrics.level;
|
||||
if (level == _hls.loadLevel) {
|
||||
_updateAudioTrackforLevel(level);
|
||||
}
|
||||
};
|
||||
|
||||
private function _updateAudioTrackforLevel(level : uint) : void {
|
||||
var audioTrackList : Vector.<AudioTrack> = new Vector.<AudioTrack>();
|
||||
var streamId : String = _hls.levels[level].audio_stream_id;
|
||||
// check if audio stream id is set, and alternate audio tracks available
|
||||
if (streamId && _hls.altAudioTracks) {
|
||||
// try to find alternate audio streams matching with this ID
|
||||
for (var idx : int = 0; idx < _hls.altAudioTracks.length; idx++) {
|
||||
var altAudioTrack : AltAudioTrack = _hls.altAudioTracks[idx];
|
||||
if (altAudioTrack.group_id == streamId) {
|
||||
var isDefault : Boolean = (altAudioTrack.default_track == true || altAudioTrack.autoselect == true);
|
||||
CONFIG::LOGGING {
|
||||
Log.debug(" audio track[" + audioTrackList.length + "]:" + (isDefault ? "default:" : "alternate:") + altAudioTrack.name);
|
||||
}
|
||||
audioTrackList.push(new AudioTrack(altAudioTrack.name, AudioTrack.FROM_PLAYLIST, idx, isDefault, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if audio tracks matching with current level have changed since last time
|
||||
var audioTrackChanged : Boolean = false;
|
||||
if (_audioTracksfromManifest.length != audioTrackList.length) {
|
||||
audioTrackChanged = true;
|
||||
} else {
|
||||
for (idx = 0; idx < _audioTracksfromManifest.length; ++idx) {
|
||||
if (_audioTracksfromManifest[idx].id != audioTrackList[idx].id) {
|
||||
audioTrackChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update audio list
|
||||
if (audioTrackChanged) {
|
||||
_audioTracksfromManifest = audioTrackList;
|
||||
_audioTracksMerge();
|
||||
}
|
||||
}
|
||||
|
||||
// merge audio track info from demux and from manifest into a unified list that will be exposed to upper layer
|
||||
private function _audioTracksMerge() : void {
|
||||
var i : int;
|
||||
var defaultDemux : int = -1;
|
||||
var defaultManifest : int = -1;
|
||||
var defaultFound : Boolean = false;
|
||||
var defaultTrackTitle : String;
|
||||
var audioTrack_ : AudioTrack;
|
||||
_audioTracks = new Vector.<AudioTrack>();
|
||||
|
||||
// first look for default audio track.
|
||||
for (i = 0; i < _audioTracksfromManifest.length; i++) {
|
||||
if (_audioTracksfromManifest[i].isDefault) {
|
||||
defaultManifest = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < _audioTracksfromDemux.length; i++) {
|
||||
if (_audioTracksfromDemux[i].isDefault) {
|
||||
defaultDemux = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* default audio track from manifest should take precedence */
|
||||
if (defaultManifest != -1) {
|
||||
audioTrack_ = _audioTracksfromManifest[defaultManifest];
|
||||
// if URL set, default audio track is not embedded into MPEG2-TS
|
||||
if (_hls.altAudioTracks[audioTrack_.id].url || defaultDemux == -1) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("default audio track found in Manifest");
|
||||
}
|
||||
defaultFound = true;
|
||||
_audioTracks.push(audioTrack_);
|
||||
} else {
|
||||
// empty URL, default audio track is embedded into MPEG2-TS. retrieve track title from manifest and override demux title
|
||||
defaultTrackTitle = audioTrack_.title;
|
||||
if (defaultDemux != -1) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("default audio track signaled in Manifest, will be retrieved from MPEG2-TS");
|
||||
}
|
||||
audioTrack_ = _audioTracksfromDemux[defaultDemux];
|
||||
audioTrack_.title = defaultTrackTitle;
|
||||
defaultFound = true;
|
||||
_audioTracks.push(audioTrack_);
|
||||
}
|
||||
}
|
||||
} else if (defaultDemux != -1 ) {
|
||||
audioTrack_ = _audioTracksfromDemux[defaultDemux];
|
||||
defaultFound = true;
|
||||
_audioTracks.push(audioTrack_);
|
||||
}
|
||||
// then append other audio tracks, start from manifest list, then continue with demux list
|
||||
for (i = 0; i < _audioTracksfromManifest.length; i++) {
|
||||
if (i != defaultManifest) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("alternate audio track found in Manifest");
|
||||
}
|
||||
audioTrack_ = _audioTracksfromManifest[i];
|
||||
_audioTracks.push(audioTrack_);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < _audioTracksfromDemux.length; i++) {
|
||||
if (i != defaultDemux) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("alternate audio track retrieved from demux");
|
||||
}
|
||||
audioTrack_ = _audioTracksfromDemux[i];
|
||||
_audioTracks.push(audioTrack_);
|
||||
}
|
||||
}
|
||||
// notify audio track list update
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.AUDIO_TRACKS_LIST_CHANGE));
|
||||
|
||||
// switch track id to default audio track, if found
|
||||
if (defaultFound == true && _audioTrackId == -1) {
|
||||
audioTrack = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** triggered by demux, it should return the audio track to be parsed */
|
||||
public function audioTrackSelectionHandler(audioTrackList : Vector.<AudioTrack>) : AudioTrack {
|
||||
var audioTrackChanged : Boolean = false;
|
||||
audioTrackList = audioTrackList.sort(function(a : AudioTrack, b : AudioTrack) : int {
|
||||
return a.id - b.id;
|
||||
});
|
||||
if (_audioTracksfromDemux.length != audioTrackList.length) {
|
||||
audioTrackChanged = true;
|
||||
} else {
|
||||
for (var idx : int = 0; idx < _audioTracksfromDemux.length; ++idx) {
|
||||
if (_audioTracksfromDemux[idx].id != audioTrackList[idx].id) {
|
||||
audioTrackChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update audio list if changed
|
||||
if (audioTrackChanged) {
|
||||
_audioTracksfromDemux = audioTrackList;
|
||||
_audioTracksMerge();
|
||||
}
|
||||
|
||||
/* if audio track not defined, or audio from external source (playlist)
|
||||
return null (demux audio not selected) */
|
||||
if (_audioTrackId == -1 || _audioTrackId >= _audioTracks.length || _audioTracks[_audioTrackId].source == AudioTrack.FROM_PLAYLIST) {
|
||||
return null;
|
||||
} else {
|
||||
// source is demux,return selected audio track
|
||||
return _audioTracks[_audioTrackId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/* 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.controller {
|
||||
import org.mangui.hls.constant.HLSLoaderTypes;
|
||||
import org.mangui.hls.event.HLSEvent;
|
||||
import org.mangui.hls.event.HLSLoadMetrics;
|
||||
import org.mangui.hls.HLS;
|
||||
import org.mangui.hls.HLSSettings;
|
||||
|
||||
CONFIG::LOGGING {
|
||||
import org.mangui.hls.utils.Log;
|
||||
}
|
||||
/** Class that manages buffer threshold values (minBufferLength/lowBufferLength)
|
||||
*/
|
||||
public class BufferThresholdController {
|
||||
/** Reference to the HLS controller. **/
|
||||
private var _hls : HLS;
|
||||
private var _targetduration : Number;
|
||||
private var _minBufferLength : Number;
|
||||
|
||||
/** Create the loader. **/
|
||||
public function BufferThresholdController(hls : HLS) : void {
|
||||
_hls = hls;
|
||||
_hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
||||
_hls.addEventListener(HLSEvent.TAGS_LOADED, _fragmentLoadedHandler);
|
||||
_hls.addEventListener(HLSEvent.FRAGMENT_LOADED, _fragmentLoadedHandler);
|
||||
};
|
||||
|
||||
public function dispose() : void {
|
||||
_hls.removeEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
||||
_hls.removeEventListener(HLSEvent.TAGS_LOADED, _fragmentLoadedHandler);
|
||||
_hls.removeEventListener(HLSEvent.FRAGMENT_LOADED, _fragmentLoadedHandler);
|
||||
}
|
||||
|
||||
public function get minBufferLength() : Number {
|
||||
if (HLSSettings.minBufferLength == -1) {
|
||||
return _minBufferLength;
|
||||
} else {
|
||||
return HLSSettings.minBufferLength;
|
||||
}
|
||||
}
|
||||
|
||||
public function get lowBufferLength() : Number {
|
||||
if (HLSSettings.minBufferLength == -1) {
|
||||
// in automode, low buffer threshold should be less than min auto buffer
|
||||
return Math.min(minBufferLength / 2, HLSSettings.lowBufferLength);
|
||||
} else {
|
||||
return HLSSettings.lowBufferLength;
|
||||
}
|
||||
}
|
||||
|
||||
private function _manifestLoadedHandler(event : HLSEvent) : void {
|
||||
_targetduration = event.levels[_hls.startLevel].targetduration;
|
||||
_minBufferLength = _targetduration;
|
||||
};
|
||||
|
||||
private function _fragmentLoadedHandler(event : HLSEvent) : void {
|
||||
var metrics : HLSLoadMetrics = event.loadMetrics;
|
||||
// only monitor main fragment metrics for buffer threshold computing
|
||||
if(metrics.type == HLSLoaderTypes.FRAGMENT_MAIN) {
|
||||
/* set min buf len to be the time to process a complete segment, using current processing rate */
|
||||
_minBufferLength = metrics.processing_duration * (_targetduration / metrics.duration);
|
||||
// avoid min > max
|
||||
if (HLSSettings.maxBufferLength) {
|
||||
_minBufferLength = Math.min(HLSSettings.maxBufferLength, _minBufferLength);
|
||||
}
|
||||
|
||||
// avoid _minBufferLength > minBufferLengthCapping
|
||||
if (HLSSettings.minBufferLengthCapping > 0) {
|
||||
_minBufferLength = Math.min(HLSSettings.minBufferLengthCapping, _minBufferLength);
|
||||
}
|
||||
|
||||
CONFIG::LOGGING {
|
||||
Log.debug2("AutoBufferController:minBufferLength:" + _minBufferLength);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/* 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.controller {
|
||||
// import flash.events.ThrottleEvent;
|
||||
// import flash.events.ThrottleType;
|
||||
import flash.events.Event;
|
||||
import flash.events.TimerEvent;
|
||||
import flash.system.Capabilities;
|
||||
import flash.utils.getTimer;
|
||||
import flash.utils.Timer;
|
||||
import org.mangui.hls.constant.HLSPlayStates;
|
||||
import org.mangui.hls.event.HLSEvent;
|
||||
import org.mangui.hls.HLS;
|
||||
import org.mangui.hls.HLSSettings;
|
||||
CONFIG::LOGGING {
|
||||
import org.mangui.hls.utils.Log;
|
||||
}
|
||||
/*
|
||||
* class that control/monitor FPS
|
||||
*/
|
||||
public class FPSController {
|
||||
/** Reference to the HLS controller. **/
|
||||
private var _hls : HLS;
|
||||
private var _timer : Timer;
|
||||
private var _throttling : Boolean;
|
||||
private var _playing : Boolean;
|
||||
private var _lastTime : int;
|
||||
private var _lastDroppedFrames : int;
|
||||
// hardcode event name and state to avoid compilation issue with target player < 11.2
|
||||
private static const THROTTLE : String = "throttle";
|
||||
private static const RESUME : String = "resume";
|
||||
|
||||
public function FPSController(hls : HLS) {
|
||||
_hls = hls;
|
||||
_throttling = false;
|
||||
_playing = false;
|
||||
_lastTime = 0;
|
||||
/** Check that Flash Player version is sufficient (11.2 or above) to use throttling event **/
|
||||
if(_checkVersion() >= 11.2) {
|
||||
_hls.addEventListener(HLSEvent.PLAYBACK_STATE, _playbackStateHandler);
|
||||
_hls.addEventListener(HLSEvent.STAGE_SET, _stageSetHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private function _checkVersion() : Number {
|
||||
var verArray : Array = Capabilities.version.split(/\s|,/);
|
||||
return Number(String(verArray[1] + "." + verArray[2]));
|
||||
}
|
||||
|
||||
public function dispose() : void {
|
||||
if(_checkVersion() >= 11.2) {
|
||||
_hls.removeEventListener(HLSEvent.PLAYBACK_STATE, _playbackStateHandler);
|
||||
_hls.removeEventListener(HLSEvent.STAGE_SET, _stageSetHandler);
|
||||
if(_timer) {
|
||||
_timer.removeEventListener(TimerEvent.TIMER, _checkFPS);
|
||||
_timer.stop();
|
||||
}
|
||||
if(_hls.stage) {
|
||||
_hls.stage.removeEventListener(THROTTLE, onThrottle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function _stageSetHandler(event : HLSEvent) : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("FPSController:stage defined, listen to throttle event");
|
||||
}
|
||||
_timer = new Timer(HLSSettings.fpsDroppedMonitoringPeriod,0);
|
||||
_timer.addEventListener(TimerEvent.TIMER, _checkFPS);
|
||||
_timer.start();
|
||||
_hls.stage.addEventListener(THROTTLE, onThrottle);
|
||||
}
|
||||
|
||||
private function _playbackStateHandler(event : HLSEvent) : void {
|
||||
switch(event.state) {
|
||||
case HLSPlayStates.PLAYING:
|
||||
// start fps monitoring when switching to playing state
|
||||
_playing = true;
|
||||
_lastTime = 0;
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("FPSController:playback starting, start monitoring FPS");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
_playing = false;
|
||||
// stop fps monitoring in all other cases
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("FPSController:playback stopped, stop monitoring FPS");
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private function onThrottle(e : Object) : void {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("FPSController:onThrottle:" + e.state + ',fps:' + e.targetFrameRate);
|
||||
}
|
||||
switch(e.state) {
|
||||
case RESUME:
|
||||
_throttling=false;
|
||||
_lastTime = 0;
|
||||
break;
|
||||
default:
|
||||
_throttling=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function _checkFPS(e : Event) : void {
|
||||
var currentTime : int = getTimer();
|
||||
var droppedFrames : int = _hls.stream.info.droppedFrames;
|
||||
// monitor only if not throttling AND playing AND we hold a time reference with nb of dropped frames
|
||||
if(_throttling == false && _playing == true && _lastTime) {
|
||||
var currentPeriod : int = currentTime-_lastTime;
|
||||
var currentDropped : int = droppedFrames - _lastDroppedFrames;
|
||||
var currentDropFPS : Number = 1000*currentDropped/currentPeriod;
|
||||
var currentFPS : Number = _hls.stream.currentFPS;
|
||||
CONFIG::LOGGING {
|
||||
Log.debug2("FPSController:currentDropped,currentPeriod,currentDropFPS," + currentDropped +',' + currentPeriod +',' + currentDropFPS.toFixed(1));
|
||||
}
|
||||
if(currentDropFPS > HLSSettings.fpsDroppedMonitoringThreshold*currentFPS) {
|
||||
CONFIG::LOGGING {
|
||||
Log.warn("FPSController:drop FPS ratio >" + HLSSettings.fpsDroppedMonitoringThreshold + ',drop/displayed:' +currentDropFPS.toFixed(1)+","+currentFPS.toFixed(1) + " in the last " + HLSSettings.fpsDroppedMonitoringPeriod + "ms");
|
||||
}
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.FPS_DROP, _hls.currentLevel));
|
||||
if(HLSSettings.capLevelonFPSDrop) {
|
||||
var currentLevel : int = _hls.currentLevel;
|
||||
if(currentLevel > 0 && (_hls.autoLevelCapping == -1 || _hls.autoLevelCapping >= _hls.currentLevel)) {
|
||||
var capLevel : int = currentLevel-1;
|
||||
CONFIG::LOGGING {
|
||||
Log.warn("FPSController:cap level on FPS drop to " + capLevel);
|
||||
}
|
||||
_hls.autoLevelCapping = capLevel;
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.FPS_DROP_LEVEL_CAPPING, capLevel));
|
||||
if(HLSSettings.smoothAutoSwitchonFPSDrop) {
|
||||
if(_hls.autoLevel == true) {
|
||||
CONFIG::LOGGING {
|
||||
Log.warn("FPSController:trigger smooth level switch on frame drop");
|
||||
}
|
||||
_hls.nextLevel = -1;
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.FPS_DROP_SMOOTH_LEVEL_SWITCH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_lastTime = currentTime;
|
||||
_lastDroppedFrames = droppedFrames;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
/* 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.controller {
|
||||
import flash.display.Stage;
|
||||
import org.mangui.hls.constant.HLSLoaderTypes;
|
||||
import org.mangui.hls.constant.HLSMaxLevelCappingMode;
|
||||
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.Level;
|
||||
|
||||
CONFIG::LOGGING {
|
||||
import org.mangui.hls.utils.Log;
|
||||
}
|
||||
/** Class that manages auto level selection
|
||||
*
|
||||
* this is an implementation based on Serial segment fetching method from
|
||||
* http://www.cs.tut.fi/~moncef/publications/rate-adaptation-IC-2011.pdf
|
||||
*/
|
||||
public class LevelController {
|
||||
/** Reference to the HLS controller. **/
|
||||
private var _hls : HLS;
|
||||
/** switch up threshold **/
|
||||
private var _switchup : Vector.<Number> = null;
|
||||
/** switch down threshold **/
|
||||
private var _switchdown : Vector.<Number> = null;
|
||||
/** bitrate array **/
|
||||
private var _bitrate : Vector.<uint> = null;
|
||||
/** vector of levels with unique dimension with highest bandwidth **/
|
||||
private var _maxUniqueLevels : Vector.<Level> = null;
|
||||
/** nb level **/
|
||||
private var _nbLevel : int = 0;
|
||||
private var _lastSegmentDuration : Number;
|
||||
private var _lastFetchDuration : Number;
|
||||
private var lastBandwidth : int;
|
||||
private var _autoLevelCapping : int;
|
||||
private var _startLevel : int = -1;
|
||||
private var _fpsController : FPSController;
|
||||
|
||||
/** Create the loader. **/
|
||||
public function LevelController(hls : HLS) : void {
|
||||
_hls = hls;
|
||||
_fpsController = new FPSController(hls);
|
||||
/* low priority listener, so that other listeners with default priority
|
||||
could seamlessly set hls.startLevel in their HLSEvent.MANIFEST_PARSED listener */
|
||||
_hls.addEventListener(HLSEvent.MANIFEST_PARSED, _manifestParsedHandler, false, int.MIN_VALUE);
|
||||
_hls.addEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
||||
_hls.addEventListener(HLSEvent.FRAGMENT_LOADED, _fragmentLoadedHandler);
|
||||
_hls.addEventListener(HLSEvent.FRAGMENT_LOAD_EMERGENCY_ABORTED, _fragmentLoadedHandler);
|
||||
}
|
||||
;
|
||||
|
||||
public function dispose() : void {
|
||||
_fpsController.dispose();
|
||||
_fpsController = null;
|
||||
_hls.removeEventListener(HLSEvent.MANIFEST_PARSED, _manifestParsedHandler);
|
||||
_hls.removeEventListener(HLSEvent.MANIFEST_LOADED, _manifestLoadedHandler);
|
||||
_hls.removeEventListener(HLSEvent.FRAGMENT_LOADED, _fragmentLoadedHandler);
|
||||
_hls.removeEventListener(HLSEvent.FRAGMENT_LOAD_EMERGENCY_ABORTED, _fragmentLoadedHandler);
|
||||
}
|
||||
|
||||
private function _fragmentLoadedHandler(event : HLSEvent) : void {
|
||||
var metrics : HLSLoadMetrics = event.loadMetrics;
|
||||
// only monitor main fragment metrics for level switching
|
||||
if(metrics.type == HLSLoaderTypes.FRAGMENT_MAIN) {
|
||||
lastBandwidth = metrics.bandwidth;
|
||||
_lastSegmentDuration = metrics.duration;
|
||||
_lastFetchDuration = metrics.processing_duration;
|
||||
}
|
||||
}
|
||||
|
||||
private function _manifestParsedHandler(event : HLSEvent) : void {
|
||||
if(HLSSettings.autoStartLoad) {
|
||||
// upon manifest parsed event, trigger a level switch to load startLevel playlist
|
||||
_hls.dispatchEvent(new HLSEvent(HLSEvent.LEVEL_SWITCH, startLevel));
|
||||
}
|
||||
}
|
||||
|
||||
private function _manifestLoadedHandler(event : HLSEvent) : void {
|
||||
var levels : Vector.<Level> = event.levels;
|
||||
var maxswitchup : Number = 0;
|
||||
var minswitchdwown : Number = Number.MAX_VALUE;
|
||||
_nbLevel = levels.length;
|
||||
_bitrate = new Vector.<uint>(_nbLevel, true);
|
||||
_switchup = new Vector.<Number>(_nbLevel, true);
|
||||
_switchdown = new Vector.<Number>(_nbLevel, true);
|
||||
_autoLevelCapping = -1;
|
||||
_lastSegmentDuration = 0;
|
||||
_lastFetchDuration = 0;
|
||||
lastBandwidth = 0;
|
||||
|
||||
var i : int;
|
||||
|
||||
for (i = 0; i < _nbLevel; i++) {
|
||||
_bitrate[i] = levels[i].bitrate;
|
||||
}
|
||||
|
||||
for (i = 0; i < _nbLevel - 1; i++) {
|
||||
_switchup[i] = (_bitrate[i + 1] - _bitrate[i]) / _bitrate[i];
|
||||
maxswitchup = Math.max(maxswitchup, _switchup[i]);
|
||||
}
|
||||
for (i = 0; i < _nbLevel - 1; i++) {
|
||||
_switchup[i] = Math.min(maxswitchup, 2 * _switchup[i]);
|
||||
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("_switchup[" + i + "]=" + _switchup[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 1; i < _nbLevel; i++) {
|
||||
_switchdown[i] = (_bitrate[i] - _bitrate[i - 1]) / _bitrate[i];
|
||||
minswitchdwown = Math.min(minswitchdwown, _switchdown[i]);
|
||||
}
|
||||
for (i = 1; i < _nbLevel; i++) {
|
||||
_switchdown[i] = Math.max(2 * minswitchdwown, _switchdown[i]);
|
||||
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("_switchdown[" + i + "]=" + _switchdown[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (HLSSettings.capLevelToStage) {
|
||||
_maxUniqueLevels = _maxLevelsWithUniqueDimensions;
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
public function getAutoStartBestLevel(downloadBandwidth : int, initialDelay : int, lastSegmentDuration : int) : int {
|
||||
var bwFactor : Number;
|
||||
var max_level : int = _maxLevel;
|
||||
// in case initial delay is capped
|
||||
if(HLSSettings.autoStartMaxDuration != -1) {
|
||||
// if we are above initial delay, stick to level 0
|
||||
if(initialDelay >= HLSSettings.autoStartMaxDuration) {
|
||||
return 0;
|
||||
} else {
|
||||
// if we still have some time to load another fragment, determine load factor:
|
||||
// if we have 10000ms fragment, and we have 1000s left, we need a bw 10 times bigger to accomodate
|
||||
bwFactor = lastSegmentDuration/(HLSSettings.autoStartMaxDuration-initialDelay);
|
||||
}
|
||||
} else {
|
||||
bwFactor = 1;
|
||||
}
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("getAutoStartBestLevel,initialDelay/max delay/bwFactor=" + initialDelay + "/" + HLSSettings.autoStartMaxDuration + "/" + bwFactor.toFixed(2));
|
||||
}
|
||||
for (var i : int = max_level; i >= 0; i--) {
|
||||
if (_bitrate[i]*bwFactor <= downloadBandwidth) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function get _maxLevelsWithUniqueDimensions() : Vector.<Level> {
|
||||
var filter : Function = function(l : Level, i : int, v : Vector.<Level>) : Boolean {
|
||||
if (l.width > 0 && l.height > 0) {
|
||||
if (i + 1 < v.length) {
|
||||
var nextLevel : Level = v[i + 1];
|
||||
if (l.width != nextLevel.width && l.height != nextLevel.height) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return _hls.levels.filter(filter);
|
||||
}
|
||||
|
||||
|
||||
/** Return the capping/max level value that could be used by automatic level selection algorithm **/
|
||||
public function get autoLevelCapping() : int {
|
||||
return _autoLevelCapping;
|
||||
}
|
||||
|
||||
/** set the capping/max level value that could be used by automatic level selection algorithm **/
|
||||
public function set autoLevelCapping(newLevel : int) : void {
|
||||
_autoLevelCapping = newLevel;
|
||||
}
|
||||
|
||||
private function get _maxLevel() : int {
|
||||
// if set, _autoLevelCapping takes precedence
|
||||
if(_autoLevelCapping >= 0) {
|
||||
return Math.min(_nbLevel - 1, _autoLevelCapping);
|
||||
} else if (HLSSettings.capLevelToStage) {
|
||||
var maxLevelsCount : int = _maxUniqueLevels.length;
|
||||
|
||||
if (_hls.stage && maxLevelsCount) {
|
||||
var maxLevel : Level = this._maxUniqueLevels[0],
|
||||
maxLevelIdx : int = maxLevel.index,
|
||||
stage : Stage = _hls.stage,
|
||||
sWidth : Number = stage.stageWidth,
|
||||
sHeight : Number = stage.stageHeight,
|
||||
lWidth : int,
|
||||
lHeight : int,
|
||||
i : int;
|
||||
|
||||
// retina display support
|
||||
// contentsScaleFactor was added in FP11.5, but this allows us to include the option in all builds
|
||||
try {
|
||||
var contentsScaleFactor : int = stage['contentsScaleFactor'];
|
||||
sWidth*=contentsScaleFactor;
|
||||
sHeight*=contentsScaleFactor;
|
||||
} catch(e : Error) {
|
||||
// Ignore errors, we're running in FP < 11.5
|
||||
}
|
||||
|
||||
|
||||
switch (HLSSettings.maxLevelCappingMode) {
|
||||
case HLSMaxLevelCappingMode.UPSCALE:
|
||||
for (i = maxLevelsCount - 1; i >= 0; i--) {
|
||||
maxLevel = this._maxUniqueLevels[i];
|
||||
maxLevelIdx = maxLevel.index;
|
||||
lWidth = maxLevel.width;
|
||||
lHeight = maxLevel.height;
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("stage size: " + sWidth + "x" + sHeight + " ,level" + maxLevelIdx + " size: " + lWidth + "x" + lHeight);
|
||||
}
|
||||
if (sWidth >= lWidth || sHeight >= lHeight) {
|
||||
break;
|
||||
// from for loop
|
||||
}
|
||||
}
|
||||
break;
|
||||
case HLSMaxLevelCappingMode.DOWNSCALE:
|
||||
for (i = 0; i < maxLevelsCount; i++) {
|
||||
maxLevel = this._maxUniqueLevels[i];
|
||||
maxLevelIdx = maxLevel.index;
|
||||
lWidth = maxLevel.width;
|
||||
lHeight = maxLevel.height;
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("stage size: " + sWidth + "x" + sHeight + " ,level" + maxLevelIdx + " size: " + lWidth + "x" + lHeight);
|
||||
}
|
||||
if (sWidth <= lWidth || sHeight <= lHeight) {
|
||||
break;
|
||||
// from for loop
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("max capped level idx: " + maxLevelIdx);
|
||||
}
|
||||
}
|
||||
return maxLevelIdx;
|
||||
} else {
|
||||
return _nbLevel - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the quality level for the next fragment load. **/
|
||||
public function getnextlevel(current_level : int, buffer : Number) : int {
|
||||
if (_lastFetchDuration == 0 || _lastSegmentDuration == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* rsft : remaining segment fetch time : available time to fetch next segment
|
||||
it depends on the current playback timestamp , the timestamp of the first frame of the next segment
|
||||
and TBMT, indicating a desired latency between the time instant to receive the last byte of a
|
||||
segment to the playback of the first media frame of a segment
|
||||
buffer is start time of next segment
|
||||
TBMT is the buffer size we need to ensure (we need at least 2 segments buffered */
|
||||
var rsft : Number = 1000 * buffer - 2 * _lastFetchDuration;
|
||||
var sftm : Number = Math.min(_lastSegmentDuration, rsft) / _lastFetchDuration;
|
||||
var max_level : Number = _maxLevel;
|
||||
var switch_to_level : int = current_level;
|
||||
// CONFIG::LOGGING {
|
||||
// Log.info("rsft:" + rsft);
|
||||
// Log.info("sftm:" + sftm);
|
||||
// }
|
||||
// }
|
||||
/* to switch level up :
|
||||
rsft should be greater than switch up condition
|
||||
*/
|
||||
if ((current_level < max_level) && (sftm > (1 + _switchup[current_level]))) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("sftm:> 1+_switchup[_level]=" + (1 + _switchup[current_level]));
|
||||
}
|
||||
switch_to_level = current_level + 1;
|
||||
}
|
||||
|
||||
/* to switch level down :
|
||||
rsft should be smaller than switch up condition,
|
||||
or the current level is greater than max level
|
||||
*/ else if ((current_level > max_level && current_level > 0) || (current_level > 0 && (sftm < 1 - _switchdown[current_level]))) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("sftm < 1-_switchdown[current_level]=" + _switchdown[current_level]);
|
||||
}
|
||||
var bufferratio : Number = 1000 * buffer / _lastSegmentDuration;
|
||||
/* find suitable level matching current bandwidth, starting from current level
|
||||
when switching level down, we also need to consider that we might need to load two fragments.
|
||||
the condition (bufferratio > 2*_levels[j].bitrate/_lastBandwidth)
|
||||
ensures that buffer time is bigger than than the time to download 2 fragments from level j, if we keep same bandwidth.
|
||||
*/
|
||||
|
||||
for (var j : int = current_level - 1; j >= 0; j--) {
|
||||
if (_bitrate[j] <= lastBandwidth && (bufferratio > 2 * _bitrate[j] / lastBandwidth)) {
|
||||
switch_to_level = j;
|
||||
break;
|
||||
}
|
||||
if (j == 0) {
|
||||
switch_to_level = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then we should check if selected level is higher than max_level if so, than take the min of those two
|
||||
switch_to_level = Math.min(max_level, switch_to_level);
|
||||
|
||||
CONFIG::LOGGING {
|
||||
if (switch_to_level != current_level) {
|
||||
Log.debug("switch to level " + switch_to_level);
|
||||
}
|
||||
}
|
||||
|
||||
return switch_to_level;
|
||||
}
|
||||
|
||||
// get level index of first level appearing in the manifest
|
||||
public function get firstLevel() : int {
|
||||
var levels : Vector.<Level> = _hls.levels;
|
||||
for (var i : int = 0; i < levels.length; i++) {
|
||||
if (levels[i].manifest_index == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function isStartLevelSet() : Boolean {
|
||||
return (_startLevel >=0);
|
||||
}
|
||||
|
||||
/* set the quality level used when starting a fresh playback */
|
||||
public function set startLevel(level : int) : void {
|
||||
_startLevel = level;
|
||||
};
|
||||
|
||||
public function get startLevel() : int {
|
||||
var start_level : int = -1;
|
||||
var levels : Vector.<Level> = _hls.levels;
|
||||
if (levels) {
|
||||
// if set, _startLevel takes precedence
|
||||
if(_startLevel >=0) {
|
||||
return Math.min(levels.length-1,_startLevel);
|
||||
} else if (HLSSettings.startFromLevel === -2) {
|
||||
// playback will start from the first level appearing in Manifest (not sorted by bitrate)
|
||||
return firstLevel;
|
||||
} else if (HLSSettings.startFromLevel === -1 && HLSSettings.startFromBitrate === -1) {
|
||||
/* if startFromLevel is set to -1, it means that effective startup level
|
||||
* will be determined from first segment download bandwidth
|
||||
* let's use lowest bitrate for this download bandwidth assessment
|
||||
* this should speed up playback start time
|
||||
*/
|
||||
return 0;
|
||||
} else {
|
||||
// set up start level as being the lowest non-audio level.
|
||||
for (var i : int = 0; i < levels.length; i++) {
|
||||
if (!levels[i].audio) {
|
||||
start_level = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// in case of audio only playlist, force startLevel to 0
|
||||
if (start_level == -1) {
|
||||
CONFIG::LOGGING {
|
||||
Log.info("playlist is audio-only");
|
||||
}
|
||||
start_level = 0;
|
||||
} else {
|
||||
if (HLSSettings.startFromBitrate > 0) {
|
||||
start_level = findIndexOfClosestLevel(HLSSettings.startFromBitrate);
|
||||
} else if (HLSSettings.startFromLevel > 0) {
|
||||
// adjust start level using a rule by 3
|
||||
start_level += Math.round(HLSSettings.startFromLevel * (levels.length - start_level - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("start level :" + start_level);
|
||||
}
|
||||
}
|
||||
return start_level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param desiredBitrate
|
||||
* @return The index of the level that has a bitrate closest to the desired bitrate.
|
||||
*/
|
||||
private function findIndexOfClosestLevel(desiredBitrate : Number) : int {
|
||||
var levelIndex : int = -1;
|
||||
var minDistance : Number = Number.MAX_VALUE;
|
||||
var levels : Vector.<Level> = _hls.levels;
|
||||
|
||||
for (var index : int = 0; index < levels.length; index++) {
|
||||
var level : Level = levels[index];
|
||||
|
||||
var distance : Number = Math.abs(desiredBitrate - level.bitrate);
|
||||
|
||||
if (distance < minDistance) {
|
||||
levelIndex = index;
|
||||
minDistance = distance;
|
||||
}
|
||||
}
|
||||
return levelIndex;
|
||||
}
|
||||
|
||||
public function get seekLevel() : int {
|
||||
var seek_level : int = -1;
|
||||
var levels : Vector.<Level> = _hls.levels;
|
||||
if (HLSSettings.seekFromLevel == -1) {
|
||||
// keep last level, but don't exceed _maxLevel
|
||||
return Math.min(_hls.loadLevel,_maxLevel);
|
||||
}
|
||||
|
||||
// set up seek level as being the lowest non-audio level.
|
||||
for (var i : int = 0; i < levels.length; i++) {
|
||||
if (!levels[i].audio) {
|
||||
seek_level = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// in case of audio only playlist, force seek_level to 0
|
||||
if (seek_level == -1) {
|
||||
seek_level = 0;
|
||||
} else {
|
||||
if (HLSSettings.seekFromLevel > 0) {
|
||||
// adjust start level using a rule by 3
|
||||
seek_level += Math.round(HLSSettings.seekFromLevel * (levels.length - seek_level - 1));
|
||||
}
|
||||
}
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("seek level :" + seek_level);
|
||||
}
|
||||
return seek_level;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user