no commit message
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
/* 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.playlist {
|
||||
public class AltAudioTrack {
|
||||
public var group_id : String;
|
||||
public var lang : String;
|
||||
public var name : String;
|
||||
public var autoselect : Boolean;
|
||||
public var default_track : Boolean;
|
||||
public var url : String;
|
||||
|
||||
/** Create the quality level. **/
|
||||
public function AltAudioTrack(alt_group_id : String, alt_lang : String, alt_name : String, alt_autoselect : Boolean, alt_default : Boolean, alt_url : String) {
|
||||
group_id = alt_group_id;
|
||||
lang = alt_lang;
|
||||
name = alt_name;
|
||||
autoselect = alt_autoselect;
|
||||
default_track = alt_default;
|
||||
url = alt_url;
|
||||
};
|
||||
|
||||
public function toString() : String {
|
||||
return "AltAudioTrack url: " + url + " group_id: " + group_id + " lang: " + lang + " name: " + name + ' default: ' + default_track ;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/* 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.playlist {
|
||||
CONFIG::LOGGING {
|
||||
import org.mangui.hls.utils.Log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facilitates extracting information from a data URI.
|
||||
*/
|
||||
public class DataUri {
|
||||
|
||||
private static const DATA_PROTOCOL : String = "data:";
|
||||
private static const BASE_64 : String = "base64";
|
||||
|
||||
private var _dataUri : String;
|
||||
|
||||
public function DataUri(dataUri : String) {
|
||||
_dataUri = dataUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the data portion of the data URI if it is able extract the information,
|
||||
* null otherwise.
|
||||
*/
|
||||
public function extractData() : String {
|
||||
if (_dataUri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var base64Index : int = _dataUri.indexOf(BASE_64 + ',');
|
||||
var dataIndex : int = _dataUri.indexOf(',') + 1;
|
||||
|
||||
if (dataIndex > _dataUri.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var data : String = _dataUri.substr(dataIndex);
|
||||
return (base64Index === -1) ? _extractPlainData(data) : _extractBase64Data(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data URIs support base 64 encoding the data section.
|
||||
* This is not typically used for plain text files, which includes HLS manifests.
|
||||
* As such, decoded base 64 data sections is not currently (6/18/14) supported.
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
private function _extractBase64Data(data : String) : String {
|
||||
CONFIG::LOGGING {
|
||||
Log.warn("Base 64 encoded Data URIs are not supported.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data
|
||||
* @return The URL decoded data section from the data URI.
|
||||
*/
|
||||
private function _extractPlainData(data : String) : String {
|
||||
var decodedData : String = decodeURIComponent(data);
|
||||
CONFIG::LOGGING {
|
||||
Log.debug2("Decoded data from data URI into: " + decodedData);
|
||||
}
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataUri
|
||||
* @return True if the provided string is a data URI, false otherwise.
|
||||
*/
|
||||
public static function isDataUri(dataUri : String) : Boolean {
|
||||
return dataUri != null && dataUri.indexOf(DATA_PROTOCOL) === 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/* 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.playlist {
|
||||
import flash.events.Event;
|
||||
import flash.events.IOErrorEvent;
|
||||
import flash.events.ProgressEvent;
|
||||
import flash.events.SecurityErrorEvent;
|
||||
import flash.net.URLLoader;
|
||||
import flash.net.URLRequest;
|
||||
import flash.utils.ByteArray;
|
||||
import flash.utils.Dictionary;
|
||||
import flash.utils.getTimer;
|
||||
|
||||
import org.mangui.hls.HLS;
|
||||
import org.mangui.hls.constant.HLSLoaderTypes;
|
||||
import org.mangui.hls.constant.HLSTypes;
|
||||
import org.mangui.hls.event.HLSLoadMetrics;
|
||||
import org.mangui.hls.model.Fragment;
|
||||
import org.mangui.hls.model.Level;
|
||||
import org.mangui.hls.utils.DateUtil;
|
||||
import org.mangui.hls.utils.Hex;
|
||||
import org.mangui.hls.utils.StringUtil;
|
||||
|
||||
CONFIG::LOGGING {
|
||||
import org.mangui.hls.utils.Log;
|
||||
}
|
||||
/** Helpers for parsing M3U8 files. **/
|
||||
public class Manifest {
|
||||
/** Starttag for a fragment. **/
|
||||
public static const FRAGMENT : String = '#EXTINF:';
|
||||
/** Header tag that must be at the first line. **/
|
||||
public static const HEADER : String = '#EXTM3U';
|
||||
/** Version of the playlist file. **/
|
||||
public static const VERSION : String = '#EXT-X-VERSION';
|
||||
/** Starttag for a level. **/
|
||||
public static const LEVEL : String = '#EXT-X-STREAM-INF:';
|
||||
/** Tag that delimits the end of a playlist. **/
|
||||
public static const ENDLIST : String = '#EXT-X-ENDLIST';
|
||||
/** Tag that provides info related to alternative audio tracks */
|
||||
public static const ALTERNATE_AUDIO : String = '#EXT-X-MEDIA:TYPE=AUDIO,';
|
||||
/** Tag that provides info related to alternative rendition */
|
||||
private static const MEDIA : String = '#EXT-X-MEDIA:';
|
||||
/** Tag that provides the sequence number. **/
|
||||
private static const SEQNUM : String = '#EXT-X-MEDIA-SEQUENCE:';
|
||||
/** Tag that provides the target duration for each segment. **/
|
||||
private static const TARGETDURATION : String = '#EXT-X-TARGETDURATION:';
|
||||
/** Tag that indicates discontinuity in the stream */
|
||||
private static const DISCONTINUITY : String = '#EXT-X-DISCONTINUITY';
|
||||
/** Tag that indicates discontinuity sequence in the stream */
|
||||
private static const DISCONTINUITY_SEQ : String = '#EXT-X-DISCONTINUITY-SEQUENCE:';
|
||||
/** Tag that provides date/time information */
|
||||
private static const PROGRAMDATETIME : String = '#EXT-X-PROGRAM-DATE-TIME:';
|
||||
/** Tag that provides fragment decryption info */
|
||||
private static const KEY : String = '#EXT-X-KEY:';
|
||||
/** Tag that provides byte range info */
|
||||
private static const BYTERANGE : String = '#EXT-X-BYTERANGE:';
|
||||
/** useful regular expression */
|
||||
private static const replacespace : RegExp = new RegExp("\\s+", "g");
|
||||
private static const replacesinglequote : RegExp = new RegExp("\\\'", "g");
|
||||
private static const replacedoublequote : RegExp = new RegExp("\\\"", "g");
|
||||
private static const trimwhitespace : RegExp = /^\s*|\s*$/gim;
|
||||
/** Index in the array with levels. **/
|
||||
private var _index : int;
|
||||
/** URLLoader instance. **/
|
||||
private var _urlloader : URLLoader;
|
||||
/** Function to callback loading to. **/
|
||||
private var _success : Function;
|
||||
/** URL of an M3U8 playlist. **/
|
||||
private var _url : String;
|
||||
/** load metrics **/
|
||||
private var _metrics : HLSLoadMetrics;
|
||||
|
||||
/** Load a playlist M3U8 file. **/
|
||||
public function loadPlaylist(hls : HLS, url : String, success : Function, error : Function, index : int, type : String, flushLiveURLcache : Boolean) : void {
|
||||
_url = url;
|
||||
_success = success;
|
||||
_index = index;
|
||||
var urlLoaderClass : Class = hls.URLloader as Class;
|
||||
_urlloader = (new urlLoaderClass()) as URLLoader;
|
||||
_urlloader.addEventListener(Event.COMPLETE, _loadCompleteHandler);
|
||||
_urlloader.addEventListener(ProgressEvent.PROGRESS, _loadProgressHandler);
|
||||
_urlloader.addEventListener(IOErrorEvent.IO_ERROR, error);
|
||||
_urlloader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, error);
|
||||
|
||||
if (flushLiveURLcache && type == HLSTypes.LIVE) {
|
||||
/*
|
||||
add time parameter to force reload URL, there are some issues with browsers/CDN reloading from cache even if the URL has been updated ...
|
||||
see http://stackoverflow.com/questions/14448219/as3-resetting-urlloader-cache
|
||||
*/
|
||||
var extra : String = "time=" + new Date().getTime();
|
||||
if (_url.indexOf("?") == -1) {
|
||||
url += "?" + extra;
|
||||
} else {
|
||||
url += "&" + extra;
|
||||
}
|
||||
}
|
||||
if (DataUri.isDataUri(url)) {
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("Identified playlist <" + url + "> as a data URI.");
|
||||
}
|
||||
var data : String = new DataUri(url).extractData();
|
||||
onLoadedData(data || "");
|
||||
return;
|
||||
}
|
||||
_metrics = new HLSLoadMetrics(HLSLoaderTypes.LEVEL_MAIN);
|
||||
_metrics.level = index;
|
||||
_metrics.loading_request_time = getTimer();
|
||||
_urlloader.load(new URLRequest(url));
|
||||
};
|
||||
|
||||
/* cancel loading in progress */
|
||||
public function close() : void {
|
||||
try {
|
||||
_urlloader.close();
|
||||
} catch(e : Error) {
|
||||
}
|
||||
}
|
||||
|
||||
/** loading progress handler, use to determine loading latency **/
|
||||
private function _loadProgressHandler(event : Event) : void {
|
||||
if(_metrics.loading_begin_time == 0) {
|
||||
_metrics.loading_begin_time = getTimer();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** loading complete handler **/
|
||||
private function _loadCompleteHandler(event : Event) : void {
|
||||
_metrics.loading_end_time = getTimer();
|
||||
onLoadedData(StringUtil.toLF(String(_urlloader.data)));
|
||||
};
|
||||
|
||||
private function onLoadedData(data : String) : void {
|
||||
_success(data, _url, _index, _metrics);
|
||||
}
|
||||
|
||||
private static function zeropad(str : String, length : uint) : String {
|
||||
while (str.length < length) {
|
||||
str = "0" + str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/** Extract fragments from playlist data. **/
|
||||
public static function getFragments(data : String, base : String, level : int) : Vector.<Fragment> {
|
||||
var fragments : Vector.<Fragment> = new Vector.<Fragment>();
|
||||
var lines : Vector.<String> = StringUtil.toLines(data);
|
||||
// fragment seqnum
|
||||
var seqnum : int = 0;
|
||||
// fragment start time (in sec)
|
||||
var start_time : Number = 0;
|
||||
// nb of ms since epoch
|
||||
var program_date : Number = 0;
|
||||
var program_date_defined : Boolean = false;
|
||||
/* URL of decryption key */
|
||||
var decrypt_url : String = null;
|
||||
/* Initialization Vector */
|
||||
var decrypt_iv : ByteArray = null;
|
||||
// fragment continuity index incremented at each discontinuity
|
||||
var continuity_index : int = 0;
|
||||
var i : int = 0;
|
||||
var extinf_found : Boolean = false;
|
||||
var byterange_start_offset : int = -1;
|
||||
var byterange_end_offset : int = -1;
|
||||
var tag_list : Vector.<String> = new Vector.<String>();
|
||||
|
||||
while (i < lines.length) {
|
||||
var line : String = lines[i++];
|
||||
|
||||
// discard blank line, length could be 0 or if DOS terminated line (CR/LF), only a CR char
|
||||
if (line.length <= 0 || line.indexOf("\r") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// discard tags pertaining to a playlist and not a fragment, that require no further processing
|
||||
if (line.indexOf(HEADER) == 0 || line.indexOf(TARGETDURATION) == 0 || line.indexOf(VERSION) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.indexOf(SEQNUM) == 0) {
|
||||
seqnum = parseInt(line.substr(SEQNUM.length));
|
||||
} else if (line.indexOf(DISCONTINUITY_SEQ) == 0) {
|
||||
continuity_index = parseInt(line.substr(DISCONTINUITY_SEQ.length));
|
||||
} else if (line.indexOf(BYTERANGE) == 0) {
|
||||
var params : Array = line.substr(BYTERANGE.length).split('@');
|
||||
if (params.length == 1) {
|
||||
byterange_start_offset = byterange_end_offset;
|
||||
} else {
|
||||
byterange_start_offset = parseInt(params[1]);
|
||||
}
|
||||
byterange_end_offset = parseInt(params[0]) + byterange_start_offset;
|
||||
tag_list.push(line);
|
||||
} else if (line.indexOf(KEY) == 0) {
|
||||
// #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52",IV=.....
|
||||
var keyLine : String = line.substr(KEY.length);
|
||||
// reset previous values
|
||||
decrypt_url = null;
|
||||
decrypt_iv = null;
|
||||
// remove space, single and double quote
|
||||
keyLine = keyLine.replace(replacespace, "");
|
||||
keyLine = keyLine.replace(replacesinglequote, "");
|
||||
keyLine = keyLine.replace(replacedoublequote, "");
|
||||
var keyArray : Array = keyLine.split(",");
|
||||
var keyArray2 : Array = new Array();
|
||||
/* correctly parse #EXT-X-KEY:METHOD=AES-128,URI="Keys(LiveTest-m3u8-aapl,format=m3u8-aapl).key"
|
||||
* take into account comma not followed by attribute name, append value to previous entry
|
||||
*/
|
||||
for each (var key : String in keyArray) {
|
||||
if (key.indexOf("METHOD=") == 0 || key.indexOf("URI=") == 0 || key.indexOf("IV=") == 0 || key.indexOf("KEYFORMAT=") == 0 || key.indexOf("KEYFORMATVERSIONS=") == 0) {
|
||||
keyArray2.push(key);
|
||||
} else {
|
||||
if (keyArray2.length) {
|
||||
// append to previous element
|
||||
keyArray2[keyArray2.length - 1] += "," + key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for each (var keyProperty : String in keyArray2) {
|
||||
var delimiter : int = keyProperty.indexOf("=");
|
||||
if (delimiter == -1) {
|
||||
throw new Error("invalid playlist, no delimiter while parsing:" + keyProperty);
|
||||
}
|
||||
var tag : String = keyProperty.substr(0, delimiter).toUpperCase();
|
||||
var value : String = keyProperty.substr(delimiter + 1);
|
||||
switch(tag) {
|
||||
case "METHOD":
|
||||
switch (value) {
|
||||
case "NONE":
|
||||
case "AES-128":
|
||||
break;
|
||||
case "AES-SAMPLE":
|
||||
throw new Error("encryption method " + value + "not supported (yet ;-))");
|
||||
default:
|
||||
throw new Error("invalid encryption method " + value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "URI":
|
||||
decrypt_url = _extractURL(value, base);
|
||||
break;
|
||||
case "IV":
|
||||
decrypt_iv = Hex.toArray(zeropad(value.substr("0x".length), 32));
|
||||
break;
|
||||
case "KEYFORMAT":
|
||||
case "KEYFORMATVERSIONS":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
tag_list.push(line);
|
||||
} else if (line.indexOf(PROGRAMDATETIME) == 0) {
|
||||
// CONFIG::LOGGING {
|
||||
// Log.info(line);
|
||||
// }
|
||||
program_date = DateUtil.parseW3CDTF(line.substr(PROGRAMDATETIME.length)).getTime();
|
||||
program_date_defined = true;
|
||||
tag_list.push(line);
|
||||
} else if (line.indexOf(DISCONTINUITY) == 0) {
|
||||
continuity_index++;
|
||||
tag_list.push(line);
|
||||
} else if (line.indexOf(FRAGMENT) == 0) {
|
||||
var comma_position : int = line.indexOf(',');
|
||||
var duration : Number = (comma_position == -1) ? parseFloat(line.substr(FRAGMENT.length)) : parseFloat(line.substr(FRAGMENT.length, comma_position - FRAGMENT.length));
|
||||
extinf_found = true;
|
||||
tag_list.push(line);
|
||||
} else if (line.indexOf('#') == 0) {
|
||||
// unsupported/custom tags, store them
|
||||
tag_list.push(line);
|
||||
} else if (extinf_found == true) {
|
||||
var url : String = _extractURL(line, base);
|
||||
var fragment_decrypt_iv : ByteArray;
|
||||
if (decrypt_url != null) {
|
||||
/* as per HLS spec :
|
||||
if IV not defined, then use seqnum as IV :
|
||||
http://tools.ietf.org/html/draft-pantos-http-live-streaming-11#section-5.2
|
||||
*/
|
||||
if (decrypt_iv != null) {
|
||||
fragment_decrypt_iv = decrypt_iv;
|
||||
} else {
|
||||
fragment_decrypt_iv = Hex.toArray(zeropad(seqnum.toString(16), 32));
|
||||
}
|
||||
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("sn/key/iv:" + seqnum + "/" + decrypt_url + "/" + Hex.fromArray(fragment_decrypt_iv));
|
||||
}
|
||||
} else {
|
||||
fragment_decrypt_iv = null;
|
||||
}
|
||||
fragments.push(new Fragment(url, duration, level, seqnum++, start_time, continuity_index, program_date, decrypt_url, fragment_decrypt_iv, byterange_start_offset, byterange_end_offset, tag_list));
|
||||
start_time += duration;
|
||||
if (program_date_defined) {
|
||||
program_date += 1000 * duration;
|
||||
}
|
||||
extinf_found = false;
|
||||
tag_list = new Vector.<String>();
|
||||
byterange_start_offset = -1;
|
||||
}
|
||||
}
|
||||
if (fragments.length == 0) {
|
||||
// throw new Error("No TS fragments found in " + base);
|
||||
null;
|
||||
// just to avoid compilation warnings if CONFIG::LOGGING is false
|
||||
CONFIG::LOGGING {
|
||||
Log.warn("No TS fragments found in " + base);
|
||||
}
|
||||
}
|
||||
return fragments;
|
||||
};
|
||||
|
||||
/** Extract levels from manifest data. **/
|
||||
public static function extractLevels(data : String, base : String = '') : Vector.<Level> {
|
||||
var levels : Vector.<Level> = new Vector.<Level>();
|
||||
var bitrateDictionary : Dictionary = new Dictionary();
|
||||
var level : Level;
|
||||
var lines : Vector.<String> = StringUtil.toLines(data);
|
||||
var level_found : Boolean = false;
|
||||
var i : int = 0;
|
||||
while (i < lines.length) {
|
||||
var line : String = lines[i++];
|
||||
// discard blank line, length could be 0 or 1 if DOS terminated line (CR/LF)
|
||||
if (line.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
if (line.indexOf(LEVEL) == 0) {
|
||||
level_found = true;
|
||||
level = new Level();
|
||||
var params : Array = line.substr(LEVEL.length).split(',');
|
||||
for (var j : int = 0; j < params.length; j++) {
|
||||
var param : String = params[j];
|
||||
if (param.indexOf('BANDWIDTH') > -1) {
|
||||
level.bitrate = parseInt(param.split('=')[1]);
|
||||
} else if (param.indexOf('RESOLUTION') > -1) {
|
||||
var res : String = param.split('=')[1] as String;
|
||||
var dim : Array = res.split('x');
|
||||
level.width = parseInt(dim[0]);
|
||||
level.height = parseInt(dim[1]);
|
||||
} else if (param.indexOf('CODECS') > -1) {
|
||||
if (line.indexOf('avc1') > -1) {
|
||||
level.codec_h264 = true;
|
||||
} else {
|
||||
level.audio = true;
|
||||
}
|
||||
if (line.indexOf('mp4a.40.2') > -1 || line.indexOf('mp4a.40.5') > -1) {
|
||||
level.codec_aac = true;
|
||||
} else if (line.indexOf('mp4a.40.34') > -1) {
|
||||
level.codec_mp3 = true;
|
||||
}
|
||||
} else if (param.indexOf('AUDIO') > -1) {
|
||||
level.audio_stream_id = (param.split('=')[1] as String).replace(replacedoublequote, "").replace(trimwhitespace, "");
|
||||
} else if (param.indexOf('CLOSED-CAPTIONS') > -1) {
|
||||
level.closed_captions = (param.split('=')[1] as String).replace(replacedoublequote, "").replace(trimwhitespace, "");
|
||||
} else if (param.indexOf('NAME') > -1) {
|
||||
level.name = (param.split('=')[1] as String).replace(replacedoublequote, "");
|
||||
}
|
||||
}
|
||||
} else if (level_found == true) {
|
||||
if(!(level.bitrate in bitrateDictionary)) {
|
||||
level.urls = new Vector.<String>();
|
||||
level.urls.push(_extractURL(line, base));
|
||||
level.manifest_index = levels.length;
|
||||
levels.push(level);
|
||||
bitrateDictionary[level.bitrate] = level;
|
||||
} else {
|
||||
level = bitrateDictionary[level.bitrate];
|
||||
var redundantURL:String = _extractURL(line, base);
|
||||
level.urls.push(redundantURL);
|
||||
CONFIG::LOGGING {
|
||||
Log.debug("found failover level with url " + redundantURL);
|
||||
}
|
||||
}
|
||||
level_found = false;
|
||||
}
|
||||
}
|
||||
levels.sort(compareLevel);
|
||||
for (i = 0; i < levels.length; i++) {
|
||||
levels[i].index = i;
|
||||
}
|
||||
return levels;
|
||||
};
|
||||
|
||||
/* compare level, smallest bitrate first */
|
||||
private static function compareLevel(x : Level, y : Level) : Number {
|
||||
return (x.bitrate - y.bitrate);
|
||||
}
|
||||
|
||||
/** Extract Alternate Audio Tracks from manifest data. **/
|
||||
public static function extractAltAudioTracks(data : String, base : String = '') : Vector.<AltAudioTrack> {
|
||||
var altAudioTracks : Vector.<AltAudioTrack> = new Vector.<AltAudioTrack>();
|
||||
var lines : Vector.<String> = StringUtil.toLines(data);
|
||||
var i : int = 0;
|
||||
while (i < lines.length) {
|
||||
var line : String = lines[i++];
|
||||
if (line.indexOf(MEDIA) == 0) {
|
||||
var params : Object = _parseAlternateRendition(line);
|
||||
|
||||
// #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 1",AUTOSELECT=YES,DEFAULT=YES
|
||||
// #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",LANGUAGE="eng",NAME="BipBop Audio 2",AUTOSELECT=NO,DEFAULT=NO,URI="alternate_audio_aac_sinewave/prog_index.m3u8"
|
||||
// #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="captions.m3u8"
|
||||
|
||||
var uri : String = params['URI'];
|
||||
if (uri) {
|
||||
uri = _extractURL(uri, base);
|
||||
}
|
||||
if (params['TYPE'] == 'AUDIO') {
|
||||
var alternate_audio : AltAudioTrack = new AltAudioTrack(params['GROUP-ID'], params['LANGUAGE'], params['NAME'], params['AUTOSELECT'] == 'YES', params['DEFAULT'] == 'YES', uri);
|
||||
altAudioTracks.push(alternate_audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
return altAudioTracks;
|
||||
};
|
||||
|
||||
private static const RENDITION_STATE_READKEY : Number = 1;
|
||||
private static const RENDITION_STATE_READVALUESTART : Number = 2;
|
||||
private static const RENDITION_STATE_READSIMPLEVALUE : Number = 3;
|
||||
private static const RENDITION_STATE_READQUOTEDVALUE : Number = 4;
|
||||
private static const STATE_READQUOTEDVALUE_END : Number = 5;
|
||||
|
||||
private static function _parseAlternateRendition(line : String) : Object {
|
||||
var variables : Object = new Object();
|
||||
var state : Number = RENDITION_STATE_READKEY;
|
||||
var pos : Number = 0;
|
||||
var c : String;
|
||||
var key : String = "";
|
||||
var value : String = "";
|
||||
line = line.substr(MEDIA.length);
|
||||
|
||||
while (pos < line.length) {
|
||||
c = line.charAt(pos);
|
||||
pos++;
|
||||
switch (state) {
|
||||
case RENDITION_STATE_READKEY:
|
||||
if (c == '=') {
|
||||
state = RENDITION_STATE_READVALUESTART;
|
||||
} else {
|
||||
key += c;
|
||||
}
|
||||
break;
|
||||
case RENDITION_STATE_READVALUESTART:
|
||||
if (c == '"') {
|
||||
state = RENDITION_STATE_READQUOTEDVALUE;
|
||||
} else {
|
||||
value += c;
|
||||
state = RENDITION_STATE_READSIMPLEVALUE;
|
||||
}
|
||||
break;
|
||||
case RENDITION_STATE_READSIMPLEVALUE:
|
||||
if (c == ",") {
|
||||
variables[StringUtil.trim(key)] = value;
|
||||
key = "";
|
||||
value = "";
|
||||
state = RENDITION_STATE_READKEY;
|
||||
} else {
|
||||
value += c;
|
||||
}
|
||||
break;
|
||||
case RENDITION_STATE_READQUOTEDVALUE:
|
||||
if (c == '"') {
|
||||
state = STATE_READQUOTEDVALUE_END;
|
||||
} else {
|
||||
value += c;
|
||||
}
|
||||
break;
|
||||
case STATE_READQUOTEDVALUE_END:
|
||||
if (c == ",") {
|
||||
variables[StringUtil.trim(key)] = value;
|
||||
key = "";
|
||||
value = "";
|
||||
state = RENDITION_STATE_READKEY;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (key) {
|
||||
variables[StringUtil.trim(key)] = value;
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/** Extract whether the stream is live or ondemand. **/
|
||||
public static function hasEndlist(data : String) : Boolean {
|
||||
if (data.indexOf(ENDLIST) > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
public static function getTargetDuration(data : String) : Number {
|
||||
var lines : Vector.<String> = StringUtil.toLines(data);
|
||||
var i : int = 0;
|
||||
var targetduration : Number = 0;
|
||||
|
||||
// first look for target duration
|
||||
while (i < lines.length) {
|
||||
var line : String = lines[i++];
|
||||
if (line.indexOf(TARGETDURATION) == 0) {
|
||||
targetduration = parseFloat(line.substr(TARGETDURATION.length));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return targetduration;
|
||||
}
|
||||
|
||||
/** Extract URL (check if absolute or not). **/
|
||||
private static function _extractURL(path : String, base : String) : String {
|
||||
var _prefix : String = null;
|
||||
var _suffix : String = null;
|
||||
// trim white space if any
|
||||
path.replace(replacespace, "");
|
||||
if (path.substr(0, 7) == 'http://' || path.substr(0, 8) == 'https://') {
|
||||
return path;
|
||||
} else {
|
||||
// Remove querystring
|
||||
if (base.indexOf('?') > -1) {
|
||||
base = base.substr(0, base.indexOf('?'));
|
||||
}
|
||||
// domain-absolute
|
||||
if (path.charAt(0) == '/') {
|
||||
// base = http[s]://domain/subdomain:1234/otherstuff
|
||||
// prefix = http[s]://
|
||||
// suffix = domain/subdomain:1234/otherstuff
|
||||
_prefix = base.substr(0, base.indexOf("//") + 2);
|
||||
_suffix = base.substr(base.indexOf("//") + 2);
|
||||
if(path.charAt(1) == '/') {
|
||||
return _prefix + path.substr(2);
|
||||
} else {
|
||||
// return http[s]://domain/subdomain:1234/path
|
||||
return _prefix + _suffix.substr(0, _suffix.indexOf("/")) + path;
|
||||
}
|
||||
} else {
|
||||
return base.substr(0, base.lastIndexOf('/') + 1) + path;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user