From cd2d25cbb2e2c6845833341d5cdbe8a372c5c1cb Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Fri, 12 Feb 2016 13:36:40 -0800 Subject: [PATCH] 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 --- build/types/core | 2 +- docs/design/REDESIGN.md | 17 +- docs/design/dataflow.gv | 6 +- docs/design/ownership.gv | 6 +- lib/media/media_source_engine.js | 70 ++++-- lib/media/streaming_engine.js | 6 +- lib/media/text_engine.js | 247 +++++++++++++++++++ lib/media/text_source_buffer.js | 203 ---------------- lib/media/vtt_text_parser.js | 7 +- shaka-player.uncompiled.js | 2 +- test/media_source_engine_integration.js | 5 +- test/media_source_engine_unit.js | 78 +++--- test/text_engine_unit.js | 310 ++++++++++++++++++++++++ test/text_source_buffer_unit.js | 301 ----------------------- 14 files changed, 671 insertions(+), 589 deletions(-) create mode 100644 lib/media/text_engine.js delete mode 100644 lib/media/text_source_buffer.js create mode 100644 test/text_engine_unit.js delete mode 100644 test/text_source_buffer_unit.js diff --git a/build/types/core b/build/types/core index 6bd3d1f77..fbcdee688 100644 --- a/build/types/core +++ b/build/types/core @@ -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 diff --git a/docs/design/REDESIGN.md b/docs/design/REDESIGN.md index 3e536ae90..ce15ea95e 100644 --- a/docs/design/REDESIGN.md +++ b/docs/design/REDESIGN.md @@ -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) diff --git a/docs/design/dataflow.gv b/docs/design/dataflow.gv index 888204964..453904f15 100644 --- a/docs/design/dataflow.gv +++ b/docs/design/dataflow.gv @@ -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 ] diff --git a/docs/design/ownership.gv b/docs/design/ownership.gv index f8bd2991a..2d38847e9 100644 --- a/docs/design/ownership.gv +++ b/docs/design/ownership.gv @@ -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 diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index ca78da875..eb06dff0b 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -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.} */ + /** @private {!Object.} */ this.sourceBuffers_ = {}; + /** @private {shaka.media.TextEngine} */ + this.textEngine_ = null; + /** * @private {!Object.>} @@ -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)); diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index adb93ca17..2b306a974 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -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'); diff --git a/lib/media/text_engine.js b/lib/media/text_engine.js new file mode 100644 index 000000000..e5dbbb93b --- /dev/null +++ b/lib/media/text_engine.js @@ -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.} + * @exportDoc + */ +shaka.media.TextEngine.TextParser; + + +/** @private {!Object.} */ +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; +}; diff --git a/lib/media/text_source_buffer.js b/lib/media/text_source_buffer.js deleted file mode 100644 index 20d5c98c7..000000000 --- a/lib/media/text_source_buffer.js +++ /dev/null @@ -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.} - * @exportDoc - */ -shaka.media.TextSourceBuffer.TextParser; - - -/** @private {!Object.} */ -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; -}; diff --git a/lib/media/vtt_text_parser.js b/lib/media/vtt_text_parser.js index 64736e03e..49de0ef3e 100644 --- a/lib/media/vtt_text_parser.js +++ b/lib/media/vtt_text_parser.js @@ -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.} @@ -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); diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index c838e281d..3c00a1b9b 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -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'); diff --git a/test/media_source_engine_integration.js b/test/media_source_engine_integration.js index f2dc55b1f..002455845 100644 --- a/test/media_source_engine_integration.js +++ b/test/media_source_engine_integration.js @@ -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) { diff --git a/test/media_source_engine_unit.js b/test/media_source_engine_unit.js index 51a071484..bac037120 100644 --- a/test/media_source_engine_unit.js +++ b/test/media_source_engine_unit.js @@ -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() {}; diff --git a/test/text_engine_unit.js b/test/text_engine_unit.js new file mode 100644 index 000000000..1b364ee34 --- /dev/null +++ b/test/text_engine_unit.js @@ -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 }; + } +}); diff --git a/test/text_source_buffer_unit.js b/test/text_source_buffer_unit.js deleted file mode 100644 index 0df1612e2..000000000 --- a/test/text_source_buffer_unit.js +++ /dev/null @@ -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 }; - } -});