From c70367dc97bdc460cd11c9c19fc2fc702b9be7cc Mon Sep 17 00:00:00 2001 From: Sandra Lokshina Date: Mon, 5 Jun 2017 14:56:16 -0700 Subject: [PATCH] Separate text parsing and display logic. Closes #796. Closes #923. Change-Id: Ifc2017b40a0fb570103f0fed7bc130aa24819e9f --- build/types/text | 4 +- externs/shaka/player.js | 5 +- externs/shaka/text.js | 65 +++++- lib/media/media_source_engine.js | 12 +- lib/player.js | 58 ++--- lib/text/cue.js | 241 +++++++++++++++++++++ lib/text/mp4_vtt_parser.js | 13 +- lib/text/simple_text_displayer.js | 225 +++++++++++++++++++ lib/text/text_engine.js | 143 ++++-------- lib/text/ttml_text_parser.js | 90 +++++--- lib/text/vtt_text_parser.js | 124 ++++++++--- shaka-player.uncompiled.js | 2 + test/media/media_source_engine_unit.js | 3 +- test/player_integration.js | 2 +- test/player_unit.js | 12 +- test/test/util/fake_media_source_engine.js | 1 - test/test/util/simple_fakes.js | 84 ++++++- test/text/cue_integration.js | 13 +- test/text/mp4_vtt_parser_unit.js | 58 +++-- test/text/simple_text_displayer_unit.js | 240 ++++++++++++++++++++ test/text/text_engine_unit.js | 140 +++++------- test/text/ttml_text_parser_unit.js | 239 ++++++++++++++------ test/text/vtt_text_parser_unit.js | 123 +++++------ 23 files changed, 1425 insertions(+), 472 deletions(-) create mode 100644 lib/text/cue.js create mode 100644 lib/text/simple_text_displayer.js create mode 100644 test/text/simple_text_displayer_unit.js diff --git a/build/types/text b/build/types/text index af8461bad..f97ac5f73 100644 --- a/build/types/text +++ b/build/types/text @@ -1,6 +1,8 @@ -# All standard text parsing plugins. +# All files related to text parsing and displaying. ++../../lib/text/cue.js +../../lib/text/mp4_ttml_parser.js +../../lib/text/mp4_vtt_parser.js ++../../lib/text/simple_text_displayer.js +../../lib/text/ttml_text_parser.js +../../lib/text/vtt_text_parser.js diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 07efd851b..680c187a7 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -606,7 +606,8 @@ shakaExtern.AbrConfiguration; * preferredTextLanguage: string, * restrictions: shakaExtern.Restrictions, * playRangeStart: number, - * playRangeEnd: number + * playRangeEnd: number, + * textDisplayFactory: shakaExtern.TextDisplayer.Factory * }} * * @property {shakaExtern.DrmConfiguration} drm @@ -637,6 +638,8 @@ shakaExtern.AbrConfiguration; * @property {number} playRangeEnd * Optional playback and seek end time in seconds. Defaults to the end of * the presentation if not provided. + * @property {shakaExtern.TextDisplayer.Factory} textDisplayFactory + * A factory to construct text displayer. * @exportDoc */ shakaExtern.PlayerConfiguration; diff --git a/externs/shaka/text.js b/externs/shaka/text.js index c88fe2ac4..7e746e1ba 100644 --- a/externs/shaka/text.js +++ b/externs/shaka/text.js @@ -70,7 +70,7 @@ shakaExtern.TextParser.prototype.parseInit = function(data) {}; * @param {shakaExtern.TextParser.TimeContext} timeContext * The time information that should be used to adjust the times values * for each cue. - * @return {!Array.} + * @return {!Array.} * * @exportDoc */ @@ -81,3 +81,66 @@ shakaExtern.TextParser.prototype.parseMedia = function(data, timeContext) {}; * @typedef {function(new:shakaExtern.TextParser)} */ shakaExtern.TextParserPlugin; + + + +/** + * An interface for plugins that display text. + * + * @interface + * @extends {shaka.util.IDestroyable} + * @exportDoc + */ +shakaExtern.TextDisplayer = function() {}; + + +/** + * Append given text cues to the list of cues to be displayed. + * + * @param {!Array.} cues + * Text cues to be appended. + * + * @exportDoc + */ +shakaExtern.TextDisplayer.prototype.append = function(cues) {}; + + +/** + * Remove cues in a given time range. + * + * @param {number} start + * @param {number} end + * @return {boolean} + * + * @exportDoc + */ +shakaExtern.TextDisplayer.prototype.remove = function(start, end) {}; + + +/** + * Returns true if text is currently visible. + * + * @return {boolean} + * + * @exportDoc + */ +shakaExtern.TextDisplayer.prototype.isTextVisible = function() {}; + + +/** + * Set text visibility. + * + * @param {boolean} on + * + * @exportDoc + */ +shakaExtern.TextDisplayer.prototype.setTextVisibility = function(on) {}; + + +/** + * A factory for creating a TextDisplayer. + * + * @typedef {function(new:shakaExtern.TextDisplayer)} + * @exportDoc + */ +shakaExtern.TextDisplayer.Factory; diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 3bb82b0c5..228907ef9 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -40,13 +40,13 @@ goog.require('shaka.util.PublicPromise'); * when MediaSource operations fail. * @param {MediaSource} mediaSource The MediaSource, which must be in the * 'open' state. - * @param {TextTrack} textTrack The TextTrack to use for subtitles/captions. + * @param {shakaExtern.TextDisplayer} textDisplayer * * @struct * @constructor * @implements {shaka.util.IDestroyable} */ -shaka.media.MediaSourceEngine = function(video, mediaSource, textTrack) { +shaka.media.MediaSourceEngine = function(video, mediaSource, textDisplayer) { goog.asserts.assert(mediaSource.readyState == 'open', 'The MediaSource should be in the \'open\' state.'); @@ -56,8 +56,8 @@ shaka.media.MediaSourceEngine = function(video, mediaSource, textTrack) { /** @private {MediaSource} */ this.mediaSource_ = mediaSource; - /** @private {TextTrack} */ - this.textTrack_ = textTrack; + /** @private {shakaExtern.TextDisplayer} */ + this.textDisplayer_ = textDisplayer; /** @private {!Object.} */ @@ -204,8 +204,8 @@ shaka.media.MediaSourceEngine.prototype.destroy = function() { this.eventManager_ = null; this.video_ = null; this.mediaSource_ = null; - this.textTrack_ = null; this.textEngine_ = null; + this.textDisplayer_ = null; this.sourceBuffers_ = {}; if (!COMPILED) { for (var contentType in this.queues_) { @@ -265,7 +265,7 @@ shaka.media.MediaSourceEngine.prototype.init = function(typeConfig) { */ shaka.media.MediaSourceEngine.prototype.reinitText = function(mimeType) { if (!this.textEngine_) { - this.textEngine_ = new shaka.text.TextEngine(this.textTrack_); + this.textEngine_ = new shaka.text.TextEngine(this.textDisplayer_); } this.textEngine_.initParser(mimeType); }; diff --git a/lib/player.js b/lib/player.js index f8694e67c..79e6a2233 100644 --- a/lib/player.js +++ b/lib/player.js @@ -28,6 +28,7 @@ goog.require('shaka.media.PlayheadObserver'); goog.require('shaka.media.SegmentReference'); goog.require('shaka.media.StreamingEngine'); goog.require('shaka.net.NetworkingEngine'); +goog.require('shaka.text.SimpleTextDisplayer'); goog.require('shaka.util.CancelableChain'); goog.require('shaka.util.ConfigUtils'); goog.require('shaka.util.Error'); @@ -67,8 +68,8 @@ shaka.Player = function(video, opt_dependencyInjector) { /** @private {HTMLMediaElement} */ this.video_ = video; - /** @private {TextTrack} */ - this.textTrack_ = null; + /** @private {shakaExtern.TextDisplayer} */ + this.textDisplayer_ = null; /** @private {shaka.util.EventManager} */ this.eventManager_ = new shaka.util.EventManager(); @@ -207,16 +208,16 @@ shaka.Player.prototype.destroy = function() { this.unloadChain_, this.destroyStreaming_(), this.eventManager_ ? this.eventManager_.destroy() : null, - this.networkingEngine_ ? this.networkingEngine_.destroy() : null + this.networkingEngine_ ? this.networkingEngine_.destroy() : null, + this.textDisplayer_ ? this.textDisplayer_.destroy() : null ]); this.video_ = null; - this.textTrack_ = null; + this.textDisplayer_ = null; this.eventManager_ = null; this.abrManager_ = null; this.networkingEngine_ = null; this.config_ = null; - return p; }.bind(this)); }; @@ -494,6 +495,8 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime, this.abrManager_ = new abrManagerFactory(); this.abrManager_.configure(this.config_.abr); + this.textDisplayer_ = new this.config_.textDisplayFactory(); + goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed'); return shaka.media.ManifestParser.getFactory( manifestUri, @@ -561,7 +564,6 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime, this.drmEngine_.configure(this.config_.drm); return this.drmEngine_.init(manifest, false /* isOffline */); }.bind(this)).then(function() { - // Re-filter the manifest after DRM has been initialized. this.manifest_.periods.forEach(this.filterPeriod_.bind(this)); @@ -604,7 +606,6 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime, ]); }.bind(this)).then(function() { this.abrManager_.init(this.switch_.bind(this)); - // MediaSource is open, so create the Playhead, MediaSourceEngine, and // StreamingEngine. var startTime = opt_startTime || this.config_.playRangeStart; @@ -618,7 +619,6 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime, // If the content is multi-codec and the browser can play more than one of // them, choose codecs now before we initialize streaming. this.chooseCodecsAndFilterManifest_(); - return this.streamingEngine_.init(); }.bind(this)).then(function() { if (this.config_.streaming.startAtSegmentBoundary) { @@ -821,7 +821,7 @@ shaka.Player.prototype.createMediaSource = function() { */ shaka.Player.prototype.createMediaSourceEngine = function() { return new shaka.media.MediaSourceEngine( - this.video_, this.mediaSource_, this.textTrack_); + this.video_, this.mediaSource_, this.textDisplayer_); }; @@ -1432,7 +1432,7 @@ shaka.Player.prototype.selectTextLanguage = function(language, opt_role) { * @export */ shaka.Player.prototype.isTextTrackVisible = function() { - return this.textTrack_.mode == 'showing'; + return this.textDisplayer_.isTextVisible(); }; @@ -1443,7 +1443,7 @@ shaka.Player.prototype.isTextTrackVisible = function() { * @export */ shaka.Player.prototype.setTextTrackVisibility = function(on) { - this.textTrack_.mode = on ? 'showing' : 'hidden'; + this.textDisplayer_.setTextVisibility(on); this.onTextTrackVisibility_(); }; @@ -1662,30 +1662,6 @@ shaka.Player.prototype.initialize_ = function() { // Start the (potentially slow) process of opening MediaSource now. this.mediaSourceOpen_ = this.createMediaSource(); - // If the video element has TextTracks, disable them. If we see one that - // was created by a previous instance of Shaka Player, reuse it. - for (var i = 0; i < this.video_.textTracks.length; ++i) { - var track = this.video_.textTracks[i]; - track.mode = 'disabled'; - - if (track.label == shaka.Player.TextTrackLabel_) { - this.textTrack_ = track; - } - } - - if (!this.textTrack_) { - // As far as I can tell, there is no observable difference between setting - // kind to 'subtitles' or 'captions' when creating the TextTrack object. - // The individual text tracks from the manifest will still have their own - // kinds which can be displayed in the app's UI. - this.textTrack_ = this.video_.addTextTrack( - 'subtitles', shaka.Player.TextTrackLabel_); - } - this.textTrack_.mode = 'hidden'; - - // TODO: test that in all cases, the built-in CC controls in the video element - // are toggling our TextTrack. - // Listen for video errors. this.eventManager_.listen(this.video_, 'error', this.onVideoError_.bind(this)); @@ -1831,13 +1807,6 @@ shaka.Player.prototype.resetStreaming_ = function() { }; -/** - * @const {string} - * @private - */ -shaka.Player.TextTrackLabel_ = 'Shaka Player TextTrack'; - - /** * @return {!Object} * @private @@ -1904,6 +1873,9 @@ shaka.Player.prototype.defaultConfig_ = function() { jumpLargeGaps: false }, abrFactory: shaka.abr.SimpleAbrManager, + textDisplayFactory: function(videoElement) { + return new shaka.text.SimpleTextDisplayer(videoElement); + }.bind(null, this.video_), abr: { enabled: true, // This is a relatively safe default, since 3G cell connections @@ -2355,7 +2327,7 @@ shaka.Player.prototype.chooseStreamsAndSwitch_ = function(period) { languageMatches[ContentType.TEXT] && chosen[ContentType.TEXT].language != chosen[ContentType.AUDIO].language) { - this.textTrack_.mode = 'showing'; + this.textDisplayer_.setTextVisibility(true); this.onTextTrackVisibility_(); } } diff --git a/lib/text/cue.js b/lib/text/cue.js new file mode 100644 index 000000000..c88d41842 --- /dev/null +++ b/lib/text/cue.js @@ -0,0 +1,241 @@ +/** + * @license + * Copyright 2016 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.text.Cue'); + + + +/** + * Creates a Cue object. + * + * @param {number} startTime + * @param {number} endTime + * @param {!string} payload + * + * @constructor + * @struct + * @export + */ +shaka.text.Cue = function(startTime, endTime, payload) { + var Cue = shaka.text.Cue; + + /** + * The start time of the cue in seconds and fractions of a second. + * @type {number} + */ + this.startTime = startTime; + + /** + * The end time of the cue in seconds and fractions of a second. + * @type {number} + */ + this.endTime = endTime; + + /** + * The text payload of the cue. + * @type {!string} + */ + this.payload = payload; + + /** + * The indent (in percent) of the cue box in the direction defined by the + * writing direction. + * @type {?number} + */ + this.position = null; + + /** + * Position alignment of the cue. + * @type {shaka.text.Cue.positionAlign} + */ + this.positionAlign = Cue.positionAlign.AUTO; + + /** + * Size of the cue box (in percents). + * @type {number} + */ + this.size = 100; + + /** + * Alignment of the text inside the cue box. + * @type {shaka.text.Cue.textAlign} + */ + this.textAlign = Cue.textAlign.CENTER; + + /** + * Text writing direction of the cue. + * (Vertical growing left, vertical growing right or horizontal). + * NOTE: Horizontal right-to-left text is handled by setting + * cue.textAlign to 'end'. + * @type {shaka.text.Cue.writingDirection} + */ + this.writingDirection = Cue.writingDirection.HORIZONTAL; + + /** + * The way to interpret line field. (Either as an integer line number or + * percentage from the display box). + * @type {shaka.text.Cue.lineInterpretation} + */ + this.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER; + + /** + * The offset from the display box in either number of lines or + * percentage depending on the value of lineInterpretation. + * @type {?number} + */ + this.line = null; + + /** + * Line alignment of the cue box. + * @type {shaka.text.Cue.lineAlign} + */ + this.lineAlign = Cue.lineAlign.CENTER; + + /** + * Vertical alignments of the cues within their extents. + * @type {shaka.text.Cue.displayAlign} + */ + this.displayAlign = Cue.displayAlign.BEFORE; + + /** + * Text color represented by any string that would be + * accepted in CSS. + * E. g. '#FFFFFF' or 'white'. + * @type {!string} + */ + this.color = ''; + + /** + * Text background color represented by any string that would be + * accepted in CSS. + * E. g. '#FFFFFF' or 'white'. + * @type {!string} + */ + this.backgroundColor = ''; + + /** + * Text font size in pixels. + * @type {?number} + */ + this.fontSize = null; + + /** + * Text font weight. Either normal or bold. + * @type {shaka.text.Cue.fontWeight} + */ + this.fontWeight = Cue.fontWeight.NORMAL; + + /** + * Text font family. + * @type {!string} + */ + this.fontFamily = ''; + + /** + * Whether or not line wrapping should be applied + * to the cue. + * @type {boolean} + */ + this.wrapLine = true; + + /** + * Id of the cue. + * @type {!string} + */ + this.id = ''; +}; + + +/** + * @enum {string} + * @export + */ +shaka.text.Cue.positionAlign = { + LEFT: 'line-left', + RIGHT: 'line-right', + CENTER: 'center', + AUTO: 'auto' +}; + + +/** + * @enum {string} + * @export + */ +shaka.text.Cue.textAlign = { + LEFT: 'left', + RIGHT: 'right', + CENTER: 'center', + START: 'start', + END: 'end' +}; + + +/** + * @enum {string} + * @export + */ +shaka.text.Cue.displayAlign = { + BEFORE: 'before', + CENTER: 'center', + AFTER: 'after' +}; + + +/** + * @enum {number} + * @export + */ +shaka.text.Cue.writingDirection = { + HORIZONTAL: 0, + VERTICAL_LEFT: 1, + VERTICAL_RIGHT: 2 +}; + + +/** + * @enum {number} + * @export + */ +shaka.text.Cue.lineInterpretation = { + LINE_NUMBER: 0, + PERCENTAGE: 1 +}; + + +/** + * @enum {string} + * @export + */ +shaka.text.Cue.lineAlign = { + CENTER: 'center', + START: 'start', + END: 'end' +}; + + +/** + * In CSS font weight can be a number, where 400 is normal + * and 700 is bold. Use these values for the enum for consistency. + * @enum {number} + * @export + */ +shaka.text.Cue.fontWeight = { + NORMAL: 400, + BOLD: 700 +}; + diff --git a/lib/text/mp4_vtt_parser.js b/lib/text/mp4_vtt_parser.js index 1bab40041..bf775590c 100644 --- a/lib/text/mp4_vtt_parser.js +++ b/lib/text/mp4_vtt_parser.js @@ -19,6 +19,7 @@ goog.provide('shaka.text.Mp4VttParser'); goog.require('goog.asserts'); goog.require('shaka.log'); +goog.require('shaka.text.Cue'); goog.require('shaka.text.TextEngine'); goog.require('shaka.text.VttTextParser'); goog.require('shaka.util.DataViewReader'); @@ -110,7 +111,7 @@ shaka.text.Mp4VttParser.prototype.parseMedia = function(data, time) { var presentations = []; /** @type {!Array.} */ var payloads = []; - /** @type {!Array.} */ + /** @type {!Array.} */ var cues = []; var sawTFDT = false; @@ -267,7 +268,7 @@ shaka.text.Mp4VttParser.parseTRUN_ = function(version, flags, reader) { * @param {!ArrayBuffer} data * @param {number} startTime * @param {number} endTime - * @return {TextTrackCue} + * @return {shaka.text.Cue} * @private */ shaka.text.Mp4VttParser.parseVTTC_ = function(data, startTime, endTime) { @@ -307,7 +308,7 @@ shaka.text.Mp4VttParser.parseVTTC_ = function(data, startTime, endTime) { * @param {?string} settings * @param {number} startTime * @param {number} endTime - * @return {TextTrackCue} + * @return {!shaka.text.Cue} * @private */ shaka.text.Mp4VttParser.assembleCue_ = function(payload, @@ -315,16 +316,16 @@ shaka.text.Mp4VttParser.assembleCue_ = function(payload, settings, startTime, endTime) { - var cue = shaka.text.TextEngine.makeCue( + var cue = new shaka.text.Cue( startTime, endTime, payload); - if (cue && id) { + if (id) { cue.id = id; } - if (cue && settings) { + if (settings) { var parser = new shaka.util.TextParser(settings); var word = parser.readWord(); diff --git a/lib/text/simple_text_displayer.js b/lib/text/simple_text_displayer.js new file mode 100644 index 000000000..46f087d4b --- /dev/null +++ b/lib/text/simple_text_displayer.js @@ -0,0 +1,225 @@ +/** + * @license + * Copyright 2016 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.text.SimpleTextDisplayer'); + +goog.require('shaka.log'); + + + +/** + *

