/* 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.; // index of used url (non 0 if we switch to a redundant stream) private var _redundantStreamId : int = 0; /** Level fragments **/ public var fragments : Vector.; /** 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.(); }; 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.(); 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.) : 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.) : 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!"); } } } } }