blob: af43c99ae35abbfd8874343134e873e31d132e75 [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';
/**
* Class to parse binary EBML data.
* @constructor
*/
function EbmlParser() {
}
/**
* Return code signaling everything is fine.
* @const
* @type {number}
*/
EbmlParser.STATUS_OK = 0;
/**
* Invalid data return code.
* @const
* @type {number}
*/
EbmlParser.STATUS_INVALID_DATA = -1;
/**
* Return code signaling more data is needed.
* @const
* @type {number}
*/
EbmlParser.STATUS_NEED_MORE_DATA = -2;
/**
* Maximum size of an EBML header in bytes.
* @const
* @type {number}
*/
EbmlParser.MAX_ELEMENT_HEADER_SIZE = 12;
/**
* Static function to parse an EMBL number.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @param {number} maxBytes Max bytes of number.
* @param {boolean} maskFirstByte Flag telling if the first byte of the number
* is EBML encoded.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is number.
* @private
*/
EbmlParser.parseNum_ = function(buf, start, size, maxBytes, maskFirstByte) {
if (size <= 0)
return {status: EbmlParser.STATUS_NEED_MORE_DATA, bytesNeeded: 1};
var mask = 0x80;
var ch = buf[start];
var extraBytes = -1;
var num = 0;
for (var i = 0; i < maxBytes; ++i) {
if ((ch & mask) == mask) {
num = maskFirstByte ? ch & ~mask : ch;
extraBytes = i;
break;
}
mask >>= 1;
}
if (extraBytes == -1)
return {status: EbmlParser.STATUS_INVALID_DATA,
reason: 'Invalid extraBytes field'};
if ((1 + extraBytes) > size)
return {status: EbmlParser.STATUS_NEED_MORE_DATA,
bytesNeeded: (1 + extraBytes - size)};
// TODO(acolwell) : Add support for signalling "reserved" values (ie all 1s).
var bytesUsed = 1;
for (var i = 0; i < extraBytes; ++i, ++bytesUsed)
num = (num << 8) | (0xff & buf[start + bytesUsed]);
return {status: EbmlParser.STATUS_OK,
bytesUsed: bytesUsed,
value: num};
};
/**
* Static function to parse EBML element header.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read, 'id' is the EBML id, and
* 'elementSize' is the size of the element in bytes.
*/
EbmlParser.parseElementHeader = function(buf, start, size) {
if (size == 0)
return {status: EbmlParser.STATUS_OK, bytesUsed: 0};
var res = EbmlParser.parseNum_(buf, start, size, 4, false);
if (res.status != EbmlParser.STATUS_OK)
return res;
if (res.bytesUsed <= 0)
return {status: EbmlParser.STATUS_INVALID_DATA};
var bytesUsed = res.bytesUsed;
var id = res.value;
res = EbmlParser.parseNum_(buf, start + bytesUsed, size - bytesUsed, 8, true);
if (res.status != EbmlParser.STATUS_OK)
return res;
if (res.bytesUsed <= 0)
return {status: EbmlParser.STATUS_INVALID_DATA};
bytesUsed += res.bytesUsed;
//log(start + " ID : " + webmGetIdName(id));
return {status: EbmlParser.STATUS_OK,
bytesUsed: bytesUsed,
id: id,
elementSize: res.value};
};
/**
* Static function to parse an unsigned integer.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is number.
*/
EbmlParser.parseUInt = function(buf, start, size) {
if (size < 1 || size > 8)
return {status: EbmlParser.STATUS_INVALID_DATA};
var val = 0;
for (var i = 0; i < size; ++i) {
val <<= 8;
val |= buf[start + i] & 0xff;
}
return {status: EbmlParser.STATUS_OK,
bytesUsed: size,
value: val};
};
/**
* Static function to parse a floating point number.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is number.
*/
EbmlParser.parseFloat = function(buf, start, size) {
if (size == 4) {
return EbmlParser.parseFloat4(buf, start);
} else if (size == 8) {
return EbmlParser.parseFloat8(buf, start);
}
return {status: EbmlParser.STATUS_INVALID_DATA};
};
/**
* Static function to parse a single precision floating point number.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is number.
*/
EbmlParser.parseFloat4 = function(buf, start) {
var val = 0;
for (var i = 0; i < 4; ++i) {
val <<= 8;
val |= buf[start + i] & 0xff;
}
var sign = val >> 31;
var exponent = ((val >> 23) & 0xff) - 127;
var significand = val & 0x7fffff;
if (exponent > -127) {
if (exponent == 128) {
if (significand == 0) {
// Infinity numbers
if (sign == 0) {
return {status: EbmlParser.STATUS_OK,
bytesUsed: 4,
value: Number.POSITIVE_INFINITY};
} else {
return {status: EbmlParser.STATUS_OK,
bytesUsed: 4,
value: Number.NEGATIVE_INFINITY};
}
}
// NaN
return {status: EbmlParser.STATUS_OK,
bytesUsed: 4,
value: NaN};
}
// Normal numbers
significand |= 0x800000;
} else {
if (significand == 0) {
// +0 or -0
return {status: EbmlParser.STATUS_OK,
bytesUsed: 4,
value: 0};
}
// Subnormal numbers
exponent = -126;
}
var num = Math.pow(-1, sign) * (significand * Math.pow(2, -23)) *
Math.pow(2, exponent);
return {status: EbmlParser.STATUS_OK,
bytesUsed: 4,
value: num};
};
/**
* Static function to parse a double precision floating point number.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is number.
*/
EbmlParser.parseFloat8 = function(buf, start) {
var sign = (buf[start] >> 7) & 0x1;
var exponent = (((buf[start] & 0x7f) << 4) |
((buf[start + 1] >> 4) & 0xf)) - 1023;
// Javascript performs bitwise and shift operations on 32 bit integers.
var significand = 0;
var shift = Math.pow(2, 6 * 8);
significand += (buf[start + 1] & 0xf) * shift;
for (var i = 2; i < 8; ++i) {
var shift = Math.pow(2, (8 - i - 1) * 8);
significand += (buf[start + i] & 0xff) * shift;
}
if (exponent > -1023) {
if (exponent == 1024) {
if (significand == 0) {
// Infinity numbers
if (sign == 0) {
return {status: EbmlParser.STATUS_OK,
bytesUsed: 8,
value: Number.POSITIVE_INFINITY};
} else {
return {status: EbmlParser.STATUS_OK,
bytesUsed: 8,
value: Number.NEGATIVE_INFINITY};
}
}
// NaN
return {status: EbmlParser.STATUS_OK,
bytesUsed: 8,
value: NaN};
}
// Normal numbers
significand += 0x10000000000000;
} else {
if (significand == 0) {
// +0 or -0
return {status: EbmlParser.STATUS_OK,
bytesUsed: 8,
value: 0};
}
// Subnormal numbers
exponent = -1022;
}
var num = Math.pow(-1, sign) * (significand * Math.pow(2, -52)) *
Math.pow(2, exponent);
return {status: EbmlParser.STATUS_OK,
bytesUsed: 8,
value: num};
};
/**
* Static function to parse string.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is the string.
*/
EbmlParser.parseString = function(buf, start, size) {
if (size < 1)
return {status: EbmlParser.STATUS_INVALID_DATA};
var val = '';
for (var i = 0; i < size; ++i) {
val += String.fromCharCode(buf[start + i]);
}
return {status: EbmlParser.STATUS_OK,
bytesUsed: size,
value: val};
};
/**
* Class to parse WebM elements.
* @constructor
*/
function WebMParser() {
this.segmentOffset_ = -1;
this.seekHead_ = null;
this.infoElement_ = null;
this.tracksElement_ = null;
this.trackObjects_ = [];
this.timecodeScale_ = 1000000;
this.duration_ = -1;
this.cues_ = null;
}
/**
* Return code signaling everything is fine.
* @const
* @type {number}
*/
WebMParser.STATUS_OK = 0;
/**
* Invalid data return code.
* @const
* @type {number}
*/
WebMParser.STATUS_INVALID_DATA = -1;
/**
* Return code signaling more data is needed.
* @const
* @type {number}
*/
WebMParser.STATUS_NEED_MORE_DATA = -2;
/**
* Static function to return version string.
* @return {string} version.
*/
WebMParser.version = function() {
return '0.1.0.0';
};
/**
* Logging function to be set by the application.
* @param {string} str The input string to be logged.
*/
WebMParser.prototype.log = function(str) {};
/**
* Static function to create a function to prperty mapping.
* @param {string} parseFunctionName Function name.
* @param {string} propertyToSet Property name.
* @return {Object} Mapping.
* @private
*/
WebMParser.createIdInfo_ = function(parseFunctionName, propertyToSet) {
var func = null;
if (parseFunctionName == null) {
return {func: null, prop: propertyToSet};
} else if (parseFunctionName == 'parseUInt') {
func = function(t, buf, start, size, elementStart) {
return EbmlParser.parseUInt(buf, start, size);
};
} else if (parseFunctionName == 'parseFloat') {
func = function(t, buf, start, size, elementStart) {
return EbmlParser.parseFloat(buf, start, size);
};
} else if (parseFunctionName == 'parseString') {
func = function(t, buf, start, size, elementStart) {
return EbmlParser.parseString(buf, start, size);
};
} else if (parseFunctionName == 'parseSimpleBlock_') {
func = function(t, buf, start, size, elementStart) {
return t.parseSimpleBlock_(buf, start, size, elementStart);
};
} else {
func = function(t, buf, start, size, elementStart) {
return t[parseFunctionName](buf, start, size);
};
}
return {func: func, prop: propertyToSet};
};
/**
* Static parse function for an element we want to skip.
* @private
*/
WebMParser.SKIP_ = WebMParser.createIdInfo_(null, null);
/**
* Static parse function for an element that contains an unsigned integer.
* @param {string} propertyName Indicates the property to assign the parsed
* value to.
* @return {Object} Mapping.
* @private
*/
WebMParser.parseUInt_ = function(propertyName) {
return WebMParser.createIdInfo_('parseUInt', propertyName);
};
/**
* Static parse function for an element that contains a float.
* @param {string} propertyName Indicates the property to assign the parsed
* value to.
* @return {Object} Mapping.
* @private
*/
WebMParser.parseFloat_ = function(propertyName) {
return WebMParser.createIdInfo_('parseFloat', propertyName);
};
/**
* Static parse function for an element that contains a string.
* @param {string} propertyName Indicates the property to assign the parsed
* value to.
* @return {Object} Mapping.
* @private
*/
WebMParser.parseString_ = function(propertyName) {
return WebMParser.createIdInfo_('parseString', propertyName);
};
/**
* Global element IDs that can appear in any element.
* @private
*/
WebMParser.GLOBAL_IDS_ = {
'EC': WebMParser.SKIP_, // Void
'BF': WebMParser.SKIP_ // CRC32
};
/**
* EBML Header IDs.
* @private
*/
WebMParser.EBML_HEADER_IDS_ = {
'4286': WebMParser.parseUInt_('version'), // EBMLVersion
'42F7': WebMParser.parseUInt_('readVersion'), // EBMLReadVersion
'42F2': WebMParser.parseUInt_('maxIdLength'), // EBMLMaxIDLength
'42F3': WebMParser.parseUInt_('maxSizeLength'), // EBMLMaxSizeLength
'4282': WebMParser.parseString_('docType'), // EBMLDocType
'4287': WebMParser.parseUInt_('docTypeVersion'), // EBMLDocTypeVersion
'4285': WebMParser.parseUInt_('docTypeReadVersion') // EBMLDocTypeReadVersion
};
/**
* Segment IDs.
* @private
*/
WebMParser.SEGMENT_IDS_ = {
'114D9B74': WebMParser.createIdInfo_('parseSeekHead_', // SeekHead
'seekHead'),
'1549A966': WebMParser.createIdInfo_('parseInfo_', // Info
'info'),
'1654AE6B': WebMParser.createIdInfo_('parseTracks_', // Tracks
'tracks'),
'1F43B675': null, // Cluster
'1C53BB6B': WebMParser.SKIP_ // Cues
};
/**
* SeekHead IDs.
* @private
*/
WebMParser.SEEKHEAD_IDS_ = {
'4DBB': WebMParser.createIdInfo_('parseSeekEntry_', null) // SeekHead
};
/**
* SeekEntry IDs.
* @private
*/
WebMParser.SEEK_ENTRY_IDS_ = {
'53AB': WebMParser.parseUInt_('id'), // SeekID
'53AC': WebMParser.parseUInt_('position') // SeekPosition
};
/**
* SegmentInfo IDs.
* @private
*/
WebMParser.INFO_IDS_ = {
'73A4': WebMParser.SKIP_, // SegmentUID
'2AD7B1': WebMParser.parseUInt_('timecodeScale'), // TimecodeScale
'4489': WebMParser.parseFloat_('duration'), // Duration
'7BA9': WebMParser.SKIP_, // Title
'5741': WebMParser.SKIP_, // WritingApp
'4D80': WebMParser.SKIP_, // MuxingApp
'4461': WebMParser.SKIP_ // DateUTC
};
/**
* Tracks IDs.
* @private
*/
WebMParser.TRACKS_IDS_ = {
'AE': WebMParser.createIdInfo_('parseTrackEntry_', 'track') // TrackEntry
};
/**
* Track IDs.
* @private
*/
WebMParser.TRACK_IDS_ = {
'D7': WebMParser.parseUInt_('TrackNumber'), // TrackNumber
'73C5': WebMParser.parseUInt_('TrackUID'), // TrackUID
'83': WebMParser.parseUInt_('TrackType'), // TrackType
'B9': WebMParser.SKIP_, // FlagEnabled
'88': WebMParser.SKIP_, // FlagDefault
'55AA': WebMParser.SKIP_, // FlagForced
'9C': WebMParser.SKIP_, // FlagLacing
'6DE7': WebMParser.SKIP_, // MinCache
'6DE8': WebMParser.SKIP_, // MaxCache
'23E383': WebMParser.SKIP_, // DefaultDuration
'23314F': WebMParser.SKIP_, // TrackTimecodeScale
'55EE': WebMParser.SKIP_, // MaxBlockAdditionID
'536E': WebMParser.SKIP_, // Name
'22B59C': WebMParser.SKIP_, // Language
'86': WebMParser.SKIP_, // CodecID
'63A2': WebMParser.SKIP_, // CodecPrivate
'258688': WebMParser.SKIP_, // CodecName
'7446': WebMParser.SKIP_, // AttachmentLink
'AA': WebMParser.SKIP_, // CodecDecodeAll
'6FAB': WebMParser.SKIP_, // TrackOverlay
'6624': WebMParser.SKIP_, // TrackTranslate
'E0': WebMParser.SKIP_, // Video
'E1': WebMParser.SKIP_, // Audio
'E2': WebMParser.SKIP_, // TrackOperation
'6D80': WebMParser.SKIP_ // ContentEncodings
};
/**
* Cues IDs.
* @private
*/
WebMParser.CUES_IDS_ = {
'BB': WebMParser.createIdInfo_('parsePointEntry_', null) // PointEntry
};
/**
* CuePointEntry IDs.
* @private
*/
WebMParser.POINT_ENTRY_IDS_ = {
'B3': WebMParser.parseUInt_('cueTime'), // CUETIME
'B7': WebMParser.createIdInfo_('parseTrackPosition_', // CUETRACKPOSITION
'trackPosition')
};
/**
* CueTrackPositions IDs.
* @private
*/
WebMParser.TRACK_POSITIONS_IDS_ = {
'F7': WebMParser.parseUInt_('cueTrack'), // CUETRACK
'F1': WebMParser.parseUInt_('cueClusterPos'), // CUECLUSTERPOSITION
'5378': WebMParser.SKIP_ // CUEBLOCKNUMBER
};
/**
* Cluster IDs.
* @private
*/
WebMParser.CLUSTER_IDS_ = {
'E7': WebMParser.parseUInt_('clusterTimecode'), // CLUSTERTIMECODE
'AB': WebMParser.SKIP_, // CLUSTERPREVSIZE
'A3': WebMParser.createIdInfo_('parseSimpleBlock_', // SIMPLEBLOCK
'blockInfo')
};
/**
* Set the Segment offset that is used by the Cues. |offset| will only be set
* if it is greater than 0.
* @param {number} offset Segment offset in bytes.
*/
WebMParser.prototype.setSegmentOffset = function(offset) {
if (offset > 0)
this.segmentOffset_ = offset;
};
/**
* Check if |obj| is an Array.
* @param {Object} obj Object to check.
* @return {boolean} Returns true if |obj| is an Array.
* @private
*/
WebMParser.prototype.isList_ = function(obj) {
return ((typeof obj === 'object') &&
(obj instanceof Array));
};
/**
* Parses a WebM master element. Entire list needs to be in |buf| and within
* the specified size. Returns an Object with values of the parsed list
* according to |idInfo|.
* @param {Object} idInfo Mapping of IDs to parsing functions.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @param {Object} obj Starting Object or Array.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is the Object of
* parsed values.
* @private
*/
WebMParser.prototype.parseList_ = function(idInfo, buf, start, size, obj) {
var curStart = start;
var curSize = size;
while (curSize > 0) {
var res = EbmlParser.parseElementHeader(buf, curStart, curSize);
if (res.status != EbmlParser.STATUS_OK)
return res;
var elementOffset = curStart;
var dataOffset = elementOffset + res.bytesUsed;
var elementSize = res.elementSize;
var nextElementOffset = dataOffset + elementSize;
curStart += res.bytesUsed;
curSize -= res.bytesUsed;
var idInfoKey = res.id.toString(16).toUpperCase();
var info = null;
if (idInfoKey in WebMParser.GLOBAL_IDS_) {
info = WebMParser.GLOBAL_IDS_[idInfoKey];
} else if (idInfoKey in idInfo) {
info = idInfo[idInfoKey];
} else {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'No idInfo for ID ' + webmGetIdName(res.id)};
}
// A null idInfo entry indicates that we should stop parsing
// right before this id.
if (info == null) {
return {status: WebMParser.STATUS_OK,
bytesUsed: elementOffset - start,
value: obj};
}
if (elementSize > curSize) {
return {status: WebMParser.STATUS_NEED_MORE_DATA,
bytesNeeded: elementSize - curSize};
}
var parseFunction = info.func;
var propertyName = info.prop;
if (parseFunction) {
res = parseFunction(this, buf, dataOffset, elementSize, elementOffset);
if (res.status != WebMParser.STATUS_OK)
return res;
if (res.bytesUsed != elementSize) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'bytesUsed does not match elementSize'};
}
if (res.storeElementFunc) {
res.storeElementFunc(new Uint8Array(
buf.subarray(elementOffset, nextElementOffset)));
}
if ((propertyName === null) && this.isList_(obj)) {
obj.push(res.value);
} else if ((propertyName in obj) &&
this.isList_(obj[propertyName])) {
obj[propertyName].push(res.value);
} else {
obj[propertyName] = res.value;
}
} else {
//log('Skipping ID ' + webmGetIdName(res.id));
}
curStart = nextElementOffset;
curSize -= elementSize;
}
return {status: WebMParser.STATUS_OK,
bytesUsed: size - curSize,
value: obj};
};
/**
* Parses a WebM element. Returns an Uint8Array with the element's data.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @param {number} id WEbM element id.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is an Uint8Array of
* the element's data.
*/
WebMParser.prototype.parseElement = function(buf, start, size, id) {
var res = EbmlParser.parseElementHeader(buf, start, size);
if (res.status != EbmlParser.STATUS_OK)
return res;
if (res.id != id) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Unexpected id : ' + webmGetIdName(res.id)};
}
var elementSize = res.bytesUsed + res.elementSize;
if (elementSize > size) {
return {status: WebMParser.STATUS_NEED_MORE_DATA,
bytesNeeded: elementSize - size};
}
var end = start + elementSize;
var element = new Uint8Array(buf.subarray(start, end));
return {status: WebMParser.STATUS_OK,
bytesUsed: elementSize,
value: element};
};
/**
* Parses an EBML file header. Returns an Object of the EBML header data.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is an Object with
* the EBML header data.
* @private
*/
WebMParser.prototype.parseEBMLHeader_ = function(buf, start, size) {
var bytesUsed = 0;
var res = EbmlParser.parseElementHeader(buf, start, size);
if (res.status != EbmlParser.STATUS_OK)
return res;
if (res.id != 0x1A45DFA3) { // HEADER
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Unexpected id : ' + webmGetIdName(res.id)};
}
start += res.bytesUsed;
size -= res.bytesUsed;
bytesUsed += res.bytesUsed;
res = this.parseList_(WebMParser.EBML_HEADER_IDS_, buf, start,
res.elementSize, {});
if (res.status != WebMParser.STATUS_OK)
return res;
var header = res.value;
if ((header.version < 1) ||
(header.readVersion < 1) ||
(header.maxIdLength < 1) ||
(header.maxIdLength > 4) ||
(header.maxSizeLength < 1) ||
(header.maxSizeLength > 8) ||
(header.docType != 'webm') ||
(header.docTypeVersion != 2) ||
(header.docTypeReadVersion != 2)) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Invalid header values.'};
}
bytesUsed += res.bytesUsed;
return {status: WebMParser.STATUS_OK,
bytesUsed: bytesUsed,
value: header};
};
/**
* Parses and returns a WebM SimpleBlock.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @param {number} elementStart Starting offset of the SimpleBlock.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is an Object with
* information on the SimpleBlock and the data from the SimpleBlock.
* @private
*/
WebMParser.prototype.parseSimpleBlock_ = function(buf, start, size,
elementStart) {
if (size < 4)
return {status: WebMParser.STATUS_INVALID_DATA};
if ((buf[start] & 0x80) != 0x80) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'TrackNumber > 127 not supported'};
}
var value = {};
value.trackNum = buf[start] & 0x7f;
value.timecode = buf[start + 1] << 8 | buf[start + 2];
value.flags = buf[start + 3] & 0xff;
value.lacing = (value.flags >> 1) & 0x3;
value.blockOffset = elementStart;
value.dataOffset = start + 4;
value.dataSize = size - (value.dataOffset - start);
return {status: WebMParser.STATUS_OK,
bytesUsed: size,
value: value};
};
/**
* Parses and returns a WebM SeekHead element.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is an Object
* representing a WebM SeekHead.
* @private
*/
WebMParser.prototype.parseSeekHead_ = function(buf, start, size) {
var res = this.parseList_(WebMParser.SEEKHEAD_IDS_, buf, start, size, []);
if (res.status != WebMParser.STATUS_OK)
return res;
var entries = res.value;
var seekHead = {};
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var name = webmGetIdName(entry.id);
seekHead[name] = this.segmentOffset_ + entry.position;
}
res.value = seekHead;
return res;
};
/**
* Parses and returns a WebM SeekEntry element.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is an Object
* representing a WebM SeekEntry.
* @private
*/
WebMParser.prototype.parseSeekEntry_ = function(buf, start, size) {
return this.parseList_(WebMParser.SEEK_ENTRY_IDS_, buf, start, size, {});
};
/**
* Parses and returns a WebM SegmentInfo element.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read, 'value' is an Object
* representing a WebM SegmentInfo, 'storeElementFunc' gets set so the
* WebM SegmentInfo element data gets stored in |infoElement_|.
* @private
*/
WebMParser.prototype.parseInfo_ = function(buf, start, size) {
var info = {timecodeScale: 1000000, duration: -1};
var res = this.parseList_(WebMParser.INFO_IDS_, buf, start, size, info);
if (res.status == WebMParser.STATUS_OK) {
var t = this;
res.storeElementFunc = function(buf) {t.infoElement_ = buf;};
}
return res;
};
/**
* Parses and returns a WebM Tracks element.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read, 'value' is an Object
* representing a WebM Tracks, 'storeElementFunc' gets set so the
* WebM Tracks element data gets stored in |tracksElement_|.
* @private
*/
WebMParser.prototype.parseTracks_ = function(buf, start, size) {
var res = this.parseList_(WebMParser.TRACKS_IDS_, buf, start, size, []);
if (res.status == WebMParser.STATUS_OK) {
var t = this;
res.storeElementFunc = function(buf) {t.tracksElement_ = buf;};
}
return res;
};
/**
* Parses and returns a WebM Track element.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read, 'value' is an Object
* representing a WebM Track, 'storeElementFunc' gets set so the
* WebM Track element data gets stored in |trackObjects_|.
* @private
*/
WebMParser.prototype.parseTrackEntry_ = function(buf, start, size) {
var track = {
TrackNumber: 0,
TrackUID: 0,
TrackType: 0
};
var res = this.parseList_(WebMParser.TRACK_IDS_, buf, start, size, track);
if (res.status == WebMParser.STATUS_OK) {
var t = this;
res.storeElementFunc = function(buf) {
var node = {
track: res.value,
buf: buf
};
t.trackObjects_.push(node);
};
}
return res;
};
/**
* Parses and returns a WebM CuePoint element. |segmentOffset_| must be set
* before calling this function.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is an Object
* representing a WebM SeekHead. 'value.cueTime' gets set to seconds from
* nanoseconds and 'res.value.trackPosition.cueClusterPos' is adjusted to
* the Segment offset.
* @private
*/
WebMParser.prototype.parsePointEntry_ = function(buf, start, size) {
var res = this.parseList_(WebMParser.POINT_ENTRY_IDS_, buf, start, size, {});
if (res.status != WebMParser.STATUS_OK) {
return res;
}
var time_scale = this.timecodeScale_ / 1000000000.0;
res.value.cueTime = res.value.cueTime * time_scale;
res.value.trackPosition.cueClusterPos += this.segmentOffset_;
return res;
};
/**
* Parses and returns a WebM CueTrackPositions element.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read and 'value' is an Object
* representing a WebM CueTrackPositions.
* @private
*/
WebMParser.prototype.parseTrackPosition_ = function(buf, start, size) {
return this.parseList_(WebMParser.TRACK_POSITIONS_IDS_, buf, start, size, {});
};
/**
* Parses and a WebM Cues element. The list of CuePoints is stored in |cues_|.
* The function will add a seek entry for a Cues element if one was not
* previously added.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @param {number} offset Start offset of the Cues element.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read.
* @private
*/
WebMParser.prototype.parseCues_ = function(buf, start, size, offset) {
var readSize = size;
var readOffset = start;
var res = EbmlParser.parseElementHeader(buf, readOffset, readSize);
if (res.status != EbmlParser.STATUS_OK) {
return res;
}
readOffset += res.bytesUsed;
readSize -= res.bytesUsed;
res = this.parseList_(WebMParser.CUES_IDS_, buf, readOffset, readSize, []);
if (res.status != WebMParser.STATUS_OK) {
return res;
}
this.cues_ = res.value;
if (this.seekHead_ == null)
this.seekHead_ = {};
if (!('CUES' in this.seekHead_)) {
this.seekHead_['CUES'] = offset;
}
return {status: WebMParser.STATUS_OK, bytesUsed: readOffset - start};
};
/**
* This function parses all of the Segment header elements. |seekHead_|,
* |timecodeScale_|, and SeekHead Cluster entry are set in this function.
* |segmentOffset_| will be set if it has not been set already.
* @param {Uint8Array} buf Source buffer.
* @param {number} start Starting offset.
* @param {number} size Size left in current element.
* @param {number} bufOffset Start offset for |buf|.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'bytesUsed' is the number of bytes read.
*/
WebMParser.prototype.parseSegmentHeaders = function(buf, start, size,
bufOffset) {
var readOffset = start;
this.log('parseSegmentHeaders start=' + start + ' size=' + size +
' bufOffset=' + bufOffset);
var res = EbmlParser.parseElementHeader(buf, readOffset, size);
if (res.status != EbmlParser.STATUS_OK) {
return res;
}
if (res.id != 0x18538067) { // SEGMENT
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Unexpected element ID. Expected a Segment ID'};
}
readOffset += res.bytesUsed;
if (this.segmentOffset_ == -1)
this.segmentOffset_ = readOffset;
res = this.parseList_(WebMParser.SEGMENT_IDS_, buf, readOffset,
size - res.bytesUsed, {});
if (res.status != WebMParser.STATUS_OK) {
return res;
}
readOffset += res.bytesUsed;
this.seekHead_ = res.value.seekHead;
this.seekHead_['CLUSTER'] = readOffset + bufOffset;
this.timecodeScale_ = res.value.info.timecodeScale;
var timeScale = this.timecodeScale_ / 1000000000.0;
if (res.value.info.duration > 0 && this.duration_ == -1)
this.duration_ = res.value.info.duration * timeScale;
return {status: WebMParser.STATUS_OK, bytesUsed: readOffset - start};
};
/**
* Returns the SeekHead Object.
* @return {object} SeekHead Object.
*/
WebMParser.prototype.getSeekHead = function() {
return this.seekHead_;
};
/**
* Returns a buffer containing the SegmentInfo element. The buffer includes
* the element header.
* @return {Uint8Array} SegmentInfo buffer.
*/
WebMParser.prototype.getInfo = function() {
return this.infoElement_;
};
/**
* Returns a buffer containing the Tracks element. The buffer includes the
* element header.
* @return {Uint8Array} Tracks buffer.
*/
WebMParser.prototype.getTracks = function() {
return this.tracksElement_;
};
/**
* Return an object which contains the track number, uid, type, and buffer
* containing a track element which includes the element header.
* @param {number} index Index into the |trackObjects_| array.
* @return {Object} Track Object or null.
*/
WebMParser.prototype.getTrackObject = function(index) {
if (index < 0 || index >= this.trackObjects_.length)
return null;
return this.trackObjects_[index];
};
/**
* Return the number of track objects.
* @return {number} Number of track objects.
*/
WebMParser.prototype.getTrackObjectLength = function() {
return this.trackObjects_.length;
};
/**
* Returns the file offset of the first Cluster form the SeekHead.
* @return {number} File offset or -1 if there is no entry.
*/
WebMParser.prototype.getFirstClusterOffset = function() {
if (!this.seekHead_['CLUSTER']) {
return -1;
}
return this.seekHead_['CLUSTER'];
};
/**
* Removes cue elements after the first cue element in the same cluster.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is true on success.
*/
WebMParser.prototype.squishCues = function() {
if (this.cues_ == null) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Cues is not valid.'};
}
for (var i = 0; i < this.cues_.length - 1; ++i) {
var cueCurr = this.cues_[i];
var cueNext = this.cues_[i + 1];
if (cueCurr.trackPosition.cueClusterPos ==
cueNext.trackPosition.cueClusterPos) {
this.cues_.splice(i + 1, 1);
// Check this element again.
--i;
}
}
return {status: WebMParser.STATUS_OK, value: true};
};
/**
* Returns the Cues array.
* @return {Array} Array of CuePoint elements.
*/
WebMParser.prototype.getCues = function() {
return this.cues_;
};
/**
* Set the WebM duration.
* @param {number} duration Duration of the file in seconds.
*/
WebMParser.prototype.setDuration = function(duration) {
this.duration_ = duration;
};
/**
* Class to handle WebM files.
* @param {string} url Link to WebM file.
* @param {function} opt_log Optional logging function for this class.
* @constructor
*/
function WebMFileParser(url, opt_log) {
if (opt_log)
this.log = opt_log;
this.parser = new WebMParser();
this.parser.log = this.log;
this.file = new HttpFile(url, this.log);
this.EMPTY_CLUSTER_ = new Uint8Array([0x1F, 0x43, 0xB6, 0x75,
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00]);
}
/**
* The maximum size in bytes of the partial download.
* @type {number}
*/
WebMFileParser.prototype.partialDownloadSize = 131072;
/**
* Logging function to be set by the application.
* @param {string} str The input string to be logged.
*/
WebMFileParser.prototype.log = function(str) {};
/**
* Asynchronous function to get a WebM element.
* @param {number} fileOffset Starting offset of the element.
* @param {number} id ID of the element.
* @param {number} emptyReadSize Try to get this many bytes if there are no
* bytes stored in memory.
* @param {function} doneCallback Return function. First parameter passes
* back an UintArray8 buffer containing the element and element header.
* @private
*/
WebMFileParser.prototype.fetchElement_ = function(fileOffset, id, emptyReadSize,
doneCallback) {
this.file.seek(fileOffset);
if (this.file.getBytesAvailable() == 0) {
var t = this;
this.file.fetchBytes(emptyReadSize, function(success) {
if (!success) {
doneCallback(null);
return;
}
t.fetchElement_(fileOffset, id, emptyReadSize, doneCallback);
});
return;
}
var res = this.parser.parseElement(this.file.getBuffer(),
this.file.getIndex(),
this.file.getBytesAvailable(),
id);
if (res.status == WebMParser.STATUS_NEED_MORE_DATA) {
var t = this;
var bytesNeeded = this.file.getBytesAvailable() + res.bytesNeeded;
// Check if the player can request a little more than we need. We read
// just enough extra so the next element header can be parsed without
// another request.
var fileLength = this.file.getFileLength();
if (fileLength != -1) {
var end = fileOffset + bytesNeeded;
if (fileLength >= end + EbmlParser.MAX_ELEMENT_HEADER_SIZE)
bytesNeeded += EbmlParser.MAX_ELEMENT_HEADER_SIZE;
}
this.file.ensureEnoughBytes(bytesNeeded, function(success) {
if (!success) {
doneCallback(null);
return;
}
t.fetchElement_(fileOffset, id, emptyReadSize, doneCallback);
});
return;
}
if (res.status != WebMParser.STATUS_OK) {
doneCallback(null);
return;
}
var element = res.value;
this.file.read(element.length);
doneCallback(element);
};
/**
* Asynchronous function to parse the WebM Segment headers and Cues element.
* @param {function} doneCallback Return function. First parameter passes
* back a boolean with a value of true if the call was successful.
* @param {number} ensureSize Value to make sure |ensureSize| bytes are stored
* in memory.
*/
WebMFileParser.prototype.doParseHeaders = function(doneCallback, ensureSize) {
var t = this;
this.file.ensureEnoughBytes(ensureSize, function(success) {
if (!success) {
doneCallback(false);
return;
}
var res = t.parser.parseEBMLHeader_(t.file.getBuffer(), t.file.getIndex(),
t.file.getBytesAvailable());
if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
var originalOffset = t.file.getCurrentOffset();
t.file.read(res.bytesUsed);
var readOffset = t.file.getIndex();
var bufferOffset = t.file.getCurrentOffset();
res = t.parser.parseSegmentHeaders(
t.file.getBuffer(),
readOffset,
t.file.getBytesAvailable() - (readOffset - bufferOffset),
bufferOffset - readOffset);
if (res.status == WebMParser.STATUS_OK) {
t.file.read(res.bytesUsed);
} else if (res.status == WebMParser.STATUS_NEED_MORE_DATA) {
t.file.seek(originalOffset);
// Fetch more bytes and try again.
window.setTimeout(function() {
t.doParseHeaders(doneCallback, 2 * ensureSize);
}, 0);
return;
} else if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
t.fetchIndex(function(success) {
doneCallback(success);
});
});
};
/**
* Asynchronous function to parse the WebM Segment headers and Cues element.
* @param {function} doneCallback Return function. First parameter passes
* back a boolean with a value of true if the call was successful.
*/
WebMFileParser.prototype.parseHeaders = function(doneCallback) {
this.doParseHeaders(doneCallback, 4096);
};
/**
* Returns a buffer containing the SegmentInfo element. The buffer includes
* the element header.
* @return {Uint8Array} SegmentInfo buffer or null.
*/
WebMFileParser.prototype.getInfo = function() {
if (!this.parser)
return null;
return this.parser.getInfo();
};
/**
* Returns a buffer containing the Tracks element. The buffer includes the
* element header.
* @return {Uint8Array} Tracks buffer or null.
*/
WebMFileParser.prototype.getTracks = function() {
if (!this.parser)
return null;
return this.parser.getTracks();
};
/**
* Return an object which contains the track number, uid, type, and buffer
* containing a track element which includes the element header.
* @param {number} index Index into the |trackObjects_| array.
* @return {Object} Track Object or null.
*/
WebMFileParser.prototype.getTrackObject = function(index) {
if (!this.parser)
return null;
return this.parser.getTrackObject(index);
};
/**
* Return the number of track objects.
* @return {number} Number of track objects.
*/
WebMFileParser.prototype.getTrackObjectLength = function() {
if (!this.parser)
return 0;
return this.parser.getTrackObjectLength();
};
/**
* Returns the file offset of the first Cluster form the SeekHead.
* @return {number} File offset or -1 if there was an error.
*/
WebMFileParser.prototype.getFirstClusterOffset = function() {
if (!this.parser)
return -1;
return this.parser.getFirstClusterOffset();
};
/**
* Checks if the WebM file is ready to seek.
* @return {boolean} Returns true if the file headers and seek index have been
* parsed.
*/
WebMFileParser.prototype.canSeek = function() {
return this.parser != null && this.parser.getCues() != null;
};
/**
* Removes Cue elements after the first Cue element in the same Cluster.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is true on success.
*/
WebMFileParser.prototype.squishCues = function() {
if (!this.parser) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'WebMParser is not valid.'};
}
return this.parser.squishCues();
};
/**
* Asynchronous function to get and parse a WebM Cues element. The list of
* CuePoints is stored in |cues_|. There must be a seek entry for a Cues
* element or the function will return an error.
* @param {function} doneCallback Return function. First parameter passes
* back a boolean with a value of true if the call was successful.
*/
WebMFileParser.prototype.fetchIndex = function(doneCallback) {
var seekHead = this.parser.getSeekHead();
if (!('CUES' in seekHead)) {
doneCallback(false);
return;
}
var t = this;
var cuesOffset = seekHead['CUES'];
this.fetchElement_(
cuesOffset, 0x1C53BB6B, 4096,
function(element) {
if (!element) {
doneCallback(false);
return;
}
var res = t.parser.parseCues_(element, 0, element.length, cuesOffset);
if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
doneCallback(true);
});
};
/**
* Asynchronous function to get a Cluster time and offset. If the Cues has not
* been parsed the function will try and parse the Cues.
* @param {number} seekTime Time to find closest Cluster.
* @param {function} doneCallback Return function. First parameter passes back
* the Cluster time in seconds or -1 on error. Second parameter passes
* back the Cluster offset or -1 on error.
*/
WebMFileParser.prototype.getClusterOffset = function(seekTime, doneCallback) {
var cues = this.parser.getCues();
if (cues == null) {
var t = this;
this.fetchIndex(function(success) {
if (!success) {
doneCallback(-1, -1);
return;
}
t.getClusterOffset(seekTime, doneCallback);
});
return;
}
var l = 0;
var r = cues.length - 1;
if (seekTime >= cues[r].cueTime)
l = r;
while (l + 1 < r) {
var m = l + Math.round((r - l) / 2);
var timestamp = cues[m].cueTime;
if (timestamp <= seekTime) {
l = m;
} else {
r = m;
}
}
doneCallback(cues[l].cueTime, cues[l].trackPosition.cueClusterPos);
};
/**
* Asynchronous function to get a Cluster element.
* @param {number} clusterOffset Starting Cluster offset.
* @param {function} doneCallback Return function. First parameter passes back
* the Cluster length or -1 on error. Second parameter passes
* back the Cluster buffer or null on error.
*/
WebMFileParser.prototype.getCluster = function(clusterOffset, doneCallback) {
this.fetchElement_(clusterOffset, 0x1F43B675, 4 * 4096, function(element) {
if (!element) {
doneCallback(-1, null);
return;
}
doneCallback(clusterOffset + element.length, element);
});
};
/**
* Parses a Cluster element.
* @param {UintArray8} cluster Cluster element including the header.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is an Object representing a WebM Cluster element.
* 'value.clusterTime' is the time of the Cluster in seconds and
* 'value.blockInfo' is a list of WebM Blocks.
*/
WebMFileParser.prototype.parseCluster = function(cluster) {
var start = 0;
var size = cluster.length;
var res = EbmlParser.parseElementHeader(cluster, start, size);
if (res.status != WebMParser.STATUS_OK) {
return res;
}
start += res.bytesUsed;
size -= res.bytesUsed;
var clusterInfo = {dataOffset: start, blockInfo: []};
res = this.parser.parseList_(WebMParser.CLUSTER_IDS_, cluster, start, size,
clusterInfo);
if (res.status != WebMParser.STATUS_OK) {
return res;
}
var timeScale = this.timecodeScale_ / 1000000000.0;
res.value.clusterTime = res.value.clusterTimecode * timeScale;
var blockInfoCount = res.value.blockInfo.length;
for (var i = 0; i < blockInfoCount; i++) {
var bi = res.value.blockInfo[i];
bi.blockTime = res.value.clusterTime + bi.timecode * timeScale;
}
return res;
};
/**
* Returns a new Cluster buffer truncated to |time|.
* @param {UintArray8} cluster Source Cluster buffer.
* @param {number} time Time in seconds to truncate too.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'cluster' is an Object representing the truncated WebM Cluster element
* including the header and 'endTime' is the end time of the truncate
* Cluster in seconds.
*/
WebMFileParser.prototype.truncateCluster = function(cluster, time) {
var res = this.parseCluster(cluster);
if (res.status != WebMParser.STATUS_OK) {
return res;
}
var ci = res.value;
if (ci.clusterTime > time) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Specified time is before the start of the cluster'};
}
var endOffset = -1;
var endTime = -1;
var blockCount = ci.blockInfo.length;
for (var i = 0; i < blockCount; i++) {
var bi = ci.blockInfo[i];
if (bi.blockTime > time) {
// TODO(fgalligan): Change function to work with Blocks too.
endOffset = bi.blockOffset;
endTime = bi.blockTime;
break;
}
}
if (endOffset == -1) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Failed to find a block after ' + time};
}
var headerSize = this.EMPTY_CLUSTER_.length;
var dataSize = endOffset - ci.dataOffset;
var newCluster = new Uint8Array(headerSize + dataSize);
newCluster.set(this.EMPTY_CLUSTER_, 0);
newCluster.set(cluster.subarray(ci.dataOffset, endOffset), headerSize);
var tmp = dataSize;
for (var i = 0; i < 7; ++i) {
newCluster[11 - i] = tmp & 0xff;
tmp >>= 8;
}
return {status: WebMParser.STATUS_OK,
endTime: endTime,
cluster: newCluster};
};
/**
* Set the file's duration.
* @param {number} duration Duration of the file in seconds.
*/
WebMFileParser.prototype.setDuration = function(duration) {
this.parser.setDuration(duration);
};
/**
* Returns a cueDesc object.
* @param {Object} index Index into the Cues list.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is a cueDesc object.
* @private
*/
WebMFileParser.prototype.getCueDescFromCue_ = function(index) {
var cues = this.parser.getCues();
if (!cues || cues.length == 0) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Cues is not valid.'};
}
if (index >= cues.length) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Cues index is out of bounds.'};
}
// Set size and endTime to unknown.
var size = -1;
var endTime = -1;
var startOffset = cues[index].trackPosition.cueClusterPos;
var cueEnd = index + 1;
while ((cueEnd < cues.length) &&
(startOffset == cues[cueEnd].trackPosition.cueClusterPos)) {
cueEnd++;
}
// TODO(fgalligan): Pre-calculate the size and duration of all the cues.
if (cueEnd < cues.length) {
size = cues[cueEnd].trackPosition.cueClusterPos - startOffset;
endTime = cues[cueEnd].cueTime;
} else {
// On the last cue element. Use the start of the Cues element as the
// ending offset for the last cluster.
var seekHead = this.parser.getSeekHead();
if (seekHead && seekHead['CUES'] > startOffset) {
size = seekHead['CUES'] - startOffset;
}
endTime = this.parser.duration_;
}
var cueDesc = {
time: cues[index].cueTime,
offset: startOffset,
size: size,
endTime: endTime
};
return {status: WebMParser.STATUS_OK,
value: cueDesc};
};
/**
* Returns the first cueDesc object.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is a cueDesc object.
*/
WebMFileParser.prototype.getFirstCueDesc = function() {
var seekHead = this.parser.getSeekHead();
if (!seekHead || !seekHead['CLUSTER']) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'SeekHead does not contain a valid Cluster entry.'};
}
return this.getCueDescFromCue_(0);
};
/**
* Returns a cueDesc object that has a starting offset that is closest to
* |offset| with |offset| being greater than or equal to cueDesc's
* starting offset.
* @param {number} offset Byte offset into the WebM file.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is a cueDesc object.
*/
WebMFileParser.prototype.getCueDescFromOffset = function(offset) {
var cues = this.parser.getCues();
if (!cues || cues.length == 0) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Cues is not valid.'};
}
var l = 0;
var r = cues.length - 1;
if (offset >= cues[r].trackPosition.cueClusterPos)
l = r;
while (l + 1 < r) {
var m = l + Math.round((r - l) / 2);
var off = cues[m].trackPosition.cueClusterPos;
if (off <= offset) {
l = m;
} else {
r = m;
}
}
return this.getCueDescFromCue_(l);
};
/**
* Returns a Cue index that has a starting time that is closest to |time| with
* |time| being greater than or equal to the Cue's starting time.
* @param {number} time In seconds.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is a cueDesc index.
* @private
*/
WebMFileParser.prototype.getCueIndexFromTime_ = function(time) {
var cues = this.parser.getCues();
if (!cues || cues.length == 0) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Cues is not valid.'};
}
var l = 0;
var r = cues.length - 1;
if (time >= cues[r].cueTime)
l = r;
while (l + 1 < r) {
var m = l + Math.round((r - l) / 2);
var timestamp = cues[m].cueTime;
if (timestamp <= time) {
l = m;
} else {
r = m;
}
}
return {status: WebMParser.STATUS_OK,
value: l};
};
/**
* Returns a cueDesc object that has a starting time that is closest to |time|
* with |time| being greater than or equal to cueDesc's starting time.
* @param {number} time In seconds.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'value' is a cueDesc object.
*/
WebMFileParser.prototype.getCueDescFromTime = function(time) {
var res = this.getCueIndexFromTime_(time);
if (res.status != WebMParser.STATUS_OK)
return res;
return this.getCueDescFromCue_(res.value);
};
/**
* Asynchronous function that returns a full cluster element and the next
* cueDesc.
* @param {Object} cueDesc cueDesc to read.
* @param {number} bytesRead Number of bytes read in the current cluster.
* @param {function} doneCallback Callback function. First parameter is the
* next cueDesc object or null on error. Second parameter is the cluster
* element or null on error.
*/
WebMFileParser.prototype.getClusterFromCueDesc = function(cueDesc,
bytesRead,
doneCallback) {
var size = cueDesc.size - bytesRead;
if (size <= 0)
size = 4 * 4096;
var offset = cueDesc.offset + bytesRead;
var t = this;
this.fetchElement_(offset, 0x1F43B675, size,
function(element) {
if (!element) {
doneCallback(null, null);
return;
}
var res = t.getCueDescFromOffset(offset + element.length);
if (res.status != WebMParser.STATUS_OK) {
//this.log('Could not get cueDesc from offset. ' + res.reason);
doneCallback(null, null);
return;
}
var nextCueDesc = res.value;
// Check if the current cueDesc is the last cue.
if (nextCueDesc.offset == cueDesc.offset)
nextCueDesc = null;
doneCallback(nextCueDesc, element);
});
};
/**
* Asynchronous function that parses the WebM file headers.
* @param {number} offset Offset to start reading the file from.
* @param {number} size Number of bytes to read.
* @param {function} doneCallback Callback function. First parameter passes
* back a boolean with a value of true if the call was successful.
*/
WebMFileParser.prototype.parseFirstHeaders = function(offset, size,
doneCallback) {
var t = this;
this.file.seek(offset);
this.file.ensureEnoughBytes(size, function(success) {
if (!success) {
doneCallback(false);
return;
}
var res = t.parser.parseEBMLHeader_(t.file.getBuffer(), t.file.getIndex(),
t.file.getBytesAvailable());
if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
// Update file to indicate that we have used some bytes.
t.file.read(res.bytesUsed);
var readOffset = t.file.getIndex();
var bufferOffset = t.file.getCurrentOffset();
if (!t.parser.parseSegmentHeaders(t.file.getBuffer(), readOffset,
t.file.getBytesAvailable() - (readOffset - bufferOffset),
bufferOffset)) {
doneCallback(false);
return;
}
doneCallback(true);
});
};
/**
* Asynchronous function that parses the WebM Cues element.
* @param {number} offset Offset to start reading the file from.
* @param {number} size Number of bytes to read.
* @param {number} segmentOffset Offset to the Segment element.
* @param {function} doneCallback Callback function. First parameter passes
* back a boolean with a value of true if the call was successful.
*/
WebMFileParser.prototype.fetchCues = function(offset, size, segmentOffset,
doneCallback) {
//this.log('fetchCues offset=' + offset + ' size=' + size);
this.parser.setSegmentOffset(segmentOffset);
var t = this;
this.fetchElement_(offset, 0x1C53BB6B, size, function(element) {
if (!element) {
doneCallback(false);
return;
}
var res = t.parser.parseCues_(element, 0, element.length, offset);
if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
doneCallback(true);
});
};
/**
* Calcualtes how much time will be buffered after a given amount of wallclock
* time has passed. The CUES element must be parsed before this function is
* called.
* @param {number} time The time in seconds of the stream to start from.
* @param {number} timeToSearch The wallclock time in seconds to emulate.
* @param {number} kbps The download rate in kilobits per second.
* @param {number} buffer The amount of time in seconds currently buffered.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'seconds' the wallclock time in seconds used to perform the calculation
* and 'buffer' is the amount of time in seconds in the buffer.
*/
WebMFileParser.prototype.bufferSizeAfterTime = function(time,
timeToSearch,
kbps,
buffer) {
var res = this.getCueIndexFromTime_(time);
if (res.status != WebMParser.STATUS_OK)
return res;
var startingIndex = res.value;
res = this.getCueDescFromCue_(startingIndex);
if (res.status != WebMParser.STATUS_OK)
return res;
var currentCueDesc = res.value;
var secDownloading = 0.0;
var secDownloaded = 0.0;
var index = startingIndex;
//Check for non cue start time.
if (time > currentCueDesc.time) {
var cueSec = currentCueDesc.endTime - time;
var percent = cueSec / (currentCueDesc.endTime - currentCueDesc.time);
var cueBytes = currentCueDesc.size * percent;
var timeToDownload = ((cueBytes * 8) / 1000.0) / kbps;
secDownloading += timeToDownload;
secDownloaded += cueSec;
if (secDownloading > timeToSearch) {
secDownloaded = (timeToSearch / secDownloading) * secDownloaded;
secDownloading = timeToSearch;
}
index++;
}
var cues = this.parser.getCues();
if (!cues || cues.length == 0) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Cues is not valid.'};
}
while (index < cues.length && secDownloading < timeToSearch) {
res = this.getCueDescFromCue_(index);
if (res.status != WebMParser.STATUS_OK)
return res;
var currentCueDesc = res.value;
//this.log(' cue time:' + currentCueDesc.time + ' offset:' +
// currentCueDesc.offset + ' endTime:' + currentCueDesc.endTime +
// ' size:' + currentCueDesc.size);
var cueBytes = currentCueDesc.size;
var cueSec = currentCueDesc.endTime - currentCueDesc.time;
var timeToDownload = ((cueBytes * 8) / 1000.0) / kbps;
secDownloading += timeToDownload;
secDownloaded += cueSec;
if (secDownloading > timeToSearch) {
secDownloaded = (timeToSearch / secDownloading) * secDownloaded;
secDownloading = timeToSearch;
break;
}
index++;
}
var bufferCounted = secDownloaded - secDownloading + buffer;
var secCounted = secDownloading;
return {
status: WebMParser.STATUS_OK,
seconds: secCounted,
buffer: bufferCounted
};
};
/**
* Calcualtes how much time will be buffered after the player has buffered a
* certain amouint of time in the stream. The CUES element must be parsed
* before this function is called.
* @param {number} time The time in seconds of the stream to start from.
* @param {number} timeToSearch The number of seconds to search for in the
* stream.
* @param {number} kbps The download rate in kilobits per second.
* @param {number} minBuffer The minimum time in seconds that must be in the
* buffer.
* @param {number} buffer The amount of time in seconds currently buffered.
* @return {Object} Status object. If 'status' is EbmlParser.STATUS_OK,
* 'underrun' is true if a buffer under-run condition occurred,
* 'downloadTime' is the time in seconds that it took to download the data,
* and 'buffer' is the amount of time in seconds in the buffer.
*/
WebMFileParser.prototype.bufferSizeAfterTimeDownloaded = function(time,
timeToSearch,
kbps,
minBuffer,
buffer) {
var res = this.getCueIndexFromTime_(time);
if (res.status != WebMParser.STATUS_OK)
return res;
var startingIndex = res.value;
res = this.getCueDescFromCue_(startingIndex);
if (res.status != WebMParser.STATUS_OK)
return res;
var currentCueDesc = res.value;
var bufferUnderrun = false;
var endTime = time + timeToSearch;
var secToDownload = 0.0;
var secDownloaded = 0.0;
var index = startingIndex;
// Check for non cue start time.
if (time > currentCueDesc.time) {
var cueSec = currentCueDesc.endTime - time;
var percent = cueSec / (currentCueDesc.endTime - currentCueDesc.time);
var cueBytes = currentCueDesc.size * percent;
var timeToDownload = ((cueBytes * 8) / 1000.0) / kbps;
secDownloaded += cueSec - timeToDownload;
secToDownload += timeToDownload;
// Check if the search ends within the first cue.
if (currentCueDesc.endTime >= endTime) {
var percentToSub = timeToSearch / (currentCueDesc.endTime - time);
secDownloaded = percentToSub * secDownloaded;
secToDownload = percentToSub * secToDownload;
if ((secDownloaded + buffer) <= minBuffer)
bufferUnderrun = true;
return {
status: WebMParser.STATUS_OK,
underrun: bufferUnderrun,
downloadTime: secToDownload,
buffer: buffer + secDownloaded
};
} else if ((secDownloaded + buffer) <= minBuffer) {
bufferUnderrun = true;
return {
status: WebMParser.STATUS_OK,
underrun: bufferUnderrun,
downloadTime: secToDownload,
buffer: buffer + secDownloaded
};
}
index++;
}
var cues = this.parser.getCues();
if (!cues || cues.length == 0) {
return {status: WebMParser.STATUS_INVALID_DATA,
reason: 'Cues is not valid.'};
}
while (index < cues.length) {
res = this.getCueDescFromCue_(index);
if (res.status != WebMParser.STATUS_OK)
return res;
var currentCueDesc = res.value;
//this.log(' cue time:' + currentCueDesc.time + ' offset:' +
// currentCueDesc.offset + ' endTime:' + currentCueDesc.endTime +
// ' size:' + currentCueDesc.size);
var cueBytes = currentCueDesc.size;
var cueSec = currentCueDesc.endTime - currentCueDesc.time;
var timeToDownload = ((cueBytes * 8) / 1000.0) / kbps;
secDownloaded += cueSec - timeToDownload;
secToDownload += timeToDownload;
if (currentCueDesc.endTime >= endTime) {
var percentToSub = timeToSearch / (currentCueDesc.endTime - time);
secDownloaded = percentToSub * secDownloaded;
secToDownload = percentToSub * secToDownload;
if ((secDownloaded + buffer) <= minBuffer)
bufferUnderrun = true;
break;
}
if ((secDownloaded + buffer) <= minBuffer) {
bufferUnderrun = true;
break;
}
index++;
}
return {
status: WebMParser.STATUS_OK,
underrun: bufferUnderrun,
downloadTime: secToDownload,
buffer: buffer + secDownloaded
};
};
/**
* Asynchronous function that parses the file headers using unbuffered reads.
* @param {number} offset Starting offset in the file to read from.
* @param {number} size Number of bytes to read.
* @param {function} doneCallback Callback function. First parameter passes
* back a boolean with a value of true if the call was successful.
*/
WebMFileParser.prototype.parseFirstHeadersUnbuffered = function(offset,
size,
doneCallback) {
var t = this;
this.file.fetchBytesUnbuffered(offset, size, function(buffer) {
if (!buffer) {
doneCallback(false);
return;
}
var readOffset = 0;
var res = t.parser.parseEBMLHeader_(buffer, readOffset, buffer.length);
if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
readOffset += res.bytesUsed;
res = t.parser.parseSegmentHeaders(buffer, readOffset,
buffer.length - (readOffset - offset),
offset);
if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
doneCallback(true);
});
};
/**
* Asynchronous function that parses the Cues using unbuffered reads.
* @param {number} offset Starting offset in the file to read from.
* @param {number} size Number of bytes to read.
* @param {number} segmentOffset Segment offset in a WebM file.
* @param {function} doneCallback Callback function. First parameter passes
* back a boolean with a value of true if the call was successful.
*/
WebMFileParser.prototype.fetchCuesUnbuffered = function(offset, size,
segmentOffset,
doneCallback) {
this.log('fetchCuesUnbuffered offset=' + offset + ' size=' + size);
this.parser.setSegmentOffset(offset);
var t = this;
this.file.fetchBytesUnbuffered(offset, size, function(buffer) {
if (!buffer) {
doneCallback(false);
return;
}
var res = t.parser.parseCues_(element, 0, element.length, offset);
if (res.status != WebMParser.STATUS_OK) {
doneCallback(false);
return;
}
doneCallback(true);
});
};
/**
* Sets the file position and returns partial cluster data until the full Cue
* element is returned. Data returned will be only in the current Cue
* element. Calling funciton must expect one or more callbacks until next
* cueDesc is different than |cueDesc|, next cueDesc is null,
* or data returned is null and the next cueDesc is null. The next
* cueDesc returned will have the same values as |cueDesc| to
* signal that there is more data to come in the current Cue element. When
* all of the data for the current Cue element has been returned the next
* cueDesc will represent the next Cue element of the current WebM
* file. If the next cueDesc is null then that signals that the cluster
* data is EOF. If the data returned is null and the next cueDesc is
* null then that signals an error condition.
* @param {Object} cueDesc cueDesc to read.
* @param {function} doneCallback Callback function. First parameter is the
* next cueDesc object or null on error. Second parameter is the cluster
* element or null on error.
*/
WebMFileParser.prototype.fetchCueData = function(cueDesc, doneCallback) {
//this.log('fetchCueData offset:' + cueDesc.offset + ' url:' +
// this.file.url_);
// Seek to the beginning of the cluster element.
this.file.seek(cueDesc.offset);
this.sendCueData_(cueDesc, doneCallback);
};
/**
* Returns partial cluster data until the full Cue element is returned. Will
* not return data in the next cue element. The next cueDesc returned
* will have the same values as |cueDesc| to signal that there is more
* data to come in the current Cue element. When all of the data for the
* current Cue element has been returned the next cueDesc will
* represent the next Cue element of the current WebM file. If the next
* cueDesc is null then that signals that the cluster data is EOF. If
* the data returned is null and the next cueDesc is null then that
* signals an error condition.
* @param {Object} cueDesc cueDesc to read.
* @param {function} doneCallback Callback function. First parameter is the
* next cueDesc object or null on error. Second parameter is the cluster
* element or null on error.
* @private
*/
WebMFileParser.prototype.sendCueData_ = function(cueDesc, doneCallback) {
var sendFullChunks = true;
var chunkSize = cueDesc.size;
var bufferSize = this.file.getBytesAvailable();
var bytesSent = this.file.getCurrentOffset() - cueDesc.offset;
var chunkSizeLeft = chunkSize - bytesSent;
var bytesToSend = bufferSize;
if (bytesToSend > chunkSizeLeft)
bytesToSend = chunkSizeLeft;
//this.log('sendCueData_ chunkSize:' + chunkSize +
// ' bufferSize:' + bufferSize + ' bytesSent:' + bytesSent +
// ' chunkSizeLeft:' + chunkSizeLeft +
// ' bytesToSend:' + bytesToSend + ' url:' + this.file.url_);
if ((!sendFullChunks && bytesToSend > 0) || (bytesToSend == chunkSize)) {
var nextCueDesc = null;
// Check if we have downloaded all of the current cluster's data and we
// should get the next cueDesc.
if (bytesToSend == chunkSizeLeft) {
var res = this.getCueDescFromOffset(cueDesc.offset + cueDesc.size);
if (res.status != WebMParser.STATUS_OK) {
this.log('sendCueData_ error Could not get cueDesc from offset. :' +
res.reason);
doneCallback(null, null);
return;
}
nextCueDesc = res.value;
// Check if the current Cue is the last Cue.
if (nextCueDesc.offset == cueDesc.offset)
nextCueDesc = null;
} else {
// Signal there is still more data in the current Cue.
nextCueDesc = cueDesc;
}
var start = this.file.getIndex();
var end = start + bytesToSend;
var element = new Uint8Array(this.file.getBuffer().subarray(start, end));
this.file.read(element.length);
// If |downloadMoreData| is false do not request any more data. This might
// be because of an error or maybe the user seeked. Do not treat this as an
// error.
var downloadMoreData = doneCallback(nextCueDesc, element);
// Check to see if we need to download anymore data from the current cue
// chunk.
if (nextCueDesc == null || nextCueDesc.offset != cueDesc.offset ||
!downloadMoreData)
return;
bytesSent += bytesToSend;
chunkSizeLeft -= bytesToSend;
}
var size = this.file.getBytesAvailable();
if (sendFullChunks) {
size += chunkSizeLeft;
} else {
size += this.partialDownloadSize;
}
if (size > chunkSizeLeft)
size = chunkSizeLeft;
if (this.file.getBytesAvailable() < size) {
var t = this;
this.file.fetchBytes(size, function(success) {
if (!success) {
this.log('sendCueData_ error fetchBytes offset:' + offset + ' size:' +
size);
doneCallback(null, null);
return;
}
t.sendCueData_(cueDesc, doneCallback);
});
}
};
/**
* Getter for partialDownloadSize.
* @return {number} partialDownloadSize.
*/
WebMFileParser.prototype.getPartialDownloadSize = function() {
return this.partialDownloadSize;
};
/**
* Setter for partialDownloadSize.
* @param {number} value The value to set.
*/
WebMFileParser.prototype.setPartialDownloadSize = function(value) {
this.partialDownloadSize = value;
};