1347 lines
65 KiB
ActionScript
1347 lines
65 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.stream {
|
|
import flash.events.Event;
|
|
import flash.events.TimerEvent;
|
|
import flash.utils.Dictionary;
|
|
import flash.utils.getTimer;
|
|
import flash.utils.Timer;
|
|
import org.mangui.hls.constant.HLSLoaderTypes;
|
|
import org.mangui.hls.constant.HLSPlayStates;
|
|
import org.mangui.hls.constant.HLSSeekMode;
|
|
import org.mangui.hls.constant.HLSSeekStates;
|
|
import org.mangui.hls.constant.HLSTypes;
|
|
import org.mangui.hls.controller.AudioTrackController;
|
|
import org.mangui.hls.controller.LevelController;
|
|
import org.mangui.hls.event.HLSEvent;
|
|
import org.mangui.hls.event.HLSMediatime;
|
|
import org.mangui.hls.flv.FLVTag;
|
|
import org.mangui.hls.HLS;
|
|
import org.mangui.hls.HLSSettings;
|
|
import org.mangui.hls.loader.AltAudioFragmentLoader;
|
|
import org.mangui.hls.loader.FragmentLoader;
|
|
import org.mangui.hls.model.AudioTrack;
|
|
import org.mangui.hls.model.Fragment;
|
|
import org.mangui.hls.model.Level;
|
|
|
|
CONFIG::LOGGING {
|
|
import org.mangui.hls.utils.Log;
|
|
}
|
|
/*
|
|
* intermediate FLV Tag Buffer
|
|
* input : FLV tags retrieved from different fragment loaders (video/alt-audio...)
|
|
* output : provide muxed FLV tags to HLSNetStream
|
|
*/
|
|
public class StreamBuffer {
|
|
private var _hls : HLS;
|
|
private var _fragmentLoader : FragmentLoader;
|
|
private var _altaudiofragmentLoader : AltAudioFragmentLoader;
|
|
/** Timer used to process FLV tags. **/
|
|
private var _timer : Timer;
|
|
private var _audioTags : Vector.<FLVData>, _videoTags : Vector.<FLVData>,_metaTags : Vector.<FLVData>, _headerTags : Vector.<FLVData>;
|
|
private var _audioIdx : uint, _videoIdx : uint, _metaIdx : uint, _headerIdx : uint;
|
|
private var _fragMainLevel : int, _fragMainSN : int;
|
|
// last main frag injected in NetStream
|
|
private var _fragMainLevelNetStream : int, _fragMainSNNetStream : int;
|
|
private var _fragMainInitialContinuity : int,_fragMainInitialStartPosition : Number,_fragMainInitialPTS : Number;
|
|
private var _fragAltAudioLevel : int, _fragAltAudioSN : int;
|
|
private var _fragAltAudioInitialContinuity : int,_fragAltAudioInitialStartPosition : Number,_fragAltAudioInitialPTS : Number;
|
|
private var _fragMainIdx : uint, _fragAltAudioIdx : uint;
|
|
private var _filteringStartIdx : uint;
|
|
/** playlist duration **/
|
|
private var _playlistDuration : Number = 0;
|
|
/** requested start position (absolute position) **/
|
|
private var _seekPositionRequested : Number;
|
|
/** real start position , retrieved from first fragment (absolute position) **/
|
|
private var _seekPositionReal : Number;
|
|
/** start position of first injected tag **/
|
|
private var _seekPositionReached : Boolean;
|
|
private static const MIN_NETSTREAM_BUFFER_SIZE : Number = 3.0;
|
|
private static const MAX_NETSTREAM_BUFFER_SIZE : Number = 4.0;
|
|
/** means that last fragment of a VOD playlist has been loaded */
|
|
private var _reachedEnd : Boolean;
|
|
/* are we using alt-audio ? */
|
|
private var _useAltAudio : Boolean;
|
|
/** playlist sliding (non null for live playlist) **/
|
|
private var _liveSlidingMain : Number;
|
|
private var _liveSlidingAltAudio : Number;
|
|
// these 2 variables are used to compute main and altaudio live playlist sliding
|
|
private var _nextExpectedAbsoluteStartPosMain : Number;
|
|
private var _nextExpectedAbsoluteStartPosAltAudio : Number;
|
|
/** is live loading stalled **/
|
|
private var _liveLoadingStalled : Boolean;
|
|
/** is playback completed **/
|
|
private var _playbackCompleted : Boolean;
|
|
/* last media time data */
|
|
private var _lastPos : Number;
|
|
private var _lastBufLen : Number;
|
|
private var _lastDuration : Number;
|
|
private var _lastMediaTimeUpdate : int;
|
|
/* overlapping fragment stuff */
|
|
private var _overlappingTags : Vector.<FLVTag>;
|
|
private var _overlappingStartPosition : Number;
|
|
private var _overlappingMinPTS : Number;
|
|
|
|
|
|
public function StreamBuffer(hls : HLS, audioTrackController : AudioTrackController, levelController : LevelController) {
|
|
_hls = hls;
|
|
_fragmentLoader = new FragmentLoader(hls, audioTrackController, levelController, this);
|
|
_altaudiofragmentLoader = new AltAudioFragmentLoader(hls, this);
|
|
flushBuffer();
|
|
_timer = new Timer(100, 0);
|
|
_timer.addEventListener(TimerEvent.TIMER, _checkBuffer);
|
|
_hls.addEventListener(HLSEvent.LIVE_LOADING_STALLED, _liveLoadingStalledHandler);
|
|
_hls.addEventListener(HLSEvent.PLAYLIST_DURATION_UPDATED, _playlistDurationUpdated);
|
|
_hls.addEventListener(HLSEvent.LAST_VOD_FRAGMENT_LOADED, _lastVODFragmentLoadedHandler);
|
|
_hls.addEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackChange);
|
|
_hls.addEventListener(HLSEvent.PLAYBACK_COMPLETE, _playbackComplete);
|
|
}
|
|
|
|
public function dispose() : void {
|
|
flushBuffer();
|
|
_hls.removeEventListener(HLSEvent.LIVE_LOADING_STALLED, _liveLoadingStalledHandler);
|
|
_hls.removeEventListener(HLSEvent.PLAYLIST_DURATION_UPDATED, _playlistDurationUpdated);
|
|
_hls.removeEventListener(HLSEvent.LAST_VOD_FRAGMENT_LOADED, _lastVODFragmentLoadedHandler);
|
|
_hls.removeEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackChange);
|
|
_hls.removeEventListener(HLSEvent.PLAYBACK_COMPLETE, _playbackComplete);
|
|
_timer.stop();
|
|
_timer.removeEventListener(TimerEvent.TIMER, _checkBuffer);
|
|
_fragmentLoader.dispose();
|
|
_altaudiofragmentLoader.dispose();
|
|
_fragmentLoader = null;
|
|
_altaudiofragmentLoader = null;
|
|
_hls = null;
|
|
_timer = null;
|
|
}
|
|
|
|
public function stop() : void {
|
|
_fragmentLoader.stop();
|
|
_altaudiofragmentLoader.stop();
|
|
flushBuffer();
|
|
}
|
|
|
|
/*
|
|
* if requested position is available in StreamBuffer, trim buffer
|
|
* and inject from that point
|
|
* if seek position out of buffer, ask fragment loader to retrieve data
|
|
*/
|
|
public function seek(position : Number) : void {
|
|
var loadLevel : Level;
|
|
// cap max position if known playlist duration
|
|
var maxPosition : Number = Number.POSITIVE_INFINITY;
|
|
if(_hls.loadLevel < _hls.levels.length) {
|
|
loadLevel = _hls.levels[_hls.loadLevel];
|
|
// if defined, set max position as being end of playlist - 1 second
|
|
if(!isNaN(loadLevel.duration)) {
|
|
maxPosition = loadLevel.duration-1;
|
|
}
|
|
}
|
|
if (position == -1 && loadLevel && _hls.type == HLSTypes.LIVE) {
|
|
/* If start position not specified for a live stream, follow HLS spec :
|
|
If the EXT-X-ENDLIST tag is not present
|
|
and the client intends to play the media regularly (i.e. in playlist
|
|
order at the nominal playback rate), the client SHOULD NOT
|
|
choose a segment which starts less than three target durations from
|
|
the end of the Playlist file */
|
|
_seekPositionRequested = Math.max(0, loadLevel.duration - 3 * loadLevel.averageduration);
|
|
} else {
|
|
_seekPositionRequested = Math.min(Math.max(position, 0), maxPosition);
|
|
}
|
|
CONFIG::LOGGING {
|
|
Log.debug("seek : requested position:" + position.toFixed(2) + ",seek position:" + _seekPositionRequested.toFixed(2) + ",min/max buffer position:" + min_pos.toFixed(2) + "/" + max_pos.toFixed(2));
|
|
}
|
|
// check if we can seek in buffer
|
|
if (_seekPositionRequested >= min_pos && _seekPositionRequested <= max_pos) {
|
|
_seekPositionReached = false;
|
|
_audioIdx = _videoIdx = _metaIdx = _headerIdx = 0;
|
|
CONFIG::LOGGING {
|
|
Log.debug("seek in buffer");
|
|
// seek position requested is an absolute position, add sliding main to make it absolute
|
|
_seekPositionRequested+= _liveSlidingMain;
|
|
}
|
|
} else {
|
|
// stop any load in progress ...
|
|
_fragmentLoader.stop();
|
|
_altaudiofragmentLoader.stop();
|
|
// seek position is out of buffer : load from fragment
|
|
_liveLoadingStalled = false;
|
|
_fragmentLoader.seek(_seekPositionRequested);
|
|
// check if we need to use alt audio fragment loader
|
|
if (_hls.audioTracks && _hls.audioTracks.length && _hls.audioTrack >= 0 && _hls.audioTracks[_hls.audioTrack].source == AudioTrack.FROM_PLAYLIST) {
|
|
CONFIG::LOGGING {
|
|
Log.info("seek : need to load alt audio track");
|
|
}
|
|
_altaudiofragmentLoader.seek(_seekPositionRequested);
|
|
_useAltAudio = true;
|
|
} else {
|
|
_useAltAudio = false;
|
|
}
|
|
flushBuffer();
|
|
}
|
|
_playbackCompleted = false;
|
|
_lastMediaTimeUpdate = 0;
|
|
_timer.start();
|
|
}
|
|
|
|
public function appendTags(fragmentType : int, fragLevel : int, fragSN : int, tags : Vector.<FLVTag>, min_pts : Number, max_pts : Number, continuity : int, startPosition : Number) : void {
|
|
// compute playlist sliding here : it is the difference between expected start position and real start position
|
|
var sliding:Number;
|
|
var nextRelativeStartPos: Number = startPosition + (max_pts - min_pts) / 1000;
|
|
var headerAppended : Boolean = false, metaAppended : Boolean = false;
|
|
// compute sliding in case of live playlist, or in case of VoD playlist that slided in the past (live sliding ended playlist)
|
|
var computeSliding : Boolean = (_hls.type == HLSTypes.LIVE || _liveSlidingMain || _liveSlidingAltAudio);
|
|
|
|
var fragIdx : int;
|
|
if(fragmentType == HLSLoaderTypes.FRAGMENT_MAIN) {
|
|
sliding = _liveSlidingMain;
|
|
// if a new fragment is being appended
|
|
if(fragLevel != _fragMainLevel || fragSN != _fragMainSN) {
|
|
if(continuity == _fragMainInitialContinuity) {
|
|
/// don't check overlapping on live playlists or VoD playlists that slided in the past
|
|
if(!computeSliding) {
|
|
// this whole part of the code is here to handle playlists with fragments overlapping with each other (when switching level usually)
|
|
// for example let's imagine we have data already buffered, and new tags are appended, overlapping with existing buffer :
|
|
// existing ------------------------------------------->
|
|
// new tags K--K----K----->
|
|
//
|
|
// here we are trying to filter new tags to convert them into the following
|
|
// existing ------------------------------------------->
|
|
// filtered new tags K----->
|
|
// this is more complex than it seems, as we need to deal with keyframes for video and all that kind of stuff
|
|
// video filtered new tags should start with a keyframe ...
|
|
// if we already have overlapping tags, adjust startPosition/minPTS to match the one from first tag
|
|
// we only trigger this logic if detected overlapping is more than 500 ms
|
|
|
|
if(_overlappingTags.length) {
|
|
startPosition = _overlappingStartPosition;
|
|
min_pts = _overlappingMinPTS;
|
|
nextRelativeStartPos = startPosition + (max_pts - min_pts) / 1000;
|
|
} else {
|
|
_overlappingStartPosition = startPosition;
|
|
_overlappingMinPTS = min_pts;
|
|
}
|
|
var end : Number = startPosition+(max_pts-min_pts)/1000;
|
|
// check if any overlapping occurs, let 500ms tolerance
|
|
if(startPosition+0.5 < max_audio_pos && startPosition+0.5 < max_video_pos) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("fragment overlapping with buffered one, start/end/max_audio_pos/max_video_pos:"+startPosition.toFixed(3)+"/"+ end.toFixed(3) + "/" + max_audio_pos.toFixed(3)+"/"+max_video_pos.toFixed(3));
|
|
}
|
|
_overlappingTags = _overlappingTags.concat(tags);
|
|
// filter out overlapping tags
|
|
tags = filterOverlappingTags(_overlappingTags, max_pos - startPosition);
|
|
if(tags.length == 0) {
|
|
// not enough data, cannot filter overlapping tags, don't push those tags
|
|
// fragloader will call us back later with more data
|
|
return;
|
|
} else {
|
|
// reset overlapping tags
|
|
_overlappingTags = new Vector.<FLVTag>();
|
|
// adjust startPosition and min_pts
|
|
startPosition = max_pos;
|
|
min_pts = tags[0].pts;
|
|
// recompute value
|
|
nextRelativeStartPos = startPosition + (max_pts - min_pts) / 1000;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// different continuity counter, reset any overlapping tags
|
|
if(_overlappingTags.length) {
|
|
// reset overlapping tags
|
|
_overlappingTags = new Vector.<FLVTag>();
|
|
}
|
|
}
|
|
_fragMainLevel = fragLevel;
|
|
_fragMainSN = fragSN;
|
|
_fragMainIdx++;
|
|
// compute sliding if needed
|
|
if(computeSliding) {
|
|
// if not -1 : it is not the first appending for this fragment type : we can compute playlist sliding
|
|
if(_nextExpectedAbsoluteStartPosMain !=-1) {
|
|
// if same continuity counter, sliding can be computed using PTS, it will be more accurate
|
|
if(continuity == _fragMainInitialContinuity) {
|
|
sliding = _liveSlidingMain = _fragMainInitialStartPosition + (min_pts-_fragMainInitialPTS)/1000 - startPosition;
|
|
CONFIG::LOGGING {
|
|
var debugString : String = "sliding/min_pts/_fragMainInitialPTS/startPosition/_fragMainInitialStartPosition:" + sliding + '/' + min_pts + '/' + _fragMainInitialPTS + '/' + startPosition.toFixed(3) + '/' + _fragMainInitialStartPosition;
|
|
// allow negative but small values, that could happen because of frag drift
|
|
if(sliding < -1) {
|
|
Log.warn('negative sliding:'+debugString);
|
|
} else {
|
|
Log.debug2('sliding on same cc:'+ debugString);
|
|
}
|
|
}
|
|
} else {
|
|
sliding = _liveSlidingMain = _nextExpectedAbsoluteStartPosMain - startPosition;
|
|
CONFIG::LOGGING {
|
|
Log.debug2('sliding on different cc: expectedAbsoluteStart/relativeStart/sliding' + _nextExpectedAbsoluteStartPosMain.toFixed(3) + '/' + startPosition.toFixed(3) + '/' + sliding.toFixed(3));
|
|
}
|
|
}
|
|
} else {
|
|
_fragMainInitialStartPosition = startPosition;
|
|
_fragMainInitialPTS = min_pts;
|
|
_fragMainInitialContinuity = continuity;
|
|
}
|
|
}
|
|
CONFIG::LOGGING {
|
|
Log.debug('new main frag,start/sliding/cc/idx:' + startPosition.toFixed(3) + '/' + sliding.toFixed(3) + '/' + continuity + '/' + _fragMainIdx);
|
|
}
|
|
}
|
|
fragIdx = _fragMainIdx;
|
|
/* update next expected absolute start pos at this point : absolute = relative + sliding
|
|
useful to compute sliding when discontinuity occurs
|
|
*/
|
|
_nextExpectedAbsoluteStartPosMain = nextRelativeStartPos + sliding;
|
|
} else {
|
|
sliding = _liveSlidingAltAudio;
|
|
// if a new fragment is being appended
|
|
if(fragLevel != _fragAltAudioLevel || fragSN != _fragAltAudioSN) {
|
|
_fragAltAudioLevel = fragLevel;
|
|
_fragAltAudioSN = fragSN;
|
|
_fragAltAudioIdx++;
|
|
// compute sliding if needed
|
|
if(computeSliding) {
|
|
// if -1 : it is not the first appending for this fragment type : we can compute playlist sliding
|
|
if(_nextExpectedAbsoluteStartPosAltAudio !=-1) {
|
|
// if same continuity counter, sliding can be computed using PTS, it will be more accurate
|
|
if(continuity == _fragAltAudioInitialContinuity) {
|
|
sliding = _liveSlidingAltAudio = _fragAltAudioInitialStartPosition + (min_pts - _fragAltAudioInitialPTS)/1000 - startPosition;
|
|
} else {
|
|
sliding = _liveSlidingAltAudio = _nextExpectedAbsoluteStartPosAltAudio - startPosition;
|
|
}
|
|
} else {
|
|
_fragAltAudioInitialStartPosition = startPosition;
|
|
_fragAltAudioInitialPTS = min_pts;
|
|
_fragAltAudioInitialContinuity = continuity;
|
|
}
|
|
}
|
|
CONFIG::LOGGING {
|
|
Log.debug('new altaudio frag,start/sliding/idx:' + startPosition + '/' + sliding + '/' + _fragAltAudioIdx);
|
|
}
|
|
}
|
|
fragIdx = _fragAltAudioIdx;
|
|
/* update next expected absolute start pos at this point : absolute = relative + sliding
|
|
useful to compute sliding when discontinuity occurs
|
|
*/
|
|
_nextExpectedAbsoluteStartPosAltAudio = nextRelativeStartPos + sliding;
|
|
}
|
|
|
|
for each (var tag : FLVTag in tags) {
|
|
// CONFIG::LOGGING {
|
|
// Log.debug2('append type/dts/pts:' + tag.typeString + '/' + tag.dts + '/' + tag.pts);
|
|
// }
|
|
var pos : Number = startPosition + (tag.pts - min_pts) / 1000;
|
|
var tagData : FLVData = new FLVData(tag, pos, sliding, continuity, fragmentType, fragIdx, fragLevel, fragSN);
|
|
switch(tag.type) {
|
|
case FLVTag.DISCONTINUITY:
|
|
case FLVTag.AAC_HEADER:
|
|
case FLVTag.AVC_HEADER:
|
|
_headerTags.push(tagData);
|
|
headerAppended = true;
|
|
break;
|
|
case FLVTag.AAC_RAW:
|
|
case FLVTag.MP3_RAW:
|
|
_audioTags.push(tagData);
|
|
break;
|
|
case FLVTag.AVC_NALU:
|
|
_videoTags.push(tagData);
|
|
break;
|
|
case FLVTag.METADATA:
|
|
_metaTags.push(tagData);
|
|
metaAppended = true;
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
|
|
if(headerAppended) {
|
|
_headerTags = _headerTags.sort(compareTags);
|
|
}
|
|
|
|
if(metaAppended) {
|
|
_metaTags = _metaTags.sort(compareTags);
|
|
}
|
|
|
|
|
|
if (_hls.seekState == HLSSeekStates.SEEKING) {
|
|
/* if in seeking mode, force timer start here, this could help reducing the seek time by 100ms */
|
|
_timer.start();
|
|
}
|
|
}
|
|
|
|
/** Return current media position **/
|
|
public function get position() : Number {
|
|
switch(_hls.seekState) {
|
|
case HLSSeekStates.SEEKING:
|
|
// _seekPositionRequested is an absolute position, convert it to relative by substracting sliding
|
|
return _seekPositionRequested - _liveSlidingMain;
|
|
case HLSSeekStates.SEEKED:
|
|
case HLSSeekStates.IDLE:
|
|
default:
|
|
/** Relative playback position = Absolute Position (which is Absolute seek position + NetStream playback time) - playlist sliding **/
|
|
var pos: Number = _seekPositionReal + _hls.stream.time - _liveSlidingMain;
|
|
if(isNaN(pos)) {
|
|
pos = 0;
|
|
}
|
|
return pos;
|
|
}
|
|
}
|
|
|
|
public function get liveSlidingMain() : Number {
|
|
return _liveSlidingMain;
|
|
}
|
|
|
|
public function get liveSlidingAltAudio() : Number {
|
|
return _liveSlidingAltAudio;
|
|
}
|
|
|
|
public function get reachedEnd() : Boolean {
|
|
return _reachedEnd;
|
|
}
|
|
|
|
public function get liveLoadingStalled() : Boolean {
|
|
return _liveLoadingStalled;
|
|
}
|
|
|
|
public function flushBuffer() : void {
|
|
_audioTags = new Vector.<FLVData>();
|
|
_videoTags = new Vector.<FLVData>();
|
|
_metaTags = new Vector.<FLVData>();
|
|
_headerTags = new Vector.<FLVData>();
|
|
_overlappingTags = new Vector.<FLVTag>();
|
|
_fragMainLevel = _fragAltAudioLevel = _fragMainLevelNetStream = -1;
|
|
_fragMainSN = _fragAltAudioSN = _fragMainSNNetStream = 0;
|
|
FLVData.refPTSMain = FLVData.refPTSAltAudio = NaN;
|
|
_audioIdx = _videoIdx = _metaIdx = _headerIdx = 0;
|
|
_fragMainIdx = _fragAltAudioIdx = 0;
|
|
_seekPositionReached = false;
|
|
_reachedEnd = false;
|
|
_liveSlidingMain = _liveSlidingAltAudio = 0;
|
|
_nextExpectedAbsoluteStartPosMain = _nextExpectedAbsoluteStartPosAltAudio = -1;
|
|
CONFIG::LOGGING {
|
|
Log.debug("StreamBuffer flushed");
|
|
}
|
|
}
|
|
|
|
private function flushAudio() : void {
|
|
// flush audio buffer and AAC HEADER tags (if any)
|
|
_audioTags = new Vector.<FLVData>();
|
|
_audioIdx = 0;
|
|
FLVData.refPTSAltAudio = NaN;
|
|
_nextExpectedAbsoluteStartPosAltAudio = -1;
|
|
_liveSlidingAltAudio = 0;
|
|
var _filteredHeaderTags : Vector.<FLVData> = _headerTags.filter(filterAACHeader);
|
|
_headerIdx -= (_headerTags.length - _filteredHeaderTags.length);
|
|
}
|
|
|
|
private function filterAACHeader(item : FLVData, index : int, vector : Vector.<FLVData>) : Boolean {
|
|
return (item.tag.type != FLVTag.AAC_HEADER);
|
|
}
|
|
|
|
public function flushLastFragment(fragLevel : int, fragSN : int) : Boolean {
|
|
/* check if we already started injecting that fragment in NetStream
|
|
if that is the case, then we cannot flush seamlessly, return false
|
|
*/
|
|
if(fragLevel == _fragMainLevelNetStream && fragSN == _fragMainSNNetStream) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("StreamBuffer.flushLastFragment, cannot flush, tag already pushed in NetStream");
|
|
}
|
|
return false;
|
|
} else if(fragLevel == _fragMainLevel && fragSN == _fragMainSN) {
|
|
CONFIG::LOGGING {
|
|
Log.warn("StreamBuffer.flushLastFragment, clip end of StreamBuffer");
|
|
}
|
|
// flush StreamBuffer if needed
|
|
_headerTags = _headerTags.filter(filterlastFragment);
|
|
_videoTags = _videoTags.filter(filterlastFragment);
|
|
_audioTags = _audioTags.filter(filterlastFragment);
|
|
_metaTags = _metaTags.filter(filterlastFragment);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function filterlastFragment(item : FLVData, index : int, vector : Vector.<FLVData>) : Boolean {
|
|
// keep if return true. we want to keep everything but last main fragment appended tags
|
|
return (!(item.loaderType == HLSLoaderTypes.FRAGMENT_MAIN && item.fragLevel == _fragMainLevel && item.fragSN == _fragMainSN));
|
|
}
|
|
|
|
/* compare two tags, smallest continuity
|
|
* then smallest pts. then discontinuity then aac/avc header then metadata, then others
|
|
*
|
|
return a negative number, if x should appear before y in the sorted sequence
|
|
retun 0, if x equals y
|
|
return a positive number, if x should appear after y in the sorted sequence*
|
|
*
|
|
* */
|
|
private function compareTags(x : FLVData, y : FLVData) : Number {
|
|
if (x.continuity != y.continuity) {
|
|
return (x.continuity - y.continuity);
|
|
} else {
|
|
if (x.tag.dts != y.tag.dts) {
|
|
return (x.tag.dts - y.tag.dts);
|
|
} else {
|
|
return (gettagrank(x.tag) - gettagrank(y.tag));
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
helper function used to sort tags, lower values have highest priority
|
|
*/
|
|
private function gettagrank(tag : FLVTag) : uint {
|
|
switch(tag.type) {
|
|
case FLVTag.DISCONTINUITY:
|
|
return 0;
|
|
case FLVTag.METADATA:
|
|
return 1;
|
|
case FLVTag.AVC_HEADER:
|
|
case FLVTag.AAC_HEADER:
|
|
return 2;
|
|
default:
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
private function get audioExpected() : Boolean {
|
|
return (_fragmentLoader.audioExpected || _useAltAudio);
|
|
}
|
|
|
|
private function get videoExpected() : Boolean {
|
|
return _fragmentLoader.videoExpected;
|
|
}
|
|
|
|
public function get audioBufferLength() : Number {
|
|
return getbuflen(_audioTags, _audioIdx);
|
|
}
|
|
|
|
public function get videoBufferLength() : Number {
|
|
return getbuflen(_videoTags, _videoIdx);
|
|
}
|
|
|
|
public function get bufferLength() : Number {
|
|
switch(_hls.seekState) {
|
|
case HLSSeekStates.SEEKING:
|
|
/* max_pos is a relative max, seekPositionRequested is absolute. we need to add _liveSlidingMain
|
|
in order to compare apple to apple */
|
|
return Math.max(0, max_pos + _liveSlidingMain - _seekPositionRequested);
|
|
case HLSSeekStates.SEEKED:
|
|
if (audioExpected) {
|
|
if (videoExpected) {
|
|
return Math.min(audioBufferLength, videoBufferLength);
|
|
} else {
|
|
return audioBufferLength;
|
|
}
|
|
} else {
|
|
return videoBufferLength;
|
|
}
|
|
case HLSSeekStates.IDLE:
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
public function get backBufferLength() : Number {
|
|
if (min_pos != Number.POSITIVE_INFINITY) {
|
|
return (position - min_pos);
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/** Return the quality level of the next played fragment **/
|
|
public function get nextLevel() : int {
|
|
if(_videoIdx < _videoTags.length) {
|
|
return _videoTags[_videoIdx].fragLevel;
|
|
} else {
|
|
return _hls.currentLevel;
|
|
}
|
|
};
|
|
|
|
/* set quality level for next loaded fragment (-1 for automatic level selection) */
|
|
public function set nextLevel(level : int) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("StreamBuffer:set nextLevel:" + level);
|
|
}
|
|
if(_videoIdx < _videoTags.length) {
|
|
/* remove tags not injected into NetStream,
|
|
as tags are injected on fragment boundary, tags not injected in NetStream corresponds
|
|
with next fragment tags
|
|
*/
|
|
|
|
//find idx for next tag not matching with _fragMainLevelNetStream/_fragMainSNNetStream
|
|
for(var i : int = _videoIdx; i < _videoTags.length ; i++) {
|
|
var tagData : FLVData = _videoTags[i];
|
|
if(tagData.fragLevel != _fragMainLevelNetStream ||tagData.fragSN != _fragMainSNNetStream) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("first video FLV tag from next frag is @ " + i + " of [" + _videoIdx + "," + (_videoTags.length-1) + "]");
|
|
}
|
|
var lastLevel : Number = tagData.fragLevel;
|
|
var lastSN : Number = tagData.fragSN;
|
|
// if we want to seek to the right fragment, we need to set lastFrag with lastSN-1
|
|
var lastFrag : Fragment = _hls.levels[lastLevel].getFragmentfromSeqNum(lastSN-1);
|
|
if(lastFrag) {
|
|
CONFIG::LOGGING {
|
|
Log.debug("StreamBuffer:lastFrag defined, flush buffer and seekFromLastFrag");
|
|
}
|
|
// flush all video tags not injected into NetStream, from next fragment onwards
|
|
_videoTags.splice(i, _videoTags.length-i);
|
|
// flush all audio tags from next main fragment onwards
|
|
_filteringStartIdx = _audioIdx;
|
|
_audioTags = _audioTags.filter(filternextFragments);
|
|
// flush all header tags from next main fragment onwards
|
|
_filteringStartIdx = _headerIdx;
|
|
_headerTags = _headerTags.filter(filternextFragments);
|
|
// flush all metadata tags from next main fragment onwards
|
|
_filteringStartIdx = _metaIdx;
|
|
_metaTags = _metaTags.filter(filternextFragments);
|
|
// stop any load in progress ...
|
|
_fragmentLoader.stop();
|
|
// seek position is out of buffer : load after fragment
|
|
_fragmentLoader.seekFromLastFrag(lastFrag);
|
|
// reset overlapping tags, if any
|
|
_overlappingTags = new Vector.<FLVTag>();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
CONFIG::LOGGING {
|
|
Log.debug("StreamBuffer:can't find tags associated to next frag, don't flush anything");
|
|
}
|
|
}
|
|
};
|
|
|
|
private function filternextFragments(item : FLVData, index : int, vector : Vector.<FLVData>) : Boolean {
|
|
// keep if return true
|
|
// we want to keep only if alt audio loader OR
|
|
// index less than start filtering index (i.e. we want to keep backbuffer) OR
|
|
// matching with current fragment appended tags
|
|
return (item.loaderType == HLSLoaderTypes.FRAGMENT_ALTAUDIO ||
|
|
index < _filteringStartIdx ||
|
|
(item.fragLevel == _fragMainLevelNetStream && item.fragSN == _fragMainSNNetStream));
|
|
}
|
|
|
|
/** StreamBuffer Timer, responsible of
|
|
* reporting media time event
|
|
* injecting tags into NetStream
|
|
* clipping backbuffer
|
|
*/
|
|
private function _checkBuffer(e : Event) : void {
|
|
var pos : Number = position;
|
|
var bufLen : Number = _hls.stream.bufferLength;
|
|
var watched : Number = (_hls.stream as HLSNetStream).watched;
|
|
var currentTime : int = getTimer();
|
|
var dispatchTimeUpdate : Boolean = (currentTime - _lastMediaTimeUpdate > HLSSettings.mediaTimePeriod);
|
|
var duration : Number;
|
|
if(_playbackCompleted) {
|
|
// as playback is completed, align stream duration to last playback position
|
|
duration = position;
|
|
} else {
|
|
duration = _playlistDuration;
|
|
}
|
|
// dispatch media time event only if position/buffer or playlist duration has changed
|
|
if(dispatchTimeUpdate && (pos != _lastPos || bufLen != _lastBufLen || duration != _lastDuration) ) {
|
|
_hls.dispatchEvent(new HLSEvent(HLSEvent.MEDIA_TIME, new HLSMediatime(pos, duration, bufLen, backBufferLength, _liveSlidingMain, _liveSlidingAltAudio,watched)));
|
|
_lastPos = pos;
|
|
_lastDuration = duration;
|
|
_lastBufLen = bufLen;
|
|
_lastMediaTimeUpdate = currentTime;
|
|
}
|
|
|
|
var netStreamBuffer : Number = (_hls.stream as HLSNetStream).netStreamBufferLength;
|
|
/* only append tags if seek position has been reached, otherwise wait for more tags to come
|
|
* this is to ensure that accurate seeking will work appropriately
|
|
*/
|
|
CONFIG::LOGGING {
|
|
Log.debug2("position/duration/buffer/backBuffer/audio/video/NetStream bufferLength/audioExpected/videoExpected:" + position.toFixed(2) + "/" + duration.toFixed(2) + "/" + bufLen.toFixed(2) + "/" + backBufferLength.toFixed(2) + "/" + audioBufferLength.toFixed(2) + "/" + videoBufferLength.toFixed(2) + "/" + netStreamBuffer.toFixed(2) + "/" + audioExpected + "/" + videoExpected);
|
|
}
|
|
|
|
var tagDuration : Number = 0;
|
|
if (_seekPositionReached) {
|
|
if (netStreamBuffer < MIN_NETSTREAM_BUFFER_SIZE && _hls.playbackState != HLSPlayStates.IDLE) {
|
|
tagDuration = MAX_NETSTREAM_BUFFER_SIZE - netStreamBuffer;
|
|
}
|
|
} else {
|
|
/* seek position not reached yet.
|
|
* check if buffer max absolute position is greater than requested seek position
|
|
* if it is the case, then we can start injecting tags in NetStream
|
|
* max_pos is a relative max, here we need to compare against absolute max position, so
|
|
* we need to add _liveSlidingMain to convert from relative to absolute
|
|
*/
|
|
if ((max_pos+_liveSlidingMain) >= _seekPositionRequested) {
|
|
// inject enough tags to reach seek position
|
|
tagDuration = _seekPositionRequested + MAX_NETSTREAM_BUFFER_SIZE - min_min_pos;
|
|
}
|
|
}
|
|
if (tagDuration > 0) {
|
|
var data : Vector.<FLVData> = shiftMultipleTags(tagDuration);
|
|
if (!_seekPositionReached) {
|
|
data = seekFilterTags(data, _seekPositionRequested);
|
|
if(data.length) {
|
|
_seekPositionReached = true;
|
|
}
|
|
}
|
|
|
|
var tags : Vector.<FLVTag> = new Vector.<FLVTag>();
|
|
for each (var flvdata : FLVData in data) {
|
|
if(flvdata.loaderType == HLSLoaderTypes.FRAGMENT_MAIN) {
|
|
_fragMainLevelNetStream = flvdata.fragLevel;
|
|
_fragMainSNNetStream = flvdata.fragSN;
|
|
}
|
|
tags.push(flvdata.tag);
|
|
}
|
|
if (tags.length) {
|
|
CONFIG::LOGGING {
|
|
var flvdata0 : FLVData = data[0];
|
|
Log.debug('appending first level/sn/type/dts/pts/position:' + flvdata0.fragLevel + '/' + flvdata0.fragSN + '/' + flvdata0.tag.typeString + '/' + flvdata0.tag.dts + '/' + flvdata0.tag.pts + '/' + (flvdata0.positionAbsolute - _liveSlidingMain).toFixed(2));
|
|
flvdata0 = data[data.length-1];
|
|
Log.debug('appending last level/sn/type/dts/pts/position:' + flvdata0.fragLevel + '/' + flvdata0.fragSN + '/' + flvdata0.tag.typeString + '/' + flvdata0.tag.dts + '/' + flvdata0.tag.pts + '/' + (flvdata0.positionAbsolute - _liveSlidingMain).toFixed(2));
|
|
}
|
|
(_hls.stream as HLSNetStream).appendTags(tags);
|
|
}
|
|
}
|
|
// clip backbuffer if needed
|
|
if (HLSSettings.maxBackBufferLength > 0) {
|
|
_clipBackBuffer(HLSSettings.maxBackBufferLength);
|
|
}
|
|
}
|
|
|
|
/* filter incoming tags overlapping with existing buffer */
|
|
private function filterOverlappingTags(tags : Vector.<FLVTag>, overlap : Number) : Vector.<FLVTag> {
|
|
var aacIdx : int,avcIdx : int,disIdx : int,metIdx : int, videoIdx : int,audioIdx : int, lastIdx : int;
|
|
var first_pts : Number = tags[0].pts + 1000*overlap;
|
|
var audio_dts : Number, video_dts : Number;
|
|
var flvData : FLVData = getNextTag(false);
|
|
if(flvData) {
|
|
first_pts = Math.max(first_pts,flvData.tag.pts);
|
|
}
|
|
var filteredTags : Vector.<FLVTag>= new Vector.<FLVTag>();
|
|
var filteredDuration : Number = 0;
|
|
aacIdx = avcIdx = disIdx = metIdx = videoIdx = audioIdx = lastIdx = -1;
|
|
audio_dts = video_dts = Number.POSITIVE_INFINITY;
|
|
for (var i : int = 0; i < tags.length; i++) {
|
|
var tag : FLVTag = tags[i];
|
|
switch(tag.type) {
|
|
case FLVTag.DISCONTINUITY:
|
|
// retrieve last DISCONTINUITY tag before first_pts
|
|
if (tag.pts <= first_pts)
|
|
disIdx = i;
|
|
break;
|
|
case FLVTag.METADATA:
|
|
// retrieve last METADATA tag before first_pts
|
|
if (tag.pts <= first_pts)
|
|
metIdx = i;
|
|
break;
|
|
case FLVTag.AAC_HEADER:
|
|
// retrieve last AAC_HEADER tag before first_pts
|
|
if (tag.pts <= first_pts)
|
|
aacIdx = i;
|
|
break;
|
|
case FLVTag.AVC_HEADER:
|
|
// retrieve last AVC_HEADER tag before first_pts
|
|
if (tag.pts <= first_pts)
|
|
avcIdx = i;
|
|
break;
|
|
case FLVTag.AVC_NALU:
|
|
// retrieve first keyframe after first_pts
|
|
if (videoIdx == -1 && tag.pts > first_pts && tag.keyframe) {
|
|
lastIdx = videoIdx = i;
|
|
video_dts = tag.dts;
|
|
CONFIG::LOGGING {
|
|
Log.debug("found AVC_NALU_K after overlap @PTS:" + tag.pts);
|
|
}
|
|
} else if (videoIdx >=0) {
|
|
lastIdx = i;
|
|
}
|
|
break;
|
|
case FLVTag.MP3_RAW:
|
|
case FLVTag.AAC_RAW:
|
|
// retrieve first frame after first_pts
|
|
if (audioIdx == -1 && tag.pts > first_pts) {
|
|
lastIdx = audioIdx = i;
|
|
audio_dts = tag.dts;
|
|
} else if (audioIdx >=0) {
|
|
lastIdx = i;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(lastIdx >=0) {
|
|
filteredDuration = tags[lastIdx].pts - first_pts;
|
|
CONFIG::LOGGING {
|
|
Log.debug("filterOverlappingTags : filtered duration:" + Math.round(filteredDuration));
|
|
}
|
|
}
|
|
|
|
// only push tags, if we found more than 500ms of tags AND
|
|
// (audio not expected OR audio tags found) AND
|
|
// (video not expected OR video tags found)
|
|
if((!audioExpected || audioIdx>=0) &&
|
|
(!videoExpected || videoIdx>=0) &&
|
|
filteredDuration > 500) {
|
|
// modify PTS for DISCONTINUITY/METADATA/AAC_HEADER/AVC_HEADER tag
|
|
// and push as filtered tag
|
|
if(disIdx >=0) {
|
|
tags[disIdx].pts = tags[disIdx].dts = Math.min(audio_dts,video_dts);
|
|
filteredTags.push(tags[disIdx]);
|
|
}
|
|
|
|
if(metIdx >=0) {
|
|
tags[metIdx].pts = tags[metIdx].dts = Math.min(audio_dts,video_dts);
|
|
filteredTags.push(tags[metIdx]);
|
|
}
|
|
if(aacIdx >=0) {
|
|
tags[aacIdx].pts = tags[aacIdx].dts = audio_dts;
|
|
filteredTags.push(tags[aacIdx]);
|
|
}
|
|
if(avcIdx >=0) {
|
|
tags[avcIdx].pts = tags[avcIdx].dts = video_dts;
|
|
filteredTags.push(tags[avcIdx]);
|
|
}
|
|
|
|
CONFIG::LOGGING {
|
|
Log.debug("filterOverlappingTags : header appended:" + filteredTags.length);
|
|
}
|
|
|
|
for (i= 0; i < tags.length; i++) {
|
|
tag = tags[i];
|
|
switch(tag.type) {
|
|
case FLVTag.AVC_NALU:
|
|
// push all AVC NALU located after videoIdx
|
|
if (videoIdx >= 0 && i >= videoIdx) {
|
|
filteredTags.push(tag);
|
|
}
|
|
break;
|
|
case FLVTag.MP3_RAW:
|
|
case FLVTag.AAC_RAW:
|
|
// push all audio tags located after audioIdx
|
|
if (audioIdx >=0 && i >= audioIdx) {
|
|
filteredTags.push(tag);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
CONFIG::LOGGING {
|
|
Log.debug("filterOverlappingTags : tags appended:" + filteredTags.length);
|
|
}
|
|
} else {
|
|
CONFIG::LOGGING {
|
|
Log.debug("filterOverlappingTags: only overlapping tags found");
|
|
}
|
|
}
|
|
CONFIG::LOGGING {
|
|
Log.debug("filterOverlappingTags: filtered " + filteredTags.length + " out of " + tags.length);
|
|
// for (i= 0; i < filteredTags.length; i++) {
|
|
// Log.debug("filteredOverlappingTags: filtered " + filteredTags[i].typeString + '/' + filteredTags[i].dts + '/' + filteredTags[i].pts);
|
|
// }
|
|
}
|
|
return filteredTags;
|
|
}
|
|
|
|
|
|
/* filter/tweak tags to seek accurately into the stream */
|
|
private function seekFilterTags(tags : Vector.<FLVData>, absoluteStartPosition : Number) : Vector.<FLVData> {
|
|
var aacIdx : int,avcIdx : int,disIdx : int,metIdxMain : int,metIdxAltAudio : int, keyIdx : int,lastIdx : int;
|
|
aacIdx = avcIdx = disIdx = metIdxMain = metIdxAltAudio = keyIdx = lastIdx = -1;
|
|
var filteredTags : Vector.<FLVData>= new Vector.<FLVData>();
|
|
var idx2Clone : Vector.<int> = new Vector.<int>();
|
|
|
|
// loop through all tags and find index position of header tags located before start position
|
|
while(lastIdx ==-1) {
|
|
for (var i : int = 0; i < tags.length; i++) {
|
|
var data : FLVData = tags[i];
|
|
if (data.positionAbsolute <= absoluteStartPosition) {
|
|
lastIdx = i;
|
|
// current tag is before requested start position
|
|
// grab AVC/AAC/DISCONTINUITY/METADATA/KEYFRAMES tag located just before
|
|
switch(data.tag.type) {
|
|
case FLVTag.DISCONTINUITY:
|
|
disIdx = i;
|
|
break;
|
|
case FLVTag.METADATA:
|
|
if(data.loaderType == HLSLoaderTypes.FRAGMENT_MAIN) {
|
|
metIdxMain = i;
|
|
} else {
|
|
metIdxAltAudio = i;
|
|
}
|
|
break;
|
|
case FLVTag.AAC_HEADER:
|
|
aacIdx = i;
|
|
break;
|
|
case FLVTag.AVC_HEADER:
|
|
avcIdx = i;
|
|
break;
|
|
case FLVTag.AVC_NALU:
|
|
if (data.tag.keyframe) keyIdx = i;
|
|
default:
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if(lastIdx == -1) {
|
|
// all filtered tags are located after seek position ... tweak start position
|
|
CONFIG::LOGGING {
|
|
Log.warn("seekFilterTags: startPosition > first tag position:" + absoluteStartPosition.toFixed(3) + '/' + tags[0].positionAbsolute.toFixed(3));
|
|
}
|
|
if(absoluteStartPosition != tags[0].positionAbsolute) {
|
|
absoluteStartPosition = tags[0].positionAbsolute;
|
|
} else {
|
|
// nothing found yet, let's return empty an array
|
|
return filteredTags;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (keyIdx == -1) {
|
|
// audio only stream, no keyframe. we can seek accurately
|
|
keyIdx = lastIdx;
|
|
}
|
|
|
|
var firstIdx : int;
|
|
if (HLSSettings.seekMode == HLSSeekMode.ACCURATE_SEEK) {
|
|
// start injecting from last tag before start position
|
|
firstIdx = lastIdx;
|
|
} else {
|
|
// start injecting from keyframe tag
|
|
firstIdx = keyIdx;
|
|
}
|
|
|
|
var firstPTS : Number = tags[firstIdx].tag.pts;
|
|
var firstDTS : Number = tags[firstIdx].tag.dts;
|
|
_seekPositionReal = tags[firstIdx].positionAbsolute;
|
|
|
|
// inject discontinuity/metadata/AVC header/AAC header if available
|
|
if (disIdx != -1) idx2Clone.push(disIdx);
|
|
if (metIdxMain != -1) idx2Clone.push(metIdxMain);
|
|
if (metIdxAltAudio != -1) idx2Clone.push(metIdxAltAudio);
|
|
if (aacIdx != -1) idx2Clone.push(aacIdx);
|
|
if (avcIdx != -1) idx2Clone.push(avcIdx);
|
|
|
|
for each (i in idx2Clone) {
|
|
data = tags[i];
|
|
var tagclone : FLVTag = data.tag.clone();
|
|
// use keyframe DTS as reference timestamp
|
|
tagclone.dts = firstDTS;
|
|
if(tagclone.type == FLVTag.AVC_HEADER) {
|
|
// for video, PTS could be different than PTS, use correct PTS value
|
|
tagclone.pts = firstPTS;
|
|
} else {
|
|
// for other tags, force PTS = DTS
|
|
tagclone.pts = firstDTS;
|
|
}
|
|
var dataclone : FLVData = new FLVData(tagclone, _seekPositionReal, 0, data.continuity, data.loaderType, data.fragIdx, data.fragLevel, data.fragSN);
|
|
filteredTags.push(dataclone);
|
|
}
|
|
|
|
// inject tags from nearest keyframe to start position
|
|
for (i = keyIdx; i < lastIdx; i++) {
|
|
data = tags[i];
|
|
// if accurate seek mode, adjust tags with pts and position from start position
|
|
if (HLSSettings.seekMode == HLSSeekMode.ACCURATE_SEEK) {
|
|
// only push NALU to be able to reconstruct frame at seek position
|
|
if (data.tag.type == FLVTag.AVC_NALU) {
|
|
tagclone = data.tag.clone();
|
|
tagclone.pts = tagclone.dts = firstPTS;
|
|
dataclone = new FLVData(tagclone, _seekPositionReal, 0, data.continuity, data.loaderType, data.fragIdx, data.fragLevel, data.fragSN);
|
|
filteredTags.push(dataclone);
|
|
}
|
|
} else {
|
|
// keyframe seeking : push straight away
|
|
filteredTags.push(data);
|
|
}
|
|
}
|
|
// tags located after start position, push straight away
|
|
for (i = lastIdx; i < tags.length; i++) {
|
|
filteredTags.push(tags[i]);
|
|
}
|
|
return filteredTags;
|
|
}
|
|
|
|
private function _clipBackBuffer(maxBackBufferLength : Number) : void {
|
|
/* min_pos last video
|
|
* keyframe before current
|
|
maxBackBufferLength position
|
|
* *--------------K-----------*---------------------*----
|
|
* <------------><-----------><-------------------->
|
|
* to be clipped to be kept maxBackBufferLength
|
|
*/
|
|
|
|
// determine clipping position. don't keep negative position (which are out of sliding window and not seekable)
|
|
var clipping_position0 : Number = Math.max(0,position - maxBackBufferLength);
|
|
var clipping_position : Number;
|
|
|
|
if(videoExpected) {
|
|
// find last video keyframe before clipping_position : loop through video tags and find last keyframe before clipping position
|
|
for each (var data : FLVData in _videoTags) {
|
|
if ((data.positionAbsolute - _liveSlidingMain ) <= clipping_position0 && data.tag.keyframe) {
|
|
clipping_position = data.positionAbsolute - _liveSlidingMain;
|
|
}
|
|
}
|
|
|
|
if(isNaN(clipping_position)) {
|
|
return;
|
|
}
|
|
} else {
|
|
clipping_position = clipping_position0;
|
|
}
|
|
// don't clip if clipping position is greater than current position ! this could happen on live stream with long pause
|
|
if(clipping_position >= position) {
|
|
return;
|
|
}
|
|
|
|
// CONFIG::LOGGING {
|
|
// if (clipping_position != clipping_position0) {
|
|
// Log.info("clipping_position/clipping_position0 " + clipping_position + '/' + clipping_position0);
|
|
// }
|
|
// }
|
|
|
|
var clipped_tags : uint = 0;
|
|
|
|
// loop through each tag list and clip tags if out of max back buffer boundary
|
|
while (_audioTags.length && (_audioTags[0].positionAbsolute - _liveSlidingMain ) < clipping_position) {
|
|
_audioTags.shift();
|
|
_audioIdx--;
|
|
clipped_tags++;
|
|
}
|
|
|
|
while (_videoTags.length && (_videoTags[0].positionAbsolute - _liveSlidingMain ) < clipping_position) {
|
|
_videoTags.shift();
|
|
_videoIdx--;
|
|
clipped_tags++;
|
|
}
|
|
|
|
while (_metaTags.length && (_metaTags[0].positionAbsolute - _liveSlidingMain ) < clipping_position) {
|
|
_metaTags.shift();
|
|
_metaIdx--;
|
|
clipped_tags++;
|
|
}
|
|
|
|
/* clip header tags : the tricky thing here is that we need to keep the last AAC HEADER / AVC HEADER before clip position
|
|
* if we dont keep these tags, we will have audio/video playback issues when seeking into the buffer
|
|
*
|
|
* so we loop through all header tags and we retrieve the position of last AAC/AVC header tags
|
|
* then if any subsequent header tag is found after clipping position, we create a new Vector in which we first append the previously
|
|
* found AAC/AVC header
|
|
*
|
|
*/
|
|
var _aacHeader : FLVData;
|
|
var _avcHeader : FLVData;
|
|
var _disHeader : FLVData;
|
|
var headercounter : uint = 0;
|
|
var _newheaderTags : Vector.<FLVData> = new Vector.<FLVData>();
|
|
for each (data in _headerTags) {
|
|
if ((data.positionAbsolute - _liveSlidingMain ) < clipping_position) {
|
|
headercounter++;
|
|
switch(data.tag.type) {
|
|
case FLVTag.DISCONTINUITY:
|
|
_disHeader = data;
|
|
break;
|
|
case FLVTag.AAC_HEADER:
|
|
_aacHeader = data;
|
|
break;
|
|
case FLVTag.AVC_HEADER:
|
|
_avcHeader = data;
|
|
default:
|
|
break;
|
|
}
|
|
} else {
|
|
/* push header tags located after clipping position into a new array */
|
|
_newheaderTags.push(data);
|
|
}
|
|
}
|
|
|
|
/* now push any DISCONTINUITY/AVC HEADER/AAC HEADER tag located before the clip position */
|
|
if (_disHeader) {
|
|
headercounter--;
|
|
// Log.info("push DISCONTINUITY header tags/position:" + _disHeader.position);
|
|
_disHeader.position = clipping_position;
|
|
_disHeader.sliding = 0;
|
|
_newheaderTags.unshift(_disHeader);
|
|
}
|
|
if (_aacHeader) {
|
|
headercounter--;
|
|
// Log.info("push AAC header tags/position:" + _aacHeader.position);
|
|
_aacHeader.position = clipping_position;
|
|
_aacHeader.sliding = 0;
|
|
_newheaderTags.unshift(_aacHeader);
|
|
}
|
|
if (_avcHeader) {
|
|
headercounter--;
|
|
// Log.info("push AVC header tags/position:" + _avcHeader.position);
|
|
_avcHeader.position = clipping_position;
|
|
_avcHeader.sliding = 0;
|
|
_newheaderTags.unshift(_avcHeader);
|
|
}
|
|
|
|
if (headercounter != 0) {
|
|
// Log.info("clipped " + headercounter + " header tags");
|
|
_headerTags = _newheaderTags;
|
|
// we need to adjust headerIdx, as the size of the Vector has been adjusted
|
|
_headerIdx -= headercounter;
|
|
clipped_tags += headercounter;
|
|
}
|
|
|
|
CONFIG::LOGGING {
|
|
if (clipped_tags > 0) {
|
|
Log.debug2("clipped " + clipped_tags + " tags, clipping_position0/clipping_position/position/new backBufferLength :" + clipping_position0.toFixed(3) + '/' + clipping_position.toFixed(3) + '/' + position.toFixed(3) + '/' + (position-clipping_position).toFixed(3));
|
|
}
|
|
}
|
|
}
|
|
|
|
private function _playlistDurationUpdated(event : HLSEvent) : void {
|
|
_playlistDuration = event.duration;
|
|
}
|
|
|
|
private function getbuflen(tags : Vector.<FLVData>, startIdx : uint) : Number {
|
|
if (tags.length > startIdx) {
|
|
var startPos : Number = tags[startIdx].positionAbsolute;
|
|
var endPos : Number = tags[tags.length - 1].positionAbsolute;
|
|
return (endPos - startPos);
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* retrieve next tag and update pointer, using the following priority :
|
|
* smallest continuity
|
|
* then smallest dts
|
|
* then header then video then audio then metadata tags
|
|
*/
|
|
private function getNextTag(shift : Boolean) : FLVData {
|
|
var mtag : FLVData ,vtag : FLVData,atag : FLVData, htag : FLVData;
|
|
|
|
var continuity : int = int.MAX_VALUE;
|
|
// find smallest continuity counter
|
|
if (_headerTags.length > _headerIdx) {
|
|
htag = _headerTags[_headerIdx];
|
|
continuity = Math.min(continuity, htag.continuity);
|
|
}
|
|
if (_videoTags.length > _videoIdx) {
|
|
vtag = _videoTags[_videoIdx];
|
|
continuity = Math.min(continuity, vtag.continuity);
|
|
}
|
|
if (_audioTags.length > _audioIdx) {
|
|
atag = _audioTags[_audioIdx];
|
|
continuity = Math.min(continuity, atag.continuity);
|
|
}
|
|
if (_metaTags.length > _metaIdx) {
|
|
mtag = _metaTags[_metaIdx];
|
|
continuity = Math.min(continuity, mtag.continuity);
|
|
}
|
|
if (continuity == int.MAX_VALUE)
|
|
return null;
|
|
|
|
var dts : Number = Number.MAX_VALUE;
|
|
// for this continuity counter, find smallest DTS
|
|
|
|
if (htag && htag.continuity == continuity) dts = Math.min(dts, htag.tag.dts);
|
|
if (vtag && vtag.continuity == continuity) dts = Math.min(dts, vtag.tag.dts);
|
|
if (atag && atag.continuity == continuity) dts = Math.min(dts, atag.tag.dts);
|
|
if (mtag && mtag.continuity == continuity) dts = Math.min(dts, mtag.tag.dts);
|
|
|
|
// for this continuity counter, this DTS, prioritize tags with the following order : header/metadata/video/audio
|
|
if (htag && htag.continuity == continuity && htag.tag.dts == dts) {
|
|
if(shift) _headerIdx++;
|
|
return htag;
|
|
}
|
|
if (mtag && mtag.continuity == continuity && mtag.tag.dts == dts) {
|
|
if(shift) _metaIdx++;
|
|
return mtag;
|
|
}
|
|
if (vtag && vtag.continuity == continuity && vtag.tag.dts == dts) {
|
|
if(shift) _videoIdx++;
|
|
return vtag;
|
|
} else {
|
|
if(shift) _audioIdx++;
|
|
return atag;
|
|
}
|
|
}
|
|
|
|
// shift tags until different frag
|
|
private function shiftMultipleTags(maxDuration : Number) : Vector.<FLVData> {
|
|
var tags : Vector.<FLVData>= new Vector.<FLVData>(),
|
|
mainIdx : uint = 0,
|
|
altIdx : uint = 0,
|
|
firstPos : Number = undefined,
|
|
tagPos : Number,
|
|
tag : FLVData,
|
|
isMain : Boolean,
|
|
tagIdx : uint;
|
|
|
|
// get tag, without shifting it
|
|
while ((tag = getNextTag(false)) != null) {
|
|
isMain = (tag.loaderType == HLSLoaderTypes.FRAGMENT_MAIN);
|
|
tagIdx = tag.fragIdx;
|
|
tagPos = tag.positionAbsolute;
|
|
if(isNaN(firstPos)) {
|
|
firstPos = tagPos;
|
|
}
|
|
if(isMain && !mainIdx) {
|
|
// first tag coming from main frag loader, store idx
|
|
mainIdx = tagIdx;
|
|
} else if(!isMain && !altIdx) {
|
|
// first tag coming from alt audio frag loader, store idx
|
|
altIdx = tagIdx;
|
|
} else {
|
|
// not first tag, ensure idx is within same frag, otherwise exit
|
|
if ( (tagPos - firstPos) > maxDuration && ((isMain && tagIdx != mainIdx) || (!isMain && tagIdx != altIdx))) {
|
|
// CONFIG::LOGGING {
|
|
// Log.info('discard level/sn/type/dts/pts:' + tag.fragLevel + '/' + tag.fragSN + '/' + tag.tag.typeString + '/' + tag.tag.dts + '/' + tag.tag.pts);
|
|
// }
|
|
break;
|
|
}
|
|
}
|
|
// tag matches, shift it and push it in new array
|
|
tags.push(getNextTag(true));
|
|
}
|
|
return tags;
|
|
}
|
|
|
|
private function get min_pos() : Number {
|
|
if (audioExpected) {
|
|
if (videoExpected) {
|
|
return Math.max(min_audio_pos, min_video_pos);
|
|
} else {
|
|
return min_audio_pos;
|
|
}
|
|
} else {
|
|
return min_video_pos;
|
|
}
|
|
}
|
|
|
|
private function get min_min_pos() : Number {
|
|
if (audioExpected) {
|
|
if (videoExpected) {
|
|
return Math.min(min_audio_pos, min_video_pos);
|
|
} else {
|
|
return min_audio_pos;
|
|
}
|
|
} else {
|
|
return min_video_pos;
|
|
}
|
|
}
|
|
|
|
private function get min_audio_pos() : Number {
|
|
var min_pos_ : Number = Number.POSITIVE_INFINITY;
|
|
if (_audioTags.length) min_pos_ = Math.min(min_pos_, _audioTags[0].positionAbsolute - _liveSlidingMain );
|
|
return min_pos_;
|
|
}
|
|
|
|
private function get min_video_pos() : Number {
|
|
var min_pos_ : Number = Number.POSITIVE_INFINITY;
|
|
if (_videoTags.length) min_pos_ = Math.min(min_pos_, _videoTags[0].positionAbsolute - _liveSlidingMain );
|
|
return min_pos_;
|
|
}
|
|
|
|
private function get max_pos() : Number {
|
|
if (audioExpected) {
|
|
if (videoExpected) {
|
|
return Math.min(max_audio_pos, max_video_pos);
|
|
} else {
|
|
return max_audio_pos;
|
|
}
|
|
} else {
|
|
return max_video_pos;
|
|
}
|
|
}
|
|
|
|
private function get max_audio_pos() : Number {
|
|
var max_pos_ : Number = Number.NEGATIVE_INFINITY;
|
|
if (_audioTags.length) max_pos_ = Math.max(max_pos_, _audioTags[_audioTags.length - 1].positionAbsolute - _liveSlidingMain );
|
|
return max_pos_;
|
|
}
|
|
|
|
private function get max_video_pos() : Number {
|
|
var max_pos_ : Number = Number.NEGATIVE_INFINITY;
|
|
if (_videoTags.length) max_pos_ = Math.max(max_pos_, _videoTags[_videoTags.length - 1].positionAbsolute - _liveSlidingMain );
|
|
return max_pos_;
|
|
}
|
|
|
|
private function _lastVODFragmentLoadedHandler(event : HLSEvent) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("last fragment loaded");
|
|
}
|
|
_reachedEnd = true;
|
|
}
|
|
|
|
private function _audioTrackChange(event : HLSEvent) : void {
|
|
CONFIG::LOGGING {
|
|
Log.debug("StreamBuffer : audio track changed, flushing audio buffer:" + event.audioTrack);
|
|
}
|
|
flushAudio();
|
|
}
|
|
|
|
/** monitor fragment loader stall events, arm a boolean **/
|
|
private function _liveLoadingStalledHandler(event : HLSEvent) : void {
|
|
_liveLoadingStalled = true;
|
|
}
|
|
|
|
/** playback complete handler **/
|
|
private function _playbackComplete(event : HLSEvent) : void {
|
|
// reset lastMediaTimeUpdate to quickly trigger a MEDIA_TIME event
|
|
_lastMediaTimeUpdate = 0;
|
|
_playbackCompleted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
import org.mangui.hls.constant.HLSLoaderTypes;
|
|
import org.mangui.hls.flv.FLVTag;
|
|
|
|
|
|
class FLVData {
|
|
public var tag : FLVTag;
|
|
public var position : Number;
|
|
public var sliding : Number;
|
|
public var continuity : int;
|
|
public var loaderType : int;
|
|
public var fragIdx : int;
|
|
public var fragLevel : int;
|
|
public var fragSN : int;
|
|
public static var refPTSMain : Number;
|
|
public static var refPTSAltAudio : Number;
|
|
|
|
public function FLVData(tag : FLVTag, position : Number, sliding : Number, continuity : int, loaderType : int, fragIdx : int, fragLevel : int, fragSN : int) {
|
|
this.tag = tag;
|
|
// relative position
|
|
this.position = position;
|
|
this.sliding = sliding;
|
|
this.continuity = continuity;
|
|
this.loaderType = loaderType;
|
|
this.fragIdx = fragIdx;
|
|
this.fragLevel = fragLevel;
|
|
this.fragSN = fragSN;
|
|
switch(loaderType) {
|
|
case HLSLoaderTypes.FRAGMENT_MAIN:
|
|
if(isNaN(refPTSMain)) {
|
|
refPTSMain = tag.pts - 1000*position - sliding;
|
|
}
|
|
break;
|
|
case HLSLoaderTypes.FRAGMENT_ALTAUDIO:
|
|
default:
|
|
if(isNaN(refPTSAltAudio)) {
|
|
refPTSAltAudio = tag.pts - 1000*position - sliding;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// return absolute tag position, compared to beginning of playback and main playlist
|
|
public function get positionAbsolute() : Number {
|
|
var pos : Number = this.position + this.sliding;
|
|
// if this tag is altaudio, and we know ref PTS for main, we need to offset absolute pos by PTS difference
|
|
if(loaderType == HLSLoaderTypes.FRAGMENT_ALTAUDIO && !isNaN(refPTSMain)) {
|
|
pos += (refPTSAltAudio - refPTSMain)/1000;
|
|
}
|
|
return pos;
|
|
}
|
|
}
|