Files
moeplayer/source/org/mangui/hls/model/Level.as
T
2020-09-17 18:40:01 +08:00

410 lines
18 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.model {
CONFIG::LOGGING {
import org.mangui.hls.utils.Log;
}
import org.mangui.hls.utils.PTS;
/** HLS streaming quality level. **/
public class Level {
/** audio only Level ? **/
public var audio : Boolean;
/** AAC codec signaled ? **/
public var codec_aac : Boolean;
/** MP3 codec signaled ? **/
public var codec_mp3 : Boolean;
/** H264 codec signaled ? **/
public var codec_h264 : Boolean;
/** Level Bitrate. **/
public var bitrate : uint;
/** captions . **/
public var closed_captions : String;
/** Level Name. **/
public var name : String;
/** level index (sorted by bitrate) **/
public var index : int = 0;
/** level index (manifest order) **/
public var manifest_index : int = 0;
/** video width (from playlist) **/
public var width : int;
/** video height (from playlist) **/
public var height : int;
/** URL of this bitrate level (for M3U8). (it is a vector so that we can store redundant streams in same level) **/
public var urls : Vector.<String>;
// index of used url (non 0 if we switch to a redundant stream)
private var _redundantStreamId : int = 0;
/** Level fragments **/
public var fragments : Vector.<Fragment>;
/** min sequence number from M3U8. **/
public var start_seqnum : int;
/** max sequence number from M3U8. **/
public var end_seqnum : int;
/** target fragment duration from M3U8 **/
public var targetduration : Number;
/** average fragment duration **/
public var averageduration : Number;
/** Total duration **/
public var duration : Number;
/** Audio Identifier **/
public var audio_stream_id : String;
/** Create the quality level. **/
public function Level() : void {
this.fragments = new Vector.<Fragment>();
};
public function get url() : String {
return urls[_redundantStreamId];
}
public function get redundantStreamsNb() : int {
if(urls && urls.length) {
return urls.length-1;
} else {
return 0;
}
}
public function get redundantStreamId() : int {
return _redundantStreamId;
}
// when switching to a redundant stream, reset fragments. they will be retrieved from new playlist
public function set redundantStreamId(id : int) : void {
if(id < urls.length && id != _redundantStreamId) {
_redundantStreamId = id;
fragments = new Vector.<Fragment>();
start_seqnum = end_seqnum = NaN;
}
}
/** Return the Fragment before a given time position. **/
public function getFragmentBeforePosition(position : Number) : Fragment {
if (fragments[0].data.valid && position < fragments[0].start_time)
return fragments[0];
var len : int = fragments.length;
for (var i : int = 0; i < len; i++) {
/* check whether fragment contains current position */
if (fragments[i].data.valid && fragments[i].start_time <= position && fragments[i].start_time + fragments[i].duration > position) {
return fragments[i];
}
}
return fragments[len - 1];
};
/** Return the sequence number nearest a given program date **/
public function getSeqNumNearestProgramDate(program_date : Number) : int {
if (program_date < fragments[0].program_date)
return -1;
var len : int = fragments.length;
if(len) {
if (program_date > (fragments[len-1].program_date + 1000*fragments[len-1].duration))
return -1;
for (var i : int = 0; i < len; i++) {
var frag : Fragment = fragments[i];
/* check whether fragment contains current position */
if (frag.data.valid &&
Math.abs(frag.program_date - program_date) < Math.abs(frag.program_date + 1000 * frag.duration - program_date)) {
return frag.seqnum;
}
}
}
return -1;
};
/** Return the sequence number nearest a PTS **/
public function getSeqNumNearestPTS(pts : Number, continuity : int) : Number {
if (fragments.length == 0)
return -1;
var firstIndex : Number = getFirstIndexfromContinuity(continuity);
if (firstIndex == -1 || isNaN(fragments[firstIndex].data.pts_start_computed))
return -1;
var lastIndex : Number = getLastIndexfromContinuity(continuity);
for (var i : int = firstIndex; i <= lastIndex; i++) {
var frag : Fragment = fragments[i];
var start : Number = frag.data.pts_start_computed;
var duration :Number = frag.duration;
var end : Number = start + 1000*duration;
// CONFIG::LOGGING {
// Log.debug("getSeqNumNearestPTS: pts/start/end/duration:" + pts + '/' + start + '/' + end + '/' + duration);
// }
/* check nearest fragment */
if ( frag.data.valid &&
(duration >= 0) &&
// if PTS is closer from start
((Math.abs(start - pts) < Math.abs(end - pts))
// start PTS end
// *----|-----------------------*
//
// PTS start end
// |--*-----------------------*
//
//
// OR if PTS is bigger than start PTS AND more than 10% before frag end
//
// start PTS end
// *----------------------|-----*
// <10%>
|| ((pts > start) &&
(end - pts ) > 100*duration))) {
return frag.seqnum;
}
}
// if we are not at the end of the playlist, then return first sn of next cc range
// this is needed to deal with PTS analysis on streams with discontinuity
if (lastIndex < end_seqnum) {
return frag.seqnum+1;
} else {
// requested PTS above max PTS of this level
return Number.POSITIVE_INFINITY;
}
};
public function getLevelstartPTS() : Number {
if (fragments.length)
return fragments[0].data.pts_start_computed;
else
return NaN;
}
/** Return the fragment index from fragment sequence number **/
public function getFragmentfromSeqNum(seqnum : Number) : Fragment {
var index : int = getIndexfromSeqNum(seqnum);
if (index != -1) {
return fragments[index];
} else {
return null;
}
}
/** Return the fragment index from fragment sequence number **/
private function getIndexfromSeqNum(seqnum : int) : int {
if (seqnum >= start_seqnum && seqnum <= end_seqnum) {
return (fragments.length - 1 - (end_seqnum - seqnum));
} else {
return -1;
}
}
/** Return the first index matching with given continuity counter **/
private function getFirstIndexfromContinuity(continuity : int) : int {
// look for first fragment matching with given continuity index
var len : int = fragments.length;
for (var i : int = 0; i < len; i++) {
if (fragments[i].continuity == continuity)
return i;
}
return -1;
}
/** Return the first seqnum matching with given continuity counter **/
public function getFirstSeqNumfromContinuity(continuity : int) : Number {
var index : int = getFirstIndexfromContinuity(continuity);
if (index == -1) {
return Number.NEGATIVE_INFINITY;
}
return fragments[index].seqnum;
}
/** Return the last seqnum matching with given continuity counter **/
public function getLastSeqNumfromContinuity(continuity : int) : Number {
var index : int = getLastIndexfromContinuity(continuity);
if (index == -1) {
return Number.NEGATIVE_INFINITY;
}
return fragments[index].seqnum;
}
/** Return the last index matching with given continuity counter **/
private function getLastIndexfromContinuity(continuity : Number) : int {
var firstIndex : int = getFirstIndexfromContinuity(continuity);
if (firstIndex == -1)
return -1;
var lastIndex : int = firstIndex;
// look for first fragment matching with given continuity index
for (var i : int = firstIndex; i < fragments.length; i++) {
if (fragments[i].continuity == continuity)
lastIndex = i;
else
break;
}
return lastIndex;
}
/** set Fragments **/
public function updateFragments(_fragments : Vector.<Fragment>) : void {
var idx_with_metrics : int = -1;
var len : int = _fragments.length;
var continuity_offset : int;
var frag : Fragment;
// update PTS from previous fragments
for (var i : int = 0; i < len; i++) {
frag = getFragmentfromSeqNum(_fragments[i].seqnum);
if (frag != null) {
continuity_offset = frag.continuity - _fragments[i].continuity;
if(!isNaN(frag.data.pts_start)) {
_fragments[i].data = frag.data;
idx_with_metrics = i;
}
}
}
if(continuity_offset) {
CONFIG::LOGGING {
Log.debug("updateFragments: discontinuity sliding from live playlist,take into account discontinuity drift:" + continuity_offset);
}
for (i = 0; i < len; i++) {
_fragments[i].continuity+= continuity_offset;
}
}
updateFragmentsProgramDate(_fragments);
fragments = _fragments;
if (len > 0) {
start_seqnum = _fragments[0].seqnum;
end_seqnum = _fragments[len - 1].seqnum;
if (idx_with_metrics != -1) {
frag = fragments[idx_with_metrics];
// if at least one fragment contains PTS info, recompute PTS information for all fragments
CONFIG::LOGGING {
Log.debug("updateFragments: found PTS info from previous playlist,seqnum/PTS:" + frag.seqnum + "/" + frag.data.pts_start);
}
updateFragment(frag.seqnum, true, frag.data.pts_start, frag.data.pts_start + 1000 * frag.duration);
} else {
CONFIG::LOGGING {
Log.debug("updateFragments: unknown PTS info for this level");
}
duration = _fragments[len - 1].start_time + _fragments[len - 1].duration;
}
averageduration = duration / len;
} else {
duration = 0;
averageduration = 0;
}
}
private function updateFragmentsProgramDate(_fragments : Vector.<Fragment>) : void {
var len : int = _fragments.length;
var continuity : int;
var program_date : Number;
var frag : Fragment;
for (var i : int = 0; i < len; i++) {
frag = _fragments[i];
if (frag.continuity != continuity) {
continuity = frag.continuity;
program_date = 0;
}
if (frag.program_date) {
program_date = frag.program_date + 1000 * frag.duration;
} else if (program_date) {
frag.program_date = program_date;
}
}
}
private function _updatePTS(from_index : int, to_index : int) : void {
// CONFIG::LOGGING {
// Log.info("updateFragmentPTS from/to:" + from_index + "/" + to_index);
// }
var frag_from : Fragment = fragments[from_index];
var frag_to : Fragment = fragments[to_index];
if (frag_from.data.valid && frag_to.data.valid) {
if (!isNaN(frag_to.data.pts_start)) {
// we know PTS[to_index]
frag_to.data.pts_start_computed = frag_to.data.pts_start;
/* normalize computed PTS value based on known PTS value.
* this is to avoid computing wrong fragment duration in case of PTS looping */
var from_pts : Number = PTS.normalize(frag_to.data.pts_start, frag_from.data.pts_start_computed);
/* update fragment duration.
it helps to fix drifts between playlist reported duration and fragment real duration */
if (to_index > from_index) {
frag_from.duration = (frag_to.data.pts_start - from_pts) / 1000;
CONFIG::LOGGING {
if (frag_from.duration < 0) {
Log.error("negative duration computed for " + frag_from + ", there should be some duration drift between playlist and fragment!");
}
}
} else {
frag_to.duration = ( from_pts - frag_to.data.pts_start) / 1000;
CONFIG::LOGGING {
if (frag_to.duration < 0) {
Log.error("negative duration computed for " + frag_to + ", there should be some duration drift between playlist and fragment!");
}
}
}
} else {
// we dont know PTS[to_index]
if (to_index > from_index)
frag_to.data.pts_start_computed = frag_from.data.pts_start_computed + 1000 * frag_from.duration;
else
frag_to.data.pts_start_computed = frag_from.data.pts_start_computed - 1000 * frag_to.duration;
}
}
}
public function updateFragment(seqnum : Number, valid : Boolean, min_pts : Number = 0, max_pts : Number = 0) : void {
// CONFIG::LOGGING {
// Log.info("updatePTS : seqnum/min/max:" + seqnum + '/' + min_pts + '/' + max_pts);
// }
// get fragment from seqnum
var fragIdx : int = getIndexfromSeqNum(seqnum);
if (fragIdx != -1) {
var frag : Fragment = fragments[fragIdx];
// update fragment start PTS + duration
if (valid) {
frag.data.pts_start = min_pts;
frag.data.pts_start_computed = min_pts;
frag.duration = (max_pts - min_pts) / 1000;
} else {
frag.duration = 0;
}
frag.data.valid = valid;
// CONFIG::LOGGING {
// Log.info("SN["+fragments[fragIdx].seqnum+"]:pts/duration:" + fragments[fragIdx].start_pts_computed + "/" + fragments[fragIdx].duration);
// }
// adjust fragment PTS/duration from seqnum-1 to frag 0
for (var i : int = fragIdx; i > 0 && fragments[i - 1].continuity == frag.continuity; i--) {
_updatePTS(i, i - 1);
// CONFIG::LOGGING {
// Log.info("SN["+fragments[i-1].seqnum+"]:pts/duration:" + fragments[i-1].start_pts_computed + "/" + fragments[i-1].duration);
// }
}
// adjust fragment PTS/duration from seqnum to last frag
for (i = fragIdx; i < fragments.length - 1 && fragments[i + 1].continuity == frag.continuity; i++) {
_updatePTS(i, i + 1);
// CONFIG::LOGGING {
// Log.info("SN["+fragments[i+1].seqnum+"]:pts/duration:" + fragments[i+1].start_pts_computed + "/" + fragments[i+1].duration);
// }
}
// second, adjust fragment offset
var start_time_offset : Number = fragments[0].start_time;
var len : int = fragments.length;
for (i = 0; i < len; i++) {
fragments[i].start_time = start_time_offset;
start_time_offset += fragments[i].duration;
// CONFIG::LOGGING {
// Log.info("SN["+fragments[i].seqnum+"]:start_time/continuity/pts/duration:" + fragments[i].start_time + "/" + fragments[i].continuity + "/"+ fragments[i].start_pts_computed + "/" + fragments[i].duration);
// }
}
duration = start_time_offset;
} else {
CONFIG::LOGGING {
Log.error("updateFragment:seqnum " + seqnum + " not found!");
}
}
}
}
}