Convert TextSourceBuffer to TextEngine

This changes the text APIs to correctly handle buffered ranges of
segmented text.

b/25517444

Related to issue #150

Change-Id: I3a11b87e8d93376a5012566deb3bf0d015f52391
This commit is contained in:
Joey Parrish
2016-02-12 13:36:40 -08:00
parent 6ae1474e65
commit cd2d25cbb2
14 changed files with 671 additions and 589 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
+../../lib/media/segment_index.js
+../../lib/media/segment_reference.js
+../../lib/media/streaming_engine.js
+../../lib/media/text_source_buffer.js
+../../lib/media/text_engine.js
+../../lib/media/time_ranges_utils.js
+../../lib/media/webm_segment_index_parser.js
+8 -9
View File
@@ -1,6 +1,6 @@
# Shaka v2.0 Redesign
last update: 2016-01-15
last update: 2016-02-12
by: [joeyparrish@google.com](mailto:joeyparrish@google.com)
@@ -10,8 +10,8 @@ by: [joeyparrish@google.com](mailto:joeyparrish@google.com)
We are redesigning Shaka Player to reduce overall complexity, increase
modularity, and make it easier to introduce new features that would be too
messy in Shaka Player v1.x. We posted code to the preview branch on github
at the end of November 2015 and *hope* to have a fully-functional public beta
release some time in January 2016.
at the end of November 2015 and *hope* to have a public beta release by the
end of February 2016.
## Background
@@ -175,11 +175,10 @@ StreamingEngine will own MediaSourceEngine, and will be responsible for reading
an internal representation of a manifest, fetching content, and feeding content
to MediaSourceEngine.
**[Simplicity]** To simplify segmented text and non-native text formats, we will create a
work-alike for SourceBuffer called TextSourceBuffer. MediaSourceEngine and the
layers above it will not have to know the details of how text is handled, and
segmented text can be streamed exactly the same way as segmented audio and
video.
**[Simplicity]** To simplify segmented text and non-native text formats, we
will create TextEngine. MediaSourceEngine and the layers above it will not
have to know the details of how text is handled, and segmented text can be
streamed exactly the same way as segmented audio and video.
**[Extensibility]** Text parsers will be plugin-based, allowing new text formats to be supported
without modifying the library.
@@ -247,7 +246,7 @@ shaka.net.NetworkingEngine.prototype.registerRequestFilter(filterCallback)
shaka.net.NetworkingEngine.prototype.registerResponseFilter(filterCallback)
shaka.media.TextSourceBuffer.registerParser(mimeType, parserCallback)
shaka.media.TextEngine.registerParser(mimeType, parserCallback)
shaka.media.ManifestParser.registerParserByMime(mimeType, parser)
+3 -3
View File
@@ -23,9 +23,9 @@ digraph shaka2_data {
StreamingEngine -> MediaSourceEngine [ label="ArrayBuffer" ]
MediaSourceEngine -> SourceBuffer [ label="ArrayBuffer" ]
MediaSourceEngine -> TextSourceBuffer [ label="ArrayBuffer" ]
TextSourceBuffer -> TextParser [ label="ArrayBuffer => TextCues" dir=both arrowtail=onormal ]
TextSourceBuffer -> TextTrack [ label="TextCues" ]
MediaSourceEngine -> TextEngine [ label="ArrayBuffer" ]
TextEngine -> TextParser [ label="ArrayBuffer => TextCues" dir=both arrowtail=onormal ]
TextEngine -> TextTrack [ label="TextCues" ]
NetworkingEngine -> RequestFilter [ label="Request => Request" dir=both arrowtail=onormal ]
NetworkingEngine -> ResponseFilter [ label="Response => Response" dir=both arrowtail=onormal ]
+3 -3
View File
@@ -30,10 +30,10 @@ digraph shaka2_ownership {
MediaSourceEngine -> MediaSource
MediaSourceEngine -> SourceBuffer
MediaSourceEngine -> SourceBuffer
MediaSourceEngine -> TextSourceBuffer
MediaSourceEngine -> TextEngine
TextSourceBuffer -> TextTrack
TextSourceBuffer -> TextParser
TextEngine -> TextTrack
TextEngine -> TextParser
subgraph cluster_legend {
style=rounded
+49 -21
View File
@@ -18,7 +18,7 @@
goog.provide('shaka.media.MediaSourceEngine');
goog.require('shaka.asserts');
goog.require('shaka.media.TextSourceBuffer');
goog.require('shaka.media.TextEngine');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IDestroyable');
@@ -50,9 +50,12 @@ shaka.media.MediaSourceEngine = function(mediaSource, textTrack) {
/** @private {TextTrack} */
this.textTrack_ = textTrack;
/** @private {!Object.<string, !SourceBuffer>} */
/** @private {!Object.<string, SourceBuffer>} */
this.sourceBuffers_ = {};
/** @private {shaka.media.TextEngine} */
this.textEngine_ = null;
/**
* @private {!Object.<string,
* !Array.<shaka.media.MediaSourceEngine.Operation>>}
@@ -89,7 +92,7 @@ shaka.media.MediaSourceEngine.Operation;
* @return {boolean}
*/
shaka.media.MediaSourceEngine.isTypeSupported = function(mimeType) {
return shaka.media.TextSourceBuffer.isTypeSupported(mimeType) ||
return shaka.media.TextEngine.isTypeSupported(mimeType) ||
MediaSource.isTypeSupported(mimeType);
};
@@ -169,6 +172,7 @@ shaka.media.MediaSourceEngine.prototype.destroy = function() {
this.eventManager_ = null;
this.mediaSource_ = null;
this.textTrack_ = null;
this.textEngine_ = null;
this.sourceBuffers_ = {};
if (!COMPILED) {
for (var contentType in this.queues_) {
@@ -201,26 +205,17 @@ shaka.media.MediaSourceEngine.prototype.init = function(typeConfig) {
shaka.media.MediaSourceEngine.isTypeSupported(mimeType),
'Type negotiation should happen before MediaSourceEngine.init!');
var sourceBuffer;
if (contentType == 'text') {
var textSourceBuffer =
new shaka.media.TextSourceBuffer(this.textTrack_, mimeType);
// This crazy cast is a hack to satisfy the compiler, since
// TextSourceBuffer isn't a subclass of SourceBuffer.
sourceBuffer = /** @type {!SourceBuffer} */(/** @type {*} */(
textSourceBuffer));
this.textEngine_ = new shaka.media.TextEngine(this.textTrack_, mimeType);
} else {
sourceBuffer = /** @type {!SourceBuffer} */(
this.mediaSource_.addSourceBuffer(mimeType));
var sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
this.eventManager_.listen(
sourceBuffer, 'error', this.onError_.bind(this, contentType));
this.eventManager_.listen(
sourceBuffer, 'updateend', this.onUpdateEnd_.bind(this, contentType));
this.sourceBuffers_[contentType] = sourceBuffer;
this.queues_[contentType] = [];
}
this.eventManager_.listen(
sourceBuffer, 'error', this.onError_.bind(this, contentType));
this.eventManager_.listen(
sourceBuffer, 'updateend', this.onUpdateEnd_.bind(this, contentType));
this.sourceBuffers_[contentType] = sourceBuffer;
this.queues_[contentType] = [];
}
};
@@ -232,6 +227,9 @@ shaka.media.MediaSourceEngine.prototype.init = function(typeConfig) {
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferStart = function(contentType) {
if (contentType == 'text') {
return this.textEngine_.bufferStart();
}
return shaka.media.TimeRangesUtils.bufferStart(
this.sourceBuffers_[contentType].buffered);
};
@@ -244,6 +242,9 @@ shaka.media.MediaSourceEngine.prototype.bufferStart = function(contentType) {
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferEnd = function(contentType) {
if (contentType == 'text') {
return this.textEngine_.bufferEnd();
}
return shaka.media.TimeRangesUtils.bufferEnd(
this.sourceBuffers_[contentType].buffered);
};
@@ -259,6 +260,9 @@ shaka.media.MediaSourceEngine.prototype.bufferEnd = function(contentType) {
*/
shaka.media.MediaSourceEngine.prototype.bufferedAheadOf =
function(contentType, time) {
if (contentType == 'text') {
return this.textEngine_.bufferedAheadOf(time);
}
return shaka.media.TimeRangesUtils.bufferedAheadOf(
this.sourceBuffers_[contentType].buffered, time);
};
@@ -266,13 +270,23 @@ shaka.media.MediaSourceEngine.prototype.bufferedAheadOf =
/**
* Enqueue an operation to append data to the SourceBuffer.
* Start and end times are needed for TextEngine, but not for MediaSource.
* Start and end times may be null for initialization segments.
*
* @param {string} contentType
* @param {!ArrayBuffer|!ArrayBufferView} data
* @param {?number} startTime
* @param {?number} endTime
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.appendBuffer =
function(contentType, data) {
function(contentType, data, startTime, endTime) {
if (contentType == 'text') {
shaka.asserts.assert(startTime != null && endTime != null,
'text streams do not have init segments!');
return this.textEngine_.appendBuffer(
data, /** @type {number} */(startTime), /** @type {number} */(endTime));
}
return this.enqueueOperation_(
contentType,
this.append_.bind(this, contentType, data));
@@ -293,6 +307,9 @@ shaka.media.MediaSourceEngine.prototype.remove =
// See https://github.com/google/shaka-player/issues/251
shaka.asserts.assert(endTime < Number.MAX_VALUE,
'remove() with MAX_VALUE or POSITIVE_INFINITY is not IE-compatible!');
if (contentType == 'text') {
return this.textEngine_.remove(startTime, endTime);
}
return this.enqueueOperation_(
contentType,
this.remove_.bind(this, contentType, startTime, endTime));
@@ -306,6 +323,9 @@ shaka.media.MediaSourceEngine.prototype.remove =
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.clear = function(contentType) {
if (contentType == 'text') {
return this.textEngine_.remove(0, Number.POSITIVE_INFINITY);
}
// Note that not all platforms allow clearing to Number.POSITIVE_INFINITY.
return this.enqueueOperation_(
contentType,
@@ -324,6 +344,10 @@ shaka.media.MediaSourceEngine.prototype.clear = function(contentType) {
*/
shaka.media.MediaSourceEngine.prototype.setTimestampOffset = function(
contentType, timestampOffset) {
if (contentType == 'text') {
this.textEngine_.setTimestampOffset(timestampOffset);
return Promise.resolve();
}
return this.enqueueOperation_(
contentType,
this.setTimestampOffset_.bind(this, contentType, timestampOffset));
@@ -340,6 +364,10 @@ shaka.media.MediaSourceEngine.prototype.setTimestampOffset = function(
*/
shaka.media.MediaSourceEngine.prototype.setAppendWindowEnd = function(
contentType, appendWindowEnd) {
if (contentType == 'text') {
this.textEngine_.setAppendWindowEnd(appendWindowEnd);
return Promise.resolve();
}
return this.enqueueOperation_(
contentType,
this.setAppendWindowEnd_.bind(this, contentType, appendWindowEnd));
+4 -2
View File
@@ -1064,7 +1064,8 @@ shaka.media.StreamingEngine.prototype.initSourceBuffer_ = function(
if (this.destroyed_) return;
shaka.log.v1(logPrefix, 'appending init segment');
return this.mediaSourceEngine_.appendBuffer(mediaState.type, initSegment);
return this.mediaSourceEngine_.appendBuffer(
mediaState.type, initSegment, null /* startTime */, null /* endTime */);
}.bind(this));
return Promise.all([setTimestampOffset, setAppendWindowEnd, appendInit]);
@@ -1115,7 +1116,8 @@ shaka.media.StreamingEngine.prototype.append_ = function(
if (this.destroyed_) return;
shaka.log.v1(logPrefix, 'appending media segment');
return this.mediaSourceEngine_.appendBuffer(mediaState.type, segment);
return this.mediaSourceEngine_.appendBuffer(
mediaState.type, segment, reference.startTime, reference.endTime);
}.bind(this)).then(function() {
if (this.destroyed_) return;
shaka.log.v1(logPrefix, 'appended media segment');
+247
View File
@@ -0,0 +1,247 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.TextEngine');
goog.require('shaka.asserts');
/**
* Manages text parsers and cues.
*
* @struct
* @constructor
* @param {TextTrack} track
* @param {string} mimeType
*/
shaka.media.TextEngine = function(track, mimeType) {
/** @private {shaka.media.TextEngine.TextParser} */
this.parser_ = shaka.media.TextEngine.parserMap_[mimeType];
// A more accurate work-alike would throw NotSupportedError here, but this
// should not happen if type-negotiation is working as it should.
shaka.asserts.assert(this.parser_,
'Text type negotiation should have happened already');
/** @private {TextTrack} */
this.track_ = track;
/** @private {number} */
this.timestampOffset_ = 0;
/** @private {number} */
this.appendWindowEnd_ = Number.POSITIVE_INFINITY;
/** @private {?number} */
this.bufferStart_ = null;
/** @private {?number} */
this.bufferEnd_ = null;
};
/**
* Parses a text buffer into an array of cues.
*
* @typedef {function((ArrayBuffer|ArrayBufferView)):!Array.<!TextTrackCue>}
* @exportDoc
*/
shaka.media.TextEngine.TextParser;
/** @private {!Object.<string, !shaka.media.TextEngine.TextParser>} */
shaka.media.TextEngine.parserMap_ = {};
/**
* @param {string} mimeType
* @param {!shaka.media.TextEngine.TextParser} parser
* @export
*/
shaka.media.TextEngine.registerParser = function(mimeType, parser) {
shaka.media.TextEngine.parserMap_[mimeType] = parser;
};
/**
* @param {string} mimeType
* @export
*/
shaka.media.TextEngine.unregisterParser = function(mimeType) {
delete shaka.media.TextEngine.parserMap_[mimeType];
};
/**
* @param {string} mimeType
* @return {boolean}
*/
shaka.media.TextEngine.isTypeSupported = function(mimeType) {
return !!shaka.media.TextEngine.parserMap_[mimeType];
};
/**
* @param {ArrayBuffer|ArrayBufferView} buffer
* @param {number} startTime
* @param {number} endTime
* @return {!Promise}
*/
shaka.media.TextEngine.prototype.appendBuffer =
function(buffer, startTime, endTime) {
var offset = this.timestampOffset_;
startTime += offset;
endTime += offset;
// Start the operation asynchronously to avoid blocking the caller.
return Promise.resolve().then(function() {
// Parse the buffer and add the new cues.
var cues = this.parser_(buffer);
for (var i = 0; i < cues.length; ++i) {
cues[i].startTime += offset;
cues[i].endTime += offset;
if (cues[i].startTime >= this.appendWindowEnd_) break;
this.track_.addCue(cues[i]);
}
// NOTE: We update the buffered range from the start and end times passed
// down from the segment reference, not with the start and end times of the
// parsed cues. This is important because some segments may contain no
// cues, but we must still consider those ranges buffered.
if (this.bufferStart_ == null) {
this.bufferStart_ = startTime;
} else {
// We already had something in buffer, and we assume we are extending the
// range from the end.
shaka.asserts.assert((startTime - this.bufferEnd_) <= 1,
'There should not be a gap in text references >1s');
}
this.bufferEnd_ = Math.min(endTime, this.appendWindowEnd_);
}.bind(this));
};
/**
* @param {number} start
* @param {number} end
* @return {!Promise}
*/
shaka.media.TextEngine.prototype.remove = function(start, end) {
// Start the operation asynchronously to avoid blocking the caller.
return Promise.resolve().then(function() {
var cues = this.track_.cues;
var removeMe = [];
for (var i = 0; i < cues.length; ++i) {
if (cues[i].startTime >= end || cues[i].endTime <= start) {
// Outside the remove range. Hang on to it.
} else {
// Remove these in another loop to avoid mutating the TextTrackCueList
// while iterating over it. This allows us to avoid making assumptions
// about whether or not this.track_.remove() will alter that list.
removeMe.push(cues[i]);
}
}
for (var i = 0; i < removeMe.length; ++i) {
this.track_.removeCue(removeMe[i]);
}
if (this.bufferStart_ == null) {
shaka.asserts.assert(this.bufferEnd_ == null,
'end must be null if start is null');
shaka.asserts.assert(removeMe.length == 0,
'buffer empty, should have removed nothing');
} else {
shaka.asserts.assert(this.bufferEnd_ != null,
'end must be non-null if start is non-null');
// Update buffered range.
if (end <= this.bufferStart_ || start >= this.bufferEnd_) {
// No intersection. Nothing was removed.
shaka.asserts.assert(removeMe.length == 0,
'no intersection, should have removed nothing');
} else if (start <= this.bufferStart_ && end >= this.bufferEnd_) {
// We wiped out everything.
shaka.asserts.assert(this.track_.cues.length == 0,
'should be no cues left');
this.bufferStart_ = this.bufferEnd_ = null;
} else if (start <= this.bufferStart_ && end < this.bufferEnd_) {
// We removed from the beginning of the range.
this.bufferStart_ = end;
} else if (start > this.bufferStart_ && end >= this.bufferEnd_) {
// We removed from the end of the range.
this.bufferEnd_ = start;
} else {
// We removed from the middle? StreamingEngine isn't supposed to.
shaka.asserts.assert(
false, 'removal from the middle is not supported by TextEngine');
}
}
}.bind(this));
};
/** @param {number} timestampOffset */
shaka.media.TextEngine.prototype.setTimestampOffset =
function(timestampOffset) {
this.timestampOffset_ = timestampOffset;
};
/** @param {number} windowEnd */
shaka.media.TextEngine.prototype.setAppendWindowEnd =
function(windowEnd) {
this.appendWindowEnd_ = windowEnd;
};
/**
* @return {?number} Time in seconds of the beginning of the buffered range,
* or null if nothing is buffered.
*/
shaka.media.TextEngine.prototype.bufferStart = function() {
return this.bufferStart_;
};
/**
* @return {?number} Time in seconds of the end of the buffered range,
* or null if nothing is buffered.
*/
shaka.media.TextEngine.prototype.bufferEnd = function() {
return this.bufferEnd_;
};
/**
* @param {number} t A timestamp
* @return {number} Number of seconds ahead of 't' we have buffered
*/
shaka.media.TextEngine.prototype.bufferedAheadOf = function(t) {
if (this.bufferEnd_ == null || this.bufferEnd_ < t) return 0;
shaka.asserts.assert(this.bufferStart_ != null,
'start should not be null if end is not null');
if (t < this.bufferStart_) return 0;
return this.bufferEnd_ - t;
};
-203
View File
@@ -1,203 +0,0 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.media.TextSourceBuffer');
goog.require('shaka.asserts');
goog.require('shaka.util.FakeEventTarget');
/**
* A SourceBuffer work-alike for text types.
*
* @struct
* @constructor
* @param {TextTrack} track
* @param {string} mimeType
* @extends {shaka.util.FakeEventTarget}
* @see http://w3c.github.io/media-source/#sourcebuffer
*/
shaka.media.TextSourceBuffer = function(track, mimeType) {
shaka.util.FakeEventTarget.call(this, null);
/** @private {shaka.media.TextSourceBuffer.TextParser} */
this.parser_ = shaka.media.TextSourceBuffer.parserMap_[mimeType];
// A more accurate work-alike would throw NotSupportedError here, but this
// should not happen if type-negotiation is working as it should.
shaka.asserts.assert(this.parser_,
'Text type negotiation should have happened already');
/** @private {TextTrack} */
this.track_ = track;
/** @type {boolean} */
this.updating = false;
/**
* A work-alike for TimeRanges.
* @type {{ length: number,
* start: function(number): number,
* end: function(number): number }}
*/
this.buffered = {
'length': 0,
'start': this.bufferStart_.bind(this),
'end': this.bufferEnd_.bind(this)
};
/**
* Ignored.
* @type {number}
*/
this.timestampOffset = 0;
};
goog.inherits(shaka.media.TextSourceBuffer, shaka.util.FakeEventTarget);
/**
* Parses a text buffer into an array of cues.
*
* @typedef {function((ArrayBuffer|ArrayBufferView)):!Array.<!TextTrackCue>}
* @exportDoc
*/
shaka.media.TextSourceBuffer.TextParser;
/** @private {!Object.<string, !shaka.media.TextSourceBuffer.TextParser>} */
shaka.media.TextSourceBuffer.parserMap_ = {};
/**
* @param {string} mimeType
* @param {!shaka.media.TextSourceBuffer.TextParser} parser
* @export
*/
shaka.media.TextSourceBuffer.registerParser = function(mimeType, parser) {
shaka.media.TextSourceBuffer.parserMap_[mimeType] = parser;
};
/**
* @param {string} mimeType
* @export
*/
shaka.media.TextSourceBuffer.unregisterParser = function(mimeType) {
delete shaka.media.TextSourceBuffer.parserMap_[mimeType];
};
/**
* @param {string} mimeType
* @return {boolean}
*/
shaka.media.TextSourceBuffer.isTypeSupported = function(mimeType) {
return !!shaka.media.TextSourceBuffer.parserMap_[mimeType];
};
/**
* @param {ArrayBuffer|ArrayBufferView} buffer
*/
shaka.media.TextSourceBuffer.prototype.appendBuffer = function(buffer) {
shaka.asserts.assert(this.updating == false,
'Text appendBuffer called while updating!');
this.updating = true;
// Start the operation asynchronously to avoid blocking the caller.
Promise.resolve().then(function() {
// Parse the buffer and add the new cues.
var cues = this.parser_(buffer);
for (var i = 0; i < cues.length; ++i) {
this.track_.addCue(cues[i]);
}
this.update_();
}.bind(this));
};
/**
* @param {number} start
* @param {number} end
*/
shaka.media.TextSourceBuffer.prototype.remove = function(start, end) {
shaka.asserts.assert(this.updating == false,
'Text remove called while updating!');
this.updating = true;
// Start the operation asynchronously to avoid blocking the caller.
Promise.resolve().then(function() {
var cues = this.track_.cues;
var removeMe = [];
for (var i = 0; i < cues.length; ++i) {
if (cues[i].startTime > end || cues[i].endTime < start) {
// Outside the remove range. Hang on to it.
} else {
// Remove these in another loop to avoid mutating the TextTrackCueList
// while iterating over it. This allows us to avoid making assumptions
// about whether or not this.track_.remove() will alter that list.
removeMe.push(cues[i]);
}
}
for (var i = 0; i < removeMe.length; ++i) {
this.track_.removeCue(removeMe[i]);
}
this.update_();
}.bind(this));
};
/**
* Perform actions common to all updates (appendBuffer, remove).
* @private
*/
shaka.media.TextSourceBuffer.prototype.update_ = function() {
this.buffered.length = this.track_.cues.length ? 1 : 0;
this.updating = false;
var updateEnd = shaka.util.FakeEvent.create({'type': 'updateend'});
this.dispatchEvent(updateEnd);
};
/**
* @param {number} index
* @return {number}
* @private
*/
shaka.media.TextSourceBuffer.prototype.bufferStart_ = function(index) {
shaka.asserts.assert(index == 0 && this.buffered.length == 1,
'Only one text buffered range allowed!');
return this.track_.cues[0].startTime;
};
/**
* @param {number} index
* @return {number}
* @private
*/
shaka.media.TextSourceBuffer.prototype.bufferEnd_ = function(index) {
shaka.asserts.assert(index == 0 && this.buffered.length == 1,
'Only one text buffered range allowed!');
return this.track_.cues[this.track_.cues.length - 1].endTime;
};
+3 -4
View File
@@ -17,14 +17,14 @@
goog.provide('shaka.media.VttTextParser');
goog.require('shaka.media.TextSourceBuffer');
goog.require('shaka.media.TextEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.TextParser');
goog.require('shaka.util.Uint8ArrayUtils');
/**
* A TextSourceBuffer plugin that parses WebVTT files.
* A TextEngine plugin that parses WebVTT files.
*
* @param {ArrayBuffer|ArrayBufferView} data
* @return {!Array.<!TextTrackCue>}
@@ -169,5 +169,4 @@ shaka.media.VttTextParser.parseTime_ = function(parser) {
return (miliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
};
shaka.media.TextSourceBuffer.registerParser(
'text/vtt', shaka.media.VttTextParser);
shaka.media.TextEngine.registerParser('text/vtt', shaka.media.VttTextParser);
+1 -1
View File
@@ -19,7 +19,7 @@
goog.require('shaka.Player');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.TextSourceBuffer');
goog.require('shaka.media.TextEngine');
goog.require('shaka.polyfill.installAll');
goog.require('shaka.util.Error');
goog.require('shaka.util.StringUtils');
+3 -2
View File
@@ -37,6 +37,7 @@ describe('MediaSourceEngine', function() {
mimeType: 'audio/mp4; codecs="mp4a.40.2"',
generator: null
}
// TODO: add text streams to MSE integration tests
};
var presentationDuration = 840;
@@ -95,13 +96,13 @@ describe('MediaSourceEngine', function() {
function appendInit(type) {
var segment = metadata[type].generator.getInitSegment(Date.now() / 1000);
return mediaSourceEngine.appendBuffer(type, segment);
return mediaSourceEngine.appendBuffer(type, segment, null, null);
}
function append(type, segmentNumber) {
var segment = metadata[type].generator.
getSegment(segmentNumber, Date.now() / 1000);
return mediaSourceEngine.appendBuffer(type, segment);
return mediaSourceEngine.appendBuffer(type, segment, null, null);
}
function buffered(type, time) {
+39 -39
View File
@@ -17,7 +17,7 @@
describe('MediaSourceEngine', function() {
var originalIsTypeSupported;
var originalTextSourceBuffer;
var originalTextEngine;
var audioSourceBuffer;
var videoSourceBuffer;
var mockMediaSource;
@@ -36,13 +36,13 @@ describe('MediaSourceEngine', function() {
return contentType == 'video' || contentType == 'audio';
};
originalTextSourceBuffer = shaka.media.TextSourceBuffer;
shaka.media.TextSourceBuffer = createMockTextSourceBufferCtor();
originalTextEngine = shaka.media.TextEngine;
shaka.media.TextEngine = createMockTextEngineCtor();
});
afterAll(function() {
window.MediaSource.prototype.isTypeSupported = originalIsTypeSupported;
shaka.media.TextSourceBuffer = originalTextSourceBuffer;
shaka.media.TextEngine = originalTextEngine;
});
beforeEach(function() {
@@ -63,13 +63,13 @@ describe('MediaSourceEngine', function() {
mediaSourceEngine.init({'audio': 'audio/foo', 'video': 'video/foo'});
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledWith('audio/foo');
expect(mockMediaSource.addSourceBuffer).toHaveBeenCalledWith('video/foo');
expect(shaka.media.TextSourceBuffer).not.toHaveBeenCalled();
expect(shaka.media.TextEngine).not.toHaveBeenCalled();
});
it('creates TextSourceBuffers for text types', function() {
it('creates TextEngines for text types', function() {
mediaSourceEngine.init({'text': 'text/foo'});
expect(mockMediaSource.addSourceBuffer).not.toHaveBeenCalled();
expect(shaka.media.TextSourceBuffer).toHaveBeenCalled();
expect(shaka.media.TextEngine).toHaveBeenCalled();
});
});
@@ -169,7 +169,7 @@ describe('MediaSourceEngine', function() {
});
it('appends the given data', function(done) {
mediaSourceEngine.appendBuffer('audio', 1).then(function() {
mediaSourceEngine.appendBuffer('audio', 1, null, null).then(function() {
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(1);
done();
});
@@ -178,7 +178,7 @@ describe('MediaSourceEngine', function() {
it('rejects the promise if this operation throws', function(done) {
audioSourceBuffer.appendBuffer.and.throwError(new Error());
mediaSourceEngine.appendBuffer('audio', 1).then(function() {
mediaSourceEngine.appendBuffer('audio', 1, null, null).then(function() {
fail('not reached');
done();
}, function() {
@@ -188,7 +188,7 @@ describe('MediaSourceEngine', function() {
});
it('rejects the promise if this operation fails async', function(done) {
mediaSourceEngine.appendBuffer('audio', 1).then(function() {
mediaSourceEngine.appendBuffer('audio', 1, null, null).then(function() {
fail('not reached');
done();
}, function() {
@@ -200,8 +200,8 @@ describe('MediaSourceEngine', function() {
});
it('queues operations on a single SourceBuffer', function(done) {
var p1 = mediaSourceEngine.appendBuffer('audio', 1);
var p2 = mediaSourceEngine.appendBuffer('audio', 2);
var p1 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p2 = mediaSourceEngine.appendBuffer('audio', 2, null, null);
Util.capturePromiseStatus(p1);
Util.capturePromiseStatus(p2);
@@ -225,9 +225,9 @@ describe('MediaSourceEngine', function() {
});
it('queues operations independently for different types', function(done) {
var p1 = mediaSourceEngine.appendBuffer('audio', 1);
var p2 = mediaSourceEngine.appendBuffer('audio', 2);
var p3 = mediaSourceEngine.appendBuffer('video', 3);
var p1 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p2 = mediaSourceEngine.appendBuffer('audio', 2, null, null);
var p3 = mediaSourceEngine.appendBuffer('video', 3, null, null);
Util.capturePromiseStatus(p1);
Util.capturePromiseStatus(p2);
Util.capturePromiseStatus(p3);
@@ -268,9 +268,9 @@ describe('MediaSourceEngine', function() {
}
});
var p1 = mediaSourceEngine.appendBuffer('audio', 1);
var p2 = mediaSourceEngine.appendBuffer('audio', 2);
var p3 = mediaSourceEngine.appendBuffer('audio', 3);
var p1 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p2 = mediaSourceEngine.appendBuffer('audio', 2, null, null);
var p3 = mediaSourceEngine.appendBuffer('audio', 3, null, null);
Util.capturePromiseStatus(p1);
Util.capturePromiseStatus(p2);
Util.capturePromiseStatus(p3);
@@ -467,8 +467,8 @@ describe('MediaSourceEngine', function() {
});
it('waits for all previous operations to complete', function(done) {
var p1 = mediaSourceEngine.appendBuffer('audio', 1);
var p2 = mediaSourceEngine.appendBuffer('video', 1);
var p1 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p2 = mediaSourceEngine.appendBuffer('video', 1, null, null);
var p3 = mediaSourceEngine.endOfStream();
Util.capturePromiseStatus(p1);
Util.capturePromiseStatus(p2);
@@ -498,9 +498,9 @@ describe('MediaSourceEngine', function() {
it('makes subsequent operations wait', function(done) {
var p1 = mediaSourceEngine.endOfStream();
var p2 = mediaSourceEngine.appendBuffer('audio', 1);
var p3 = mediaSourceEngine.appendBuffer('video', 1);
var p4 = mediaSourceEngine.appendBuffer('video', 2);
var p2 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p3 = mediaSourceEngine.appendBuffer('video', 1, null, null);
var p4 = mediaSourceEngine.appendBuffer('video', 2, null, null);
// endOfStream hasn't been called yet because blocking multiple queues
// takes an extra tick, even when they are empty.
@@ -529,7 +529,7 @@ describe('MediaSourceEngine', function() {
it('runs subsequent operations if this operation throws', function(done) {
mockMediaSource.endOfStream.and.throwError(new Error());
var p1 = mediaSourceEngine.endOfStream();
var p2 = mediaSourceEngine.appendBuffer('audio', 1);
var p2 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
@@ -564,8 +564,8 @@ describe('MediaSourceEngine', function() {
});
it('waits for all previous operations to complete', function(done) {
var p1 = mediaSourceEngine.appendBuffer('audio', 1);
var p2 = mediaSourceEngine.appendBuffer('video', 1);
var p1 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p2 = mediaSourceEngine.appendBuffer('video', 1, null, null);
var p3 = mediaSourceEngine.setDuration(100);
Util.capturePromiseStatus(p1);
Util.capturePromiseStatus(p2);
@@ -595,9 +595,9 @@ describe('MediaSourceEngine', function() {
it('makes subsequent operations wait', function(done) {
var p1 = mediaSourceEngine.setDuration(100);
var p2 = mediaSourceEngine.appendBuffer('audio', 1);
var p3 = mediaSourceEngine.appendBuffer('video', 1);
var p4 = mediaSourceEngine.appendBuffer('video', 2);
var p2 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p3 = mediaSourceEngine.appendBuffer('video', 1, null, null);
var p4 = mediaSourceEngine.appendBuffer('video', 2, null, null);
// The setter hasn't been called yet because blocking multiple queues
// takes an extra tick, even when they are empty.
@@ -626,7 +626,7 @@ describe('MediaSourceEngine', function() {
it('runs subsequent operations if this operation throws', function(done) {
mockMediaSource.durationSetter_.and.throwError(new Error());
var p1 = mediaSourceEngine.setDuration(100);
var p2 = mediaSourceEngine.appendBuffer('audio', 1);
var p2 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
@@ -653,8 +653,8 @@ describe('MediaSourceEngine', function() {
});
it('waits for all operations to complete', function(done) {
mediaSourceEngine.appendBuffer('audio', 1);
mediaSourceEngine.appendBuffer('video', 1);
mediaSourceEngine.appendBuffer('audio', 1, null, null);
mediaSourceEngine.appendBuffer('video', 1, null, null);
var p = mediaSourceEngine.destroy();
Util.capturePromiseStatus(p);
@@ -675,7 +675,7 @@ describe('MediaSourceEngine', function() {
});
it('resolves even when a pending operation fails', function(done) {
var p1 = mediaSourceEngine.appendBuffer('audio', 1);
var p1 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p2 = mediaSourceEngine.destroy();
Util.capturePromiseStatus(p1);
Util.capturePromiseStatus(p2);
@@ -707,8 +707,8 @@ describe('MediaSourceEngine', function() {
});
it('cancels operations that have not yet started', function(done) {
mediaSourceEngine.appendBuffer('audio', 1);
var rejected = mediaSourceEngine.appendBuffer('audio', 2);
mediaSourceEngine.appendBuffer('audio', 1, null, null);
var rejected = mediaSourceEngine.appendBuffer('audio', 2, null, null);
Util.capturePromiseStatus(rejected);
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(1);
@@ -733,7 +733,7 @@ describe('MediaSourceEngine', function() {
});
it('cancels blocking operations that have not yet started', function(done) {
var p1 = mediaSourceEngine.appendBuffer('audio', 1);
var p1 = mediaSourceEngine.appendBuffer('audio', 1, null, null);
var p2 = mediaSourceEngine.endOfStream();
var p3 = mediaSourceEngine.destroy();
Util.capturePromiseStatus(p1);
@@ -756,7 +756,7 @@ describe('MediaSourceEngine', function() {
it('prevents new operations from being added', function(done) {
var p = mediaSourceEngine.destroy();
var rejected = mediaSourceEngine.appendBuffer('audio', 1);
var rejected = mediaSourceEngine.appendBuffer('audio', 1, null, null);
Util.capturePromiseStatus(rejected);
// The promise has already been rejected, but our capture requires 1 tick.
@@ -804,8 +804,8 @@ describe('MediaSourceEngine', function() {
};
}
function createMockTextSourceBufferCtor() {
var ctor = jasmine.createSpy('TextSourceBuffer');
function createMockTextEngineCtor() {
var ctor = jasmine.createSpy('TextEngine');
ctor.isTypeSupported = function() { return true; };
ctor.prototype.addEventListener = function() {};
ctor.prototype.removeEventListener = function() {};
+310
View File
@@ -0,0 +1,310 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('TextEngine', function() {
var TextEngine;
var dummyData = new ArrayBuffer(0);
var dummyMimeType = 'text/fake';
var mockParser;
var mockTrack;
var textEngine;
beforeAll(function() {
TextEngine = shaka.media.TextEngine;
});
beforeEach(function() {
mockParser = jasmine.createSpy('mockParser');
mockTrack = createMockTrack();
TextEngine.registerParser(dummyMimeType, mockParser);
textEngine = new TextEngine(mockTrack, dummyMimeType);
});
afterEach(function() {
textEngine = null;
TextEngine.unregisterParser(dummyMimeType);
mockTrack = null;
mockParser = null;
});
describe('isTypeSupported', function() {
it('reports support only when a parser is installed', function() {
TextEngine.unregisterParser(dummyMimeType);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
TextEngine.registerParser(dummyMimeType, mockParser);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(true);
TextEngine.unregisterParser(dummyMimeType);
expect(TextEngine.isTypeSupported(dummyMimeType)).toBe(false);
});
});
describe('appendBuffer', function() {
it('works asynchronously', function(done) {
mockParser.and.returnValue([1, 2, 3]);
textEngine.appendBuffer(dummyData, 0, 3).catch(fail).then(done);
expect(mockTrack.addCue).not.toHaveBeenCalled();
});
it('adds cues to the track', function(done) {
mockParser.and.returnValue([1, 2, 3]);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(mockParser).toHaveBeenCalledWith(dummyData);
expect(mockTrack.addCue).toHaveBeenCalledWith(1);
expect(mockTrack.addCue).toHaveBeenCalledWith(2);
expect(mockTrack.addCue).toHaveBeenCalledWith(3);
expect(mockTrack.removeCue).not.toHaveBeenCalled();
mockTrack.addCue.calls.reset();
mockParser.calls.reset();
mockParser.and.returnValue([4, 5]);
return textEngine.appendBuffer(dummyData, 3, 5);
}).then(function() {
expect(mockParser).toHaveBeenCalledWith(dummyData);
expect(mockTrack.addCue).toHaveBeenCalledWith(4);
expect(mockTrack.addCue).toHaveBeenCalledWith(5);
}).catch(fail).then(done);
});
});
describe('remove', function() {
var cue1;
var cue2;
var cue3;
beforeEach(function() {
cue1 = createFakeCue(0, 1);
cue2 = createFakeCue(1, 2);
cue3 = createFakeCue(2, 3);
mockParser.and.returnValue([cue1, cue2, cue3]);
});
it('works asynchronously', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
var p = textEngine.remove(0, 1);
expect(mockTrack.removeCue).not.toHaveBeenCalled();
return p;
}).catch(fail).then(done);
});
it('removes cues which overlap the range', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
return textEngine.remove(0, 1);
}).then(function() {
expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue1]]);
mockTrack.removeCue.calls.reset();
return textEngine.remove(0.5, 1.001);
}).then(function() {
expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue2]]);
mockTrack.removeCue.calls.reset();
return textEngine.remove(3, 5);
}).then(function() {
expect(mockTrack.removeCue).not.toHaveBeenCalled();
mockTrack.removeCue.calls.reset();
return textEngine.remove(2.9999, Number.POSITIVE_INFINITY);
}).then(function() {
expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue3]]);
}).catch(fail).then(done);
});
it('does nothing when nothing is buffered', function(done) {
textEngine.remove(0, 1).then(function() {
expect(mockTrack.removeCue).not.toHaveBeenCalled();
}).catch(fail).then(done);
});
});
describe('setTimestampOffset', function() {
it('affects the timestamps of parsed cues', function(done) {
mockParser.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(2, 3)];
});
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(0, 1));
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(2, 3));
mockTrack.addCue.calls.reset();
textEngine.setTimestampOffset(4);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(4, 5));
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(6, 7));
}).catch(fail).then(done);
});
});
describe('bufferStart/bufferEnd', function() {
beforeEach(function() {
mockParser.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('return null when there are no cues', function() {
expect(textEngine.bufferStart()).toBe(null);
expect(textEngine.bufferEnd()).toBe(null);
});
it('reflect newly-added cues', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(3);
textEngine.setTimestampOffset(3);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(6);
textEngine.setTimestampOffset(7);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(10);
}).catch(fail).then(done);
});
it('reflect newly-removed cues', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
textEngine.setTimestampOffset(3);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
textEngine.setTimestampOffset(7);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(0);
expect(textEngine.bufferEnd()).toBe(10);
return textEngine.remove(0, 3);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(10);
return textEngine.remove(8, 11);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(8);
return textEngine.remove(11, 20);
}).then(function() {
expect(textEngine.bufferStart()).toBe(3);
expect(textEngine.bufferEnd()).toBe(8);
return textEngine.remove(0, Number.POSITIVE_INFINITY);
}).then(function() {
expect(textEngine.bufferStart()).toBe(null);
expect(textEngine.bufferEnd()).toBe(null);
}).catch(fail).then(done);
});
});
describe('bufferedAheadOf', function() {
beforeEach(function() {
mockParser.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('returns 0 when there are no cues', function() {
expect(textEngine.bufferedAheadOf(0)).toBe(0);
});
it('returns 0 if |t| is not buffered', function(done) {
textEngine.setTimestampOffset(3);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferedAheadOf(2.9)).toBe(0);
expect(textEngine.bufferedAheadOf(6.1)).toBe(0);
}).catch(fail).then(done);
});
it('returns the distance to the end if |t| is buffered', function(done) {
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferedAheadOf(0)).toBe(3);
expect(textEngine.bufferedAheadOf(1)).toBe(2);
expect(textEngine.bufferedAheadOf(2.5)).toBeCloseTo(0.5);
}).catch(fail).then(done);
});
});
describe('setAppendWindowEnd', function() {
beforeEach(function() {
mockParser.and.callFake(function() {
return [createFakeCue(0, 1), createFakeCue(1, 2), createFakeCue(2, 3)];
});
});
it('limits appended cues', function(done) {
textEngine.setAppendWindowEnd(1.9);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(0, 1));
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(1, 2));
mockTrack.addCue.calls.reset();
textEngine.setAppendWindowEnd(2.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(0, 1));
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(1, 2));
expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(2, 3));
}).catch(fail).then(done);
});
it('limits bufferEnd', function(done) {
textEngine.setAppendWindowEnd(1.9);
textEngine.appendBuffer(dummyData, 0, 3).then(function() {
expect(textEngine.bufferEnd()).toBe(1.9);
textEngine.setAppendWindowEnd(2.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferEnd()).toBe(2.1);
textEngine.setAppendWindowEnd(4.1);
return textEngine.appendBuffer(dummyData, 0, 3);
}).then(function() {
expect(textEngine.bufferEnd()).toBe(3);
}).catch(fail).then(done);
});
});
function createMockTrack() {
var track = {
addCue: jasmine.createSpy('addCue'),
removeCue: jasmine.createSpy('removeCue'),
cues: []
};
track.addCue.and.callFake(function(cue) {
track.cues.push(cue);
});
track.removeCue.and.callFake(function(cue) {
var idx = track.cues.indexOf(cue);
shaka.asserts.assert(idx >= 0, 'cue does not exist');
track.cues.splice(idx, 1);
});
return track;
}
function createFakeCue(startTime, endTime) {
return { startTime: startTime, endTime: endTime };
}
});
-301
View File
@@ -1,301 +0,0 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('TextSourceBuffer', function() {
var TextSourceBuffer;
var dummyData = new ArrayBuffer(0);
var dummyMimeType = 'text/fake';
beforeAll(function() {
// Can't set this alias at load-time because the sources may not be loaded
// yet.
TextSourceBuffer = shaka.media.TextSourceBuffer;
});
describe('isTypeSupported', function() {
it('reports support only when a parser is installed', function() {
expect(TextSourceBuffer.isTypeSupported(dummyMimeType)).toBe(false);
TextSourceBuffer.registerParser(dummyMimeType, function() {});
expect(TextSourceBuffer.isTypeSupported(dummyMimeType)).toBe(true);
TextSourceBuffer.unregisterParser(dummyMimeType);
expect(TextSourceBuffer.isTypeSupported(dummyMimeType)).toBe(false);
});
});
describe('appendBuffer', function() {
var mockParser;
var mockTrack;
var sourceBuffer;
var eventManager;
var onUpdateEnd;
beforeEach(function() {
mockParser = jasmine.createSpy('mockParser');
mockTrack = createMockTrack();
TextSourceBuffer.registerParser(dummyMimeType, mockParser);
sourceBuffer = new TextSourceBuffer(mockTrack, dummyMimeType);
onUpdateEnd = jasmine.createSpy('onUpdateEnd');
eventManager = new shaka.util.EventManager();
eventManager.listen(sourceBuffer, 'updateend', onUpdateEnd);
});
afterEach(function() {
sourceBuffer = null;
TextSourceBuffer.unregisterParser(dummyMimeType);
mockTrack = null;
mockParser = null;
eventManager.destroy();
eventManager = null;
});
it('works asynchronously', function() {
mockParser.and.returnValue([1, 2, 3]);
sourceBuffer.appendBuffer(dummyData);
expect(mockTrack.addCue).not.toHaveBeenCalled();
});
it('dispatches an "updateend" event', function(done) {
mockParser.and.returnValue([]);
expect(sourceBuffer.updating).toBe(false);
sourceBuffer.appendBuffer(dummyData);
expect(sourceBuffer.updating).toBe(true);
eventToPromise(onUpdateEnd).then(function() {
expect(sourceBuffer.updating).toBe(false);
done();
});
});
it('adds cues to the track', function(done) {
expect(mockParser).not.toHaveBeenCalled();
expect(mockTrack.addCue).not.toHaveBeenCalled();
expect(mockTrack.removeCue).not.toHaveBeenCalled();
mockParser.and.returnValue([1, 2, 3]);
sourceBuffer.appendBuffer(dummyData);
eventToPromise(onUpdateEnd).then(function() {
expect(mockParser).toHaveBeenCalledWith(dummyData);
expect(mockTrack.addCue).toHaveBeenCalledWith(1);
expect(mockTrack.addCue).toHaveBeenCalledWith(2);
expect(mockTrack.addCue).toHaveBeenCalledWith(3);
expect(mockTrack.removeCue).not.toHaveBeenCalled();
mockTrack.addCue.calls.reset();
mockParser.calls.reset();
mockParser.and.returnValue([4, 5]);
sourceBuffer.appendBuffer(dummyData);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(mockParser).toHaveBeenCalledWith(dummyData);
expect(mockTrack.addCue).toHaveBeenCalledWith(4);
expect(mockTrack.addCue).toHaveBeenCalledWith(5);
done();
});
});
});
describe('remove', function() {
var mockTrack;
var sourceBuffer;
var eventManager;
var onUpdateEnd;
beforeEach(function() {
mockTrack = createMockTrack();
TextSourceBuffer.registerParser(dummyMimeType, function() {});
sourceBuffer = new TextSourceBuffer(mockTrack, dummyMimeType);
onUpdateEnd = jasmine.createSpy('onUpdateEnd');
eventManager = new shaka.util.EventManager();
eventManager.listen(sourceBuffer, 'updateend', onUpdateEnd);
});
afterEach(function() {
sourceBuffer = null;
TextSourceBuffer.unregisterParser(dummyMimeType);
mockTrack = null;
eventManager.destroy();
eventManager = null;
});
it('works asynchronously', function() {
var cue1 = createFakeCue(0, 1);
var cue2 = createFakeCue(1, 2);
var cue3 = createFakeCue(2, 3);
mockTrack.cues = [cue1, cue2, cue3];
sourceBuffer.remove(0, 1);
expect(mockTrack.removeCue).not.toHaveBeenCalled();
});
it('dispatches an "updateend" event', function(done) {
expect(sourceBuffer.updating).toBe(false);
sourceBuffer.remove(0, 1);
expect(sourceBuffer.updating).toBe(true);
eventToPromise(onUpdateEnd).then(function() {
expect(sourceBuffer.updating).toBe(false);
done();
});
});
it('removes cues which overlap the range', function(done) {
var cue1 = createFakeCue(0, 1);
var cue2 = createFakeCue(1, 2);
var cue3 = createFakeCue(2, 3);
mockTrack.cues = [cue1, cue2, cue3];
sourceBuffer.remove(0, 1);
eventToPromise(onUpdateEnd).then(function() {
expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue1], [cue2]]);
mockTrack.removeCue.calls.reset();
sourceBuffer.remove(0.5, 0.9);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue1]]);
mockTrack.removeCue.calls.reset();
sourceBuffer.remove(3.00001, 5);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(mockTrack.removeCue).not.toHaveBeenCalled();
mockTrack.removeCue.calls.reset();
sourceBuffer.remove(0.5, 1.5);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue1], [cue2]]);
mockTrack.removeCue.calls.reset();
sourceBuffer.remove(3, Number.POSITIVE_INFINITY);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue3]]);
done();
});
});
});
describe('buffered', function() {
var mockTrack;
var sourceBuffer;
var eventManager;
var onUpdateEnd;
beforeEach(function() {
var fakeParser = function() { return []; };
mockTrack = createMockTrack();
TextSourceBuffer.registerParser(dummyMimeType, fakeParser);
sourceBuffer = new TextSourceBuffer(mockTrack, dummyMimeType);
onUpdateEnd = jasmine.createSpy('onUpdateEnd');
eventManager = new shaka.util.EventManager();
eventManager.listen(sourceBuffer, 'updateend', onUpdateEnd);
});
afterEach(function() {
sourceBuffer = null;
TextSourceBuffer.unregisterParser(dummyMimeType);
mockTrack = null;
eventManager.destroy();
eventManager = null;
});
it('reflects newly-added cues', function(done) {
expect(sourceBuffer.buffered.length).toBe(0);
var cue1 = createFakeCue(0, 1);
var cue2 = createFakeCue(1, 2);
mockTrack.cues = [cue1, cue2];
// call appendBuffer to trigger the update of buffered.
sourceBuffer.appendBuffer(dummyData);
eventToPromise(onUpdateEnd).then(function() {
expect(sourceBuffer.buffered.length).toBe(1);
expect(sourceBuffer.buffered.start(0)).toBe(0);
expect(sourceBuffer.buffered.end(0)).toBe(2);
var cue3 = createFakeCue(2, 3);
mockTrack.cues = [cue1, cue2, cue3];
sourceBuffer.appendBuffer(dummyData);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(sourceBuffer.buffered.length).toBe(1);
expect(sourceBuffer.buffered.start(0)).toBe(0);
expect(sourceBuffer.buffered.end(0)).toBe(3);
done();
});
});
it('reflects newly-removed cues', function(done) {
var cue1 = createFakeCue(0, 1);
var cue2 = createFakeCue(1, 2);
var cue3 = createFakeCue(2, 3);
mockTrack.cues = [cue1, cue2, cue3];
// call appendBuffer to trigger the update of buffered.
sourceBuffer.appendBuffer(dummyData);
eventToPromise(onUpdateEnd).then(function() {
expect(sourceBuffer.buffered.length).toBe(1);
expect(sourceBuffer.buffered.start(0)).toBe(0);
expect(sourceBuffer.buffered.end(0)).toBe(3);
mockTrack.cues = [cue1, cue2];
// call remove to trigger the update of buffered.
sourceBuffer.remove(-1, -1);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(sourceBuffer.buffered.length).toBe(1);
expect(sourceBuffer.buffered.start(0)).toBe(0);
expect(sourceBuffer.buffered.end(0)).toBe(2);
mockTrack.cues = [];
// call remove to trigger the update of buffered.
sourceBuffer.remove(-1, -1);
return eventToPromise(onUpdateEnd);
}).then(function() {
expect(sourceBuffer.buffered.length).toBe(0);
done();
});
});
});
function eventToPromise(eventSpy) {
return new Promise(function(resolve) {
eventSpy.and.callFake(resolve);
});
}
function createMockTrack() {
return {
addCue: jasmine.createSpy('addCue'),
removeCue: jasmine.createSpy('removeCue'),
cues: []
};
}
function createFakeCue(startTime, endTime) {
return { startTime: startTime, endTime: endTime };
}
});