no commit message

This commit is contained in:
niandeng
2020-09-17 18:40:01 +08:00
parent 4382915d09
commit d957abceaa
283 changed files with 86935 additions and 63 deletions
+28
View File
@@ -0,0 +1,28 @@
/* 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 {
/** Audio Track identifier **/
public class AudioTrack {
public static const FROM_DEMUX : int = 0;
public static const FROM_PLAYLIST : int = 1;
public var title : String;
public var id : int;
public var source : int;
public var isDefault : Boolean;
public var isAAC : Boolean;
public var level : Level;
public function AudioTrack(title : String, source : int, id : int, isDefault : Boolean, isAAC : Boolean) {
this.title = title;
this.source = source;
this.id = id;
this.isDefault = isDefault;
this.isAAC = isAAC;
}
public function toString() : String {
return "AudioTrack ID: " + id + " Title: " + title + " Source: " + source + " Default: " + isDefault + " AAC: " + isAAC;
}
}
}
+107
View File
@@ -0,0 +1,107 @@
/* 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 {
import flash.net.ObjectEncoding;
import flash.utils.ByteArray;
import org.mangui.hls.demux.ID3Tag;
import org.mangui.hls.flv.FLVTag;
/** Fragment model **/
public class Fragment {
/** Duration of this chunk. **/
public var duration : Number;
/** Start time of this chunk. **/
public var start_time : Number;
/** sequence number of this chunk. **/
public var seqnum : int;
/** URL to this chunk. **/
public var url : String;
/** level of this chunk. **/
public var level : int;
/** continuity index of this chunk. **/
public var continuity : int;
/** program date of this chunk. **/
public var program_date : Number;
/** URL of the key used to decrypt content **/
public var decrypt_url : String;
/** Initialization Vector to decrypt content **/
public var decrypt_iv : ByteArray;
/** byte range start offset **/
public var byterange_start_offset : int;
/** byte range offset **/
public var byterange_end_offset : int;
/** data **/
public var data : FragmentData;
/** custom tags **/
public var tag_list : Vector.<String>;
/** Create the fragment. **/
public function Fragment(url : String, duration : Number, level : int, seqnum : int, start_time : Number, continuity : int, program_date : Number, decrypt_url : String, decrypt_iv : ByteArray, byterange_start_offset : int, byterange_end_offset : int, tag_list : Vector.<String>) {
this.url = url;
this.duration = duration;
this.seqnum = seqnum;
this.level = level;
this.start_time = start_time;
this.continuity = continuity;
this.program_date = program_date;
this.decrypt_url = decrypt_url;
this.decrypt_iv = decrypt_iv;
this.byterange_start_offset = byterange_start_offset;
this.byterange_end_offset = byterange_end_offset;
this.tag_list = tag_list;
data = new FragmentData();
// CONFIG::LOGGING {
// Log.info("Frag["+seqnum+"]:duration/start_time,cc="+duration+","+start_time+","+continuity);
// }
};
public function getMetadataTag() : FLVTag {
var tag : FLVTag = new FLVTag(FLVTag.METADATA, this.data.dts_min, this.data.dts_min, false);
var data : ByteArray = new ByteArray();
data.objectEncoding = ObjectEncoding.AMF0;
data.writeObject("onHLSFragmentChange");
data.writeObject(this.level);
data.writeObject(this.seqnum);
data.writeObject(this.continuity);
data.writeObject(this.duration);
data.writeObject(!this.data.video_found);
data.writeObject(this.program_date);
data.writeObject(this.data.video_width);
data.writeObject(this.data.video_height);
data.writeObject(this.data.auto_level);
data.writeObject(this.tag_list.length);
this.data.id3_tags ? data.writeObject(this.data.id3_tags.length) : data.writeObject(0);
for each (var custom_tag : String in this.tag_list) {
data.writeObject(custom_tag);
}
for each (var id3_tag : ID3Tag in this.data.id3_tags) {
data.writeObject(id3_tag.id);
data.writeObject(id3_tag.flag);
data.writeObject(id3_tag.base64);
data.writeObject(id3_tag.data);
}
tag.push(data, 0, data.length);
tag.build();
return tag;
}
public function getSkippedTag() : FLVTag {
var tag : FLVTag = new FLVTag(FLVTag.METADATA, this.data.pts_start_computed, this.data.pts_start_computed, false);
var data : ByteArray = new ByteArray();
data.objectEncoding = ObjectEncoding.AMF0;
data.writeObject("onHLSFragmentSkipped");
data.writeObject(this.level);
data.writeObject(this.seqnum);
data.writeObject(this.duration);
tag.push(data, 0, data.length);
tag.build();
return tag;
}
public function toString() : String {
return "Fragment (seqnum: " + seqnum + ", start_time:" + start_time + ", duration:" + duration + ")";
}
}
}
+189
View File
@@ -0,0 +1,189 @@
/* 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 {
import org.mangui.hls.demux.ID3Tag;
import org.mangui.hls.flv.FLVTag;
import org.mangui.hls.utils.AES;
import org.mangui.hls.utils.PTS;
import flash.utils.ByteArray;
/** Fragment Data. **/
public class FragmentData {
/** valid fragment **/
public var valid : Boolean;
/** fragment byte array **/
public var bytes : ByteArray;
/** bytes Loaded **/
public var bytesLoaded : int;
/** AES decryption instance **/
public var decryptAES : AES;
/** Start PTS of this chunk. **/
public var pts_start : Number;
/** computed Start PTS of this chunk. **/
public var pts_start_computed : Number;
/** min/max audio/video PTS/DTS of this chunk. **/
public var pts_min_audio : Number;
public var pts_max_audio : Number;
public var pts_min_video : Number;
public var pts_max_video : Number;
public var dts_min : Number;
/** audio/video found ? */
public var audio_found : Boolean;
public var video_found : Boolean;
/** tag related stuff */
public var metadata_tag_injected : Boolean;
private var tags_pts_min_audio : Number;
private var tags_pts_max_audio : Number;
private var tags_pts_min_video : Number;
private var tags_pts_max_video : Number;
private var tags_audio_found : Boolean;
private var tags_video_found : Boolean;
public var tags : Vector.<FLVTag>;
/* video dimension */
public var video_width : int;
public var video_height : int;
/* is fragment loaded selected by autolevel algo */
public var auto_level : Boolean;
/* ID3 tags linked to this fragment */
public var id3_tags : Vector.<ID3Tag>;
/** tag duration */
private var audio_tag_duration : Number;
private var video_tag_duration : Number;
private var audio_tag_last_dts : Number;
private var video_tag_last_dts : Number;
/** Fragment metrics **/
public function FragmentData() {
this.pts_start = NaN;
this.pts_start_computed = NaN;
this.valid = true;
this.video_width = 0;
this.video_height = 0;
};
public function appendTags(tags : Vector.<FLVTag>) : void {
// Audio PTS/DTS normalization + min/max computation
for each (var tag : FLVTag in tags) {
tag.pts = PTS.normalize(pts_start_computed, tag.pts);
tag.dts = PTS.normalize(pts_start_computed, tag.dts);
dts_min = Math.min(dts_min, tag.dts);
switch( tag.type ) {
case FLVTag.AAC_RAW:
case FLVTag.AAC_HEADER:
case FLVTag.MP3_RAW:
audio_found = true;
tags_audio_found = true;
audio_tag_duration = tag.dts - audio_tag_last_dts;
audio_tag_last_dts = tag.dts;
tags_pts_min_audio = Math.min(tags_pts_min_audio, tag.pts);
tags_pts_max_audio = Math.max(tags_pts_max_audio, tag.pts);
pts_min_audio = Math.min(pts_min_audio, tag.pts);
pts_max_audio = Math.max(pts_max_audio, tag.pts);
break;
case FLVTag.AVC_HEADER:
case FLVTag.AVC_NALU:
video_found = true;
tags_video_found = true;
video_tag_duration = tag.dts - video_tag_last_dts;
video_tag_last_dts = tag.dts;
tags_pts_min_video = Math.min(tags_pts_min_video, tag.pts);
tags_pts_max_video = Math.max(tags_pts_max_video, tag.pts);
pts_min_video = Math.min(pts_min_video, tag.pts);
pts_max_video = Math.max(pts_max_video, tag.pts);
break;
case FLVTag.DISCONTINUITY:
case FLVTag.METADATA:
default:
break;
}
this.tags.push(tag);
}
}
public function flushTags() : void {
// clean-up tags
tags = new Vector.<FLVTag>();
tags_audio_found = tags_video_found = false;
metadata_tag_injected = false;
pts_min_audio = pts_min_video = dts_min = tags_pts_min_audio = tags_pts_min_video = Number.POSITIVE_INFINITY;
pts_max_audio = pts_max_video = tags_pts_max_audio = tags_pts_max_video = Number.NEGATIVE_INFINITY;
audio_found = video_found = tags_audio_found = tags_video_found = false;
}
public function shiftTags() : void {
tags = new Vector.<FLVTag>();
if (tags_audio_found) {
tags_pts_min_audio = tags_pts_max_audio;
tags_audio_found = false;
}
if (tags_video_found) {
tags_pts_min_video = tags_pts_max_video;
tags_video_found = false;
}
}
public function get pts_min() : Number {
if (audio_found) {
return pts_min_audio;
} else {
return pts_min_video;
}
}
public function get pts_max() : Number {
if (audio_found) {
return pts_max_audio;
} else {
return pts_max_video;
}
}
public function get tag_duration() : Number {
var duration : Number;
if (audio_found) {
duration = audio_tag_duration;
} else {
duration = video_tag_duration;
}
if(isNaN(duration)) {
duration = 0;
}
return duration;
}
public function get tag_pts_min() : Number {
if (audio_found) {
return tags_pts_min_audio;
} else {
return tags_pts_min_video;
}
}
public function get tag_pts_max() : Number {
if (audio_found) {
return tags_pts_max_audio;
} else {
return tags_pts_max_video;
}
}
public function get tag_pts_start_offset() : Number {
if (tags_audio_found) {
return tags_pts_min_audio - pts_min_audio;
} else {
return tags_pts_min_video - pts_min_video;
}
}
public function get tag_pts_end_offset() : Number {
if (tags_audio_found) {
return tags_pts_max_audio - pts_min_audio;
} else {
return tags_pts_max_video - pts_min_video;
}
}
}
}
+409
View File
@@ -0,0 +1,409 @@
/* 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!");
}
}
}
}
}