Separate text parsing and display logic.

Closes #796.
Closes #923.

Change-Id: Ifc2017b40a0fb570103f0fed7bc130aa24819e9f
This commit is contained in:
Sandra Lokshina
2017-06-05 14:56:16 -07:00
parent d70f78b5c7
commit c70367dc97
23 changed files with 1425 additions and 472 deletions
+3 -1
View File
@@ -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
+4 -1
View File
@@ -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;
+64 -1
View File
@@ -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.<!TextTrackCue>}
* @return {!Array.<!shaka.text.Cue>}
*
* @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.<!shaka.text.Cue>} 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;
+6 -6
View File
@@ -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.<shaka.util.ManifestParserUtils.ContentType,
SourceBuffer>} */
@@ -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);
};
+15 -43
View File
@@ -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_();
}
}
+241
View File
@@ -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
};
+7 -6
View File
@@ -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.<ArrayBuffer>} */
var payloads = [];
/** @type {!Array.<TextTrackCue>} */
/** @type {!Array.<shaka.text.Cue>} */
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();
+225
View File
@@ -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');
/**
* <p>
* This defines the default text displayer plugin. An instance of this
* class is used when no custom displayer is given.
* </p>
* <p>
* This class simply converts shaka.text.Cue objects to
* TextTrackCues and feeds them to the browser.
* </p>
*
* @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';
+46 -97
View File
@@ -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);
};
+57 -33
View File
@@ -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.<!Element>} styles
* @param {!Array.<!Element>} 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.<!Element>} 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;
};
+92 -32
View File
@@ -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.<string>} 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;
}
+2
View File
@@ -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');
+2 -1
View File
@@ -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);
+1 -1
View File
@@ -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.
+11 -1
View File
@@ -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);
});
});
@@ -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);
};
+82 -2
View File
@@ -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;
+1 -12
View File
@@ -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.<TextTrackCue>}
* @return {!Array.<shaka.text.Cue>}
*/
function parseVtt(text, time) {
var data = shaka.util.StringUtils.toUTF8(text);
+43 -15
View File
@@ -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)
+240
View File
@@ -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.<!shaka.text.Cue>} 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);
}
}
});
+52 -88
View File
@@ -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 };
}
+169 -70
View File
@@ -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'}
],
'<tt xml:space="default">' + ttBody + '</tt>',
{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 '}
],
'<tt xml:space="preserve">' + ttBody + '</tt>',
{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'}
],
'<tt>' + ttBody + '</tt>',
{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'}
],
'<tt><body><p begin="01:02.05" ' +
'end="01:02:03.200">Test</p></body></tt>',
@@ -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'}
],
'<tt><body><p begin="01:02.05" ' +
'end="01:02:03.200">Test</p></body></tt>',
@@ -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'}
],
'<tt><body><p begin="59.45m30ms" ' +
'end="1.5h2.3s">Test</p></body></tt>',
@@ -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'}
],
'<tt xmlns:ttp="ttml#parameter" ' +
'ttp:frameRate="30"> ' +
@@ -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'}
],
'<tt xmlns:ttp="ttml#parameter" ' +
'ttp:frameRate="60" ' +
@@ -152,7 +140,7 @@ describe('TtmlTextParser', function() {
it('supports time with subframes', function() {
verifyHelper(
[
{start: 615.517, end: 663, text: 'Test'}
{start: 615.517, end: 663, payload: 'Test'}
],
'<tt xmlns:ttp="ttml#parameter" ' +
'ttp:frameRate="30" ' +
@@ -167,7 +155,7 @@ describe('TtmlTextParser', function() {
it('supports time in frame format', function() {
verifyHelper(
[
{start: 2.5, end: 10.01, text: 'Test'}
{start: 2.5, end: 10.01, payload: 'Test'}
],
'<tt xmlns:ttp="ttml#parameter" ' +
'ttp:frameRate="60" ' +
@@ -182,7 +170,7 @@ describe('TtmlTextParser', function() {
it('supports time in tick format', function() {
verifyHelper(
[
{start: 5, end: 6.02, text: 'Test'}
{start: 5, end: 6.02, payload: 'Test'}
],
'<tt xmlns:ttp="ttml#parameter" ' +
'ttp:frameRate="60" ' +
@@ -197,7 +185,7 @@ describe('TtmlTextParser', function() {
it('supports time with duration', function() {
verifyHelper(
[
{start: 62.05, end: 67.05, text: 'Test'}
{start: 62.05, end: 67.05, payload: 'Test'}
],
'<tt><body><p begin="01:02.05" ' +
'dur="5s">Test</p></body></tt>',
@@ -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
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -223,7 +216,12 @@ describe('TtmlTextParser', function() {
it('parses alignment from <style> block with id on region', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', lineAlign: 'end'}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
lineAlign: Cue.textAlign.END
}
],
'<tt xmlns:tts="ttml#styling">' +
'<styling>' +
@@ -242,7 +240,12 @@ describe('TtmlTextParser', function() {
it('parses alignment from <style> block with id on p', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', lineAlign: 'end'}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
lineAlign: Cue.textAlign.END
}
],
'<tt xmlns:tts="ttml#styling">' +
'<styling>' +
@@ -261,7 +264,8 @@ describe('TtmlTextParser', function() {
it('supports size setting', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', size: 50}
{
start: 62.05, end: 3723.2, payload: 'Test', size: 50}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -278,7 +282,13 @@ describe('TtmlTextParser', function() {
function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', position: 50, line: 16}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
position: 50,
line: 16
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -291,7 +301,13 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', position: 50, line: 16}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
position: 50,
line: 16
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -305,7 +321,13 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', position: 50, line: 16}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
position: 50,
line: 16
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -323,7 +345,13 @@ describe('TtmlTextParser', function() {
function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', position: 16, line: 50}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
position: 16,
line: 50
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -337,7 +365,13 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', position: 16, line: 50}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
position: 16,
line: 50
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -351,7 +385,13 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', position: 16, line: 50}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
position: 16,
line: 50
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -365,10 +405,15 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
});
it('supports vertical setting', function() {
it('supports writingDirection setting', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', vertical: 'lr'}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -382,7 +427,12 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', vertical: 'rl'}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
writingDirection: Cue.writingDirection.VERTICAL_RIGHT
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -396,7 +446,12 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', vertical: 'lr'}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'<tt xmlns:tts="ttml#styling">' +
'<layout>' +
@@ -413,7 +468,7 @@ describe('TtmlTextParser', function() {
it('disregards empty divs and ps', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test'}
{start: 62.05, end: 3723.2, payload: 'Test'}
],
'<tt>' +
'<body>' +
@@ -426,7 +481,7 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test'}
{start: 62.05, end: 3723.2, payload: 'Test'}
],
'<tt>' +
'<body>' +
@@ -453,14 +508,14 @@ describe('TtmlTextParser', function() {
it('inserts newline characters into <br> tags', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Line1\nLine2'}
{start: 62.05, end: 3723.2, payload: 'Line1\nLine2'}
],
'<tt><body><p begin="01:02.05" ' +
'end="01:02:03.200">Line1<br/>Line2</p></body></tt>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Line1\nLine2'}
{start: 62.05, end: 3723.2, payload: 'Line1\nLine2'}
],
'<tt><body><p begin="01:02.05" ' +
'end="01:02:03.200"><span>Line1<br/>Line2</span></p></body></tt>',
@@ -470,8 +525,14 @@ describe('TtmlTextParser', function() {
it('parses cue alignment from textAlign attribute', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', lineAlign: 'start',
align: 'left', positionAlign: 'line-left'}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
lineAlign: Cue.lineAlign.START,
textAlign: Cue.textAlign.LEFT,
positionAlign: Cue.positionAlign.LEFT
}
],
'<tt xmlns:tts="ttml#styling">' +
'<styling>' +
@@ -487,29 +548,49 @@ describe('TtmlTextParser', function() {
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
});
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;
};
it('parses text style information', function() {
verifyHelper(
[
{start: 62.05, end: 3723.2, text: 'Test', lineAlign: 'center',
align: 'middle', position: 'auto', positionAlign: 'center'}
{
start: 62.05,
end: 3723.2,
payload: 'Test',
color: 'red',
backgroundColor: 'blue',
fontWeight: Cue.fontWeight.BOLD,
fontFamily: 'Times New Roman'
}
],
'<tt xmlns:tts="ttml#styling">' +
'<styling>' +
'<style xml:id="s1" tts:textAlign="center"/>' +
'<style xml:id="s1" tts:color="red" ' +
'tts:backgroundColor="blue" ' +
'tts:fontWeight="bold" ' +
'tts:fontFamily="Times New Roman"/>' +
'</styling>' +
'<layout xmlns:tts="ttml#styling">' +
'<region xml:id="subtitleArea" />' +
'</layout>' +
'<body region="subtitleArea">' +
'<p begin="01:02.05" end="01:02:03.200" style="s1">Test</p>' +
'</body>' +
'</tt>',
{periodStart: 0, segmentStart: 0, segmentEnd: 0 });
});
it('parses wrapping option', function() {
verifyHelper(
[
{
start: 62.05,
end: 3723.2,
payload: 'Test',
wrapLine: false
}
],
'<tt xmlns:tts="ttml#styling">' +
'<styling>' +
'<style xml:id="s1" tts:wrapOption="noWrap"/>' +
'</styling>' +
'<layout xmlns:tts="ttml#styling">' +
'<region xml:id="subtitleArea" />' +
@@ -535,24 +616,42 @@ describe('TtmlTextParser', function() {
for (var i = 0; i < cues.length; i++) {
expect(result[i].startTime).toBeCloseTo(cues[i].start, 3);
expect(result[i].endTime).toBeCloseTo(cues[i].end, 3);
expect(result[i].text).toBe(cues[i].text);
expect(result[i].payload).toBe(cues[i].payload);
// Workaround a bug in the compiler's externs.
// TODO: Remove when compiler is updated.
if (cues[i].align)
expect(/** @type {?} */ (result[i]).align).toBe(cues[i].align);
if (cues[i].textAlign)
expect(/** @type {?} */ (result[i]).textAlign).toBe(cues[i].textAlign);
if (cues[i].lineAlign)
expect(result[i].lineAlign).toBe(cues[i].lineAlign);
expect(/** @type {?} */ (result[i]).lineAlign).toBe(cues[i].lineAlign);
if (cues[i].positionAlign)
expect(result[i].positionAlign).toBe(cues[i].positionAlign);
expect(/** @type {?} */ (result[i]).positionAlign)
.toBe(cues[i].positionAlign);
if (cues[i].size)
expect(/** @type {?} */ (result[i]).size).toBe(cues[i].size);
if (cues[i].line)
expect(result[i].line).toBe(cues[i].line);
expect(/** @type {?} */ (result[i]).line).toBe(cues[i].line);
if (cues[i].position)
expect(/** @type {?} */ (result[i]).position).toBe(cues[i].position);
if (cues[i].vertical)
expect(result[i].vertical).toBe(cues[i].vertical);
expect(/** @type {?} */ (result[i]).writingDirection)
.toBe(cues[i].writingDirection);
if (cues[i].vertical)
expect(/** @type {?} */ (result[i]).lineInterpretation)
.toBe(cues[i].lineInterpretation);
if (cues[i].color)
expect(/** @type {?} */ (result[i]).color).toBe(cues[i].color);
if (cues[i].backgroundColor)
expect(/** @type {?} */ (result[i]).backgroundColor)
.toBe(cues[i].backgroundColor);
if (cues[i].fontWeight)
expect(/** @type {?} */ (result[i]).fontWeight)
.toBe(cues[i].fontWeight);
if (cues[i].fontFamily)
expect(/** @type {?} */ (result[i]).fontFamily)
.toBe(cues[i].fontFamily);
if (cues[i].wrapLine)
expect(/** @type {?} */ (result[i]).wrapLine).toBe(cues[i].wrapLine);
}
}
+62 -61
View File
@@ -16,28 +16,19 @@
*/
describe('VttTextParser', function() {
/** @const */
var originalVTTCue = window.VTTCue;
/** @type {!jasmine.Spy} */
var logWarningSpy;
/** @const */
var Cue = shaka.text.Cue;
beforeAll(function() {
logWarningSpy = jasmine.createSpy('shaka.log.warning');
shaka.log.warning = shaka.test.Util.spyFunc(logWarningSpy);
});
afterAll(function() {
window.VTTCue = originalVTTCue;
});
beforeEach(function() {
logWarningSpy.calls.reset();
window.VTTCue = function(start, end, text) {
this.startTime = start;
this.endTime = end;
this.text = text;
};
});
it('supports no cues', function() {
@@ -71,7 +62,7 @@ describe('VttTextParser', function() {
it('handles a blank line at the end of the file', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
@@ -82,7 +73,7 @@ describe('VttTextParser', function() {
it('handles no blank line at the end of the file', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
@@ -94,7 +85,7 @@ describe('VttTextParser', function() {
it('handles no newline after the final text payload', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
@@ -105,7 +96,7 @@ describe('VttTextParser', function() {
it('ignores offset', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
@@ -116,8 +107,8 @@ describe('VttTextParser', function() {
it('supports cues with no settings', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test', id: '1'},
{start: 40, end: 50, text: 'Test2', id: '2'}
{start: 20, end: 40, payload: 'Test', id: '1'},
{start: 40, end: 50, payload: 'Test2', id: '2'}
],
'WEBVTT\n\n' +
'1\n' +
@@ -132,8 +123,8 @@ describe('VttTextParser', function() {
it('supports cues with no ID', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'},
{start: 40, end: 50, text: 'Test2'}
{start: 20, end: 40, payload: 'Test'},
{start: 40, end: 50, payload: 'Test2'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
@@ -146,8 +137,8 @@ describe('VttTextParser', function() {
it('supports comments within cues', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'},
{start: 40, end: 50, text: 'Test2'}
{start: 20, end: 40, payload: 'Test'},
{start: 40, end: 50, payload: 'Test2'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000\n' +
@@ -162,7 +153,7 @@ describe('VttTextParser', function() {
it('supports non-integer timecodes', function() {
verifyHelper(
[
{start: 20.1, end: 40.505, text: 'Test'}
{start: 20.1, end: 40.505, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.100 --> 00:00:40.505\n' +
@@ -173,7 +164,7 @@ describe('VttTextParser', function() {
it('supports large timecodes', function() {
verifyHelper(
[
{start: 20, end: 108000, text: 'Test'}
{start: 20, end: 108000, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 30:00:00.000\n' +
@@ -220,8 +211,18 @@ describe('VttTextParser', function() {
it('supports vertical setting', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test', vertical: 'rl'},
{start: 40, end: 50, text: 'Test2', vertical: 'lr'}
{
start: 20,
end: 40,
payload: 'Test',
writingDirection: Cue.writingDirection.VERTICAL_RIGHT
},
{
start: 40,
end: 50,
payload: 'Test2',
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 vertical:rl\n' +
@@ -234,8 +235,8 @@ describe('VttTextParser', function() {
it('supports line setting', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test', line: 0},
{start: 40, end: 50, text: 'Test2', line: -1}
{start: 20, end: 40, payload: 'Test', line: 0},
{start: 40, end: 50, payload: 'Test2', line: -1}
] ,
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 line:0\n' +
@@ -248,8 +249,8 @@ describe('VttTextParser', function() {
it('supports line setting with optional part', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test', line: 10},
{start: 40, end: 50, text: 'Test2', line: -1}
{start: 20, end: 40, payload: 'Test', line: 10},
{start: 40, end: 50, payload: 'Test2', line: -1}
] ,
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 line:10%,start\n' +
@@ -262,7 +263,7 @@ describe('VttTextParser', function() {
it('supports position setting', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test2', position: 45}
{start: 20, end: 40, payload: 'Test2', position: 45}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 position:45%\n' +
@@ -273,8 +274,8 @@ describe('VttTextParser', function() {
it('supports position setting with optional part', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test', position: 45},
{start: 20, end: 40, text: 'Test2', position: 45}
{start: 20, end: 40, payload: 'Test', position: 45},
{start: 20, end: 40, payload: 'Test2', position: 45}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 position:45%,line-left\n' +
@@ -287,7 +288,7 @@ describe('VttTextParser', function() {
it('supports size setting', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test', size: 56}
{start: 20, end: 40, payload: 'Test', size: 56}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 size:56%\n' +
@@ -298,7 +299,7 @@ describe('VttTextParser', function() {
it('supports align setting', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test', align: 'center'}
{start: 20, end: 40, payload: 'Test', align: 'center'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 align:center\n' +
@@ -312,10 +313,10 @@ describe('VttTextParser', function() {
{
start: 20,
end: 40,
text: 'Test',
align: 'center',
payload: 'Test',
textAlign: Cue.textAlign.CENTER,
size: 56,
vertical: 'lr'
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'WEBVTT\n\n' +
@@ -330,10 +331,10 @@ describe('VttTextParser', function() {
{
start: 20,
end: 40,
text: 'Test',
align: 'center',
payload: 'Test',
textAlign: Cue.textAlign.CENTER,
size: 56,
vertical: 'lr'
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'WEBVTT\n\n' +
@@ -348,10 +349,10 @@ describe('VttTextParser', function() {
{
start: 20,
end: 40,
text: 'Test',
align: 'center',
payload: 'Test',
textAlign: Cue.textAlign.CENTER,
size: 56,
vertical: 'lr'
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'WEBVTT\n\n' +
@@ -366,10 +367,10 @@ describe('VttTextParser', function() {
{
start: 20,
end: 40,
text: 'Test',
payload: 'Test',
align: 'center',
size: 56,
vertical: 'lr'
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'WEBVTT\n\n' +
@@ -384,10 +385,10 @@ describe('VttTextParser', function() {
{
start: 40, // Note these are 20s off of the cue
end: 60, // because using relative timestamps
text: 'Test',
payload: 'Test',
align: 'center',
size: 56,
vertical: 'lr'
writingDirection: Cue.writingDirection.VERTICAL_LEFT
}
],
'WEBVTT\n\n' +
@@ -401,7 +402,7 @@ describe('VttTextParser', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 vertical:es\n' +
@@ -410,7 +411,7 @@ describe('VttTextParser', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 vertical:\n' +
@@ -419,7 +420,7 @@ describe('VttTextParser', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 vertical\n' +
@@ -428,7 +429,7 @@ describe('VttTextParser', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 line:-3%\n' +
@@ -437,7 +438,7 @@ describe('VttTextParser', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 line:45%%\n' +
@@ -446,7 +447,7 @@ describe('VttTextParser', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 align:10\n' +
@@ -455,7 +456,7 @@ describe('VttTextParser', function() {
verifyHelper(
[
{start: 20, end: 40, text: 'Test'}
{start: 20, end: 40, payload: 'Test'}
],
'WEBVTT\n\n' +
'00:00:20.000 --> 00:00:40.000 align:foo\n' +
@@ -468,8 +469,8 @@ describe('VttTextParser', function() {
it('respects X-TIMESTAMP-MAP header', function() {
verifyHelper(
[
{start: 30, end: 50, text: 'Test'},
{start: 50, end: 60, text: 'Test2'}
{start: 30, end: 50, payload: 'Test'},
{start: 50, end: 60, payload: 'Test2'}
] ,
// 900000 = 10 sec, so expect every timestamp to be 10
// seconds ahead of what is specified.
@@ -497,18 +498,18 @@ describe('VttTextParser', function() {
for (var i = 0; i < cues.length; i++) {
expect(result[i].startTime).toBe(cues[i].start);
expect(result[i].endTime).toBe(cues[i].end);
expect(result[i].text).toBe(cues[i].text);
expect(result[i].payload).toBe(cues[i].payload);
// Workaround a bug in the compiler's externs.
// TODO: Remove when compiler is updated.
if (cues[i].id)
expect(result[i].id).toBe(cues[i].id);
if (cues[i].vertical)
expect(result[i].vertical).toBe(cues[i].vertical);
expect(result[i].writingDirection).toBe(cues[i].writingDirection);
if (cues[i].line)
expect(result[i].line).toBe(cues[i].line);
if (cues[i].align)
expect(/** @type {?} */ (result[i]).align).toBe(cues[i].align);
if (cues[i].textAlign)
expect(/** @type {?} */ (result[i]).textAlign).toBe(cues[i].textAlign);
if (cues[i].size)
expect(/** @type {?} */ (result[i]).size).toBe(cues[i].size);
if (cues[i].position)