+ * This defines the default text displayer plugin. An instance of this + * class is used when no custom displayer is given. + *

+ *

+ * This class simply converts shaka.text.Cue objects to + * TextTrackCues and feeds them to the browser. + *

+ * + * @param {HTMLMediaElement} video + * @constructor + * @struct + * @implements {shakaExtern.TextDisplayer} + * @export + */ +shaka.text.SimpleTextDisplayer = function(video) { + /** @private {TextTrack} */ + this.textTrack_ = null; + + // TODO: test that in all cases, the built-in CC controls in the video element + // are toggling our TextTrack. + + // If the video element has TextTracks, disable them. If we see one that + // was created by a previous instance of Shaka Player, reuse it. + for (var i = 0; i < video.textTracks.length; ++i) { + var track = video.textTracks[i]; + track.mode = 'disabled'; + + if (track.label == shaka.text.SimpleTextDisplayer.TextTrackLabel_) { + this.textTrack_ = track; + } + } + + if (!this.textTrack_) { + // As far as I can tell, there is no observable difference between setting + // kind to 'subtitles' or 'captions' when creating the TextTrack object. + // The individual text tracks from the manifest will still have their own + // kinds which can be displayed in the app's UI. + this.textTrack_ = video.addTextTrack( + 'subtitles', shaka.text.SimpleTextDisplayer.TextTrackLabel_); + } + this.textTrack_.mode = 'hidden'; +}; + + +/** + * @override + * @export + */ +shaka.text.SimpleTextDisplayer.prototype.remove = function(start, end) { + // Check that the displayer hasn't been destroyed. + if (!this.textTrack_) return false; + + this.removeWhere_(function(cue) { + if (cue.startTime >= end || cue.endTime <= start) { + // Outside the remove range. Hang on to it. + return false; + } + return true; + }); + + return true; +}; + + +/** + * @override + * @export + */ +shaka.text.SimpleTextDisplayer.prototype.append = function(cues) { + var textTrackCues = []; + for (var i = 0; i < cues.length; i++) { + var cue = this.convertToTextTrackCue_(cues[i]); + if (cue) + textTrackCues.push(cue); + } + + textTrackCues.forEach(function(cue) { + this.textTrack_.addCue(cue); + }.bind(this)); +}; + + +/** + * @override + * @export + */ +shaka.text.SimpleTextDisplayer.prototype.destroy = function() { + if (this.textTrack_) { + this.removeWhere_(function(cue) { return true; }); + } + + this.textTrack_ = null; + return Promise.resolve(); +}; + + +/** + * @override + * @export + */ +shaka.text.SimpleTextDisplayer.prototype.isTextVisible = function() { + return this.textTrack_.mode == 'showing'; +}; + + +/** + * @override + * @export + */ +shaka.text.SimpleTextDisplayer.prototype.setTextVisibility = function(on) { + this.textTrack_.mode = on ? 'showing' : 'hidden'; +}; + + +/** + * @param {!shaka.text.Cue} shakaCue + * @return {TextTrackCue} + * @private + */ +shaka.text.SimpleTextDisplayer.prototype.convertToTextTrackCue_ = + function(shakaCue) { + if (shakaCue.startTime >= shakaCue.endTime) { + // IE/Edge will throw in this case. + // See issue #501 + shaka.log.warning('Invalid cue times: ' + shakaCue.startTime + + ' - ' + shakaCue.endTime); + return null; + } + + var Cue = shaka.text.Cue; + var vttCue = new VTTCue(shakaCue.startTime, + shakaCue.endTime, + shakaCue.payload); + + // NOTE: positionAlign and lineAlign settings are not supported by Chrome + // at the moment, so setting them will have no effect. + // The bug on chromium to implement them: + // https://bugs.chromium.org/p/chromium/issues/detail?id=633690 + + vttCue.lineAlign = shakaCue.lineAlign; + vttCue.positionAlign = shakaCue.positionAlign; + vttCue.size = shakaCue.size; + vttCue.align = shakaCue.textAlign; + + if (shakaCue.textAlign == 'center' && vttCue.align != 'center') { + // Workaround for a Chrome bug http://crbug.com/663797 + // Chrome does not support align = 'center' + vttCue.position = 'auto'; + vttCue.align = 'middle'; + } + + if (shakaCue.writingDirection == Cue.writingDirection.VERTICAL_LEFT) + vttCue.vertical = 'lr'; + else if (shakaCue.writingDirection == Cue.writingDirection.VERTICAL_RIGHT) + vttCue.vertical = 'rl'; + + // snapToLines flag is true by default + if (shakaCue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) + vttCue.snapToLines = false; + + if (shakaCue.line) + vttCue.line = shakaCue.line; + + if (shakaCue.position) + vttCue.position = shakaCue.position; + + return vttCue; +}; + + +/** + * Remove all cues for which the matching function returns true. + * + * @param {function(!TextTrackCue):boolean} predicate + * @private + */ +shaka.text.SimpleTextDisplayer.prototype.removeWhere_ = function(predicate) { + var cues = this.textTrack_.cues; + var removeMe = []; + + // 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.textTrack_.remove() will alter that list. + for (var i = 0; i < cues.length; ++i) { + if (predicate(cues[i])) { + removeMe.push(cues[i]); + } + } + + for (var i = 0; i < removeMe.length; ++i) { + this.textTrack_.removeCue(removeMe[i]); + } +}; + + +/** + * @const {string} + * @private + */ +shaka.text.SimpleTextDisplayer.TextTrackLabel_ = 'Shaka Player TextTrack'; diff --git a/lib/text/text_engine.js b/lib/text/text_engine.js index 650ea813c..b04475b99 100644 --- a/lib/text/text_engine.js +++ b/lib/text/text_engine.js @@ -26,17 +26,17 @@ goog.require('shaka.util.IDestroyable'); /** * Manages text parsers and cues. * + * @param {shakaExtern.TextDisplayer} displayer * @struct * @constructor - * @param {TextTrack} track * @implements {shaka.util.IDestroyable} */ -shaka.text.TextEngine = function(track) { +shaka.text.TextEngine = function(displayer) { /** @private {shakaExtern.TextParser} */ this.parser_ = null; - /** @private {TextTrack} */ - this.track_ = track; + /** @private {shakaExtern.TextDisplayer} */ + this.displayer_ = displayer; /** @private {number} */ this.timestampOffset_ = 0; @@ -93,37 +93,21 @@ shaka.text.TextEngine.isTypeSupported = function(mimeType) { }; -/** - * Creates a cue using the best platform-specific interface available. - * - * @param {number} startTime - * @param {number} endTime - * @param {string} payload - * @return {TextTrackCue} or null if the parameters were invalid. - * @export - */ -shaka.text.TextEngine.makeCue = function(startTime, endTime, payload) { - if (startTime >= endTime) { - // IE/Edge will throw in this case. - // See issue #501 - shaka.log.warning('Invalid cue times: ' + startTime + ' - ' + endTime); - return null; - } +/** @override */ +shaka.text.TextEngine.prototype.destroy = function() { + this.parser_ = null; + this.displayer_ = null; - return new VTTCue(startTime, endTime, payload); + return Promise.resolve(); }; -/** @override */ -shaka.text.TextEngine.prototype.destroy = function() { - if (this.track_) { - this.removeWhere_(function(cue) { return true; }); - } - - this.parser_ = null; - this.track_ = null; - - return Promise.resolve(); +/** + * @param {shakaExtern.TextDisplayer} displayer + * @export + */ +shaka.text.TextEngine.prototype.setDisplayer = function(displayer) { + this.displayer_ = displayer; }; @@ -155,7 +139,7 @@ shaka.text.TextEngine.prototype.appendBuffer = // Start the operation asynchronously to avoid blocking the caller. return Promise.resolve().then(function() { // Check that TextEngine hasn't been destroyed. - if (!this.track_) return; + if (!this.parser_ || !this.displayer_) return; if (startTime == null || endTime == null) { this.parser_.parseInit(buffer); @@ -170,12 +154,12 @@ shaka.text.TextEngine.prototype.appendBuffer = }; // Parse the buffer and add the new cues. - var cues = this.parser_.parseMedia(buffer, time); + var allCues = this.parser_.parseMedia(buffer, time); + var cuesToAppend = allCues.filter(function(cue) { + return cue.startTime < this.appendWindowEnd_; + }.bind(this)); - for (var i = 0; i < cues.length; ++i) { - if (cues[i].startTime >= this.appendWindowEnd_) break; - this.track_.addCue(cues[i]); - } + this.displayer_.append(cuesToAppend); // 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 @@ -202,42 +186,31 @@ shaka.text.TextEngine.prototype.appendBuffer = shaka.text.TextEngine.prototype.remove = function(start, end) { // Start the operation asynchronously to avoid blocking the caller. return Promise.resolve().then(function() { - // Check that TextEngine hasn't been destroyed. - if (!this.track_) return; - - this.removeWhere_(function(cue) { - if (cue.startTime >= end || cue.endTime <= start) { - // Outside the remove range. Hang on to it. - return false; - } - return true; - }); - - if (this.bufferStart_ == null) { - goog.asserts.assert(this.bufferEnd_ == null, - 'end must be null if start is null'); - } else { - goog.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. - } else if (start <= this.bufferStart_ && end >= this.bufferEnd_) { - // We wiped out everything. - goog.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; + if (this.displayer_ && this.displayer_.remove(start, end)) { + if (this.bufferStart_ == null) { + goog.asserts.assert(this.bufferEnd_ == null, + 'end must be null if start is null'); } else { - // We removed from the middle? StreamingEngine isn't supposed to. - goog.asserts.assert( - false, 'removal from the middle is not supported by TextEngine'); + goog.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. + } else if (start <= this.bufferStart_ && end >= this.bufferEnd_) { + // We wiped out everything. + 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. + goog.asserts.assert( + false, 'removal from the middle is not supported by TextEngine'); + } } } }.bind(this)); @@ -299,31 +272,6 @@ shaka.text.TextEngine.prototype.bufferedAheadOf = function(t) { }; -/** - * Remove all cues for which the matching function returns true. - * - * @param {function(!TextTrackCue):boolean} predicate - * @private - */ -shaka.text.TextEngine.prototype.removeWhere_ = function(predicate) { - var cues = this.track_.cues; - var removeMe = []; - - // 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. - for (var i = 0; i < cues.length; ++i) { - if (predicate(cues[i])) { - removeMe.push(cues[i]); - } - } - - for (var i = 0; i < removeMe.length; ++i) { - this.track_.removeCue(removeMe[i]); - } -}; - - /** * @param {Function} parser @@ -350,3 +298,4 @@ shaka.text.TextEngine.TextParserWrapper_.prototype.parseMedia = function( time.segmentStart, time.segmentEnd); }; + diff --git a/lib/text/ttml_text_parser.js b/lib/text/ttml_text_parser.js index 99cb74c20..b6656a329 100644 --- a/lib/text/ttml_text_parser.js +++ b/lib/text/ttml_text_parser.js @@ -18,6 +18,7 @@ goog.provide('shaka.text.TtmlTextParser'); goog.require('goog.asserts'); +goog.require('shaka.text.Cue'); goog.require('shaka.text.TextEngine'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); @@ -271,7 +272,7 @@ shaka.text.TtmlTextParser.addNewLines_ = function(element, whitespaceTrim) { * @param {!Array.} styles * @param {!Array.} regions * @param {boolean} whitespaceTrim - * @return {TextTrackCue} + * @return {shaka.text.Cue} * @private */ shaka.text.TtmlTextParser.parseCue_ = function( @@ -310,9 +311,7 @@ shaka.text.TtmlTextParser.parseCue_ = function( start += offset; end += offset; - var cue = shaka.text.TextEngine.makeCue(start, end, payload); - if (!cue) - return null; + var cue = new shaka.text.Cue(start, end, payload); // Get other properties if available var region = shaka.text.TtmlTextParser.getElementFromCollection_( @@ -326,7 +325,7 @@ shaka.text.TtmlTextParser.parseCue_ = function( /** * Adds applicable style properties to a cue. * - * @param {!TextTrackCue} cue + * @param {!shaka.text.Cue} cue * @param {!Element} cueElement * @param {Element} region * @param {!Array.} styles @@ -335,8 +334,9 @@ shaka.text.TtmlTextParser.parseCue_ = function( shaka.text.TtmlTextParser.addStyle_ = function( cue, cueElement, region, styles) { var TtmlTextParser = shaka.text.TtmlTextParser; - var results = null; + var Cue = shaka.text.Cue; + var results = null; var extent = TtmlTextParser.getStyleAttribute_( cueElement, region, styles, 'tts:extent'); @@ -351,53 +351,77 @@ shaka.text.TtmlTextParser.addStyle_ = function( var writingMode = TtmlTextParser.getStyleAttribute_( cueElement, region, styles, 'tts:writingMode'); - var isVerticalText = true; if (writingMode == 'tb' || writingMode == 'tblr') - cue.vertical = 'lr'; + cue.writingDirection = Cue.writingDirection.VERTICAL_LEFT; else if (writingMode == 'tbrl') - cue.vertical = 'rl'; - else - isVerticalText = false; + cue.writingDirection = Cue.writingDirection.VERTICAL_RIGHT; var origin = TtmlTextParser.getStyleAttribute_( cueElement, region, styles, 'tts:origin'); if (origin) { results = TtmlTextParser.percentValues_.exec(origin); if (results != null) { - // for vertical text use first coordinate of tts:origin - // to represent line of the cue and second - for position. - // Otherwise (horizontal), use them the other way around. - if (isVerticalText) { - cue.position = Number(results[2]); - cue.line = Number(results[1]); - } else { + // for horizontal text use first coordinate of tts:origin + // to represent position of the cue and second - for line. + // Otherwise (vertical), use them the other way around. + if (cue.writingDirection == Cue.writingDirection.HORIZONTAL) { cue.position = Number(results[1]); cue.line = Number(results[2]); + } else { + cue.position = Number(results[2]); + cue.line = Number(results[1]); } - // A boolean indicating whether the line is an integer - // number of lines (using the line dimensions of the first - // line of the cue), or whether it is a percentage of the - // dimension of the video. The flag is set to true when lines - // are counted, and false otherwise. - cue.snapToLines = false; + + cue.lineInterpretation = Cue.lineInterpretation.PERCENTAGE; } } var align = TtmlTextParser.getStyleAttribute_( cueElement, region, styles, 'tts:textAlign'); if (align) { - cue.align = align; - if (align == 'center') { - if (cue.align != 'center') { - // Workaround for a Chrome bug http://crbug.com/663797 - // Chrome does not support align = 'center' - cue.align = 'middle'; - } - cue.position = 'auto'; - } cue.positionAlign = TtmlTextParser.textAlignToPositionAlign_[align]; cue.lineAlign = TtmlTextParser.textAlignToLineAlign_[align]; + + goog.asserts.assert(align.toUpperCase() in Cue.textAlign, + align.toUpperCase() + + ' Should be in Cue.textAlign values!'); + + cue.textAlign = Cue.textAlign[align.toUpperCase()]; } + + var displayAlign = TtmlTextParser.getStyleAttribute_( + cueElement, region, styles, 'tts:displayAlign'); + if (displayAlign) { + goog.asserts.assert(displayAlign.toUpperCase() in Cue.displayAlign, + displayAlign.toUpperCase() + + ' Should be in Cue.displayAlign values!'); + cue.displayAlign = Cue.displayAlign[displayAlign.toUpperCase()]; + } + + var color = TtmlTextParser.getStyleAttribute_( + cueElement, region, styles, 'tts:color'); + if (color) + cue.color = color; + + var backgroundColor = TtmlTextParser.getStyleAttribute_( + cueElement, region, styles, 'tts:backgroundColor'); + if (backgroundColor) + cue.backgroundColor = backgroundColor; + + var fontFamily = TtmlTextParser.getStyleAttribute_( + cueElement, region, styles, 'tts:fontFamily'); + if (fontFamily) + cue.fontFamily = fontFamily; + + var fontWeight = TtmlTextParser.getStyleAttribute_( + cueElement, region, styles, 'tts:fontWeight'); + if (fontWeight && fontWeight == 'bold') + cue.fontWeight = Cue.fontWeight.BOLD; + + var wrapOption = TtmlTextParser.getStyleAttribute_( + cueElement, region, styles, 'tts:wrapOption'); + if (wrapOption && wrapOption == 'noWrap') + cue.wrapLine = false; }; diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js index e437c1562..a7ad7deca 100644 --- a/lib/text/vtt_text_parser.js +++ b/lib/text/vtt_text_parser.js @@ -19,6 +19,7 @@ goog.provide('shaka.text.VttTextParser'); goog.require('goog.asserts'); goog.require('shaka.log'); +goog.require('shaka.text.Cue'); goog.require('shaka.text.TextEngine'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); @@ -103,7 +104,7 @@ shaka.text.VttTextParser.prototype.parseMedia = function(data, time) { * * @param {!Array.} text * @param {number} timeOffset - * @return {?TextTrackCue} + * @return {shaka.text.Cue} * @private */ shaka.text.VttTextParser.parseCue_ = function(text, timeOffset) { @@ -141,9 +142,7 @@ shaka.text.VttTextParser.parseCue_ = function(text, timeOffset) { // Get the payload. var payload = text.slice(1).join('\n').trim(); - var cue = shaka.text.TextEngine.makeCue(start, end, payload); - if (!cue) - return null; + var cue = new shaka.text.Cue(start, end, payload); // Parse optional settings. parser.skipWhitespace(); @@ -167,52 +166,113 @@ shaka.text.VttTextParser.parseCue_ = function(text, timeOffset) { /** * Parses a WebVTT setting from the given word. * - * @param {!TextTrackCue} cue + * @param {!shaka.text.Cue} cue * @param {string} word * @return {boolean} True on success. */ shaka.text.VttTextParser.parseSetting = function(cue, word) { - // NOTE: positionAlign and lineAlign settings are not supported by Chrome - // at the moment, so setting them will have no effect. - // The bug on chromium to implement them: - // https://bugs.chromium.org/p/chromium/issues/detail?id=633690 - + var VttTextParser = shaka.text.VttTextParser; var results = null; if ((results = /^align:(start|middle|center|end|left|right)$/.exec(word))) { - cue.align = results[1]; - if (results[1] == 'center' && cue.align != 'center') { - // Workaround for a Chrome bug http://crbug.com/663797 - // Chrome does not support align = 'center' - cue.position = 'auto'; - cue.align = 'middle'; - } + VttTextParser.setTextAlign_(cue, results[1]); } else if ((results = /^vertical:(lr|rl)$/.exec(word))) { - cue.vertical = results[1]; + VttTextParser.setVerticalWritingDirection_(cue, results[1]); } else if ((results = /^size:(\d{1,2}|100)%$/.exec(word))) { cue.size = Number(results[1]); } - // There was a disagreement between a working draft and an editor draft of - // the WebVTT spec. According to the former, optional position alignment - // options are 'start', 'end' and 'center'. According to the latter - - // 'line-left', 'center' and 'line-right'. - // We are going to support both options for now. else if ((results = /^position:(\d{1,2}|100)%(?:,(line-left|line-right|center|start|end))?$/ .exec(word))) { cue.position = Number(results[1]); - if (results[2]) - cue.positionAlign = results[2]; - } else if ((results = + if (results[2]) { + VttTextParser.setPositionAlign_(cue, results[2]); + } + } else { + return VttTextParser.parsedLineValueAndInterpretation_(cue, word); + } + + return true; +}; + + +/** + * @param {!shaka.text.Cue} cue + * @param {!string} align + * @private + */ +shaka.text.VttTextParser.setTextAlign_ = function(cue, align) { + var Cue = shaka.text.Cue; + if (align == 'middle') { + cue.textAlign = Cue.textAlign.CENTER; + } else { + goog.asserts.assert(align.toUpperCase() in Cue.textAlign, + align.toUpperCase() + + ' Should be in Cue.textAlign values!'); + + cue.textAlign = Cue.textAlign[align.toUpperCase()]; + } +}; + + +/** + * @param {!shaka.text.Cue} cue + * @param {!string} align + * @private + */ +shaka.text.VttTextParser.setPositionAlign_ = function(cue, align) { + var Cue = shaka.text.Cue; + if (align == 'line-left' || align == 'start') + cue.positionAlign = Cue.positionAlign.LEFT; + else if (align == 'line-right' || align == 'end') + cue.positionAlign = Cue.positionAlign.RIGHT; + else + cue.positionAlign = Cue.positionAlign.CENTER; +}; + + +/** + * @param {!shaka.text.Cue} cue + * @param {!string} value + * @private + */ +shaka.text.VttTextParser.setVerticalWritingDirection_ = function(cue, value) { + var Cue = shaka.text.Cue; + if (value == 'lr') + cue.writingDirection = Cue.writingDirection.VERTICAL_LEFT; + else + cue.writingDirection = Cue.writingDirection.VERTICAL_RIGHT; +}; + + +/** + * @param {!shaka.text.Cue} cue + * @param {!string} word + * @return {boolean} + * @private + */ +shaka.text.VttTextParser.parsedLineValueAndInterpretation_ = + function(cue, word) { + var Cue = shaka.text.Cue; + var results = null; + if ((results = /^line:(\d{1,2}|100)%(?:,(start|end|center))?$/.exec(word))) { - cue.snapToLines = false; + cue.lineInterpretation = Cue.lineInterpretation.PERCENTAGE; cue.line = Number(results[1]); - if (results[2]) - cue.lineAlign = results[2]; + if (results[2]) { + goog.asserts.assert(results[2].toUpperCase() in Cue.lineAlign, + results[2].toUpperCase() + + ' Should be in Cue.lineAlign values!'); + cue.lineAlign = Cue.lineAlign[results[2].toUpperCase()]; + } } else if ((results = /^line:(-?\d+)(?:,(start|end|center))?$/.exec(word))) { - cue.snapToLines = true; + cue.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER; cue.line = Number(results[1]); - if (results[2]) - cue.lineAlign = results[2]; + if (results[2]) { + goog.asserts.assert(results[2].toUpperCase() in Cue.lineAlign, + results[2].toUpperCase() + + ' Should be in Cue.lineAlign values!'); + cue.lineAlign = Cue.lineAlign[results[2].toUpperCase()]; + } } else { return false; } diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index d3fdab02a..1a27aab2b 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -48,8 +48,10 @@ goog.require('shaka.polyfill.VTTCue'); goog.require('shaka.polyfill.VideoPlayPromise'); goog.require('shaka.polyfill.VideoPlaybackQuality'); goog.require('shaka.polyfill.installAll'); +goog.require('shaka.text.Cue'); goog.require('shaka.text.Mp4TtmlParser'); goog.require('shaka.text.Mp4VttParser'); +goog.require('shaka.text.SimpleTextDisplayer'); goog.require('shaka.text.TextEngine'); goog.require('shaka.text.TtmlTextParser'); goog.require('shaka.text.VttTextParser'); diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index 56677eca8..1b03bf440 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -978,7 +978,8 @@ describe('MediaSourceEngine', function() { expect(mockTextEngine).toBeFalsy(); mockTextEngine = jasmine.createSpyObj('TextEngine', [ 'initParser', 'destroy', 'appendBuffer', 'remove', 'setTimestampOffset', - 'setAppendWindowEnd', 'bufferStart', 'bufferEnd', 'bufferedAheadOf' + 'setAppendWindowEnd', 'bufferStart', 'bufferEnd', 'bufferedAheadOf', + 'setDisplayer' ]); var resolve = Promise.resolve.bind(Promise); diff --git a/test/player_integration.js b/test/player_integration.js index 9db35f85a..4dbcc8ce0 100644 --- a/test/player_integration.js +++ b/test/player_integration.js @@ -162,7 +162,7 @@ describe('Player', function() { }); describe('plays', function() { - it('while external text tracks', function(done) { + it('with external text tracks', function(done) { player.load('test:sintel_no_text_compiled').then(function() { // For some reason, using path-absolute URLs (i.e. without the hostname) // like this doesn't work on Safari. So manually resolve the URL. diff --git a/test/player_unit.js b/test/player_unit.js index a3a9bac7c..886e104f0 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -53,6 +53,11 @@ describe('Player', function() { var playhead; /** @type {!shaka.test.FakePlayheadObserver} */ var playheadObserver; + /** @type {!shaka.test.FakeTextDisplayer} */ + var textDisplayer; + /** @type {function():shakaExtern.TextDisplayer} */ + var textDisplayFactory; + var mediaSourceEngine; /** @type {!shaka.test.FakeVideo} */ @@ -90,6 +95,9 @@ describe('Player', function() { abrManager = new shaka.test.FakeAbrManager(); abrFactory = function() { return abrManager; }; + textDisplayer = new shaka.test.FakeTextDisplayer(); + textDisplayFactory = function() { return textDisplayer; }; + function dependencyInjector(player) { networkingEngine = new shaka.test.FakeNetworkingEngine({}, new ArrayBuffer(0)); @@ -121,7 +129,8 @@ describe('Player', function() { player.configure({ // Ensures we don't get a warning about missing preference. preferredAudioLanguage: 'en', - abrFactory: abrFactory + abrFactory: abrFactory, + textDisplayFactory: textDisplayFactory }); onError = jasmine.createSpy('error event'); @@ -156,6 +165,7 @@ describe('Player', function() { expect(playheadObserver.destroy).toHaveBeenCalled(); expect(mediaSourceEngine.destroy).toHaveBeenCalled(); expect(streamingEngine.destroy).toHaveBeenCalled(); + expect(textDisplayer.destroy).toHaveBeenCalled(); }).catch(fail).then(done); }); }); diff --git a/test/test/util/fake_media_source_engine.js b/test/test/util/fake_media_source_engine.js index 55cf92c2b..16e42fa9e 100644 --- a/test/test/util/fake_media_source_engine.js +++ b/test/test/util/fake_media_source_engine.js @@ -372,4 +372,3 @@ shaka.test.FakeMediaSourceEngine.prototype.toIndex_ = function(type, ts) { shaka.test.FakeMediaSourceEngine.prototype.toTime_ = function(type, i) { return this.drift_ + (i * this.segmentData[type].segmentDuration); }; - diff --git a/test/test/util/simple_fakes.js b/test/test/util/simple_fakes.js index 4093298f6..df1ca5af8 100644 --- a/test/test/util/simple_fakes.js +++ b/test/test/util/simple_fakes.js @@ -22,6 +22,8 @@ goog.provide('shaka.test.FakePlayhead'); goog.provide('shaka.test.FakePlayheadObserver'); goog.provide('shaka.test.FakePresentationTimeline'); goog.provide('shaka.test.FakeStreamingEngine'); +goog.provide('shaka.test.FakeTextDisplayer'); +goog.provide('shaka.test.FakeTextTrack'); goog.provide('shaka.test.FakeVideo'); @@ -321,8 +323,7 @@ shaka.test.FakeVideo = function(opt_currentTime) { }; video.setMediaKeys.and.returnValue(Promise.resolve()); video.addTextTrack.and.callFake(function(kind, id) { - // TODO: mock TextTrack, if/when Player starts directly accessing it. - var track = {}; + var track = new shaka.test.FakeTextTrack(); video.textTracks.push(track); return track; }); @@ -538,3 +539,82 @@ shaka.test.FakePlayheadObserver.prototype.setRebufferingGoal; /** @type {jasmine.Spy} */ shaka.test.FakePlayheadObserver.prototype.addTimelineRegion; + + + +/** + * Creates a text track. + * + * @constructor + * @struct + * @extends {TextTrack} + * @return {!Object} + */ +shaka.test.FakeTextTrack = function() { + 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); + expect(idx).not.toBeLessThan(0); + track.cues.splice(idx, 1); + }); + return track; +}; + + +/** @type {!jasmine.Spy} */ +shaka.test.FakeTextTrack.prototype.addCue; + + +/** @type {!jasmine.Spy} */ +shaka.test.FakeTextTrack.prototype.removeCue; + + + +/** + * Creates a text track. + * + * @constructor + * @struct + * @extends {shaka.text.SimpleTextDisplayer} + * @return {!Object} + */ +shaka.test.FakeTextDisplayer = function() { + var displayer = { + append: jasmine.createSpy('append'), + remove: jasmine.createSpy('remove').and.returnValue(true), + destroy: + jasmine.createSpy('destroy').and.returnValue(Promise.resolve()), + isTextVisible: jasmine.createSpy('isTextVisible'), + setTextVisibility: jasmine.createSpy('setTextVisibility'), + textVisible: false + }; + + displayer.isTextVisible.and.callFake(function() { + return displayer.textVisible; + }); + + displayer.setTextVisibility.and.callFake(function(on) { + displayer.textVisible = on; + }); + + return displayer; +}; + + +/** @type {!jasmine.Spy} */ +shaka.test.FakeTextDisplayer.prototype.remove; + + +/** @type {!jasmine.Spy} */ +shaka.test.FakeTextDisplayer.prototype.append; + + +/** @type {!jasmine.Spy} */ +shaka.test.FakeTextDisplayer.prototype.destroy; diff --git a/test/text/cue_integration.js b/test/text/cue_integration.js index 26d953e56..fd13846d0 100644 --- a/test/text/cue_integration.js +++ b/test/text/cue_integration.js @@ -23,17 +23,6 @@ describe('Cue', function() { // The scenarios under test are not specific to WebVTT, but WebVTT is used to // exercise the platform's native cues and ensure that no errors occur. - it('skips zero-duration cues', function() { - // These cannot be constructed on IE/Edge. - // See issue #501 - var cues = parseVtt( - 'WEBVTT\n\n' + - '00:00:20.000 --> 00:00:20.000\n' + - 'Test', - {periodStart: 0, segmentStart: 0, segmentEnd: 0 }); - expect(cues.length).toBe(0); - }); - it('handles offsets', function() { // Offsets must be handled early. // See issue #502 @@ -63,7 +52,7 @@ describe('Cue', function() { /** * @param {string} text * @param {!shakaExtern.TextParser.TimeContext} time - * @return {!Array.} + * @return {!Array.} */ function parseVtt(text, time) { var data = shaka.util.StringUtils.toUTF8(text); diff --git a/test/text/mp4_vtt_parser_unit.js b/test/text/mp4_vtt_parser_unit.js index 41e4922b1..73d6dbf14 100644 --- a/test/text/mp4_vtt_parser_unit.js +++ b/test/text/mp4_vtt_parser_unit.js @@ -76,9 +76,17 @@ describe('Mp4vttParser', function() { it('parses media segment', function() { var cues = [ - {start: 111.8, end: 115.8, text: 'It has shed much innocent blood.\n'}, - {start: 118, end: 120, text: - 'You\'re a fool for traveling alone,\nso completely unprepared.\n'} + { + start: 111.8, + end: 115.8, + payload: 'It has shed much innocent blood.\n' + }, + { + start: 118, + end: 120, + payload: + 'You\'re a fool for traveling alone,\nso completely unprepared.\n' + } ]; var parser = new shaka.text.Mp4VttParser(); @@ -89,13 +97,25 @@ describe('Mp4vttParser', function() { }); it('parses media segment containing settings', function() { + var Cue = shaka.text.Cue; var cues = [ - {start: 111.8, end: 115.8, text: 'It has shed much innocent blood.\n', - align: 'right', size: 50, position: 10}, - {start: 118, end: 120, text: + { + start: 111.8, + end: 115.8, + payload: 'It has shed much innocent blood.\n', + align: 'right', + size: 50, + position: 10 + }, + { + start: 118, + end: 120, + payload: 'You\'re a fool for traveling alone,\nso completely unprepared.\n', - vertical: 'lr', line: 1} + writingDirection: Cue.writingDirection.VERTICAL_LEFT, + line: 1 + } ]; var parser = new shaka.text.Mp4VttParser(); @@ -108,9 +128,17 @@ describe('Mp4vttParser', function() { it('accounts for offset', function() { var cues = [ - {start: 121.8, end: 125.8, text: 'It has shed much innocent blood.\n'}, - {start: 128, end: 130, text: - 'You\'re a fool for traveling alone,\nso completely unprepared.\n'} + { + start: 121.8, + end: 125.8, + payload: 'It has shed much innocent blood.\n' + }, + { + start: 128, + end: 130, + payload: + 'You\'re a fool for traveling alone,\nso completely unprepared.\n' + } ]; var parser = new shaka.text.Mp4VttParser(); @@ -140,14 +168,14 @@ describe('Mp4vttParser', function() { for (var i = 0; i < actual.length; i++) { expect(actual[i].startTime).toBe(expected[i].start); expect(actual[i].endTime).toBe(expected[i].end); - expect(actual[i].text).toBe(expected[i].text); + expect(actual[i].payload).toBe(expected[i].payload); if (expected[i].line) expect(actual[i].line).toBe(expected[i].line); - if (expected[i].vertical) - expect(actual[i].vertical).toBe(expected[i].vertical); - if (expected[i].align) - expect(actual[i].align).toBe(expected[i].align); + if (expected[i].writingDirection) + expect(actual[i].writingDirection).toBe(expected[i].writingDirection); + if (expected[i].textAlign) + expect(actual[i].textAlign).toBe(expected[i].textAlign); if (expected[i].size) expect(actual[i].size).toBe(expected[i].size); if (expected[i].position) diff --git a/test/text/simple_text_displayer_unit.js b/test/text/simple_text_displayer_unit.js new file mode 100644 index 000000000..152a3bd29 --- /dev/null +++ b/test/text/simple_text_displayer_unit.js @@ -0,0 +1,240 @@ +/** + * @license + * Copyright 2016 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('SimpleTextDisplayer', function() { + /** @const */ + var originalVTTCue = window.VTTCue; + /** @const */ + var Cue = shaka.text.Cue; + /** @const */ + var SimpleTextDisplayer = shaka.text.SimpleTextDisplayer; + /** @type {!shaka.test.FakeVideo} */ + var video; + /** @type {!shaka.test.FakeTextTrack} */ + var mockTrack; + /** @type {!shaka.text.SimpleTextDisplayer} */ + var displayer; + /** @type {shaka.text.Cue} */ + var cue1; + /** @type {shaka.text.Cue} */ + var cue2; + /** @type {shaka.text.Cue} */ + var cue3; + + beforeEach(function() { + video = new shaka.test.FakeVideo(); + displayer = new SimpleTextDisplayer(video); + + expect(video.textTracks.length).toBe(1); + mockTrack = /** @type {!shaka.test.FakeTextTrack} */ (video.textTracks[0]); + expect(mockTrack).toBeTruthy(); + + window.VTTCue = function(start, end, text) { + this.startTime = start; + this.endTime = end; + this.text = text; + }; + }); + + afterAll(function() { + window.VTTCue = originalVTTCue; + }); + + describe('remove', function() { + it('removes cues which overlap the range', function() { + cue1 = new shaka.text.Cue(0, 1, 'Test'); + cue2 = new shaka.text.Cue(1, 2, 'Test'); + cue3 = new shaka.text.Cue(2, 3, 'Test'); + displayer.append([cue1, cue2, cue3]); + + displayer.remove(0, 1); + expect(mockTrack.removeCue.calls.count()).toBe(1); + expect(mockTrack.removeCue).toHaveBeenCalledWith( + jasmine.objectContaining({startTime: 0, endTime: 1})); + mockTrack.removeCue.calls.reset(); + + displayer.remove(0.5, 1.001); + expect(mockTrack.removeCue.calls.count()).toBe(1); + expect(mockTrack.removeCue).toHaveBeenCalledWith( + jasmine.objectContaining({startTime: 1, endTime: 2})); + mockTrack.removeCue.calls.reset(); + + displayer.remove(3, 5); + expect(mockTrack.removeCue).not.toHaveBeenCalled(); + mockTrack.removeCue.calls.reset(); + + displayer.remove(2.9999, Infinity); + expect(mockTrack.removeCue.calls.count()).toBe(1); + expect(mockTrack.removeCue).toHaveBeenCalledWith( + jasmine.objectContaining({startTime: 2, endTime: 3})); + mockTrack.removeCue.calls.reset(); + }); + + it('does nothing when nothing is buffered', function() { + displayer.remove(0, 1); + expect(mockTrack.removeCue).not.toHaveBeenCalled(); + }); + }); + + describe('convertToTextTrackCue', function() { + it('converts shaka.text.Cues to VttCues', function() { + verifyHelper( + [ + {start: 20, end: 40, text: 'Test'} + ], + [ + new shaka.text.Cue(20, 40, 'Test') + ]); + + cue1 = new shaka.text.Cue(20, 40, 'Test'); + cue1.positionAlign = Cue.positionAlign.LEFT; + cue1.lineAlign = Cue.lineAlign.START; + cue1.size = 80; + cue1.textAlign = Cue.textAlign.LEFT; + cue1.writingDirection = Cue.writingDirection.VERTICAL_LEFT; + cue1.lineInterpretation = Cue.lineInterpretation.LINE_NUMBER; + cue1.line = 5; + cue1.position = 10; + + cue2 = new shaka.text.Cue(20, 40, 'Test'); + cue2.positionAlign = Cue.positionAlign.RIGHT; + cue2.lineAlign = Cue.lineAlign.END; + cue2.textAlign = Cue.textAlign.RIGHT; + cue2.writingDirection = Cue.writingDirection.VERTICAL_RIGHT; + cue2.lineInterpretation = Cue.lineInterpretation.PERCENTAGE; + cue2.line = 5; + + cue3 = new shaka.text.Cue(20, 40, 'Test'); + cue3.positionAlign = Cue.positionAlign.CENTER; + cue3.lineAlign = Cue.lineAlign.CENTER; + cue3.textAlign = Cue.textAlign.START; + cue3.writingDirection = Cue.writingDirection.HORIZONTAL; + + verifyHelper( + [ + { + start: 20, + end: 40, + text: 'Test', + lineAlign: 'start', + positionAlign: 'line-left', + size: 80, + align: 'left', + vertical: 'lr', + snapToLines: true, + line: 5, + position: 10 + }, + { + start: 20, + end: 40, + text: 'Test', + lineAlign: 'end', + positionAlign: 'line-right', + align: 'right', + vertical: 'rl', + snapToLines: false, + line: 5 + }, + { + start: 20, + end: 40, + text: 'Test', + lineAlign: 'center', + positionAlign: 'center', + align: 'start', + vertical: undefined + } + ], + [cue1, cue2, cue3]); + }); + + it('uses a workaround for browsers not supporting align=center', + function() { + window.VTTCue = function(start, end, text) { + var align = 'middle'; + Object.defineProperty(this, 'align', { + get: function() { return align; }, + set: function(newValue) { + if (newValue != 'center') align = newValue; + } + }); + this.startTime = start; + this.endTime = end; + this.text = text; + }; + + cue1 = new shaka.text.Cue(20, 40, 'Test'); + cue1.textAlign = Cue.textAlign.CENTER; + + verifyHelper( + [ + { + start: 20, + end: 40, + text: 'Test', + align: 'middle' + } + ], + [cue1]); + }); + + it('ignores cues with startTime >= endTime', function() { + cue1 = new shaka.text.Cue(60, 40, 'Test'); + cue2 = new shaka.text.Cue(40, 40, 'Test'); + displayer.append([cue1, cue2]); + expect(mockTrack.addCue).not.toHaveBeenCalled(); + }); + }); + + + function createFakeCue(startTime, endTime) { + return { startTime: startTime, endTime: endTime }; + } + + /** + * @param {!Array} vttCues + * @param {!Array.} shakaCues + */ + function verifyHelper(vttCues, shakaCues) { + mockTrack.addCue.calls.reset(); + displayer.append(shakaCues); + var result = mockTrack.addCue.calls.allArgs().reduce( + shaka.util.Functional.collapseArrays, []); + expect(result).toBeTruthy(); + expect(result.length).toBe(vttCues.length); + for (var i = 0; i < vttCues.length; i++) { + expect(result[i].startTime).toBe(vttCues[i].start); + expect(result[i].endTime).toBe(vttCues[i].end); + expect(result[i].text).toBe(vttCues[i].text); + + if (vttCues[i].id) + expect(result[i].id).toBe(vttCues[i].id); + if (vttCues[i].vertical) + expect(result[i].vertical).toBe(vttCues[i].vertical); + if (vttCues[i].line) + expect(result[i].line).toBe(vttCues[i].line); + if (vttCues[i].align) + expect(result[i].align).toBe(vttCues[i].align); + if (vttCues[i].size) + expect(result[i].size).toBe(vttCues[i].size); + if (vttCues[i].position) + expect(result[i].position).toBe(vttCues[i].position); + } + } +}); diff --git a/test/text/text_engine_unit.js b/test/text/text_engine_unit.js index 696299ffc..9cdf6faa0 100644 --- a/test/text/text_engine_unit.js +++ b/test/text/text_engine_unit.js @@ -25,13 +25,18 @@ describe('TextEngine', function() { /** @type {!Function} */ var mockParserPlugIn; + + /** @type {!shaka.test.FakeTextDisplayer} */ + var mockDisplayer; + /** @type {!jasmine.Spy} */ var mockParseInit; + /** @type {!jasmine.Spy} */ var mockParseMedia; + /** @type {!shaka.text.TextEngine} */ var textEngine; - var mockTrack; beforeEach(function() { mockParseInit = jasmine.createSpy('mockParseInit'); @@ -42,9 +47,10 @@ describe('TextEngine', function() { parseMedia: mockParseMedia }; }; - mockTrack = createMockTrack(); + + mockDisplayer = new shaka.test.FakeTextDisplayer(); TextEngine.registerParser(dummyMimeType, mockParserPlugIn); - textEngine = new TextEngine(mockTrack); + textEngine = new TextEngine(mockDisplayer); textEngine.initParser(dummyMimeType); }); @@ -67,48 +73,32 @@ describe('TextEngine', function() { it('works asynchronously', function(done) { mockParseMedia.and.returnValue([1, 2, 3]); textEngine.appendBuffer(dummyData, 0, 3).catch(fail).then(done); - expect(mockTrack.addCue).not.toHaveBeenCalled(); + expect(mockDisplayer.append).not.toHaveBeenCalled(); }); - it('considers empty cues buffered', function(done) { - mockParseMedia.and.returnValue([]); - - textEngine.appendBuffer(dummyData, 0, 3).then(function() { - expect(mockParseMedia).toHaveBeenCalledWith( - dummyData, {periodStart: 0, segmentStart: 0, segmentEnd: 3}); - expect(mockTrack.addCue).not.toHaveBeenCalled(); - expect(mockTrack.removeCue).not.toHaveBeenCalled(); - - expect(textEngine.bufferStart()).toBe(0); - expect(textEngine.bufferEnd()).toBe(3); - - mockTrack.addCue.calls.reset(); - mockParseInit.calls.reset(); - mockParseMedia.calls.reset(); - }).catch(fail).then(done); - }); - - it('adds cues to the track', function(done) { - mockParseMedia.and.returnValue([1, 2, 3]); + it('calls displayer.append()', function(done) { + var cue1 = createFakeCue(1, 2); + var cue2 = createFakeCue(2, 3); + var cue3 = createFakeCue(3, 4); + var cue4 = createFakeCue(4, 5); + mockParseMedia.and.returnValue([cue1, cue2]); textEngine.appendBuffer(dummyData, 0, 3).then(function() { expect(mockParseMedia).toHaveBeenCalledWith( dummyData, {periodStart: 0, segmentStart: 0, segmentEnd: 3 }); - expect(mockTrack.addCue).toHaveBeenCalledWith(1); - expect(mockTrack.addCue).toHaveBeenCalledWith(2); - expect(mockTrack.addCue).toHaveBeenCalledWith(3); - expect(mockTrack.removeCue).not.toHaveBeenCalled(); + expect(mockDisplayer.append).toHaveBeenCalledWith([cue1, cue2]); - mockTrack.addCue.calls.reset(); + expect(mockDisplayer.remove).not.toHaveBeenCalled(); + + mockDisplayer.append.calls.reset(); mockParseMedia.calls.reset(); - mockParseMedia.and.returnValue([4, 5]); + mockParseMedia.and.returnValue([cue3, cue4]); return textEngine.appendBuffer(dummyData, 3, 5); }).then(function() { expect(mockParseMedia).toHaveBeenCalledWith( dummyData, {periodStart: 0, segmentStart: 3, segmentEnd: 5 }); - expect(mockTrack.addCue).toHaveBeenCalledWith(4); - expect(mockTrack.addCue).toHaveBeenCalledWith(5); + expect(mockDisplayer.append).toHaveBeenCalledWith([cue3, cue4]); }).catch(fail).then(done); }); @@ -134,37 +124,15 @@ describe('TextEngine', function() { it('works asynchronously', function(done) { textEngine.appendBuffer(dummyData, 0, 3).then(function() { var p = textEngine.remove(0, 1); - expect(mockTrack.removeCue).not.toHaveBeenCalled(); + expect(mockDisplayer.remove).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, Infinity); - }).then(function() { - expect(mockTrack.removeCue.calls.allArgs()).toEqual([[cue3]]); - }).catch(fail).then(done); - }); - - it('does nothing when nothing is buffered', function(done) { + it('calls displayer.remove()', function(done) { textEngine.remove(0, 1).then(function() { - expect(mockTrack.removeCue).not.toHaveBeenCalled(); + expect(mockDisplayer.remove).toHaveBeenCalledWith(0, 1); }).catch(fail).then(done); }); @@ -189,18 +157,25 @@ describe('TextEngine', function() { expect(mockParseMedia).toHaveBeenCalledWith( dummyData, {periodStart: 0, segmentStart: 0, segmentEnd: 3}); - expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(0, 1)); - expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(2, 3)); - mockTrack.addCue.calls.reset(); + expect(mockDisplayer.append).toHaveBeenCalledWith( + [ + createFakeCue(0, 1), + createFakeCue(2, 3) + ]); + + mockDisplayer.append.calls.reset(); textEngine.setTimestampOffset(4); return textEngine.appendBuffer(dummyData, 0, 3); }).then(function() { expect(mockParseMedia).toHaveBeenCalledWith( dummyData, {periodStart: 4, segmentStart: 0, segmentEnd: 3}); - expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(4, 5)); - expect(mockTrack.addCue).toHaveBeenCalledWith(createFakeCue(6, 7)); + expect(mockDisplayer.append).toHaveBeenCalledWith( + [ + createFakeCue(4, 5), + createFakeCue(6, 7) + ]); }).catch(fail).then(done); }); }); @@ -308,16 +283,22 @@ describe('TextEngine', function() { 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)); + expect(mockDisplayer.append).toHaveBeenCalledWith( + [ + createFakeCue(0, 1), + createFakeCue(1, 2) + ]); - mockTrack.addCue.calls.reset(); + mockDisplayer.append.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)); + expect(mockDisplayer.append).toHaveBeenCalledWith( + [ + createFakeCue(0, 1), + createFakeCue(1, 2), + createFakeCue(2, 3) + ]); }).catch(fail).then(done); }); @@ -360,7 +341,7 @@ describe('TextEngine', function() { describe('stateless parser', function() { describe('converted to stateful parser', function() { it('parses init segment', function(done) { - var textEngine = new TextEngine(createMockTrack()); + var textEngine = new TextEngine(mockDisplayer); textEngine.initParser(dummyMimeType); textEngine.appendBuffer(dummyData, null, null).then(function() { expect(mockParser).toHaveBeenCalledWith(dummyData, 0, null, null); @@ -368,7 +349,7 @@ describe('TextEngine', function() { }); it('parses media segment', function(done) { - var textEngine = new TextEngine(createMockTrack()); + var textEngine = new TextEngine(mockDisplayer); textEngine.initParser(dummyMimeType); textEngine.appendBuffer(dummyData, 0, 3).then(function() { expect(mockParser).toHaveBeenCalledWith(dummyData, 0, 0, 3); @@ -376,7 +357,7 @@ describe('TextEngine', function() { }); it('parses media segment with time offset', function(done) { - var textEngine = new TextEngine(createMockTrack()); + var textEngine = new TextEngine(mockDisplayer); textEngine.initParser(dummyMimeType); textEngine.setTimestampOffset(3); textEngine.appendBuffer(dummyData, 0, 3).then(function() { @@ -387,23 +368,6 @@ describe('TextEngine', function() { }); }); - 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); - expect(idx).not.toBeLessThan(0); - track.cues.splice(idx, 1); - }); - return track; - } - function createFakeCue(startTime, endTime) { return { startTime: startTime, endTime: endTime }; } diff --git a/test/text/ttml_text_parser_unit.js b/test/text/ttml_text_parser_unit.js index 73c5cee09..f48b9ea8d 100644 --- a/test/text/ttml_text_parser_unit.js +++ b/test/text/ttml_text_parser_unit.js @@ -17,19 +17,7 @@ describe('TtmlTextParser', function() { /** @const */ - var originalVTTCue = window.VTTCue; - - afterAll(function() { - window.VTTCue = originalVTTCue; - }); - - beforeEach(function() { - window.VTTCue = function(start, end, text) { - this.startTime = start; - this.endTime = end; - this.text = text; - }; - }); + var Cue = shaka.text.Cue; it('supports no cues', function() { verifyHelper([], @@ -55,21 +43,21 @@ describe('TtmlTextParser', function() { // When xml:space="default", ignore whitespace outside tags. verifyHelper( [ - {start: 62.03, end: 62.05, text: 'A B C'} + {start: 62.03, end: 62.05, payload: 'A B C'} ], '' + ttBody + '', {periodStart: 0, segmentStart: 0, segmentEnd: 0 }); // When xml:space="preserve", take them into account. verifyHelper( [ - {start: 62.03, end: 62.05, text: '\n A B C \n '} + {start: 62.03, end: 62.05, payload: '\n A B C \n '} ], '' + ttBody + '', {periodStart: 0, segmentStart: 0, segmentEnd: 0 }); // The default value for xml:space is "default". verifyHelper( [ - {start: 62.03, end: 62.05, text: 'A B C'} + {start: 62.03, end: 62.05, payload: 'A B C'} ], '' + ttBody + '', {periodStart: 0, segmentStart: 0, segmentEnd: 0 }); @@ -93,7 +81,7 @@ describe('TtmlTextParser', function() { it('supports colon formatted time', function() { verifyHelper( [ - {start: 62.05, end: 3723.2, text: 'Test'} + {start: 62.05, end: 3723.2, payload: 'Test'} ], '

Test

', @@ -103,7 +91,7 @@ describe('TtmlTextParser', function() { it('accounts for offset', function() { verifyHelper( [ - {start: 69.05, end: 3730.2, text: 'Test'} + {start: 69.05, end: 3730.2, payload: 'Test'} ], '

Test

', @@ -113,7 +101,7 @@ describe('TtmlTextParser', function() { it('supports time in 0.00h 0.00m 0.00s format', function() { verifyHelper( [ - {start: 3567.03, end: 5402.3, text: 'Test'} + {start: 3567.03, end: 5402.3, payload: 'Test'} ], '

Test

', @@ -123,7 +111,7 @@ describe('TtmlTextParser', function() { it('supports time with frame rate', function() { verifyHelper( [ - {start: 615.5, end: 663, text: 'Test'} + {start: 615.5, end: 663, payload: 'Test'} ], ' ' + @@ -137,7 +125,7 @@ describe('TtmlTextParser', function() { it('supports time with frame rate multiplier', function() { verifyHelper( [ - {start: 615.5, end: 663, text: 'Test'} + {start: 615.5, end: 663, payload: 'Test'} ], '

Test

', @@ -207,7 +195,12 @@ describe('TtmlTextParser', function() { it('parses alignment from textAlign attribute of a region', function() { verifyHelper( [ - {start: 62.05, end: 3723.2, text: 'Test', lineAlign: 'start'} + { + start: 62.05, + end: 3723.2, + payload: 'Test', + lineAlign: Cue.textAlign.START + } ], '' + '' + @@ -223,7 +216,12 @@ describe('TtmlTextParser', function() { it('parses alignment from