blob: 21aa3ef059cc3d7bba430d17eed6f1fab4f45486 [file] [log] [blame]
// Copyright (c) 2012 The WebM project authors. All Rights Reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree. An additional intellectual property rights grant can be found
// in the file PATENTS. All contributing project authors may
// be found in the AUTHORS file in the root of the source tree.
'use strict';
/**
* Download and parse Dash manifest file.
* @param {string} url The url for the manifest file.
* @constructor
*/
function DashParser(url) {
this.url_ = url;
}
/**
* Local file request has succeeded status code.
* @const
* @type {number}
*/
DashParser.FILE_OK_STATUS_CODE = 0;
/**
* HTTP request has succeeded status code.
* @const
* @type {number}
*/
DashParser.HTTP_OK_STATUS_CODE = 200;
/**
* Static function to return version string.
* @return {string} version.
*/
DashParser.version = function() {
return '0.1.0.1';
};
/**
* Static function to parse XML duration.
* @param {string} xsDuration XML duration.
* @return {Object} The status object. {Object}.value returns the duration in
* seconds.
*/
DashParser.parseXsDuration = function(xsDuration) {
// TODO(fgalligan): Handle years, months, and days.
if (xsDuration.substr(0, 2).toUpperCase() != 'PT')
return new ErrorStatus('PT not in xsDuration:' + xsDuration);
var hours = xsDuration.match(/\d+(?=H)/i);
var minutes = xsDuration.match(/\d+(?=M)/i);
var seconds = xsDuration.match(/\d+\.?\d*(?=S)/i);
var duration = 0;
if (hours)
duration += parseFloat(hours[0]) * 3600;
if (minutes)
duration += parseFloat(minutes[0]) * 60;
if (seconds)
duration += parseFloat(seconds[0]);
return {status: ErrorStatus.STATUS_OK, value: duration};
};
/**
* Url pointing to the manifest file.
* @private
* @type {string}
*/
DashParser.prototype.url_ = null;
/**
* XML document representation of the manifest.
* @private
* @type {string}
*/
DashParser.prototype.xmlDoc_ = null;
/**
* Logging function to be set by the application.
* @param {string} str The input string to be logged.
*/
DashParser.prototype.log = function(str) {};
/**
* Loads the manifest file and initializes the MPD class. The format for
* callback(object) is obj.status {number} The returned status and
* [obj.reason] {string} Extra information on the status.
* @param {function} callback Callback function.
*/
DashParser.prototype.load = function(callback) {
var xhReq = null;
var t = this;
this.callback = callback;
if (this.url_ == null) {
callback(new ErrorStatus('DashParser url is null.'));
return;
}
if (window.XMLHttpRequest) {
xhReq = new XMLHttpRequest();
} else {
xhReq = new ActiveXObject('Microsoft.XMLHTTP');
}
xhReq.onreadystatechange = function() {
if (xhReq.readyState != 4) {
return;
}
if (xhReq.status != DashParser.HTTP_OK_STATUS_CODE &&
xhReq.status != DashParser.FILE_OK_STATUS_CODE) {
t.callback(new ErrorStatus('Bad xhReq.status:' + xhReq.status));
} else {
t.xmlDoc_ = xhReq.responseXML;
t.mpd = new MPD();
var res = t.mpd.parseXmlDoc(t.xmlDoc_);
if (res.status != ErrorStatus.STATUS_OK) {
var errorStr = 'DashParser Parsing XML document failed. :' + res.reason;
this.log(errorStr);
t.callback(new ErrorStatus(errorStr));
}
t.callback(new OKStatus());
}
};
xhReq.open('GET', this.url_, true);
xhReq.send(null);
};
/**
* Class representing a SegmentBase element from a Dash file.
* @constructor
*/
function SegmentBase() {
}
/**
* Factory function to create a SegmentBase class.
* @param {XmlNodeList} node SegmentBase element list.
* @return {SegmentBase} SegmentBase object.
*/
SegmentBase.parseSegmentBaseXML = function(node) {
var sb = new SegmentBase();
var attribute = node.getAttribute('indexRange');
if (attribute) {
sb.indexRange = attribute.split('-');
sb.indexRange[0] = parseInt(sb.indexRange[0]);
if (isNaN(sb.indexRange[0]))
return NULL;
sb.indexRange[1] = parseInt(sb.indexRange[1]);
if (isNaN(sb.indexRange[1]))
return NULL;
}
// There should be at most one Initialisation element.
var initNode = node.getElementsByTagName('Initialisation');
if (initNode.length > 0) {
var attribute = initNode[0].getAttribute('range');
if (attribute) {
sb.headerRange = attribute.split('-');
sb.headerRange[0] = parseInt(sb.headerRange[0]);
if (isNaN(sb.headerRange[0]))
return NULL;
sb.headerRange[1] = parseInt(sb.headerRange[1]);
if (isNaN(sb.headerRange[1]))
return NULL;
}
}
return sb;
};
/**
* Start and end offset for the header.
* @type {Array.<number>}
*/
SegmentBase.prototype.headerRange = null;
/**
* Start and end offset for the index.
* @type {Array.<number>}
*/
SegmentBase.prototype.indexRange = null;
/**
* Class representing a MPD element.
* @constructor
*/
function MPD() {
this.periodList = [];
}
/**
* XML Document node type.
* @const
* @type {number}
*/
MPD.DOCUMENT_TYPE_NODE = 9;
/**
* Duration in seconds of the MPD.
* @private
* @type {number}
*/
MPD.prototype.mediaPresentationDuration_ = Number.NaN;
/**
* Minimum buffer time in seconds of the MPD.
* @private
* @type {number}
*/
MPD.prototype.minBufferTime_ = Number.NaN;
/**
* MPD profiles.
* @private
* @type {string}
*/
MPD.prototype.profiles_ = null;
/**
* Type of the MPD.
* @private
* @type {string}
*/
MPD.prototype.type_ = 'static';
/**
* List of Period classes.
* @type {Array.<Period>}
*/
MPD.prototype.periodList = null;
/**
* Parse the mpd element from the XML document.
* @param {XMLDocument} xmlDoc XML Document.
* @return {Object} The status object.
*/
MPD.prototype.parseXmlDoc = function(xmlDoc) {
if (xmlDoc.nodeType != MPD.DOCUMENT_TYPE_NODE)
return new ErrorStatus('xmlDoc is not a DOCUMENT_TYPE_NODE.');
var attribute = xmlDoc.documentElement.getAttribute('type');
if (attribute)
this.type_ = attribute;
var attribute =
xmlDoc.documentElement.getAttribute('mediaPresentationDuration');
if (attribute) {
var res = DashParser.parseXsDuration(attribute);
if (res.status == ErrorStatus.STATUS_OK)
this.mediaPresentationDuration_ = res.value;
}
attribute = xmlDoc.documentElement.getAttribute('minBufferTime');
if (attribute) {
var res = DashParser.parseXsDuration(attribute);
if (res.status == ErrorStatus.STATUS_OK)
this.minBufferTime_ = res.value;
}
this.profiles_ = xmlDoc.documentElement.getAttribute('profiles');
var res = this.validate();
if (res.status != ErrorStatus.STATUS_OK)
return res;
var xmlPeriodList = xmlDoc.getElementsByTagName('Period');
for (var i = 0; i < xmlPeriodList.length; i++) {
var period = new Period(this);
var res = period.parseXmlNode(xmlPeriodList[i]);
if (res.status != ErrorStatus.STATUS_OK)
return res;
this.periodList.push(period);
}
return new OKStatus();
};
/**
* Validates the values in the MPD class. This should be called after the MPD
* element has been parsed.
* @return {Object} The status object.
*/
MPD.prototype.validate = function() {
//TODO(fgalligan): Handle dynamic type.
if (this.type_.toUpperCase() !== 'STATIC')
return new ErrorStatus('MPD type is not static. :' + this.type_);
if (isNaN(this.minBufferTime_))
return new ErrorStatus('MPD is missing minBufferTime.');
// Must be present in static manifests.
if (isNaN(this.mediaPresentationDuration_))
return new ErrorStatus(
'static MPD missing mediaPresentationDuration.');
return new OKStatus();
};
/**
* Return the media duration in seconds from the mpd.
* @return {number} The duration in seconds.
*/
MPD.prototype.getDuration = function() {
return this.mediaPresentationDuration_;
};
/**
* Return the minimum buffer time in seconds from the mpd.
* @return {number} The minimum buffer time in seconds.
*/
MPD.prototype.getMinBufferType = function() {
return this.minBufferTime_;
};
/**
* Return the profiles for the MPD.
* @return {string} profiles.
*/
MPD.prototype.getProfiles = function() {
return this.profiles_;
};
/**
* Class representing a Period element.
* @param {MPD} parent Period's parent object.
* @constructor
*/
function Period(parent) {
this.parent = parent;
this.adaptationSetList = [];
}
/**
* bitstreamSwitching flag from Period.
* @private
* @type {bool}
*/
Period.prototype.bitstreamSwitching_ = false;
/**
* Duration in seconds of the Period.
* @private
* @type {number}
*/
Period.prototype.duration_ = Number.NaN;
/**
* Start time in seconds of the Period.
* @private
* @type {number}
*/
Period.prototype.start_ = Number.NaN;
/**
* Period id.
* @type {string}
*/
Period.prototype.id = null;
/**
* Reference to the MPD class that owns the Period.
* @type {MPD}
*/
Period.prototype.parent = null;
/**
* List of AdaptationSet classes.
* @type {Array.<AdaptationSet>}
*/
Period.prototype.adaptationSetList = null;
/**
* Search the adaptation set list to find the first adaption set that contains
* the search pattern.
* @param {RegExp} pattern RegExp to search for.
* @return {AdaptationSet} An AdaptationSet. Return NULL if one cannot be found.
*/
Period.prototype.findFirstAdaptationSet = function(pattern) {
var as = null;
var l = this.adaptationSetList.length;
for (var i = 0; i < l; ++i) {
// TODO(fgalligan): Add support to search through the Representation.
if (this.adaptationSetList[i].codecs_.search(pattern) != -1) {
as = this.adaptationSetList[i];
break;
}
}
return as;
};
/**
* Parse the Period element from the XML document.
* @param {XmlNodeList} node Period XML node.
* @return {Object} The status object.
*/
Period.prototype.parseXmlNode = function(node) {
if (!this.parent)
return new ErrorStatus('Period has no parent.');
this.bitstreamSwitching_ = node.getAttribute('bitstreamSwitching') === 'true';
this.id = node.getAttribute('id');
var attribute = node.getAttribute('duration');
if (attribute) {
var res = DashParser.parseXsDuration(attribute);
if (res.status == ErrorStatus.STATUS_OK)
this.duration_ = res.value;
}
attribute = node.getAttribute('start');
if (attribute) {
var res = DashParser.parseXsDuration(attribute);
if (res.status == ErrorStatus.STATUS_OK)
this.start_ = res.value;
}
var res = this.validate();
if (res.status != ErrorStatus.STATUS_OK)
return res;
var xmlASList = node.getElementsByTagName('AdaptationSet');
for (var i = 0; i < xmlASList.length; i++) {
var as = new AdaptationSet(this);
var res = as.parseXmlNode(xmlASList[i]);
if (res.status != ErrorStatus.STATUS_OK)
return res;
this.adaptationSetList.push(as);
}
return new OKStatus();
};
/**
* Validates the values in the Period class. This should be called after the
* Period element has been parsed.
* @return {Object} The status object.
*/
Period.prototype.validate = function() {
return new OKStatus();
};
/**
* Return the bitstreamSwitching flag for the Period.
* @return {bool} bitstreamSwitching.
*/
Period.prototype.getBitstreamSwitching = function() {
return this.bitstreamSwitching_;
};
/**
* Return the Period duration in seconds. If duration is 0 get the duration
* from the parent.
* @return {number} The duration in seconds.
*/
Period.prototype.getDuration = function() {
if (isNaN(this.duration_))
return this.parent.getDuration();
return this.duration_;
};
/**
* Class representing an AdaptationSet element.
* @param {Period} parent AdaptationSet's parent object.
* @constructor
*/
function AdaptationSet(parent) {
this.parent = parent;
this.representationList = [];
}
/**
* Value representing bitstreamSwitching flag from AdaptationSet. If value is
* -1 that means the value has not been set. If value is 0 that represents
* 'false'. If value is 1 that represents 'true'.
* @private
* @type {number}
*/
AdaptationSet.prototype.bitstreamSwitching_ = -1;
/**
* Codec string for all of the representations within the adaptation set.
* @private
* @type {string}
*/
AdaptationSet.prototype.codecs_ = null;
/**
* AdaptationSet height.
* @private
* @type {number}
*/
AdaptationSet.prototype.height_ = Number.NaN;
/**
* AdaptationSet id.
* @type {string}
*/
AdaptationSet.prototype.id = null;
/**
* AdaptationSet language.
* @type {string}
*/
AdaptationSet.prototype.lang = null;
/**
* Reference to the Period class that owns the adaptation set.
* @type {Period}
*/
AdaptationSet.prototype.parent = null;
/**
* Mimetype for all of the representations within the adaptation set.
* @private
* @type {string}
*/
AdaptationSet.prototype.mimetype_ = null;
/**
* List of Representation classes.
* @type {Array.<Representation>}
*/
AdaptationSet.prototype.representationList = null;
/**
* AdaptationSet segmentAlignment flag.
* @type {boolean}
*/
AdaptationSet.prototype.segmentAlignment = false;
/**
* AdaptationSet subsegmentAlignment flag. Flag telling if the adaptation set's
* sub segment transition points match for all the representations contained
* within the adaptation set.
* @private
* @type {boolean}
*/
AdaptationSet.prototype.subsegmentAlignment_ = false;
/**
* AdaptationSet subSegmentStartsWithSAP.
* within the adaptation set.
* @private
* @type {boolean}
*/
AdaptationSet.prototype.subsegmentStartsWithSAP_ = 0;
/**
* AdaptationSet width.
* @private
* @type {number}
*/
AdaptationSet.prototype.width_ = Number.NaN;
/**
* Parse the AdaptationSet element from the XML document.
* @param {XmlNodeList} node AdaptationSet nodes.
* @return {Object} The status object.
*/
AdaptationSet.prototype.parseXmlNode = function(node) {
if (!this.parent)
return new ErrorStatus('AdaptationSet has no parent.');
var attribute = node.getAttribute('bitstreamSwitching');
if (attribute) {
if (attribute === 'true') {
this.bitstreamSwitching_ = 1;
} else {
this.bitstreamSwitching_ = 0;
}
}
this.codecs_ = node.getAttribute('codecs');
this.height_ = parseInt(node.getAttribute('height'));
this.id = node.getAttribute('id');
this.lang = node.getAttribute('lang');
this.mimetype_ = node.getAttribute('mimeType');
this.segmentAlignment = node.getAttribute('segmentAlignment') === 'true';
this.subsegmentAlignment_ =
node.getAttribute('subsegmentAlignment') === 'true';
this.subsegmentStartsWithSAP_ =
parseInt(node.getAttribute('subsegmentStartsWithSAP'));
this.width_ = parseInt(node.getAttribute('width'));
var res = this.validate();
if (res.status != ErrorStatus.STATUS_OK)
return res;
var xmlRepList = node.getElementsByTagName('Representation');
for (var i = 0; i < xmlRepList.length; i++) {
var r = new Representation(this);
var res = r.parseXmlNode(xmlRepList[i]);
if (res.status != ErrorStatus.STATUS_OK)
return res;
this.representationList.push(r);
}
return new OKStatus();
};
/**
* Validates the values in the AdaptationSet class. This should be called after
* the AdaptationSet element has been parsed.
* @return {Object} The status object.
*/
AdaptationSet.prototype.validate = function() {
return new OKStatus();
};
/**
* Return the bitstreamSwitching flag for the AdaptationSet. If not set in the
* AdaptationSet check the parent.
* @return {bool} bitstreamSwitching.
*/
AdaptationSet.prototype.getBitstreamSwitching = function() {
if (this.bitstreamSwitching_ == -1)
return this.parent.getBitstreamSwitching();
return this.bitstreamSwitching_ == 1;
};
/**
* Return the codecs for the AdaptationSet.
* @return {string} codecs.
*/
AdaptationSet.prototype.getCodecs = function() {
return this.codecs_;
};
/**
* Return the duration in seconds from the parent.
* @return {number} The duration in seconds.
*/
AdaptationSet.prototype.getDuration = function() {
return this.parent.getDuration();
};
/**
* Return the height for the AdaptationSet.
* @return {number} height in pixels.
*/
AdaptationSet.prototype.getHeight = function() {
return this.height_;
};
/**
* Return the mimetype for the AdaptationSet.
* @return {string} mimetype.
*/
AdaptationSet.prototype.getMimetype = function() {
return this.mimetype_;
};
/**
* Return the subsegmentAlignment for the AdaptationSet.
* @return {bool} subsegmentAlignment.
*/
AdaptationSet.prototype.getSubsegmentAlignment = function() {
return this.subsegmentAlignment_;
};
/**
* Return the subsegmentStartsWithSAP for the AdaptationSet.
* @return {number} subsegmentStartsWithSAP.
*/
AdaptationSet.prototype.getSubsegmentStartsWithSAP = function() {
return this.subsegmentStartsWithSAP_;
};
/**
* Return the width for the AdaptationSet.
* @return {number} width in pixels.
*/
AdaptationSet.prototype.getWidth = function() {
return this.width_;
};
/**
* Class representing a Representation element.
* @param {AdaptationSet} parent Representation's parent object.
* @constructor
*/
function Representation(parent) {
this.parent = parent;
}
/**
* Representation bandwidth.
* @type {number}
*/
Representation.prototype.bandwidth = Number.NaN;
/**
* Representation codecs.
* @private
* @type {string}
*/
Representation.prototype.codecs_ = null;
/**
* Height in pixels of the video stream.
* @private
* @type {number}
*/
Representation.prototype.height_ = Number.NaN;
/**
* Representation id.
* @type {string}
*/
Representation.prototype.id = null;
/**
* Representation mimetype.
* @private
* @type {string}
*/
Representation.prototype.mimetype_ = null;
/**
* Reference to the AdaptationSet class that owns the Representation.
* @type {AdaptationSet}
*/
Representation.prototype.parent = null;
/**
* Sample rate of the audio stream.
* @type {number}
*/
Representation.prototype.audioSamplingRate = Number.NaN;
/**
* Class describing the SegmentBase.
* @private
* @type {SegmentBase}
*/
Representation.prototype.segmentBase_ = null;
/**
* Link to the media file.
* @private
* @type {string}
*/
Representation.prototype.url_ = null;
/**
* Width in pixels of the video stream.
* @private
* @type {number}
*/
Representation.prototype.width_ = Number.NaN;
/**
* Parse the Representation element from the XML document.
* @param {XmlNodeList} node Representation node.
* @return {Object} The status object.
*/
Representation.prototype.parseXmlNode = function(node) {
if (!this.parent)
return new ErrorStatus('Representation has no parent.');
this.id = node.getAttribute('id');
this.mimetype_ = node.getAttribute('mimetype');
this.codecs_ = node.getAttribute('codecs');
this.bandwidth = parseInt(node.getAttribute('bandwidth'));
this.height_ = parseInt(node.getAttribute('height'));
this.audioSamplingRate = parseInt(node.getAttribute('audioSamplingRate'));
this.width_ = parseInt(node.getAttribute('width'));
// TODO(fgalligan): Add support for more than one BaseUrl.
var baseUrlList = node.getElementsByTagName('BaseURL');
if (baseUrlList.length > 0) {
this.url_ = baseUrlList[0].childNodes[0].nodeValue;
}
// There should be at most one SegmentBase element.
var segmentBaseList = node.getElementsByTagName('SegmentBase');
if (segmentBaseList.length > 0) {
this.segmentBase_ = SegmentBase.parseSegmentBaseXML(segmentBaseList[0]);
}
var res = this.validate();
if (res.status != ErrorStatus.STATUS_OK)
return res;
return new OKStatus();
};
/**
* Validates the values in the Representation class. This should be called after
* the Representation element has been parsed.
* @return {Object} The status object.
*/
Representation.prototype.validate = function() {
if (!this.id)
return new ErrorStatus('Representation has no id.');
if (isNaN(this.bandwidth))
return new ErrorStatus('Representation has no bandwidth.');
if (this.bandwidth < 1)
return new ErrorStatus('Representation bandwidth is bad. :' +
this.bandwidth);
if (this.getCodecs() == null)
return new ErrorStatus('codecs is null.');
if (this.getMimetype() == null)
return new ErrorStatus('mimetype is null.');
return new OKStatus();
};
/**
* Return the codecs for the Representation. If not set in the Representation
* check the parent.
* @return {string} codecs.
*/
Representation.prototype.getCodecs = function() {
if (this.codecs_)
return this.codecs_;
return this.parent.getCodecs();
};
/**
* Return the duration in seconds from the parent.
* @return {number} The duration in seconds.
*/
Representation.prototype.getDuration = function() {
return this.parent.getDuration();
};
/**
* Return the height for the Representation. If not set in the Representation
* check the parent.
* @return {number} height in pixels.
*/
Representation.prototype.getHeight = function() {
if (isNaN(this.height_))
return this.parent.getHeight();
return this.height_;
};
/**
* Return the mimetype for the Representation. If not set in the Representation
* check the parent.
* @return {string} mimetype.
*/
Representation.prototype.getMimetype = function() {
if (this.mimetype_)
return this.mimetype_;
return this.parent.getMimetype();
};
/**
* Return the full URL for the Representation.
* @return {string} URL.
*/
Representation.prototype.getFullURL = function() {
return this.url_;
};
/**
* Return the width for the Representation. If not set in the Representation
* check the parent.
* @return {number} width in pixels.
*/
Representation.prototype.getWidth = function() {
if (isNaN(this.width_))
return this.parent.getWidth();
return this.width_;
};
/**
* Return the header range for the Representation.
* @return {Array} a two element array with element 0 the start offset and
* element 1 the end offset. Returns null if the header is not set.
*/
Representation.prototype.headerRange = function() {
if (!this.segmentBase_ || !this.segmentBase_.headerRange)
return null;
return this.segmentBase_.headerRange;
};
/**
* Return the index range for the Representation.
* @return {Array} a two element array with element 0 the start offset and
* element 1 the end offset. Returns null if the index is not set.
*/
Representation.prototype.indexRange = function() {
if (!this.segmentBase_ || !this.segmentBase_.indexRange)
return null;
return this.segmentBase_.indexRange;
};