From 7e0f46931e122859d57ef0507143f3ebd4d604d5 Mon Sep 17 00:00:00 2001
From: Joey Parrish
Date: Mon, 31 Jul 2017 12:09:41 -0700
Subject: [PATCH] Overhaul variant-based and stream-based interfaces
While I set out to fix failing assertions during playback, I found
many more changes necessary to clean up the code and make things more
consistent.
When we changed switch history to include variants instead of streams,
we broke the filtering logic that is applied to the history. This
caused assertions at runtime that were not caught by the tests.
This moves the filtering logic to addToSwitchHistory_ and makes it
aware of variants. It also adds a regression test that would have
caught the assertions.
This fix affected many other tests, though, which necessitated other
API changes and cleanup.
Many interfaces are simplified, as is switching logic in Player. The
data flow is also easier to follow, since there are fewer transitions
between variant and stream. Everything up to StreamingEngine uses
Variants, and StreamingEngine uses Streams internally.
- All stream-based interfaces on AbrManager replaced
- switch callback takes a variant
- chooseStreams replaced with chooseVariant
- setTextStreams has been dropped
- Player maintains compatibility with old interfaces until v2.3
- Most stream-based interfaces on StreamingEngine replaced
- onChooseStreams callback to Player returns variant & text
instead of a stream map
- switch was replaced with switchVariant and switchTextStream,
both of which delegate to switchInternal_, which is largely
unchanged from the original switch method
- still has getActiveStreams, which I hope some day can become
getActiveVariant and getActiveTextStream so Player no longer
has to convert anything
- Most stream-based logic in Player replaced
- deferred switches map broken into variant and text members
- switch history logging broken into simpler variant and text
methods
- simplified manual and automatic track selection logic using
new AbrManager and StreamingEngine APIs
- player no longer filters duplicate selections, StreamingEngine
handles that
- replaced one case of deferred switches with an assertion
Closes #954
Change-Id: Ia49f6ffb9c5fa13ed8790dd03eeeded5122f7683
---
docs/tutorials/upgrade-v2-0.md | 49 +-
docs/tutorials/upgrade-v2-1.md | 49 +-
externs/shaka/abr_manager.js | 33 +-
lib/abr/simple_abr_manager.js | 138 ++---
lib/media/streaming_engine.js | 152 ++++--
lib/player.js | 547 ++++++++++---------
lib/util/stream_utils.js | 20 +
test/abr/simple_abr_manager_unit.js | 94 ++--
test/media/streaming_engine_integration.js | 113 ++--
test/media/streaming_engine_unit.js | 160 ++++--
test/player_unit.js | 577 ++++++++++++---------
test/test/util/fake_media_source_engine.js | 121 +++--
test/test/util/simple_fakes.js | 92 ++--
13 files changed, 1190 insertions(+), 955 deletions(-)
diff --git a/docs/tutorials/upgrade-v2-0.md b/docs/tutorials/upgrade-v2-0.md
index 606a6f786d..20de79eb25 100644
--- a/docs/tutorials/upgrade-v2-0.md
+++ b/docs/tutorials/upgrade-v2-0.md
@@ -58,24 +58,28 @@ types (variant and text).
#### Setting and configuring ABR manager
-In Shaka v2.0 a custom abr manager could be set through
+In Shaka v2.0, a custom ABR manager could be set through:
+
```js
player.configure({
abr.manager: customAbrManager
});
```
-In v2.2 it's done through
+In v2.2, it's done through:
+
```js
player.configure({
abrFactory: customAbrManager
});
```
-The API for abr manager also changed.
-In v2.0 default bandwidth estimate and restrictions were set through
+The API for AbrManager has also changed.
+
+In v2.0, default bandwidth estimate and restrictions were set through
`setDefaultEstimate()` and `setRestrictions()` methods.
-In 2.2 they are set through `configure()` method which accepts a
+
+In v2.2, they are set through `configure()` method which accepts a
{@link shakaExtern.AbrConfiguration} structure. The new method is more general,
and allows for the configuration of bandwidth upgrade and downgrade targets
as well.
@@ -89,8 +93,39 @@ abrManager.setRestrictions(restrictions);
abrManager.configure(abrConfigurations);
```
-In v2.2, the v2.0 interfaces for setting and configuring abr manager are
-still supported, but are deprecated. Support will be removed in v2.3.
+In v2.0, AbrManager had a `chooseStreams()` method for the player to prompt for
+a stream selection, and a `switch()` callback to send unsolicited changes from
+AbrManager to player. In v2.2, `chooseStreams()` has been replaced with
+`chooseVariant()`, and the `switch()` callback takes a variant instead of a map
+of streams.
+
+```js
+// v2.0:
+var map = abrManager.chooseStreams(['audio', 'video']);
+console.log(map['video'], map['audio']);
+
+MyAbrManager.prototype.makeDecision_ = function() {
+ var video = this.computeBestVideo_(this.bandwidth_);
+ var audio = this.computeBestAudio_(this.bandwidth_);
+ var map = {
+ 'audio': audio,
+ 'video': video
+ };
+ this.switch_(map);
+};
+
+// v2.2:
+var variant = abrManager.chooseVariant();
+console.log(variant, variant.video, variant.audio);
+
+MyAbrManager.prototype.makeDecision_ = function() {
+ var variant = this.computeBestVariant_(this.bandwidth_);
+ this.switch_(variant);
+};
+```
+
+In v2.2, the v2.0 interfaces are still supported, but are deprecated. Support
+will be removed in v2.3.
#### Selecting tracks and adaptation settings
diff --git a/docs/tutorials/upgrade-v2-1.md b/docs/tutorials/upgrade-v2-1.md
index a0b4790d3d..080f5b81ff 100644
--- a/docs/tutorials/upgrade-v2-1.md
+++ b/docs/tutorials/upgrade-v2-1.md
@@ -82,24 +82,28 @@ See {@link shaka.text.Cue} for details.
#### Setting and configuring ABR manager
-In Shaka v2.1 a custom abr manager could be set through
+In Shaka v2.1, a custom ABR manager could be set through:
+
```js
player.configure({
abr.manager: customAbrManager
});
```
-In v2.2 it's done through
+In v2.2, it's done through:
+
```js
player.configure({
abrFactory: customAbrManager
});
```
-The API for abr manager also changed.
-In v2.1 default bandwidth estimate and restrictions were set through
+The API for AbrManager also changed.
+
+In v2.1, default bandwidth estimate and restrictions were set through
`setDefaultEstimate()` and `setRestrictions()` methods.
-In 2.2 they are set through `configure()` method which accepts a
+
+In v2.2, they are set through `configure()` method which accepts a
{@link shakaExtern.AbrConfiguration} structure. The new method is more general,
and allows for the configuration of bandwidth upgrade and downgrade targets
as well.
@@ -113,8 +117,39 @@ abrManager.setRestrictions(restrictions);
abrManager.configure(abrConfigurations);
```
-In v2.2, the v2.1 interfaces for setting and configuring abr manager are
-still supported, but are deprecated. Support will be removed in v2.3.
+In v2.1, AbrManager had a `chooseStreams()` method for the player to prompt for
+a stream selection, and a `switch()` callback to send unsolicited changes from
+AbrManager to player. In v2.2, `chooseStreams()` has been replaced with
+`chooseVariant()`, and the `switch()` callback takes a variant instead of a map
+of streams.
+
+```js
+// v2.1:
+var map = abrManager.chooseStreams(['audio', 'video']);
+console.log(map['video'], map['audio']);
+
+MyAbrManager.prototype.makeDecision_ = function() {
+ var video = this.computeBestVideo_(this.bandwidth_);
+ var audio = this.computeBestAudio_(this.bandwidth_);
+ var map = {
+ 'audio': audio,
+ 'video': video
+ };
+ this.switch_(map);
+};
+
+// v2.2:
+var variant = abrManager.chooseVariant();
+console.log(variant, variant.video, variant.audio);
+
+MyAbrManager.prototype.makeDecision_ = function() {
+ var variant = this.computeBestVariant_(this.bandwidth_);
+ this.switch_(variant);
+};
+```
+
+In v2.2, the v2.1 interfaces are still supported, but are deprecated. Support
+will be removed in v2.3.
#### Switch history changes
diff --git a/externs/shaka/abr_manager.js b/externs/shaka/abr_manager.js
index 57eaea78ca..dd2260a948 100644
--- a/externs/shaka/abr_manager.js
+++ b/externs/shaka/abr_manager.js
@@ -39,15 +39,15 @@ shakaExtern.AbrManager = function() {};
/**
- * A callback from the Player that should be called when the AbrManager decides
- * it's time to change to a different set of streams.
+ * A callback into the Player that should be called when the AbrManager decides
+ * it's time to change to a different variant.
*
- * The first argument is a map of content types to chosen streams.
+ * The first argument is a variant to switch to.
*
* The second argument is an optional boolean. If true, all data will be
* from the buffer, which will result in a buffering event.
*
- * @typedef {function(!Object., boolean=)}
+ * @typedef {function(shakaExtern.Variant, boolean=)}
* @exportDoc
*/
shakaExtern.AbrManager.SwitchCallback;
@@ -90,31 +90,16 @@ shakaExtern.AbrManager.prototype.setVariants = function(variants) {};
/**
- * Updates manager's text streams collection.
- *
- * @param {!Array.} streams
- * @exportDoc
- */
-shakaExtern.AbrManager.prototype.setTextStreams = function(streams) {};
-
-
-/**
- * Chooses one Stream from each media type in mediaTypesToUpdate to switch to.
- * All Variants and Streams must be from the same Period.
- *
- * @param {!Array.} mediaTypesToUpdate
- * @return {!Object.}
+ * Chooses one variant to switch to. Called by the Player.
+ * @return {shakaExtern.Variant}
* @exportDoc
*/
-// TODO: Consider breaking down into chooseVariant() and chooseText()
-shakaExtern.AbrManager.prototype.chooseStreams =
- function(mediaTypesToUpdate) {};
+shakaExtern.AbrManager.prototype.chooseVariant = function() {};
/**
- * Enables automatic Stream choices from the last StreamSets passed to
- * chooseStreams(). After this, the AbrManager may call switchCallback() at any
- * time.
+ * Enables automatic Variant choices from the last ones passed to setVariants.
+ * After this, the AbrManager may call switchCallback() at any time.
*
* @exportDoc
*/
diff --git a/lib/abr/simple_abr_manager.js b/lib/abr/simple_abr_manager.js
index 6ee713308d..6aab51b989 100644
--- a/lib/abr/simple_abr_manager.js
+++ b/lib/abr/simple_abr_manager.js
@@ -21,7 +21,6 @@ goog.require('goog.asserts');
goog.require('shaka.abr.EwmaBandwidthEstimator');
goog.require('shaka.log');
goog.require('shaka.util.Error');
-goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.StreamUtils');
@@ -38,13 +37,9 @@ goog.require('shaka.util.StreamUtils');
* always pick the highest bandwidth variant it thinks can be played.
*
*
- * After the initial choice (in chooseStreams), this class will call
- * switchCallback() when there is a better choice. switchCallback() will not
- * be called more than once per
- * ({@link shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS}).
- *
- *
- * This does not adapt for text streams, it will always select the first one.
+ * After initial choices are made, this class will call switchCallback() when
+ * there is a better choice. switchCallback() will not be called more than once
+ * per ({@link shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS}).
*
*
* @constructor
@@ -68,18 +63,11 @@ shaka.abr.SimpleAbrManager = function() {
*/
this.variants_ = [];
- /**
- * A filtered list of text streams to choose from.
- * @private {!Array.}
- */
- this.textStreams_ = [];
-
/** @private {boolean} */
this.startupComplete_ = false;
/**
- * The last wall-clock time, in milliseconds, when Streams were chosen via
- * chooseStreams() or switch_().
+ * The last wall-clock time, in milliseconds, when streams were chosen.
*
* @private {?number}
*/
@@ -98,7 +86,6 @@ shaka.abr.SimpleAbrManager.prototype.stop = function() {
this.switch_ = null;
this.enabled_ = false;
this.variants_ = [];
- this.textStreams_ = [];
this.lastTimeChosenMs_ = null;
// Don't reset |startupComplete_|: if we've left the startup interval then we
@@ -119,26 +106,41 @@ shaka.abr.SimpleAbrManager.prototype.init = function(switchCallback) {
* @override
* @export
*/
-shaka.abr.SimpleAbrManager.prototype.chooseStreams = function(
- mediaTypesToUpdate) {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
- // Choose streams for the specific types requested.
- var chosen = {};
-
- if (mediaTypesToUpdate.indexOf(ContentType.AUDIO) > -1 ||
- mediaTypesToUpdate.indexOf(ContentType.VIDEO) > -1) {
- // Choose a new Variant
- var variant = this.chooseVariant_();
- if (variant && variant.video)
- chosen[ContentType.VIDEO] = variant.video;
-
- if (variant && variant.audio)
- chosen[ContentType.AUDIO] = variant.audio;
+shaka.abr.SimpleAbrManager.prototype.chooseVariant = function() {
+ // Alias.
+ var SimpleAbrManager = shaka.abr.SimpleAbrManager;
+
+ // Get sorted Variants.
+ var sortedVariants = SimpleAbrManager.filterAndSortVariants_(
+ this.config_.restrictions, this.variants_);
+ var currentBandwidth = this.bandwidthEstimator_.getBandwidthEstimate(
+ this.config_.defaultBandwidthEstimate);
+
+ if (this.variants_.length && !sortedVariants.length) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MANIFEST,
+ shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET);
}
- if (mediaTypesToUpdate.indexOf(ContentType.TEXT) > -1) {
- // We don't adapt text, so just choose stream 0.
- chosen[ContentType.TEXT] = this.textStreams_[0];
+ // Start by assuming that we will use the first Stream.
+ var chosen = sortedVariants[0] || null;
+
+ for (var i = 0; i < sortedVariants.length; ++i) {
+ var variant = sortedVariants[i];
+ var nextVariant = sortedVariants[i + 1] || {bandwidth: Infinity};
+
+ var minBandwidth = variant.bandwidth /
+ this.config_.bandwidthDowngradeTarget;
+ var maxBandwidth = nextVariant.bandwidth /
+ this.config_.bandwidthUpgradeTarget;
+ shaka.log.v2('Bandwidth ranges:',
+ (variant.bandwidth / 1e6).toFixed(3),
+ (minBandwidth / 1e6).toFixed(3),
+ (maxBandwidth / 1e6).toFixed(3));
+
+ if (currentBandwidth >= minBandwidth && currentBandwidth <= maxBandwidth)
+ chosen = variant;
}
this.lastTimeChosenMs_ = Date.now();
@@ -172,7 +174,9 @@ shaka.abr.SimpleAbrManager.prototype.segmentDownloaded = function(
deltaTimeMs, numBytes) {
shaka.log.v2('Segment downloaded:',
'deltaTimeMs=' + deltaTimeMs,
- 'numBytes=' + numBytes);
+ 'numBytes=' + numBytes,
+ 'lastTimeChosenMs=' + this.lastTimeChosenMs_,
+ 'enabled=' + this.enabled_);
goog.asserts.assert(deltaTimeMs >= 0, 'expected a non-negative duration');
this.bandwidthEstimator_.sample(deltaTimeMs, numBytes);
@@ -200,15 +204,6 @@ shaka.abr.SimpleAbrManager.prototype.setVariants = function(variants) {
};
-/**
- * @override
- * @export
- */
-shaka.abr.SimpleAbrManager.prototype.setTextStreams = function(streams) {
- this.textStreams_ = streams;
-};
-
-
/**
* @override
* @export
@@ -228,8 +223,6 @@ shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
goog.asserts.assert(this.lastTimeChosenMs_ != null,
'lastTimeChosenMs_ should not be null');
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
-
if (!this.startupComplete_) {
// Check if we've got enough data yet.
if (!this.bandwidthEstimator_.hasGoodEstimate()) {
@@ -247,7 +240,7 @@ shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
}
}
- var chosen = this.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ var chosenVariant = this.chooseVariant();
var bandwidthEstimate = this.bandwidthEstimator_.getBandwidthEstimate(
this.config_.defaultBandwidthEstimate);
var currentBandwidthKbps = Math.round(bandwidthEstimate / 1000.0);
@@ -256,54 +249,7 @@ shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps');
// If any of these chosen streams are already chosen, Player will filter them
// out before passing the choices on to StreamingEngine.
- this.switch_(chosen);
-};
-
-
-/**
- * Chooses a Variant with an optimal bandwidth.
- *
- * @return {shakaExtern.Variant}
- * @private
- */
-shaka.abr.SimpleAbrManager.prototype.chooseVariant_ = function() {
- // Alias.
- var SimpleAbrManager = shaka.abr.SimpleAbrManager;
-
- // Get sorted Streams.
- var sortedVariants = SimpleAbrManager.filterAndSortVariants_(
- this.config_.restrictions, this.variants_);
- var currentBandwidth = this.bandwidthEstimator_.getBandwidthEstimate(
- this.config_.defaultBandwidthEstimate);
-
- if (this.variants_.length && !sortedVariants.length) {
- throw new shaka.util.Error(
- shaka.util.Error.Severity.CRITICAL,
- shaka.util.Error.Category.MANIFEST,
- shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET);
- }
-
- // Start by assuming that we will use the first Stream.
- var chosen = sortedVariants[0];
-
- for (var i = 0; i < sortedVariants.length; ++i) {
- var variant = sortedVariants[i];
- var nextVariant = sortedVariants[i + 1] || {bandwidth: Infinity};
-
- var minBandwidth = variant.bandwidth /
- this.config_.bandwidthDowngradeTarget;
- var maxBandwidth = nextVariant.bandwidth /
- this.config_.bandwidthUpgradeTarget;
- shaka.log.v2('Bandwidth ranges:',
- (variant.bandwidth / 1e6).toFixed(3),
- (minBandwidth / 1e6).toFixed(3),
- (maxBandwidth / 1e6).toFixed(3));
-
- if (currentBandwidth >= minBandwidth && currentBandwidth <= maxBandwidth)
- chosen = variant;
- }
-
- return chosen;
+ this.switch_(chosenVariant);
};
diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js
index 9767a02677..de42759010 100644
--- a/lib/media/streaming_engine.js
+++ b/lib/media/streaming_engine.js
@@ -137,14 +137,27 @@ shaka.media.StreamingEngine = function(manifest, playerInterface) {
};
+/**
+ * @typedef {{
+ * variant: (?shakaExtern.Variant|undefined),
+ * text: ?shakaExtern.Stream
+ * }}
+ *
+ * @property {(?shakaExtern.Variant|undefined)} variant
+ * The chosen variant. May be omitted for text re-init.
+ * @property {?shakaExtern.Stream} text
+ * The chosen text stream.
+ */
+shaka.media.StreamingEngine.ChosenStreams;
+
+
/**
* @typedef {{
* playhead: !shaka.media.Playhead,
* mediaSourceEngine: !shaka.media.MediaSourceEngine,
* netEngine: shaka.net.NetworkingEngine,
* onChooseStreams: function(!shakaExtern.Period):
- * !Object.,
+ * shaka.media.StreamingEngine.ChosenStreams,
* onCanSwitch: function(),
* onError: function(!shaka.util.Error),
* onEvent: function(!Event),
@@ -161,18 +174,19 @@ shaka.media.StreamingEngine = function(manifest, playerInterface) {
* @property {shaka.net.NetworkingEngine} netEngine
* The NetworkingEngine instance to use. The caller retains ownership.
* @property {function(!shakaExtern.Period):
- * !Object.} onChooseStreams
- * Called when the given Period needs to be buffered. The
- * StreamingEngine will switch to the Streams returned from this function.
- * The caller cannot call switch() directly until the StreamingEngine calls
- * onCanSwitch()
+ * shaka.media.StreamingEngine.ChosenStreams} onChooseStreams
+ * Called by StreamingEngine when the given Period needs to be buffered.
+ * StreamingEngine will switch to the variant and text stream returned from
+ * this function.
+ * The owner cannot call switch() directly until the StreamingEngine calls
+ * onCanSwitch().
* @property {function()} onCanSwitch
- * Called when any Stream within the current Period may be switched to.
+ * Called by StreamingEngine when the Period is set up and switching is
+ * permitted.
* @property {function(!shaka.util.Error)} onError
* Called when an error occurs. If the error is recoverable (see
* @link{shaka.util.Error}) then the caller may invoke either
- * StreamingEngine.switch() or StreamingEngine.seeked() to attempt recovery.
+ * StreamingEngine.switch*() or StreamingEngine.seeked() to attempt recovery.
* @property {function(!Event)} onEvent
* Called when an event occurs that should be sent to the app.
* @property {function()} onManifestUpdate
@@ -319,7 +333,6 @@ shaka.media.StreamingEngine.prototype.configure = function(config) {
* @return {!Promise}
*/
shaka.media.StreamingEngine.prototype.init = function() {
- var MapUtils = shaka.util.MapUtils;
goog.asserts.assert(this.config_,
'StreamingEngine configure() must be called before init()!');
@@ -328,9 +341,9 @@ shaka.media.StreamingEngine.prototype.init = function() {
var needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
// Get the initial set of Streams.
- var streamsByType = this.playerInterface_.onChooseStreams(
+ var initialStreams = this.playerInterface_.onChooseStreams(
this.manifest_.periods[needPeriodIndex]);
- if (MapUtils.empty(streamsByType)) {
+ if (!initialStreams.variant && !initialStreams.text) {
shaka.log.error('init: no Streams chosen');
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
@@ -340,7 +353,7 @@ shaka.media.StreamingEngine.prototype.init = function() {
// Setup the initial set of Streams and then begin each update cycle. After
// startup completes onUpdate_() will set up the remaining Periods.
- return this.initStreams_(streamsByType).then(function() {
+ return this.initStreams_(initialStreams).then(function() {
shaka.log.debug('init: completed initial Stream setup');
// Subtlety: onInitialStreamsSetup() may call switch() or seeked(), so we
@@ -406,13 +419,7 @@ shaka.media.StreamingEngine.prototype.getActiveStreams = function() {
* @return {!Promise}
*/
shaka.media.StreamingEngine.prototype.notifyNewTextStream = function(stream) {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- /** @type {!Object.} */
- var streamsByType = {};
- streamsByType[ContentType.TEXT] = stream;
- return this.initStreams_(streamsByType);
+ return this.initStreams_({ text: stream });
};
@@ -439,7 +446,7 @@ shaka.media.StreamingEngine.prototype.setTrickPlay = function(on) {
if (normalVideo) return; // Already in trick play.
shaka.log.debug('Engaging trick mode stream', trickModeVideo);
- this.switch(ContentType.VIDEO, trickModeVideo, false);
+ this.switchInternal_(trickModeVideo, false);
mediaState.restoreStreamAfterTrickPlay = stream;
} else {
var normalVideo = mediaState.restoreStreamAfterTrickPlay;
@@ -447,25 +454,51 @@ shaka.media.StreamingEngine.prototype.setTrickPlay = function(on) {
shaka.log.debug('Restoring non-trick-mode stream', normalVideo);
mediaState.restoreStreamAfterTrickPlay = null;
- this.switch(ContentType.VIDEO, normalVideo, true);
+ this.switchInternal_(normalVideo, true);
}
};
+/**
+ * @param {shakaExtern.Variant} variant
+ * @param {boolean} clearBuffer
+ */
+shaka.media.StreamingEngine.prototype.switchVariant =
+ function(variant, clearBuffer) {
+ if (variant.video) {
+ this.switchInternal_(variant.video, clearBuffer);
+ }
+
+ if (variant.audio) {
+ this.switchInternal_(variant.audio, clearBuffer);
+ }
+};
+
+
+/**
+ * @param {shakaExtern.Stream} textStream
+ */
+shaka.media.StreamingEngine.prototype.switchTextStream = function(textStream) {
+ goog.asserts.assert(textStream && textStream.type == 'text',
+ 'Wrong stream type passed to switchTextStream!');
+ this.switchInternal_(textStream, /* clearBuffer */ true);
+};
+
+
/**
* Switches to the given Stream. |stream| may be from any Variant or any
* Period.
*
- * @param {shaka.util.ManifestParserUtils.ContentType} contentType
- * |stream|'s content type.
* @param {shakaExtern.Stream} stream
* @param {boolean} clearBuffer
+ * @private
*/
-shaka.media.StreamingEngine.prototype.switch = function(
- contentType, stream, clearBuffer) {
+shaka.media.StreamingEngine.prototype.switchInternal_ = function(
+ stream, clearBuffer) {
var ContentType = shaka.util.ManifestParserUtils.ContentType;
- var mediaState = this.mediaStates_[contentType];
- if (!mediaState && contentType == ContentType.TEXT &&
+ var mediaState = this.mediaStates_[/** @type {!ContentType} */(stream.type)];
+
+ if (!mediaState && stream.type == ContentType.TEXT &&
this.config_.ignoreTextStreamFailures) {
this.notifyNewTextStream(stream);
return;
@@ -521,7 +554,7 @@ shaka.media.StreamingEngine.prototype.switch = function(
return;
}
- if (contentType == ContentType.TEXT) {
+ if (stream.type == ContentType.TEXT) {
// Mime types are allowed to change for text streams.
// Reinitialize the text parser, but only if we are going to fetch the init
// segment again.
@@ -645,15 +678,13 @@ shaka.media.StreamingEngine.prototype.clearAllBuffers_ = function() {
* Initializes the given streams and media states if required. This will
* schedule updates for the given types.
*
- * @param {!Object.} streamsByType
+ * @param {shaka.media.StreamingEngine.ChosenStreams} chosenStreams
* @param {number=} opt_resumeAt
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.initStreams_ = function(
- streamsByType, opt_resumeAt) {
- var MapUtils = shaka.util.MapUtils;
+ chosenStreams, opt_resumeAt) {
goog.asserts.assert(this.config_,
'StreamingEngine configure() must be called before init()!');
@@ -661,18 +692,42 @@ shaka.media.StreamingEngine.prototype.initStreams_ = function(
var playheadTime = this.playerInterface_.playhead.getTime();
var needPeriodIndex = this.findPeriodContainingTime_(playheadTime);
- // Init MediaSourceEngine.
- var typeConfig = MapUtils.map(streamsByType, function(stream) {
- return shaka.util.StreamUtils.getFullMimeType(
- stream.mimeType, stream.codecs);
- });
+ // Init/re-init MediaSourceEngine. Note that a re-init is only valid for text.
+ var ContentType = shaka.util.ManifestParserUtils.ContentType;
+
+ /** @type {!Object.} */
+ var typeConfig = {};
+ /** @type {!Object.} */
+ var streamsByType = {};
+ /** @type {!Array.} */
+ var streams = [];
+
+ if (chosenStreams.variant && chosenStreams.variant.audio) {
+ typeConfig[ContentType.AUDIO] = shaka.util.StreamUtils.getFullMimeType(
+ chosenStreams.variant.audio.mimeType,
+ chosenStreams.variant.audio.codecs);
+ streamsByType[ContentType.AUDIO] = chosenStreams.variant.audio;
+ streams.push(chosenStreams.variant.audio);
+ }
+ if (chosenStreams.variant && chosenStreams.variant.video) {
+ typeConfig[ContentType.VIDEO] = shaka.util.StreamUtils.getFullMimeType(
+ chosenStreams.variant.video.mimeType,
+ chosenStreams.variant.video.codecs);
+ streamsByType[ContentType.VIDEO] = chosenStreams.variant.video;
+ streams.push(chosenStreams.variant.video);
+ }
+ if (chosenStreams.text) {
+ typeConfig[ContentType.TEXT] = shaka.util.StreamUtils.getFullMimeType(
+ chosenStreams.text.mimeType, chosenStreams.text.codecs);
+ streamsByType[ContentType.TEXT] = chosenStreams.text;
+ streams.push(chosenStreams.text);
+ }
this.playerInterface_.mediaSourceEngine.init(typeConfig);
this.setDuration_();
// Setup the initial set of Streams and then begin each update cycle. After
// startup completes onUpdate_() will set up the remaining Periods.
- var streams = MapUtils.values(streamsByType);
return this.setupStreams_(streams).then(function() {
if (this.destroyed_) return;
@@ -1749,7 +1804,17 @@ shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
var needPeriod = this.manifest_.periods[needPeriodIndex];
shaka.log.v1(logPrefix, 'calling onChooseStreams()...');
- var streamsByType = this.playerInterface_.onChooseStreams(needPeriod);
+ var chosenStreams = this.playerInterface_.onChooseStreams(needPeriod);
+ var streamsByType = {};
+ if (chosenStreams.variant && chosenStreams.variant.video) {
+ streamsByType[ContentType.VIDEO] = chosenStreams.variant.video;
+ }
+ if (chosenStreams.variant && chosenStreams.variant.audio) {
+ streamsByType[ContentType.AUDIO] = chosenStreams.variant.audio;
+ }
+ if (chosenStreams.text) {
+ streamsByType[ContentType.TEXT] = chosenStreams.text;
+ }
// Vet |streamsByType| before switching.
for (var type in this.mediaStates_) {
@@ -1765,7 +1830,8 @@ shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
}
for (var type in streamsByType) {
- if (this.mediaStates_[type]) continue;
+ if (this.mediaStates_[/** @type {!ContentType} */(type)]) continue;
+
if (type == ContentType.TEXT) {
// initStreams_ will switch streams and schedule an update.
this.initStreams_(
@@ -1786,7 +1852,7 @@ shaka.media.StreamingEngine.prototype.handlePeriodTransition_ = function(
for (var type in this.mediaStates_) {
var stream = streamsByType[type];
if (stream) {
- this.switch(type, stream, /* clearBuffer */ false);
+ this.switchInternal_(stream, /* clearBuffer */ false);
this.scheduleUpdate_(this.mediaStates_[type], 0);
} else {
goog.asserts.assert(type == ContentType.TEXT, 'Invalid streams chosen');
diff --git a/lib/player.js b/lib/player.js
index c385dd7956..c3bdce123c 100644
--- a/lib/player.js
+++ b/lib/player.js
@@ -138,13 +138,14 @@ shaka.Player = function(video, opt_dependencyInjector) {
/** @private {Promise} */
this.unloadChain_ = null;
- /**
- * @private {!Object.}
- */
- this.deferredSwitches_ = {};
+ /** @private {?shakaExtern.Variant} */
+ this.deferredVariant_ = null;
+
+ /** @private {boolean} */
+ this.deferredVariantClearBuffer_ = false;
+
+ /** @private {?shakaExtern.Stream} */
+ this.deferredTextStream_ = null;
/** @private {!Array.} */
this.pendingTimelineRegions_ = [];
@@ -613,7 +614,15 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime,
this.mediaSourceOpen_
]);
}.bind(this)).then(function() {
- this.abrManager_.init(this.switch_.bind(this));
+ if (this.abrManager_['chooseStreams']) {
+ shaka.log.warning('AbrManager API has changed. ' +
+ 'The SwitchCallback signature has changed to accept a variant ' +
+ 'instead of a map. Please update your AbrManager. ' +
+ 'The old API will be removed in v2.3.');
+ this.abrManager_['init'](this.switchV21_.bind(this));
+ } else {
+ this.abrManager_.init(this.switch_.bind(this));
+ }
// MediaSource is open, so create the Playhead, MediaSourceEngine, and
// StreamingEngine.
var startTime = opt_startTime || this.config_.playRangeStart;
@@ -1272,7 +1281,8 @@ shaka.Player.prototype.getVariantTracks = function() {
this.manifest_, this.playhead_.getTime());
var activeStreams = this.activeStreamsByPeriod_[currentPeriod] || {};
return shaka.util.StreamUtils.getVariantTracks(
- this.manifest_.periods[currentPeriod], activeStreams[ContentType.AUDIO],
+ this.manifest_.periods[currentPeriod],
+ activeStreams[ContentType.AUDIO],
activeStreams[ContentType.VIDEO]);
};
@@ -1317,7 +1327,6 @@ shaka.Player.prototype.selectTextTrack = function(track) {
return;
var StreamUtils = shaka.util.StreamUtils;
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
var period = this.streamingEngine_.getCurrentPeriod();
var stream = StreamUtils.findTextStreamForTrack(period, track);
@@ -1328,12 +1337,9 @@ shaka.Player.prototype.selectTextTrack = function(track) {
return;
}
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var streamsToSwitch = {};
- streamsToSwitch[ContentType.TEXT] = stream;
- this.addToSwitchHistory_(period, streamsToSwitch, /* fromAdaptation */ false);
- this.deferredSwitch_(streamsToSwitch, /* opt_clearBuffer */ true);
+ // Add entries to the history.
+ this.addTextStreamToSwitchHistory_(stream, /* fromAdaptation */ false);
+ this.switchTextStream_(stream);
};
@@ -1357,51 +1363,29 @@ shaka.Player.prototype.selectVariantTrack = function(track, opt_clearBuffer) {
}
var StreamUtils = shaka.util.StreamUtils;
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
- var streamsToSwitch = {};
var period = this.streamingEngine_.getCurrentPeriod();
var variant = StreamUtils.findVariantForTrack(period, track);
- var activeStreams = this.streamingEngine_.getActiveStreams();
- if (variant) {
- // Double check that the track is allowed to be played.
- // The track list should only contain playable variants,
- // but if restrictions change and selectVariantTrack()
- // is called before the track list is updated, we could
- // get a now-restricted variant.
- var variantIsPlayable = StreamUtils.isPlayable(variant);
- if (!variantIsPlayable) {
- shaka.log.error('Unable to switch to track with id "' + track.id +
- '" because it is restricted.');
- return;
- }
-
- if (variant.audio)
- streamsToSwitch[ContentType.AUDIO] = variant.audio;
-
- if (variant.video)
- streamsToSwitch[ContentType.VIDEO] = variant.video;
+ if (!variant) {
+ shaka.log.error('Unable to locate track with id "' + track.id + '".');
+ return;
}
- // Add entries to the history.
- this.addToSwitchHistory_(period, streamsToSwitch, /* fromAdaptation */ false);
-
- // Save current text stream to ensure that it doesn't get overridden
- // by a default one inside shaka.Player.configure()
- var currentTextStream = activeStreams[ContentType.TEXT];
-
- if (currentTextStream) {
- streamsToSwitch[ContentType.TEXT] = currentTextStream;
+ // Double check that the track is allowed to be played.
+ // The track list should only contain playable variants,
+ // but if restrictions change and selectVariantTrack()
+ // is called before the track list is updated, we could
+ // get a now-restricted variant.
+ var variantIsPlayable = StreamUtils.isPlayable(variant);
+ if (!variantIsPlayable) {
+ shaka.log.error('Unable to switch to track with id "' + track.id +
+ '" because it is restricted.');
+ return;
}
- // Stream could already be active by being part of another variant.
- // If that's the case, remove it from streamsToSwitch.
- if (streamsToSwitch[ContentType.AUDIO] == activeStreams[ContentType.AUDIO])
- delete streamsToSwitch[ContentType.AUDIO];
- if (streamsToSwitch[ContentType.VIDEO] == activeStreams[ContentType.VIDEO])
- delete streamsToSwitch[ContentType.VIDEO];
-
- this.deferredSwitch_(streamsToSwitch, opt_clearBuffer);
+ // Add entries to the history.
+ this.addVariantToSwitchHistory_(variant, /* fromAdaptation */ false);
+ this.switchVariant_(variant, opt_clearBuffer);
};
@@ -1729,29 +1713,29 @@ shaka.Player.prototype.initialize_ = function() {
/**
- * @param {shakaExtern.Period} period
- * @param {!Object.} streams
+ * @param {shakaExtern.Variant} variant
* @param {boolean} fromAdaptation
* @private
*/
-shaka.Player.prototype.addToSwitchHistory_ =
- function(period, streams, fromAdaptation) {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
- var variant = null;
- var textStream = null;
+shaka.Player.prototype.addVariantToSwitchHistory_ =
+ function(variant, fromAdaptation) {
+ if (variant.video)
+ this.updateActiveStreams_(variant.video);
+ if (variant.audio)
+ this.updateActiveStreams_(variant.audio);
- if (streams[ContentType.AUDIO] || streams[ContentType.VIDEO]) {
- var audio = streams[ContentType.AUDIO] || null;
- var video = streams[ContentType.VIDEO] || null;
- variant = this.findVariantByStreams_(period, audio, video);
- goog.asserts.assert(variant != null, 'Should not be null!');
- }
-
- if (streams[ContentType.TEXT]) {
- textStream = streams[ContentType.TEXT];
- }
+ // TODO: Get StreamingEngine to track variants and create getActiveVariant()
+ var ContentType = shaka.util.ManifestParserUtils.ContentType;
+ var activePeriod = this.streamingEngine_.getActivePeriod();
+ var activeStreams = this.streamingEngine_.getActiveStreams();
+ var activeVariant = shaka.util.StreamUtils.getVariantByStreams(
+ activeStreams[ContentType.AUDIO], activeStreams[ContentType.VIDEO],
+ activePeriod ? activePeriod.variants : []);
- if (variant) {
+ // Only log the switch if the variant changes. For the initial decision,
+ // activeVariant is null and variant != activeVariant in this case, too.
+ // This allows us to avoid onAdaptation_() when nothing has changed.
+ if (variant != activeVariant) {
this.stats_.switchHistory.push({
timestamp: Date.now() / 1000,
id: variant.id,
@@ -1759,24 +1743,26 @@ shaka.Player.prototype.addToSwitchHistory_ =
fromAdaptation: fromAdaptation,
bandwidth: variant.bandwidth
});
-
- if (variant.video)
- this.updateActiveStreams_(variant.video);
- if (variant.audio)
- this.updateActiveStreams_(variant.audio);
}
+};
- if (textStream) {
- this.stats_.switchHistory.push({
- timestamp: Date.now() / 1000,
- id: textStream.id,
- type: 'text',
- fromAdaptation: fromAdaptation,
- bandwidth: null
- });
- this.updateActiveStreams_(textStream);
- }
+/**
+ * @param {shakaExtern.Stream} textStream
+ * @param {boolean} fromAdaptation
+ * @private
+ */
+shaka.Player.prototype.addTextStreamToSwitchHistory_ =
+ function(textStream, fromAdaptation) {
+ this.updateActiveStreams_(textStream);
+
+ this.stats_.switchHistory.push({
+ timestamp: Date.now() / 1000,
+ id: textStream.id,
+ type: 'text',
+ fromAdaptation: fromAdaptation,
+ bandwidth: null
+ });
};
@@ -1836,7 +1822,6 @@ shaka.Player.prototype.destroyStreaming_ = function() {
this.mediaSource_ = null;
this.pendingTimelineRegions_ = [];
this.activeStreamsByPeriod_ = {};
- this.deferredSwitches_ = {};
this.stats_ = this.getCleanStats_();
return p;
@@ -2044,26 +2029,36 @@ shaka.Player.prototype.filterPeriod_ = function(period) {
/**
- * Switches to the given streams, deferring switches if needed.
- * @param {!Object.} streamsByType
+ * Switches to the given variant, deferring if needed.
+ * @param {shakaExtern.Variant} variant
* @param {boolean=} opt_clearBuffer
* @private
*/
-shaka.Player.prototype.deferredSwitch_ = function(
- streamsByType, opt_clearBuffer) {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
- for (var type in streamsByType) {
- var stream = streamsByType[type];
- var clearBuffer = opt_clearBuffer || false;
- // TODO: consider adding a cue replacement algorithm to TextEngine to remove
- // this special case for text:
- if (type == ContentType.TEXT) clearBuffer = true;
- if (this.switchingPeriods_) {
- this.deferredSwitches_[type] = {stream: stream, clearBuffer: clearBuffer};
- } else {
- this.streamingEngine_.switch(type, stream, clearBuffer);
- }
+shaka.Player.prototype.switchVariant_ =
+ function(variant, opt_clearBuffer) {
+ if (this.switchingPeriods_) {
+ // Store this action for later.
+ this.deferredVariant_ = variant;
+ this.deferredVariantClearBuffer_ = opt_clearBuffer || false;
+ } else {
+ // Act now.
+ this.streamingEngine_.switchVariant(variant, opt_clearBuffer || false);
+ }
+};
+
+
+/**
+ * Switches to the given text stream, deferring if needed.
+ * @param {shakaExtern.Stream} textStream
+ * @private
+ */
+shaka.Player.prototype.switchTextStream_ = function(textStream) {
+ if (this.switchingPeriods_) {
+ // Store this action for later.
+ this.deferredTextStream_ = textStream;
+ } else {
+ // Act now.
+ this.streamingEngine_.switchTextStream(textStream);
}
};
@@ -2094,8 +2089,18 @@ shaka.Player.prototype.assertCorrectActiveStreams_ = function() {
var playerActive = this.activeStreamsByPeriod_[currentPeriodIndex] || {};
for (var type in streamingActive) {
var activeId = streamingActive[type].id;
- if (this.deferredSwitches_[type])
- activeId = this.deferredSwitches_[type].stream.id;
+
+ if (type == ContentType.TEXT) {
+ if (this.deferredTextStream_)
+ activeId = this.deferredTextStream_.id;
+ } else if (type == ContentType.AUDIO) {
+ if (this.deferredVariant_)
+ activeId = this.deferredVariant_.audio.id;
+ } else if (type == ContentType.VIDEO) {
+ if (this.deferredVariant_)
+ activeId = this.deferredVariant_.video.id;
+ }
+
goog.asserts.assert(activeId == playerActive[type],
'Inconsistent active stream');
}
@@ -2259,181 +2264,99 @@ shaka.Player.prototype.onSeek_ = function() {
/**
- * Chooses streams from the given Period.
+ * Choose a variant through ABR manager.
+ * On error, dispatches an error event and returns null.
*
- * @param {!shakaExtern.Period} period
- * @param {!Array.} variants
- * @param {!Array.} textStreams
- * @param {boolean=} opt_chooseAll If true, choose streams of every type.
- * @return {!Object.} A map of stream types to
- * chosen streams.
+ * @param {!Array.} variants
+ * @return {?shakaExtern.Variant}
* @private
*/
-shaka.Player.prototype.chooseStreams_ =
- function(period, variants, textStreams, opt_chooseAll) {
+shaka.Player.prototype.chooseVariant_ = function(variants) {
goog.asserts.assert(this.config_, 'Must not be destroyed');
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
-
- // Issue an error if there are no playable variants
- if (!variants || variants.length < 1) {
+ if (!variants || !variants.length) {
this.onError_(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET));
- return {};
+ return null;
}
- // Update abr manager with newly filtered streams and variants.
+ // Update abr manager with newly filtered variants.
this.abrManager_.setVariants(variants);
- this.abrManager_.setTextStreams(textStreams);
-
- var needsUpdate = [];
- if (opt_chooseAll) {
- needsUpdate = [ContentType.VIDEO, ContentType.AUDIO];
- if (period.textStreams.length) needsUpdate.push(ContentType.TEXT);
- }
- // Check if any of the active streams is no longer available
- // or is using the wrong language.
- var activeStreams = this.streamingEngine_.getActiveStreams();
- // activePeriod may reasonably be null before StreamingEngine is streaming.
- var activePeriod = this.streamingEngine_.getActivePeriod();
- var activeVariant = shaka.util.StreamUtils.getVariantByStreams(
- activeStreams[ContentType.AUDIO],
- activeStreams[ContentType.VIDEO],
- activePeriod ? activePeriod.variants : period.variants);
-
- if (activeVariant) {
- if (!activeVariant.allowedByApplication ||
- !activeVariant.allowedByKeySystem) {
- needsUpdate.push(ContentType.AUDIO);
- needsUpdate.push(ContentType.VIDEO);
- }
+ // Backward compatibility for the AbrManager plugin interface.
+ if (this.abrManager_['chooseStreams']) {
+ shaka.log.warning('AbrManager API has changed. ' +
+ 'AbrManager.chooseStreams() is deprecated. ' +
+ 'Please implement AbrManager.chooseVariant() to upgrade. ' +
+ 'The old API will be removed in v2.3.');
- for (var type in activeStreams) {
- var stream = activeStreams[type];
- if (stream.type == ContentType.AUDIO &&
- stream.language != variants[0].language) {
- needsUpdate.push(type);
- } else if (stream.type == ContentType.TEXT && textStreams.length > 0 &&
- stream.language != textStreams[0].language) {
- needsUpdate.push(type);
- }
- }
+ var ContentType = shaka.util.ManifestParserUtils.ContentType;
+ var mediaTypesToUpdate = ['video', 'audio'];
+ var chosen = this.abrManager_['chooseStreams'](mediaTypesToUpdate);
+ var chosenVariant = shaka.util.StreamUtils.getVariantByStreams(
+ chosen[ContentType.AUDIO], chosen[ContentType.VIDEO], variants);
+ return chosenVariant;
}
- needsUpdate = needsUpdate.filter(shaka.util.Functional.isNotDuplicate);
-
- if (needsUpdate.length > 0) {
- shaka.log.debug('Choosing new streams for', needsUpdate);
- var chosen = {};
- try {
- chosen = this.abrManager_.chooseStreams(needsUpdate);
- } catch (err) {
- this.onError_(err);
- }
-
- return chosen;
- } else {
- shaka.log.debug('No new streams need to be chosen.');
- return {};
- }
+ return this.abrManager_.chooseVariant();
};
/**
* Chooses streams from the given Period and switches to them.
- * Called after a config change, a new text stream, or a key status event.
+ * Called after a config change, a new text stream, a key status event, or an
+ * explicit language change.
*
* @param {!shakaExtern.Period} period
* @private
*/
shaka.Player.prototype.chooseStreamsAndSwitch_ = function(period) {
goog.asserts.assert(this.config_, 'Must not be destroyed');
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
-
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var languageMatches = {};
- languageMatches[ContentType.AUDIO] = false;
- languageMatches[ContentType.TEXT] = false;
var variants = shaka.util.StreamUtils.filterVariantsByRoleAndLanguage(
- period, this.currentAudioLanguage_, languageMatches,
+ period, this.currentAudioLanguage_, /* opt_languageMatches */ undefined,
this.currentVariantRole_);
var textStreams = shaka.util.StreamUtils.filterTextStreamsByRoleAndLanguage(
- period, this.currentTextLanguage_, languageMatches,
+ period, this.currentTextLanguage_, /* opt_languageMatches */ undefined,
this.currentTextRole_);
- // chooseStreams_ filters out choices which are already active.
- var chosen = this.chooseStreams_(period, variants, textStreams);
-
- this.addToSwitchHistory_(period, chosen, /* fromAdaptation */ true);
-
// Because we're running this after a config change (manual language change),
- // a new text stream, or a key status event, and because active streams have
- // been filtered out already, it is always okay to clear the buffer for what
- // remains.
- this.deferredSwitch_(chosen, /* opt_clearBuffer */ true);
-
- // Send an adaptation event so that the UI can show the new language/tracks.
- this.onAdaptation_();
-
- if (chosen[ContentType.TEXT]) {
- // If audio and text tracks have different languages, and the text track
- // matches the user's preference, then show the captions.
- if (chosen[ContentType.AUDIO] &&
- languageMatches[ContentType.TEXT] &&
- chosen[ContentType.TEXT].language !=
- chosen[ContentType.AUDIO].language) {
- this.textDisplayer_.setTextVisibility(true);
- this.onTextTrackVisibility_();
- }
+ // a new text stream, or a key status event, and because switching to an
+ // active stream is a no-op, it is always okay to clear the buffer here.
+ var chosenVariant = this.chooseVariant_(variants);
+ if (chosenVariant) {
+ this.addVariantToSwitchHistory_(chosenVariant, /* fromAdaptation */ true);
+ this.switchVariant_(chosenVariant, /* opt_clearBuffer */ true);
}
-};
-
-/**
- * Given audio and video streams returns a variant they are part of.
- *
- * @param {!shakaExtern.Period} period
- * @param {shakaExtern.Stream} audio
- * @param {shakaExtern.Stream} video
- * @return {?shakaExtern.Variant}
- * @private
- */
-shaka.Player.prototype.findVariantByStreams_ = function(period, audio, video) {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
- goog.asserts.assert(audio || video,
- 'Variant should have at least one stream!');
-
- goog.asserts.assert((!audio || audio.type == ContentType.AUDIO) &&
- (!video || video.type == ContentType.VIDEO),
- 'Stream parameters mismatch!');
-
- for (var i = 0; i < period.variants.length; i++) {
- var variant = period.variants[i];
- if (variant.audio == audio && variant.video == video)
- return variant;
+ var chosenText = textStreams[0];
+ if (chosenText) {
+ this.addTextStreamToSwitchHistory_(chosenText, /* fromAdaptation */ true);
+ this.switchTextStream_(chosenText);
}
- return null;
+ // Send an adaptation event so that the UI can show the new language/tracks.
+ this.onAdaptation_();
};
/**
- * Callback from StreamingEngine.
+ * Callback from StreamingEngine, invoked when a period starts.
*
* @param {!shakaExtern.Period} period
- * @return {!Object.} A map of stream types to
- * chosen streams.
+ * @return {shaka.media.StreamingEngine.ChosenStreams} An object containing the
+ * chosen variant and text stream.
* @private
*/
shaka.Player.prototype.onChooseStreams_ = function(period) {
shaka.log.debug('onChooseStreams_', period);
goog.asserts.assert(this.config_, 'Must not be destroyed');
+ var ContentType = shaka.util.ManifestParserUtils.ContentType;
+ var StreamUtils = shaka.util.StreamUtils;
+
// We are switching Periods, so the AbrManager will be disabled. But if we
// want to abr.enabled, we do not want to call AbrManager.enable before
// canSwitch_ is called.
@@ -2441,60 +2364,112 @@ shaka.Player.prototype.onChooseStreams_ = function(period) {
this.abrManager_.disable();
shaka.log.debug('Choosing new streams after period changed');
- var variants = shaka.util.StreamUtils.filterVariantsByRoleAndLanguage(
- period, this.currentAudioLanguage_, /* opt_languageMatches */ undefined,
+
+ // Create empty object first and initialize the fields through
+ // [] to allow field names to be expressions.
+ // TODO: this feedback system for language matches could be cleaned up
+ var languageMatches = {};
+ languageMatches[ContentType.AUDIO] = false;
+ languageMatches[ContentType.TEXT] = false;
+
+ var variants = StreamUtils.filterVariantsByRoleAndLanguage(
+ period, this.currentAudioLanguage_, languageMatches,
this.currentVariantRole_);
- var textStreams = shaka.util.StreamUtils.filterTextStreamsByRoleAndLanguage(
- period, this.currentTextLanguage_, /* opt_languageMatches */ undefined,
+ var textStreams = StreamUtils.filterTextStreamsByRoleAndLanguage(
+ period, this.currentTextLanguage_, languageMatches,
this.currentTextRole_);
shaka.log.v2('onChooseStreams_, variants and text streams: ',
variants, textStreams);
- var chosen = this.chooseStreams_(
- period, variants, textStreams, /* opt_chooseAll */ true);
- shaka.log.v2('onChooseStreams_, chosen=', chosen);
-
- // Override the chosen streams with the ones picked in
- // selectVariant/TextTrack. NOTE: The apparent race between
- // selectVariant/TextTrack and period transition is handled by
- // StreamingEngine, which will re-request tracks for the transition if any
- // of these deferred selections are from the wrong period.
- for (var type in this.deferredSwitches_) {
- // We are choosing initial tracks, so no segments from this Period have
- // been downloaded yet. Therefore, it is okay to ignore the .clearBuffer
- // member of this structure.
- chosen[type] = this.deferredSwitches_[type].stream;
+ var chosenVariant = this.chooseVariant_(variants);
+ var chosenTextStream = textStreams[0] || null;
+
+ shaka.log.v2('onChooseStreams_, chosen=', chosenVariant, chosenTextStream);
+
+ // Ignore deferred variant or text streams. We are starting a new period,
+ // so any deferred switches must logically have been from an older period.
+ // Verify this in uncompiled mode.
+ if (!COMPILED) {
+ var deferredPeriodIndex;
+ var deferredPeriod;
+
+ // This assertion satisfies a compiler nullability check below.
+ goog.asserts.assert(this.manifest_, 'Manifest should exist!');
+
+ if (this.deferredVariant_) {
+ deferredPeriodIndex = StreamUtils.findPeriodContainingVariant(
+ this.manifest_, this.deferredVariant_);
+ deferredPeriod = this.manifest_.periods[deferredPeriodIndex];
+ goog.asserts.assert(
+ deferredPeriod != period,
+ 'Mistakenly ignoring deferred variant from the same period!');
+ }
+
+ if (this.deferredTextStream_) {
+ deferredPeriodIndex = StreamUtils.findPeriodContainingStream(
+ this.manifest_, this.deferredTextStream_);
+ deferredPeriod = this.manifest_.periods[deferredPeriodIndex];
+ goog.asserts.assert(
+ deferredPeriod != period,
+ 'Mistakenly ignoring deferred text stream from the same period!');
+ }
}
- this.deferredSwitches_ = {};
+ this.deferredVariant_ = null;
+ this.deferredTextStream_ = null;
- this.addToSwitchHistory_(period, chosen, /* fromAdaptation */ true);
+ if (chosenVariant) {
+ this.addVariantToSwitchHistory_(chosenVariant, /* fromAdaptation */ true);
+ }
+ if (chosenTextStream) {
+ this.addTextStreamToSwitchHistory_(
+ chosenTextStream, /* fromAdaptation */ true);
+
+ // If audio and text tracks have different languages, and the text track
+ // matches the user's preference, then show the captions. Only do this
+ // when we are choosing the initial tracks during startup.
+ var startingUp = !this.streamingEngine_.getActivePeriod();
+ if (startingUp) {
+ if (chosenVariant && chosenVariant.audio &&
+ languageMatches[ContentType.TEXT] &&
+ chosenTextStream.language != chosenVariant.audio.language) {
+ this.textDisplayer_.setTextVisibility(true);
+ this.onTextTrackVisibility_();
+ }
+ }
+ }
// Don't fire a tracks-changed event since we aren't inside the new Period
// yet.
-
- return chosen;
+ return { variant: chosenVariant, text: chosenTextStream };
};
/**
- * Callback from StreamingEngine.
+ * Callback from StreamingEngine, invoked when the period is set up.
*
* @private
*/
shaka.Player.prototype.canSwitch_ = function() {
shaka.log.debug('canSwitch_');
+ goog.asserts.assert(this.config_, 'Must not be destroyed');
+
this.switchingPeriods_ = false;
+
if (this.config_.abr.enabled)
this.abrManager_.enable();
// If we still have deferred switches, switch now.
- for (var type in this.deferredSwitches_) {
- var info = this.deferredSwitches_[type];
- this.streamingEngine_.switch(type, info.stream, info.clearBuffer);
+ if (this.deferredVariant_) {
+ this.streamingEngine_.switchVariant(
+ this.deferredVariant_, this.deferredVariantClearBuffer_);
+ this.deferredVariant_ = null;
+ }
+ if (this.deferredTextStream_) {
+ this.streamingEngine_.switchTextStream(this.deferredTextStream_);
+ this.deferredTextStream_ = null;
}
- this.deferredSwitches_ = {};
};
@@ -2523,48 +2498,55 @@ shaka.Player.prototype.onSegmentAppended_ = function() {
/**
* Callback from AbrManager.
*
- * @param {!Object.} streamsByType
+ * @param {shakaExtern.Variant} variant
* @param {boolean=} opt_clearBuffer
* @private
*/
-shaka.Player.prototype.switch_ = function(streamsByType, opt_clearBuffer) {
+shaka.Player.prototype.switch_ = function(variant, opt_clearBuffer) {
shaka.log.debug('switch_');
goog.asserts.assert(this.config_.abr.enabled,
'AbrManager should not call switch while disabled!');
goog.asserts.assert(!this.switchingPeriods_,
'AbrManager should not call switch while transitioning between Periods!');
- // We have adapted to a new stream, record it in the history. Only add if
- // we are actually switching the stream.
- var oldActive = this.streamingEngine_.getActiveStreams();
- for (var type in streamsByType) {
- var stream = streamsByType[type];
- if (oldActive[type] == stream) {
- // If it's the same, remove it from the map.
- // This allows us to avoid onAdaptation_() when nothing has changed.
- delete streamsByType[type];
- }
- }
-
- var period = this.streamingEngine_.getCurrentPeriod();
- this.addToSwitchHistory_(period, streamsByType, /* fromAdaptation */ true);
+ this.addVariantToSwitchHistory_(variant, /* fromAdaptation */ true);
- if (shaka.util.MapUtils.empty(streamsByType)) {
- // There's nothing to change.
+ if (!this.streamingEngine_) {
+ // There's no way to change it.
return;
}
+ this.streamingEngine_.switchVariant(variant, opt_clearBuffer || false);
+ this.onAdaptation_();
+};
+
+
+/**
+ * Callback from v2.1 or v2.0 AbrManager plugins, for backward compatibility.
+ * To be removed in v2.3.
+ *
+ * @param {!Object.} streamsByType
+ * @param {boolean=} opt_clearBuffer
+ * @private
+ */
+shaka.Player.prototype.switchV21_ = function(streamsByType, opt_clearBuffer) {
if (!this.streamingEngine_) {
// There's no way to change it.
return;
}
- for (var type in streamsByType) {
- var clearBuffer = opt_clearBuffer || false;
- this.streamingEngine_.switch(type, streamsByType[type], clearBuffer);
+ var ContentType = shaka.util.ManifestParserUtils.ContentType;
+ var activePeriod = this.streamingEngine_.getActivePeriod();
+
+ var variant = shaka.util.StreamUtils.getVariantByStreams(
+ streamsByType[ContentType.AUDIO], streamsByType[ContentType.VIDEO],
+ activePeriod ? activePeriod.variants : []);
+
+ goog.asserts.assert(variant, 'Could not find variant to switch!');
+ if (variant) {
+ this.switch_(variant, opt_clearBuffer);
}
- this.onAdaptation_();
};
@@ -2731,6 +2713,7 @@ shaka.Player.prototype.onKeyStatus_ = function(keyStatusMap) {
});
});
+ // TODO: Get StreamingEngine to track variants and create getActiveVariant()
var activeStreams = this.streamingEngine_.getActiveStreams();
var activeVariant = shaka.util.StreamUtils.getVariantByStreams(
activeStreams[ContentType.AUDIO], activeStreams[ContentType.VIDEO],
diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js
index dceada697a..ebb9443ebb 100644
--- a/lib/util/stream_utils.js
+++ b/lib/util/stream_utils.js
@@ -623,6 +623,26 @@ shaka.util.StreamUtils.findPeriodContainingStream = function(manifest, stream) {
};
+/**
+ * @param {shakaExtern.Manifest} manifest
+ * @param {shakaExtern.Variant} variant
+ * @return {number} The index of the Period which contains |stream|, or -1 if
+ * no Period contains |stream|.
+ */
+shaka.util.StreamUtils.findPeriodContainingVariant =
+ function(manifest, variant) {
+ for (var periodIdx = 0; periodIdx < manifest.periods.length; ++periodIdx) {
+ var period = manifest.periods[periodIdx];
+ for (var j = 0; j < period.variants.length; ++j) {
+ if (period.variants[j] == variant) {
+ return periodIdx;
+ }
+ }
+ }
+ return -1;
+};
+
+
/**
* Gets the rebuffering goal from the manifest and configuration.
*
diff --git a/test/abr/simple_abr_manager_unit.js b/test/abr/simple_abr_manager_unit.js
index ac17e16026..deeba52936 100644
--- a/test/abr/simple_abr_manager_unit.js
+++ b/test/abr/simple_abr_manager_unit.js
@@ -16,9 +16,6 @@
*/
describe('SimpleAbrManager', function() {
- /** @const */
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
-
/** @const */
var sufficientBWMultiplier = 1.06;
/** @const */
@@ -45,8 +42,6 @@ describe('SimpleAbrManager', function() {
var manifest;
/** @type {!Array.} */
var variants;
- /** @type {!Array.} */
- var textStreams;
beforeAll(function() {
@@ -94,7 +89,6 @@ describe('SimpleAbrManager', function() {
};
variants = manifest.periods[0].variants;
- textStreams = manifest.periods[0].textStreams;
abrManager = new shaka.abr.SimpleAbrManager();
abrManager.init(shaka.test.Util.spyFunc(switchCallback));
@@ -102,7 +96,6 @@ describe('SimpleAbrManager', function() {
config.restrictions = defaultRestrictions;
abrManager.configure(config);
abrManager.setVariants(variants);
- abrManager.setTextStreams(textStreams);
});
afterEach(function() {
@@ -115,27 +108,21 @@ describe('SimpleAbrManager', function() {
});
it('can choose audio and video Streams right away', function() {
- var chosen = abrManager.chooseStreams([ContentType.AUDIO,
- ContentType.VIDEO]);
- expect(chosen[ContentType.AUDIO]).toBeTruthy();
- expect(chosen[ContentType.VIDEO]).toBeTruthy();
+ var chosen = abrManager.chooseVariant();
+ expect(chosen).not.toBe(null);
});
it('uses custom default estimate', function() {
config.defaultBandwidthEstimate = 3e6;
abrManager.configure(config);
- var chosen = abrManager.chooseStreams([ContentType.AUDIO,
- ContentType.VIDEO]);
- expect(chosen[ContentType.VIDEO].id).toBe(6);
+ var chosen = abrManager.chooseVariant();
+ expect(chosen.id).toBe(4);
});
it('can handle empty variants', function() {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
abrManager.setVariants([]);
- abrManager.setTextStreams([]);
- var chosen = abrManager.chooseStreams([ContentType.AUDIO,
- ContentType.VIDEO]);
- expect(Object.keys(chosen).length).toBe(0);
+ var chosen = abrManager.chooseVariant();
+ expect(chosen).toEqual(null);
});
it('can choose from audio only variants', function() {
@@ -148,10 +135,10 @@ describe('SimpleAbrManager', function() {
.build();
abrManager.setVariants(manifest.periods[0].variants);
- var chosen = abrManager.chooseStreams([ContentType.AUDIO]);
-
- expect(chosen[ContentType.AUDIO]).toBeTruthy();
- expect(chosen[ContentType.VIDEO]).toBeFalsy();
+ var chosen = abrManager.chooseVariant();
+ expect(chosen).not.toBe(null);
+ expect(chosen.audio).not.toBe(null);
+ expect(chosen.video).toBe(null);
});
it('can choose from video only variants', function() {
@@ -164,10 +151,10 @@ describe('SimpleAbrManager', function() {
.build();
abrManager.setVariants(manifest.periods[0].variants);
- var chosen = abrManager.chooseStreams([ContentType.VIDEO]);
-
- expect(chosen[ContentType.VIDEO]).toBeTruthy();
- expect(chosen[ContentType.AUDIO]).toBeFalsy();
+ var chosen = abrManager.chooseVariant();
+ expect(chosen).not.toBe(null);
+ expect(chosen.audio).toBe(null);
+ expect(chosen.video).not.toBe(null);
});
[5e5, 6e5].forEach(function(bandwidth) {
@@ -182,7 +169,7 @@ describe('SimpleAbrManager', function() {
it(description, function() {
abrManager.setVariants(variants);
- abrManager.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ abrManager.chooseVariant();
abrManager.segmentDownloaded(1000, bytesPerSecond);
abrManager.segmentDownloaded(1000, bytesPerSecond);
@@ -195,21 +182,9 @@ describe('SimpleAbrManager', function() {
// Expect variants 2 to be chosen for bandwidth = 5e5
// and variant 5 - for bandwidth = 6e5
- var audioStream = variants[2].audio;
- var videoStream = variants[2].video;
-
- if (bandwidth == 6e5) {
- audioStream = variants[5].audio;
- videoStream = variants[5].video;
- }
-
- expect(switchCallback).toHaveBeenCalled();
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var expectedObject = {};
- expectedObject[ContentType.AUDIO] = audioStream;
- expectedObject[ContentType.VIDEO] = videoStream;
- expect(switchCallback.calls.argsFor(0)[0]).toEqual(expectedObject);
+ var expectedVariant = (bandwidth == 6e5) ? variants[5] : variants[2];
+
+ expect(switchCallback).toHaveBeenCalledWith(expectedVariant);
});
});
@@ -222,7 +197,7 @@ describe('SimpleAbrManager', function() {
sufficientBWMultiplier * bandwidth / 8.0;
abrManager.setVariants(variants);
- abrManager.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ abrManager.chooseVariant();
// 0 duration segment shouldn't cause us to get stuck on the lowest variant
abrManager.segmentDownloaded(0, bytesPerSecond);
@@ -240,7 +215,7 @@ describe('SimpleAbrManager', function() {
var bandwidth = 2e6;
abrManager.setVariants(variants);
- abrManager.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ abrManager.chooseVariant();
// Simulate some segments being downloaded just above the desired
// bandwidth.
@@ -257,16 +232,9 @@ describe('SimpleAbrManager', function() {
abrManager.segmentDownloaded(1000, bytesPerSecond);
// Expect variants 4 to be chosen
- var videoStream = variants[4].video;
- var audioStream = variants[4].audio;
-
- expect(switchCallback).toHaveBeenCalled();
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var expectedObject = {};
- expectedObject[ContentType.AUDIO] = audioStream;
- expectedObject[ContentType.VIDEO] = videoStream;
- expect(switchCallback.calls.argsFor(0)[0]).toEqual(expectedObject);
+ var expectedVariant = variants[4];
+
+ expect(switchCallback).toHaveBeenCalledWith(expectedVariant);
});
it('does not call switchCallback() if not enabled', function() {
@@ -275,7 +243,7 @@ describe('SimpleAbrManager', function() {
sufficientBWMultiplier * bandwidth / 8.0;
abrManager.setVariants(variants);
- abrManager.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ abrManager.chooseVariant();
// Don't enable AbrManager.
abrManager.segmentDownloaded(1000, bytesPerSecond);
@@ -290,7 +258,7 @@ describe('SimpleAbrManager', function() {
sufficientBWMultiplier * bandwidth / 8.0;
abrManager.setVariants(variants);
- abrManager.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ abrManager.chooseVariant();
abrManager.segmentDownloaded(1000, bytesPerSecond);
abrManager.segmentDownloaded(1000, bytesPerSecond);
@@ -335,7 +303,7 @@ describe('SimpleAbrManager', function() {
sufficientBWMultiplier * bandwidth / 8.0;
abrManager.setVariants(variants);
- abrManager.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ abrManager.chooseVariant();
abrManager.segmentDownloaded(1000, bytesPerSecond);
abrManager.segmentDownloaded(1000, bytesPerSecond);
@@ -363,7 +331,7 @@ describe('SimpleAbrManager', function() {
abrManager.configure(config);
abrManager.setVariants(variants);
- abrManager.chooseStreams([ContentType.AUDIO, ContentType.VIDEO]);
+ abrManager.chooseVariant();
abrManager.segmentDownloaded(1000, bytesPerSecond);
abrManager.segmentDownloaded(1000, bytesPerSecond);
@@ -389,12 +357,12 @@ describe('SimpleAbrManager', function() {
.build();
abrManager.setVariants(manifest.periods[0].variants);
- var chosen = abrManager.chooseStreams([ContentType.VIDEO]);
- expect(chosen[ContentType.VIDEO].id).toBe(2);
+ var chosen = abrManager.chooseVariant();
+ expect(chosen.id).toBe(1);
config.restrictions.maxWidth = 100;
abrManager.configure(config);
- chosen = abrManager.chooseStreams([ContentType.VIDEO]);
- expect(chosen[ContentType.VIDEO].id).toBe(0);
+ chosen = abrManager.chooseVariant();
+ expect(chosen.id).toBe(0);
});
});
diff --git a/test/media/streaming_engine_integration.js b/test/media/streaming_engine_integration.js
index 7cc44617f2..6db61060cc 100644
--- a/test/media/streaming_engine_integration.js
+++ b/test/media/streaming_engine_integration.js
@@ -44,14 +44,10 @@ describe('StreamingEngine', function() {
var streamingEngine;
- /** @type {?shakaExtern.Stream} */
- var audioStream1;
- /** @type {?shakaExtern.Stream} */
- var videoStream1;
- /** @type {?shakaExtern.Stream} */
- var audioStream2;
- /** @type {?shakaExtern.Stream} */
- var videoStream2;
+ /** @type {shakaExtern.Variant} */
+ var variant1;
+ /** @type {shakaExtern.Variant} */
+ var variant2;
/** @type {shakaExtern.Manifest} */
var manifest;
@@ -105,7 +101,6 @@ describe('StreamingEngine', function() {
onError.and.callFake(fail);
onEvent = jasmine.createSpy('onEvent');
-
eventManager = new shaka.util.EventManager();
setupMediaSource().catch(fail).then(done);
});
@@ -314,10 +309,8 @@ describe('StreamingEngine', function() {
manifest.periods[1].variants[0].video.initSegmentReference =
new shaka.media.InitSegmentReference(makeUris('2_video_init'), 0, null);
- audioStream1 = manifest.periods[0].variants[0].audio;
- videoStream1 = manifest.periods[0].variants[0].video;
- audioStream2 = manifest.periods[1].variants[0].audio;
- videoStream2 = manifest.periods[1].variants[0].video;
+ variant1 = manifest.periods[0].variants[0];
+ variant2 = manifest.periods[1].variants[0];
}
function createStreamingEngine() {
@@ -358,7 +351,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
+ streamingEngine.init().catch(function(error) {
+ fail(error);
+ done();
+ });
});
it('plays at high playback rates', function(done) {
@@ -385,7 +381,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
+ streamingEngine.init().catch(function(error) {
+ fail(error);
+ done();
+ });
});
it('can handle buffered seeks', function(done) {
@@ -411,7 +410,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
+ streamingEngine.init().catch(function(error) {
+ fail(error);
+ done();
+ });
});
it('can handle unbuffered seeks', function(done) {
@@ -437,7 +439,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
+ streamingEngine.init().catch(function(error) {
+ fail(error);
+ done();
+ });
});
});
@@ -479,7 +484,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
+ streamingEngine.init().catch(function(error) {
+ fail(error);
+ done();
+ });
});
it('can handle seeks ahead of availability window',
@@ -511,7 +519,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
+ streamingEngine.init().catch(function(error) {
+ fail(error);
+ done();
+ });
});
it('can handle seeks behind availability window', function(done) {
@@ -553,7 +564,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
+ streamingEngine.init().catch(function(error) {
+ fail(error);
+ done();
+ });
});
});
@@ -573,12 +587,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
-
+ return streamingEngine.init();
+ }).then(function() {
return waitForTime(5);
- })
- .catch(fail)
- .then(done);
+ }).catch(fail).then(done);
});
it('jumps large gaps at the beginning', function(done) {
@@ -595,12 +607,10 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
-
+ return streamingEngine.init();
+ }).then(function() {
return waitForTime(8);
- })
- .catch(fail)
- .then(done);
+ }).catch(fail).then(done);
});
it('jumps small gaps in the middle', function(done) {
@@ -614,17 +624,14 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
-
+ return streamingEngine.init();
+ }).then(function() {
return waitForTime(23);
- })
- .then(function() {
+ }).then(function() {
// Should be close enough to still have the gap buffered.
expect(video.buffered.length).toBe(2);
expect(onEvent).not.toHaveBeenCalled();
- })
- .catch(fail)
- .then(done);
+ }).catch(fail).then(done);
});
it('jumps large gaps in the middle', function(done) {
@@ -638,17 +645,14 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
-
+ return streamingEngine.init();
+ }).then(function() {
return waitForTime(23);
- })
- .then(function() {
+ }).then(function() {
// Should be close enough to still have the gap buffered.
expect(video.buffered.length).toBe(2);
expect(onEvent).toHaveBeenCalled();
- })
- .catch(fail)
- .then(done);
+ }).catch(fail).then(done);
});
it('won\'t jump large gaps with preventDefault()', function(done) {
@@ -673,9 +677,8 @@ describe('StreamingEngine', function() {
// Let's go!
onChooseStreams.and.callFake(defaultOnChooseStreams);
- streamingEngine.init();
- })
- .catch(done.fail);
+ return streamingEngine.init();
+ }).catch(done.fail);
});
@@ -707,10 +710,7 @@ describe('StreamingEngine', function() {
video: metadata.video.segmentDuration });
manifest = setupGappyManifest(gapAtStart, dropSegment);
- audioStream1 = /** @type {shakaExtern.Stream} */ (
- manifest.periods[0].variants[0].audio);
- videoStream1 = /** @type {shakaExtern.Stream} */ (
- manifest.periods[0].variants[0].video);
+ variant1 = manifest.periods[0].variants[0];
setupPlayhead();
createStreamingEngine();
@@ -832,17 +832,10 @@ describe('StreamingEngine', function() {
* @return {!Object.}
*/
function defaultOnChooseStreams(period) {
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var ret = {};
if (period == manifest.periods[0]) {
- ret[ContentType.AUDIO] = audioStream1;
- ret[ContentType.VIDEO] = videoStream1;
- return ret;
+ return { variant: variant1, text: null };
} else if (period == manifest.periods[1]) {
- ret[ContentType.AUDIO] = audioStream2;
- ret[ContentType.VIDEO] = videoStream2;
- return ret;
+ return { variant: variant2, text: null };
} else {
throw new Error();
}
diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js
index c63905e772..e78b84f91f 100644
--- a/test/media/streaming_engine_unit.js
+++ b/test/media/streaming_engine_unit.js
@@ -53,11 +53,13 @@ describe('StreamingEngine', function() {
var audioStream1;
var videoStream1;
+ var variant1;
var textStream1;
var alternateVideoStream1;
var audioStream2;
var videoStream2;
+ var variant2;
var textStream2;
/** @type {shakaExtern.Manifest} */
@@ -388,6 +390,7 @@ describe('StreamingEngine', function() {
audioStream1 = manifest.periods[0].variants[0].audio;
videoStream1 = manifest.periods[0].variants[0].video;
+ variant1 = manifest.periods[0].variants[0];
textStream1 = manifest.periods[0].textStreams[0];
// This Stream is only used to verify that StreamingEngine can setup
@@ -412,6 +415,7 @@ describe('StreamingEngine', function() {
audioStream2 = manifest.periods[1].variants[0].audio;
videoStream2 = manifest.periods[1].variants[0].video;
+ variant2 = manifest.periods[1].variants[0];
textStream2 = manifest.periods[1].textStreams[0];
}
@@ -684,13 +688,7 @@ describe('StreamingEngine', function() {
return defaultOnChooseStreams(period);
});
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var ret = {};
- ret[ContentType.AUDIO] = audioStream2;
- ret[ContentType.VIDEO] = videoStream2;
- ret[ContentType.TEXT] = textStream2;
- return ret;
+ return { variant: variant2, text: textStream2 };
});
streamingEngine.init();
@@ -736,13 +734,7 @@ describe('StreamingEngine', function() {
return defaultOnChooseStreams(period);
});
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var ret = {};
- ret[ContentType.AUDIO] = audioStream1;
- ret[ContentType.VIDEO] = videoStream1;
- ret[ContentType.TEXT] = textStream1;
- return ret;
+ return { variant: variant1, text: textStream1 };
});
streamingEngine.init();
@@ -791,7 +783,7 @@ describe('StreamingEngine', function() {
onChooseStreams.and.callFake(function(period) {
var chosen = defaultOnChooseStreams(period);
if (period == manifest.periods[0])
- delete chosen[ContentType.TEXT];
+ chosen.text = null;
return chosen;
});
@@ -831,7 +823,7 @@ describe('StreamingEngine', function() {
onChooseStreams.and.callFake(function(period) {
var chosen = defaultOnChooseStreams(period);
if (period == manifest.periods[0])
- delete chosen[ContentType.TEXT];
+ chosen.text = null;
return chosen;
});
@@ -863,7 +855,7 @@ describe('StreamingEngine', function() {
.toBe(textStream2);
mediaSourceEngine.reinitText.calls.reset();
- streamingEngine.switch(ContentType.TEXT, textStream2, false);
+ streamingEngine.switchTextStream(textStream2);
});
});
@@ -886,7 +878,7 @@ describe('StreamingEngine', function() {
onChooseStreams.and.callFake(function(period) {
var chosen = defaultOnChooseStreams(period);
if (period == manifest.periods[1])
- delete chosen[ContentType.TEXT];
+ chosen.text = null;
return chosen;
});
@@ -928,6 +920,112 @@ describe('StreamingEngine', function() {
expect(timeline.setDuration).toHaveBeenCalledWith(35);
});
+ describe('switchVariant/switchTextStream', function() {
+ var initialVariant;
+ var sameAudioVariant;
+ var sameVideoVariant;
+ var initialTextStream;
+
+ beforeEach(function() {
+ // Set up a manifest with multiple variants and a text stream.
+ manifest = new shaka.test.ManifestGenerator()
+ .addPeriod(0)
+ .addVariant(0)
+ .addAudio(10).useSegmentTemplate('audio-10-%d.mp4', 10)
+ .addVideo(11).useSegmentTemplate('video-11-%d.mp4', 10)
+ .addVariant(1)
+ .addAudio(10) // reused
+ .addVideo(12).useSegmentTemplate('video-12-%d.mp4', 10)
+ .addVariant(2)
+ .addAudio(13).useSegmentTemplate('audio-13-%d.mp4', 10)
+ .addVideo(12) // reused
+ .addTextStream(20).useSegmentTemplate('text-20-%d.mp4', 10)
+ .build();
+
+ initialVariant = manifest.periods[0].variants[0];
+ sameAudioVariant = manifest.periods[0].variants[1];
+ sameVideoVariant = manifest.periods[0].variants[2];
+ initialTextStream = manifest.periods[0].textStreams[0];
+
+ // For these tests, we don't care about specific data appended.
+ // Just return any old ArrayBuffer for any requested segment.
+ netEngine = {
+ request: function(requestType, request) {
+ var buffer = new ArrayBuffer(0);
+ var response = { uri: request.uris[0], data: buffer, headers: {} };
+ return Promise.resolve(response);
+ }
+ };
+
+ // For these tests, we also don't need FakeMediaSourceEngine to verify
+ // its input data.
+ mediaSourceEngine = new shaka.test.FakeMediaSourceEngine({});
+ mediaSourceEngine.clear.and.returnValue(Promise.resolve());
+ mediaSourceEngine.bufferedAheadOf.and.returnValue(0);
+ mediaSourceEngine.bufferStart.and.returnValue(0);
+ mediaSourceEngine.setStreamProperties.and.returnValue(Promise.resolve());
+ mediaSourceEngine.remove.and.returnValue(Promise.resolve());
+
+ var bufferEnd = { audio: 0, video: 0, text: 0 };
+ mediaSourceEngine.appendBuffer.and.callFake(
+ function(type, data, start, end) {
+ bufferEnd[type] = end;
+ return Promise.resolve();
+ });
+ mediaSourceEngine.bufferEnd.and.callFake(function(type) {
+ return bufferEnd[type];
+ });
+ mediaSourceEngine.bufferedAheadOf.and.callFake(function(type, start) {
+ return Math.max(0, bufferEnd[type] - start);
+ });
+ mediaSourceEngine.isBuffered.and.callFake(function(type, time) {
+ return time >= 0 && time < bufferEnd[type];
+ });
+
+ playhead = new shaka.test.FakePlayhead();
+ playheadTime = 0;
+ playing = false;
+ createStreamingEngine();
+
+ playhead.getTime.and.returnValue(0);
+ onStartupComplete.and.callFake(setupFakeGetTime.bind(null, 0));
+ onChooseStreams.and.callFake(function() {
+ return { variant: initialVariant, text: initialTextStream };
+ });
+ });
+
+ it('will not clear buffers if streams have not changed', function() {
+ onCanSwitch.and.callFake(function() {
+ mediaSourceEngine.clear.calls.reset();
+ streamingEngine.switchVariant(sameAudioVariant, /* clearBuffer */ true);
+ Util.fakeEventLoop(1);
+ expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('audio');
+ expect(mediaSourceEngine.clear).toHaveBeenCalledWith('video');
+ expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text');
+
+ mediaSourceEngine.clear.calls.reset();
+ streamingEngine.switchVariant(sameVideoVariant, /* clearBuffer */ true);
+ Util.fakeEventLoop(1);
+ expect(mediaSourceEngine.clear).toHaveBeenCalledWith('audio');
+ expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('video');
+ expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text');
+
+ mediaSourceEngine.clear.calls.reset();
+ streamingEngine.switchTextStream(initialTextStream);
+ Util.fakeEventLoop(1);
+ expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('audio');
+ expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('video');
+ expect(mediaSourceEngine.clear).not.toHaveBeenCalledWith('text');
+ });
+
+ streamingEngine.init().catch(fail);
+
+ Util.fakeEventLoop(1);
+
+ expect(onCanSwitch).toHaveBeenCalled();
+ });
+ });
+
describe('handles seeks (VOD)', function() {
/** @type {!jasmine.Spy} */
var onTick;
@@ -2002,19 +2100,16 @@ describe('StreamingEngine', function() {
onStartupComplete.and.callFake(setupFakeGetTime.bind(null, 0));
- var originalRemove = shaka.test.FakeMediaSourceEngine.prototype.remove;
- // NOTE: Closure cannot type check spy's correctly. Here we have to
- // explicitly re-create remove()'s spy.
- var removeSpy = jasmine.createSpy('remove');
- mediaSourceEngine.remove = Util.spyFunc(removeSpy);
+ var originalRemove =
+ shaka.test.FakeMediaSourceEngine.prototype.removeImpl;
- removeSpy.and.callFake(function(type, start, end) {
+ mediaSourceEngine.remove.and.callFake(function(type, start, end) {
expect(playheadTime).toBe(20);
expect(start).toBe(0);
expect(end).toBe(10);
- if (removeSpy.calls.count() == 3) {
- removeSpy.and.callFake(function(type, start, end) {
+ if (mediaSourceEngine.remove.calls.count() == 3) {
+ mediaSourceEngine.remove.and.callFake(function(type, start, end) {
expect(playheadTime).toBe(30);
expect(start).toBe(10);
expect(end).toBe(20);
@@ -2556,19 +2651,10 @@ describe('StreamingEngine', function() {
* @return {!Object.}
*/
function defaultOnChooseStreams(period) {
- // Create empty object first and initialize the fields through
- // [] to allow field names to be expressions.
- var ret = {};
if (period == manifest.periods[0]) {
- ret[ContentType.AUDIO] = audioStream1;
- ret[ContentType.VIDEO] = videoStream1;
- ret[ContentType.TEXT] = textStream1;
- return ret;
+ return { variant: variant1, text: textStream1 };
} else if (period == manifest.periods[1]) {
- ret[ContentType.AUDIO] = audioStream2;
- ret[ContentType.VIDEO] = videoStream2;
- ret[ContentType.TEXT] = textStream2;
- return ret;
+ return { variant: variant2, text: textStream2 };
} else {
throw new Error();
}
diff --git a/test/player_unit.js b/test/player_unit.js
index 815acbab2c..221e5a69ae 100644
--- a/test/player_unit.js
+++ b/test/player_unit.js
@@ -36,6 +36,8 @@ describe('Player', function() {
var onError;
/** @type {shakaExtern.Manifest} */
var manifest;
+ /** @type {number} */
+ var periodIndex;
/** @type {!shaka.Player} */
var player;
/** @type {!shaka.test.FakeAbrManager} */
@@ -92,7 +94,12 @@ describe('Player', function() {
.addVariant(0)
.addAudio(1)
.addVideo(2)
+ .addPeriod(1)
+ .addVariant(1)
+ .addAudio(3)
+ .addVideo(4)
.build();
+ periodIndex = 0;
abrManager = new shaka.test.FakeAbrManager();
abrFactory = function() { return abrManager; };
@@ -106,7 +113,8 @@ describe('Player', function() {
drmEngine = new shaka.test.FakeDrmEngine();
playhead = new shaka.test.FakePlayhead();
playheadObserver = new shaka.test.FakePlayheadObserver();
- streamingEngine = new shaka.test.FakeStreamingEngine();
+ streamingEngine = new shaka.test.FakeStreamingEngine(
+ onChooseStreams, onCanSwitch);
mediaSourceEngine = {
destroy: jasmine.createSpy('destroy').and.returnValue(Promise.resolve())
};
@@ -793,11 +801,9 @@ describe('Player', function() {
.then(done);
});
- it('calls chooseStreams', function(done) {
+ it('calls chooseVariant', function(done) {
player.load('', 0, parserFactory).then(function() {
- expect(abrManager.chooseStreams).not.toHaveBeenCalled();
- chooseStreams();
- expect(abrManager.chooseStreams).toHaveBeenCalled();
+ expect(abrManager.chooseVariant).toHaveBeenCalled();
})
.catch(fail)
.then(done);
@@ -805,9 +811,8 @@ describe('Player', function() {
it('does not enable before stream startup', function(done) {
player.load('', 0, parserFactory).then(function() {
- chooseStreams();
expect(abrManager.enable).not.toHaveBeenCalled();
- canSwitch();
+ streamingEngine.onCanSwitch();
expect(abrManager.enable).toHaveBeenCalled();
})
.catch(fail)
@@ -815,10 +820,9 @@ describe('Player', function() {
});
it('does not enable if adaptation is disabled', function(done) {
+ player.configure({abr: {enabled: false}});
player.load('', 0, parserFactory).then(function() {
- player.configure({abr: {enabled: false}});
- chooseStreams();
- canSwitch();
+ streamingEngine.onCanSwitch();
expect(abrManager.enable).not.toHaveBeenCalled();
})
.catch(fail)
@@ -827,8 +831,7 @@ describe('Player', function() {
it('enables/disables though configure', function(done) {
player.load('', 0, parserFactory).then(function() {
- chooseStreams();
- canSwitch();
+ streamingEngine.onCanSwitch();
abrManager.enable.calls.reset();
abrManager.disable.calls.reset();
@@ -843,29 +846,18 @@ describe('Player', function() {
});
it('waits to enable if in-between Periods', function(done) {
+ player.configure({abr: {enabled: false}});
player.load('', 0, parserFactory).then(function() {
- player.configure({abr: {enabled: false}});
- chooseStreams();
player.configure({abr: {enabled: true}});
expect(abrManager.enable).not.toHaveBeenCalled();
- canSwitch();
+ // Until onCanSwitch is called, the first period hasn't been set up yet.
+ streamingEngine.onCanSwitch();
expect(abrManager.enable).toHaveBeenCalled();
})
.catch(fail)
.then(done);
});
- it('still disables if called after chooseStreams', function(done) {
- player.load('', 0, parserFactory).then(function() {
- chooseStreams();
- player.configure({abr: {enabled: false}});
- canSwitch();
- expect(abrManager.enable).not.toHaveBeenCalled();
- })
- .catch(fail)
- .then(done);
- });
-
it('can still be configured through deprecated config', function() {
var managerInstance = new shaka.test.FakeAbrManager();
@@ -888,6 +880,27 @@ describe('Player', function() {
jasmine.createSpy('setDefaultEstimate');
notAStruct['setRestrictions'] = jasmine.createSpy('setRestrictions');
notAStruct['configure'] = null;
+ notAStruct['chooseStreams'] = jasmine.createSpy('choostStreams');
+ notAStruct['chooseVariant'] = null;
+
+ // The return value from this matters, so set a fake implementation.
+ notAStruct['chooseStreams'].and.callFake(function(mediaTypes) {
+ var period = manifest.periods[0];
+ var variant = period.variants[0];
+ var textStream = period.textStreams[0];
+
+ var map = {};
+ if (mediaTypes.indexOf('audio') >= 0) {
+ map.audio = variant.audio;
+ }
+ if (mediaTypes.indexOf('video') >= 0) {
+ map.video = variant.video;
+ }
+ if (mediaTypes.indexOf('text') >= 0) {
+ map.text = textStream || null;
+ }
+ return map;
+ });
expect(logWarnSpy).not.toHaveBeenCalled();
player.configure({
@@ -901,6 +914,7 @@ describe('Player', function() {
expect(managerInstance.init).toHaveBeenCalled();
expect(notAStruct['setDefaultEstimate']).toHaveBeenCalled();
expect(notAStruct['setRestrictions']).toHaveBeenCalled();
+ expect(notAStruct['chooseStreams']).toHaveBeenCalled();
expect(logWarnSpy).toHaveBeenCalled();
}).catch(fail).then(done);
});
@@ -1015,7 +1029,7 @@ describe('Player', function() {
/** @type {!Array.} */
var textTracks;
- beforeEach(function() {
+ beforeEach(function(done) {
// A manifest we can use to test track expectations.
manifest = new shaka.test.ManifestGenerator()
.addPeriod(0)
@@ -1057,6 +1071,12 @@ describe('Player', function() {
.bandwidth(100).kind('caption')
.mime('application/ttml+xml')
// Both text tracks should remain, even with different MIME types.
+ .addPeriod(1)
+ .addVariant(8)
+ .bandwidth(200)
+ .language('en')
+ .addAudio(9).bandwidth(100)
+ .addVideo(10).bandwidth(100).size(100, 200)
.build();
variantTracks = [
@@ -1213,27 +1233,31 @@ describe('Player', function() {
videoBandwidth: null
}
];
- });
- beforeEach(function(done) {
goog.asserts.assert(manifest, 'manifest must be non-null');
var parser = new shaka.test.FakeManifestParser(manifest);
var parserFactory = function() { return parser; };
+
+ // Language prefs must be set before load. Used in select*Language()
+ // tests.
+ player.configure({
+ preferredAudioLanguage: 'en',
+ preferredTextLanguage: 'es'
+ });
+
player.load('', 0, parserFactory).catch(fail).then(done);
});
it('returns the correct tracks', function() {
- // Switch tracks first so we setup the "active" tracks.
- player.selectVariantTrack(variantTracks[0]);
- player.selectTextTrack(textTracks[0]);
+ streamingEngine.onCanSwitch();
- var actualVariantTracks = player.getVariantTracks();
- var actualTextTracks = player.getTextTracks();
- expect(actualVariantTracks).toEqual(variantTracks);
- expect(actualTextTracks).toEqual(textTracks);
+ expect(player.getVariantTracks()).toEqual(variantTracks);
+ expect(player.getTextTracks()).toEqual(textTracks);
});
it('doesn\'t disable AbrManager if switching variants', function() {
+ streamingEngine.onCanSwitch();
+
var config = player.getConfiguration();
expect(config.abr.enabled).toBe(true);
expect(variantTracks[1].type).toBe('variant');
@@ -1243,6 +1267,8 @@ describe('Player', function() {
});
it('doesn\'t disable AbrManager if switching text', function() {
+ streamingEngine.onCanSwitch();
+
var config = player.getConfiguration();
expect(config.abr.enabled).toBe(true);
expect(textTracks[0].type).toBe(ContentType.TEXT);
@@ -1252,187 +1278,157 @@ describe('Player', function() {
});
it('switches streams', function() {
- chooseStreams();
- canSwitch();
+ streamingEngine.onCanSwitch();
- var period = manifest.periods[0];
- var variant = period.variants[3];
- player.selectVariantTrack(variantTracks[3]);
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.AUDIO, variant.audio, false);
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.VIDEO, variant.video, false);
- });
-
- it('doesn\'t switch audio if old and new variants ' +
- 'have the same audio track', function() {
- chooseStreams();
- canSwitch();
-
- var period = manifest.periods[0];
- var variant1 = period.variants[0];
- var variant2 = period.variants[1];
- expect(variant1.audio).toEqual(variant2.audio);
+ var track = variantTracks[3];
+ var variant = manifest.periods[0].variants[3];
+ expect(track.id).toEqual(variant.id);
- player.selectVariantTrack(variantTracks[0]);
- streamingEngine.switch.calls.reset();
-
- player.selectVariantTrack(variantTracks[1]);
-
- expect(streamingEngine.switch).toHaveBeenCalledWith(
- ContentType.VIDEO, variant2.video, false);
- expect(streamingEngine.switch).not.toHaveBeenCalledWith(
- ContentType.AUDIO, variant2.audio, false);
- });
-
- it('doesn\'t switch video if old and new variants ' +
- 'have the same video track', function() {
- chooseStreams();
- canSwitch();
-
- var period = manifest.periods[0];
- var variant1 = period.variants[0];
- var variant2 = period.variants[2];
- expect(variant1.video).toEqual(variant2.video);
-
- player.selectVariantTrack(variantTracks[0]);
- streamingEngine.switch.calls.reset();
+ player.selectVariantTrack(track);
+ expect(streamingEngine.switchVariant)
+ .toHaveBeenCalledWith(variant, false);
+ });
- player.selectVariantTrack(variantTracks[2]);
+ it('still switches streams if called during startup', function() {
+ // startup is not complete until onCanSwitch is called.
- expect(streamingEngine.switch).toHaveBeenCalledWith(
- ContentType.AUDIO, variant2.audio, false);
- expect(streamingEngine.switch).not.toHaveBeenCalledWith(
- ContentType.VIDEO, variant2.video, false);
- });
+ // pick a track
+ var track = variantTracks[1];
+ // ask the player to switch to it
+ player.selectVariantTrack(track);
+ // nothing happens yet
+ expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
- it('still switches streams if called during startup', function() {
- player.selectVariantTrack(variantTracks[1]);
- expect(streamingEngine.switch).not.toHaveBeenCalled();
+ var variant = manifest.periods[0].variants[1];
+ expect(variant.id).toEqual(track.id);
- // Does not call switch, just overrides the choices made in AbrManager.
- var chosen = chooseStreams();
- var period = manifest.periods[0];
- var variant = period.variants[1];
- var expectedObject = {};
- expectedObject[ContentType.AUDIO] = variant.audio;
- expectedObject[ContentType.VIDEO] = variant.video;
- expect(chosen).toEqual(jasmine.objectContaining(expectedObject));
+ // after startup is complete, the manual selection takes effect.
+ streamingEngine.onCanSwitch();
+ expect(streamingEngine.switchVariant)
+ .toHaveBeenCalledWith(variant, false);
});
it('still switches streams if called while switching Periods', function() {
- chooseStreams();
+ // startup is complete after onCanSwitch.
+ streamingEngine.onCanSwitch();
+
+ // startup doesn't call switchVariant
+ expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
+
+ var track = variantTracks[3];
+ var variant = manifest.periods[0].variants[3];
+ expect(variant.id).toEqual(track.id);
- player.selectVariantTrack(variantTracks[3]);
- expect(streamingEngine.switch).not.toHaveBeenCalled();
+ // simulate the transition to period 1
+ transitionPeriod(1);
- canSwitch();
+ // select the new track (from period 0, which is fine)
+ player.selectVariantTrack(track);
+ expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
- var period = manifest.periods[0];
- var variant = period.variants[3];
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.AUDIO, variant.audio, false);
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.VIDEO, variant.video, false);
+ // after transition is completed by onCanSwitch, switchVariant is called
+ streamingEngine.onCanSwitch();
+ expect(streamingEngine.switchVariant)
+ .toHaveBeenCalledWith(variant, false);
});
it('switching audio doesn\'t change selected text track', function() {
- chooseStreams();
- canSwitch();
+ streamingEngine.onCanSwitch();
player.configure({
preferredTextLanguage: 'es'
});
+ var textStream = manifest.periods[0].textStreams[1];
expect(textTracks[1].type).toBe(ContentType.TEXT);
expect(textTracks[1].language).toBe('en');
- player.selectTextTrack(textTracks[1]);
- var period = manifest.periods[0];
- var textStream = period.textStreams[1];
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.TEXT, textStream, true);
+ var textTrack = textTracks[1];
+ streamingEngine.switchTextStream.calls.reset();
+ player.selectTextTrack(textTrack);
+ expect(streamingEngine.switchTextStream).toHaveBeenCalledWith(textStream);
+ // We have selected an English text track explicitly.
+ expect(getActiveTextTrack().id).toBe(textTrack.id);
- streamingEngine.switch.calls.reset();
+ var variantTrack = variantTracks[2];
+ var variant = manifest.periods[0].variants[2];
+ expect(variantTrack.id).toBe(variant.id);
+ player.selectVariantTrack(variantTrack);
- var variant = period.variants[2];
- expect(variantTracks[2].id).toBe(variant.id);
- player.selectVariantTrack(variantTracks[2]);
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.TEXT, textStream, true);
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.AUDIO, variant.audio, false);
+ // The active text track has not changed, even though the text language
+ // preference is Spanish.
+ expect(getActiveTextTrack().id).toBe(textTrack.id);
});
it('selectAudioLanguage() takes precedence over preferredAudioLanguage',
function() {
- chooseStreams();
- canSwitch();
- player.configure({
- preferredAudioLanguage: 'en'
- });
+ streamingEngine.onCanSwitch();
+
+ // This preference is set in beforeEach, before load().
+ expect(player.getConfiguration().preferredAudioLanguage).toBe('en');
+
+ expect(getActiveVariantTrack().language).toBe('en');
var period = manifest.periods[0];
- var spanishStream = period.variants[4].audio;
- var englishStream = period.variants[3].audio;
+ var spanishVariant = period.variants[4];
+ expect(spanishVariant.language).toBe('es');
- expect(streamingEngine.switch).not.toHaveBeenCalled();
+ streamingEngine.switchVariant.calls.reset();
player.selectAudioLanguage('es');
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.AUDIO, spanishStream, true);
- expect(streamingEngine.switch)
- .not.toHaveBeenCalledWith(ContentType.AUDIO, englishStream, true);
-
+ expect(streamingEngine.switchVariant)
+ .toHaveBeenCalledWith(spanishVariant, true);
+ expect(getActiveVariantTrack().language).toBe('es');
});
it('selectTextLanguage() takes precedence over preferredTextLanguage',
function() {
- chooseStreams();
- canSwitch();
- player.configure({
- preferredTextLanguage: 'es'
- });
+ streamingEngine.onCanSwitch();
+
+ // This preference is set in beforeEach, before load().
+ expect(player.getConfiguration().preferredTextLanguage).toBe('es');
+
+ expect(getActiveTextTrack().language).toBe('es');
var period = manifest.periods[0];
- var spanishStream = period.textStreams[0];
var englishStream = period.textStreams[1];
+ expect(englishStream.language).toBe('en');
- expect(streamingEngine.switch).not.toHaveBeenCalled();
+ streamingEngine.switchTextStream.calls.reset();
player.selectTextLanguage('en');
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.TEXT, englishStream, true);
- expect(streamingEngine.switch)
- .not.toHaveBeenCalledWith(ContentType.TEXT, spanishStream, true);
-
+ expect(streamingEngine.switchTextStream)
+ .toHaveBeenCalledWith(englishStream);
+ expect(getActiveTextTrack().language).toBe('en');
});
- it('changing currentAudioLanguage changes active stream', function() {
- chooseStreams();
- canSwitch();
+ it('changing current audio language changes active stream', function() {
+ streamingEngine.onCanSwitch();
- var period = manifest.periods[0];
- var spanishStream = period.variants[4].audio;
+ var spanishVariant = manifest.periods[0].variants[4];
+ expect(spanishVariant.language).toBe('es');
- expect(streamingEngine.switch).not.toHaveBeenCalled();
+ expect(getActiveVariantTrack().language).not.toBe('es');
+ expect(streamingEngine.switchVariant).not.toHaveBeenCalled();
player.selectAudioLanguage('es');
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.AUDIO, spanishStream, true);
+ expect(streamingEngine.switchVariant)
+ .toHaveBeenCalledWith(spanishVariant, true);
+ expect(getActiveVariantTrack().language).toBe('es');
});
it('changing currentTextLanguage changes active stream', function() {
- chooseStreams();
- canSwitch();
+ streamingEngine.onCanSwitch();
- var period = manifest.periods[0];
- var englishStream = period.textStreams[1];
+ var englishStream = manifest.periods[0].textStreams[1];
+ expect(englishStream.language).toBe('en');
- expect(streamingEngine.switch).not.toHaveBeenCalled();
+ expect(getActiveTextTrack().language).not.toBe('en');
+ expect(streamingEngine.switchTextStream).not.toHaveBeenCalled();
player.selectTextLanguage('en');
- expect(streamingEngine.switch)
- .toHaveBeenCalledWith(ContentType.TEXT, englishStream, true);
+ expect(streamingEngine.switchTextStream)
+ .toHaveBeenCalledWith(englishStream);
+ expect(getActiveTextTrack().language).toBe('en');
});
});
@@ -1462,7 +1458,7 @@ describe('Player', function() {
runTest(['en', 'es', 'pt-BR'], 'pt-PT', 2, done);
});
- it('enables text track if audio and text are different language',
+ it('enables text track if audio and text are different language on startup',
function(done) {
// A manifest we can use to test text visibility.
manifest = new shaka.test.ManifestGenerator()
@@ -1473,18 +1469,30 @@ describe('Player', function() {
.addTextStream(3).language('fr')
.build();
+ player.configure({
+ preferredAudioLanguage: 'en',
+ preferredTextLanguage: 'fr'
+ });
+
+ expect(player.isTextTrackVisible()).toBe(false);
+
var parser = new shaka.test.FakeManifestParser(manifest);
var factory = function() { return parser; };
player.load('', 0, factory)
.then(function() {
+ // Text was turned on during startup.
+ expect(player.isTextTrackVisible()).toBe(true);
+
+ // Turn text back off.
+ player.setTextTrackVisibility(false);
expect(player.isTextTrackVisible()).toBe(false);
- player.selectAudioLanguage('en');
- player.selectTextLanguage('fr');
- expect(player.isTextTrackVisible()).toBe(true);
- })
- .catch(fail)
- .then(done);
+ // Change text languages after startup.
+ player.selectTextLanguage('pt');
+
+ // This should not turn text back on.
+ expect(player.isTextTrackVisible()).toBe(false);
+ }).catch(fail).then(done);
});
it('chooses an arbitrary language when none given', function(done) {
@@ -1496,18 +1504,20 @@ describe('Player', function() {
.addVariant(1).language('en').addAudio(1)
.build();
- player.configure({preferredAudioLanguage: undefined});
+ player.configure({
+ preferredAudioLanguage: undefined
+ });
var parser = new shaka.test.FakeManifestParser(manifest);
var parserFactory = function() { return parser; };
- player.load('', 0, parserFactory)
- .then(function() {
- expect(abrManager.setVariants).toHaveBeenCalled();
- var variants = abrManager.setVariants.calls.argsFor(0)[0];
- expect(variants.length).toBe(1);
- })
- .catch(fail)
- .then(done);
+ player.load('', 0, parserFactory).then(function() {
+ expect(abrManager.setVariants).toHaveBeenCalled();
+
+ // If we have chosen any arbitrary language, setVariants is provided
+ // with exactly one variant.
+ var variants = abrManager.setVariants.calls.argsFor(0)[0];
+ expect(variants.length).toBe(1);
+ }).catch(fail).then(done);
});
/**
@@ -1517,7 +1527,6 @@ describe('Player', function() {
* @param {function()} done
*/
function runTest(languages, preference, expectedIndex, done) {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
var generator = new shaka.test.ManifestGenerator().addPeriod(0);
for (var i = 0; i < languages.length; i++) {
@@ -1535,18 +1544,16 @@ describe('Player', function() {
// A manifest we can use to test language selection.
manifest = generator.build();
+ // Set the user preferences, which must happen before load().
+ player.configure({
+ preferredAudioLanguage: preference
+ });
+
var parser = new shaka.test.FakeManifestParser(manifest);
var factory = function() { return parser; };
- player.load('', 0, factory)
- .then(function() {
- player.selectAudioLanguage(preference);
- player.selectTextLanguage(preference);
-
- var chosen = chooseStreams();
- expect(chosen[ContentType.AUDIO].id).toBe(expectedIndex);
- })
- .catch(fail)
- .then(done);
+ player.load('', 0, factory).then(function() {
+ expect(getActiveVariantTrack().id).toBe(expectedIndex);
+ }).catch(fail).then(done);
}
});
@@ -1582,9 +1589,8 @@ describe('Player', function() {
var parserFactory = function() { return parser; };
player.load('', 0, parserFactory)
.then(function() {
- // "initialize" the current period.
- chooseStreams();
- canSwitch();
+ // Initialize the fake streams.
+ streamingEngine.onCanSwitch();
})
.catch(fail)
.then(done);
@@ -1691,8 +1697,8 @@ describe('Player', function() {
player.selectVariantTrack(track);
var period = manifest.periods[0];
- var variant = shaka.util.StreamUtils.findVariantForTrack(period,
- track);
+ var variant =
+ shaka.util.StreamUtils.findVariantForTrack(period, track);
checkHistory([{
// We are using a mock date, so this is not a race.
@@ -1705,12 +1711,9 @@ describe('Player', function() {
});
it('includes adaptation choices', function() {
- var choices = {};
var variant = manifest.periods[0].variants[3];
- choices[ContentType.AUDIO] = variant.audio;
- choices[ContentType.VIDEO] = variant.video;
- switch_(choices);
+ switch_(variant);
checkHistory(jasmine.arrayContaining([
{
timestamp: Date.now() / 1000,
@@ -1727,28 +1730,26 @@ describe('Player', function() {
* @param {!Array.} additional
*/
function checkHistory(additional) {
- var prefix = [
- {
- timestamp: jasmine.any(Number),
- id: 0,
- type: 'variant',
- fromAdaptation: true,
- bandwidth: 200
- }
- ];
+ var prefix = {
+ timestamp: jasmine.any(Number),
+ id: 0,
+ type: 'variant',
+ fromAdaptation: true,
+ bandwidth: 200
+ };
- var stats = player.getStats();
- expect(stats.switchHistory.slice(0, 1))
- .toEqual(jasmine.arrayContaining(prefix));
- expect(stats.switchHistory.slice(1)).toEqual(additional);
+ var switchHistory = player.getStats().switchHistory;
+
+ expect(switchHistory[0]).toEqual(prefix);
+ expect(switchHistory.slice(1)).toEqual(additional);
}
/**
- * @param {!Object.} streamsByType
+ * @param {shakaExtern.Variant} variant
* @suppress {accessControls}
*/
- function switch_(streamsByType) {
- player.switch_(streamsByType);
+ function switch_(variant) {
+ player.switch_(variant);
}
});
@@ -1883,20 +1884,29 @@ describe('Player', function() {
.build();
setupPlayer(manifest).then(function() {
- var activeVariant = getActiveTrack('variant');
+ var activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(0);
- // AbrManager should choose the second track since the first is
- // restricted.
+ // Ask AbrManager to choose the 0th variant from those it is given.
abrManager.chooseIndex = 0;
- abrManager.chooseStreams.calls.reset();
- player.configure({restrictions: {maxBandwidth: 200}});
- expect(abrManager.chooseStreams).toHaveBeenCalled();
+ abrManager.chooseVariant.calls.reset();
+
+ // This restriction should make it so that the first variant (bandwidth
+ // 500, id 0) cannot be selected.
+ player.configure({
+ restrictions: { maxBandwidth: 200 }
+ });
+
+ // The restriction change should trigger a call to AbrManager.
+ expect(abrManager.chooseVariant).toHaveBeenCalled();
+
+ // The first variant is disallowed.
expect(manifest.periods[0].variants[0].id).toBe(0);
expect(manifest.periods[0].variants[0].allowedByApplication)
.toBe(false);
- activeVariant = getActiveTrack('variant');
+ // AbrManager chose the second variant (id 1).
+ activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(1);
}).catch(fail).then(done);
});
@@ -1911,20 +1921,23 @@ describe('Player', function() {
.build();
setupPlayer(manifest).then(function() {
- var activeVariant = getActiveTrack('variant');
+ var activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(0);
- // AbrManager should choose the second track since the first is
- // restricted.
abrManager.chooseIndex = 0;
- abrManager.chooseStreams.calls.reset();
- onKeyStatus({'abc': 'output-restricted'});
- expect(abrManager.chooseStreams).toHaveBeenCalled();
+ abrManager.chooseVariant.calls.reset();
+
+ // This restricts the first variant, which triggers chooseVariant.
+ onKeyStatus({ 'abc': 'output-restricted' });
+ expect(abrManager.chooseVariant).toHaveBeenCalled();
+
+ // The first variant is disallowed.
expect(manifest.periods[0].variants[0].id).toBe(0);
expect(manifest.periods[0].variants[0].allowedByKeySystem)
.toBe(false);
- activeVariant = getActiveTrack('variant');
+ // The second variant was chosen.
+ activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(1);
}).catch(fail).then(done);
});
@@ -1939,20 +1952,20 @@ describe('Player', function() {
.build();
setupPlayer(manifest).then(function() {
- var activeVariant = getActiveTrack('variant');
+ var activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(0);
// AbrManager should choose the second track since the first is
// restricted.
abrManager.chooseIndex = 0;
- abrManager.chooseStreams.calls.reset();
+ abrManager.chooseVariant.calls.reset();
onKeyStatus({'abc': 'internal-error'});
- expect(abrManager.chooseStreams).toHaveBeenCalled();
+ expect(abrManager.chooseVariant).toHaveBeenCalled();
expect(manifest.periods[0].variants[0].id).toBe(0);
expect(manifest.periods[0].variants[0].allowedByKeySystem)
.toBe(false);
- activeVariant = getActiveTrack('variant');
+ activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(1);
}).catch(fail).then(done);
});
@@ -1968,15 +1981,15 @@ describe('Player', function() {
.build();
setupPlayer(manifest).then(function() {
- abrManager.chooseStreams.calls.reset();
+ abrManager.chooseVariant.calls.reset();
- var activeVariant = getActiveTrack('variant');
+ var activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(0);
onKeyStatus({'abc': 'usable'});
- expect(abrManager.chooseStreams).not.toHaveBeenCalled();
+ expect(abrManager.chooseVariant).not.toHaveBeenCalled();
- activeVariant = getActiveTrack('variant');
+ activeVariant = getActiveVariantTrack();
expect(activeVariant.id).toBe(0);
}).catch(fail).then(done);
});
@@ -2283,20 +2296,6 @@ describe('Player', function() {
}).catch(fail).then(done);
});
- /**
- * Gets the currently active track.
- * @param {string} type
- * @return {shakaExtern.Track}
- */
- function getActiveTrack(type) {
- var activeTracks = player.getVariantTracks().filter(function(track) {
- return track.type == type && track.active;
- });
-
- expect(activeTracks.length).toBe(1);
- return activeTracks[0];
- }
-
/**
* @param {!Object.} keyStatusMap
* @suppress {accessControls}
@@ -2313,9 +2312,8 @@ describe('Player', function() {
var parser = new shaka.test.FakeManifestParser(manifest);
var parserFactory = function() { return parser; };
return player.load('', 0, parserFactory).then(function() {
- // "initialize" the current period.
- chooseStreams();
- canSwitch();
+ // Initialize the fake streams.
+ streamingEngine.onCanSwitch();
});
}
});
@@ -2357,6 +2355,37 @@ describe('Player', function() {
}).then(done);
});
+ it('does not assert when adapting', function(done) {
+ // Most of our Player unit tests never adapt. This allowed some assertions
+ // to creep in that went uncaught until they happened during manual testing.
+ // Repro only happens with audio+video variants in which we only adapt one
+ // type. This test covers https://github.com/google/shaka-player/issues/954
+
+ manifest = new shaka.test.ManifestGenerator()
+ .addPeriod(0)
+ .addVariant(0).bandwidth(100)
+ .addVideo(0).mime('video/mp4', 'good')
+ .addAudio(9).mime('audio/mp4', 'good')
+ .addVariant(1).bandwidth(200)
+ .addVideo(1).mime('video/mp4', 'good')
+ .addAudio(9) // reuse audio stream from variant 0
+ .addVariant(2).bandwidth(300)
+ .addVideo(2).mime('video/mp4', 'good')
+ .addAudio(9) // reuse audio stream from variant 0
+ .build();
+
+ var parser = new shaka.test.FakeManifestParser(manifest);
+ var parserFactory = function() { return parser; };
+
+ player.load('', 0, parserFactory).then(function() {
+ streamingEngine.onCanSwitch();
+
+ // We've already loaded variants[0]. Switch to [1] and [2].
+ abrManager.switchCallback(manifest.periods[0].variants[1]);
+ abrManager.switchCallback(manifest.periods[0].variants[2]);
+ }).catch(fail).then(done);
+ });
+
describe('isTextTrackVisible', function() {
it('does not throw before load', function() {
player.isTextTrackVisible();
@@ -2439,19 +2468,53 @@ describe('Player', function() {
});
});
+ /**
+ * Gets the currently active variant track.
+ * @return {shakaExtern.Track}
+ */
+ function getActiveVariantTrack() {
+ var activeTracks = player.getVariantTracks().filter(function(track) {
+ return track.active;
+ });
+
+ expect(activeTracks.length).toBe(1);
+ return activeTracks[0];
+ }
+
+ /**
+ * Gets the currently active text track.
+ * @return {shakaExtern.Track}
+ */
+ function getActiveTextTrack() {
+ var activeTracks = player.getTextTracks().filter(function(track) {
+ return track.active;
+ });
+
+ expect(activeTracks.length).toBe(1);
+ return activeTracks[0];
+ }
+
+ /**
+ * Simulate the transition to a new period using the fake StreamingEngine.
+ * @param {number} index
+ */
+ function transitionPeriod(index) {
+ periodIndex = index;
+ streamingEngine.onChooseStreams();
+ }
+
/**
* Choose streams for the given period.
*
* @suppress {accessControls}
- * @return {!Object.}
+ * @return {shaka.media.StreamingEngine.ChosenStreams}
*/
- function chooseStreams() {
- var period = manifest.periods[0];
- return player.onChooseStreams_(period);
+ function onChooseStreams() {
+ return player.onChooseStreams_(manifest.periods[periodIndex]);
}
/** @suppress {accessControls} */
- function canSwitch() { player.canSwitch_(); }
+ function onCanSwitch() { player.canSwitch_(); }
/**
* A Jasmine asymmetric matcher for substring matches.
diff --git a/test/test/util/fake_media_source_engine.js b/test/test/util/fake_media_source_engine.js
index 16e42fa9e8..3f1585137e 100644
--- a/test/test/util/fake_media_source_engine.js
+++ b/test/test/util/fake_media_source_engine.js
@@ -91,14 +91,42 @@ shaka.test.FakeMediaSourceEngine = function(segmentData, opt_drift) {
this.appendBuffer = jasmine.createSpy('appendBuffer')
.and.callFake(this.appendBufferImpl.bind(this));
- spyOn(this, 'destroy').and.callThrough();
- spyOn(this, 'bufferStart').and.callThrough();
- spyOn(this, 'bufferEnd').and.callThrough();
- spyOn(this, 'bufferedAheadOf').and.callThrough();
- spyOn(this, 'remove').and.callThrough();
- spyOn(this, 'clear').and.callThrough();
- spyOn(this, 'flush').and.callThrough();
- spyOn(this, 'setStreamProperties').and.callThrough();
+ /** @type {!jasmine.Spy} */
+ this.clear = jasmine.createSpy('clear')
+ .and.callFake(this.clearImpl_.bind(this));
+
+ /** @type {!jasmine.Spy} */
+ this.bufferStart = jasmine.createSpy('bufferStart')
+ .and.callFake(this.bufferStartImpl_.bind(this));
+
+ /** @type {!jasmine.Spy} */
+ this.bufferEnd = jasmine.createSpy('bufferEnd')
+ .and.callFake(this.bufferEndImpl_.bind(this));
+
+ /** @type {!jasmine.Spy} */
+ this.isBuffered = jasmine.createSpy('isBuffered')
+ .and.callFake(this.isBufferedImpl_.bind(this));
+
+ /** @type {!jasmine.Spy} */
+ this.bufferedAheadOf = jasmine.createSpy('bufferedAheadOf')
+ .and.callFake(this.bufferedAheadOfImpl_.bind(this));
+
+ /** @type {!jasmine.Spy} */
+ this.setStreamProperties = jasmine.createSpy('setStreamProperties')
+ .and.callFake(this.setStreamPropertiesImpl_.bind(this));
+
+ /** @type {!jasmine.Spy} */
+ this.remove = jasmine.createSpy('remove')
+ .and.callFake(this.removeImpl.bind(this));
+
+ /** @type {!jasmine.Spy} */
+ this.flush = jasmine.createSpy('flush').and.returnValue(Promise.resolve());
+};
+
+
+/** @override */
+shaka.test.FakeMediaSourceEngine.prototype.destroy = function() {
+ return Promise.resolve();
};
@@ -128,14 +156,12 @@ shaka.test.FakeMediaSourceEngine = function(segmentData, opt_drift) {
shaka.test.FakeMediaSourceEngine.SegmentData;
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.destroy = function() {
- return Promise.resolve();
-};
-
-
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.bufferStart = function(type) {
+/**
+ * @param {string} type
+ * @return {?number}
+ * @private
+ */
+shaka.test.FakeMediaSourceEngine.prototype.bufferStartImpl_ = function(type) {
if (this.segments[type] === undefined) throw new Error('unexpected type');
var first = this.segments[type].indexOf(true);
@@ -146,8 +172,12 @@ shaka.test.FakeMediaSourceEngine.prototype.bufferStart = function(type) {
};
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.bufferEnd = function(type) {
+/**
+ * @param {string} type
+ * @return {?number}
+ * @private
+ */
+shaka.test.FakeMediaSourceEngine.prototype.bufferEndImpl_ = function(type) {
if (this.segments[type] === undefined) throw new Error('unexpected type');
var last = this.segments[type].lastIndexOf(true);
@@ -158,8 +188,14 @@ shaka.test.FakeMediaSourceEngine.prototype.bufferEnd = function(type) {
};
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.isBuffered = function(type, time) {
+/**
+ * @param {string} type
+ * @param {number} time
+ * @return {boolean}
+ * @private
+ */
+shaka.test.FakeMediaSourceEngine.prototype.isBufferedImpl_ =
+ function(type, time) {
if (this.segments[type] === undefined) throw new Error('unexpected type');
var first = this.segments[type].indexOf(true);
@@ -171,8 +207,13 @@ shaka.test.FakeMediaSourceEngine.prototype.isBuffered = function(type, time) {
};
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.bufferedAheadOf = function(
+/**
+ * @param {string} type
+ * @param {number} start
+ * @return {number}
+ * @private
+ */
+shaka.test.FakeMediaSourceEngine.prototype.bufferedAheadOfImpl_ = function(
type, start) {
if (this.segments[type] === undefined) throw new Error('unexpected type');
@@ -268,8 +309,14 @@ shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl = function(
};
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.remove = function(type, start, end) {
+/**
+ * @param {string} type
+ * @param {number} start
+ * @param {number} end
+ * @return {!Promise}
+ */
+shaka.test.FakeMediaSourceEngine.prototype.removeImpl =
+ function(type, start, end) {
if (this.segments[type] === undefined) throw new Error('unexpected type');
var first = this.toIndex_(type, start);
@@ -292,8 +339,12 @@ shaka.test.FakeMediaSourceEngine.prototype.remove = function(type, start, end) {
};
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.clear = function(type) {
+/**
+ * @param {string} type
+ * @return {!Promise}
+ * @private
+ */
+shaka.test.FakeMediaSourceEngine.prototype.clearImpl_ = function(type) {
if (this.segments[type] === undefined) throw new Error('unexpected type');
for (var i = 0; i < this.segments[type].length; ++i) {
@@ -306,7 +357,7 @@ shaka.test.FakeMediaSourceEngine.prototype.clear = function(type) {
if (type == ContentType.VIDEO && this.segments['trickvideo']) {
// 'trickvideo' value is only used for testing.
// Cast to the ContentType enum for compatibility.
- this.clear(
+ this.clearImpl_(
/**@type {shaka.util.ManifestParserUtils.ContentType} */('trickvideo'));
}
@@ -314,14 +365,14 @@ shaka.test.FakeMediaSourceEngine.prototype.clear = function(type) {
};
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.flush = function(type) {
- return Promise.resolve();
-};
-
-
-/** @override */
-shaka.test.FakeMediaSourceEngine.prototype.setStreamProperties = function(
+/**
+ * @param {string} type
+ * @param {number} offset
+ * @param {number} appendWindowEnd
+ * @return {!Promise}
+ * @private
+ */
+shaka.test.FakeMediaSourceEngine.prototype.setStreamPropertiesImpl_ = function(
type, offset, appendWindowEnd) {
if (this.segments[type] === undefined) throw new Error('unexpected type');
this.timestampOffsets_[type] = offset;
diff --git a/test/test/util/simple_fakes.js b/test/test/util/simple_fakes.js
index df1ca5af83..49843daf05 100644
--- a/test/test/util/simple_fakes.js
+++ b/test/test/util/simple_fakes.js
@@ -47,37 +47,20 @@ goog.provide('shaka.test.FakeVideo');
shaka.test.FakeAbrManager = function() {
var ret = jasmine.createSpyObj('FakeAbrManager', [
'stop', 'init', 'enable', 'disable', 'segmentDownloaded',
- 'getBandwidthEstimate', 'chooseStreams', 'setVariants', 'setTextStreams',
- 'configure'
+ 'getBandwidthEstimate', 'chooseVariant', 'setVariants', 'configure'
]);
/** @type {!Array.} */
var variants = [];
- /** @type {!Array.} */
- var textStreams = [];
+
ret.chooseIndex = 0;
+ ret.init.and.callFake(function(switchCallback) {
+ ret.switchCallback = switchCallback;
+ });
ret.setVariants.and.callFake(function(arg) { variants = arg; });
- ret.setTextStreams.and.callFake(function(arg) { textStreams = arg; });
- ret.chooseStreams.and.callFake(function(mediaTypesToUpdate) {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
- var streams = {};
- var variant = variants[ret.chooseIndex];
-
- var textStream = null;
- if (textStreams.length > ret.chooseIndex)
- textStream = textStreams[ret.chooseIndex];
-
- if (mediaTypesToUpdate.indexOf(ContentType.AUDIO) > -1 ||
- mediaTypesToUpdate.indexOf(ContentType.VIDEO) > -1) {
- if (variant.audio) streams[ContentType.AUDIO] = variant.audio;
- if (variant.video) streams[ContentType.VIDEO] = variant.video;
- }
-
- if (mediaTypesToUpdate.indexOf(ContentType.TEXT) > -1 && textStream)
- streams[ContentType.TEXT] = textStream;
-
- return streams;
+ ret.chooseVariant.and.callFake(function() {
+ return variants[ret.chooseIndex];
});
return ret;
@@ -88,6 +71,10 @@ shaka.test.FakeAbrManager = function() {
shaka.test.FakeAbrManager.prototype.chooseIndex;
+/** @type {shakaExtern.AbrManager.SwitchCallback} */
+shaka.test.FakeAbrManager.prototype.switchCallback;
+
+
/** @type {!jasmine.Spy} */
shaka.test.FakeAbrManager.prototype.stop;
@@ -113,17 +100,13 @@ shaka.test.FakeAbrManager.prototype.getBandwidthEstimate;
/** @type {!jasmine.Spy} */
-shaka.test.FakeAbrManager.prototype.chooseStreams;
+shaka.test.FakeAbrManager.prototype.chooseVariant;
/** @type {!jasmine.Spy} */
shaka.test.FakeAbrManager.prototype.setVariants;
-/** @type {!jasmine.Spy} */
-shaka.test.FakeAbrManager.prototype.setTextStreams;
-
-
/** @type {!jasmine.Spy} */
shaka.test.FakeAbrManager.prototype.configure;
@@ -198,16 +181,18 @@ shaka.test.FakeDrmEngine.prototype.setSessionIds;
* @constructor
* @struct
* @extends {shaka.media.StreamingEngine}
+ * @param {function():shaka.media.StreamingEngine.ChosenStreams} onChooseStreams
+ * @param {function()} onCanSwitch
* @return {!Object}
*/
-shaka.test.FakeStreamingEngine = function() {
- var ContentType = shaka.util.ManifestParserUtils.ContentType;
+shaka.test.FakeStreamingEngine = function(onChooseStreams, onCanSwitch) {
var resolve = Promise.resolve.bind(Promise);
var activeStreams = {};
var ret = jasmine.createSpyObj('fakeStreamingEngine', [
'destroy', 'configure', 'init', 'getCurrentPeriod', 'getActivePeriod',
- 'getActiveStreams', 'notifyNewTextStream', 'switch', 'seeked'
+ 'getActiveStreams', 'notifyNewTextStream', 'switchVariant',
+ 'switchTextStream', 'seeked'
]);
ret.destroy.and.callFake(resolve);
ret.getCurrentPeriod.and.returnValue(null);
@@ -215,20 +200,27 @@ shaka.test.FakeStreamingEngine = function() {
ret.getActiveStreams.and.returnValue(activeStreams);
ret.notifyNewTextStream.and.callFake(resolve);
ret.init.and.callFake(function() {
- var period = ret.getCurrentPeriod();
- var variant = period.variants[0];
- if (variant.audio)
- activeStreams[ContentType.AUDIO] = variant.audio;
+ var chosen = onChooseStreams();
+ return Promise.resolve().then(function() {
+ if (chosen.variant && chosen.variant.video)
+ activeStreams['video'] = chosen.variant.video;
+ if (chosen.variant && chosen.variant.audio)
+ activeStreams['audio'] = chosen.variant.audio;
+ if (chosen.text)
+ activeStreams['text'] = chosen.text;
+ });
+ });
+ ret.switchVariant.and.callFake(function(variant) {
if (variant.video)
- activeStreams[ContentType.VIDEO] = variant.video;
- var text = period.textStreams[0];
- if (text)
- activeStreams[ContentType.TEXT] = text;
- return Promise.resolve();
+ activeStreams['video'] = variant.video;
+ if (variant.audio)
+ activeStreams['audio'] = variant.audio;
});
- ret.switch.and.callFake(function(type, stream) {
- activeStreams[type] = stream;
+ ret.switchTextStream.and.callFake(function(textStream) {
+ activeStreams['text'] = textStream;
});
+ ret.onChooseStreams = onChooseStreams;
+ ret.onCanSwitch = onCanSwitch;
return ret;
};
@@ -238,13 +230,25 @@ shaka.test.FakeStreamingEngine.prototype.init;
/** @type {jasmine.Spy} */
-shaka.test.FakeStreamingEngine.prototype.switch;
+shaka.test.FakeStreamingEngine.prototype.switchVariant;
+
+
+/** @type {jasmine.Spy} */
+shaka.test.FakeStreamingEngine.prototype.switchTextStream;
/** @type {jasmine.Spy} */
shaka.test.FakeStreamingEngine.prototype.getCurrentPeriod;
+/** @type {function()} */
+shaka.test.FakeStreamingEngine.prototype.onChooseStreams;
+
+
+/** @type {function()} */
+shaka.test.FakeStreamingEngine.prototype.onCanSwitch;
+
+
/**
* Creates a fake manifest parser.