Skip to content

Commit

Permalink
Overhaul load/unload/destroy
Browse files Browse the repository at this point in the history
player.load() now uses our new CancelableChain abstraction for its
chain of events.  player.unload() and player.destroy() will both
cancel that chain.  player.unload() will wait on the cancelation to
complete before resetting the streaming system, to avoid a race.

Change-Id: I37fcbacde33f253982c3dd5ed246855d4e363c79
  • Loading branch information
joeyparrish committed Jun 7, 2016
1 parent d8865f8 commit a644900
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 154 deletions.
245 changes: 108 additions & 137 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ goog.require('shaka.media.Playhead');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.media.StreamingEngine');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.CancelableChain');
goog.require('shaka.util.ConfigUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
Expand Down Expand Up @@ -56,6 +57,9 @@ goog.require('shaka.util.StreamUtils');
shaka.Player = function(video, opt_dependencyInjector) {
shaka.util.FakeEventTarget.call(this);

/** @private {boolean} */
this.destroyed_ = false;

/** @private {HTMLMediaElement} */
this.video_ = video;

Expand Down Expand Up @@ -111,11 +115,8 @@ shaka.Player = function(video, opt_dependencyInjector) {
/** @private {boolean} */
this.switchingPeriods_ = true;

/** @private {boolean} */
this.loadInProgress_ = false;

/** @private {number} */
this.loadCounter_ = 0;
/** @private {shaka.util.CancelableChain} */
this.loadChain_ = null;

/**
* @private {!Object.<string, {stream: shakaExtern.Stream, clear: boolean}>}
Expand Down Expand Up @@ -153,20 +154,32 @@ goog.inherits(shaka.Player, shaka.util.FakeEventTarget);
* @export
*/
shaka.Player.prototype.destroy = function() {
var p = Promise.all([
this.destroyStreaming_(),
this.eventManager_ ? this.eventManager_.destroy() : null,
this.networkingEngine_ ? this.networkingEngine_.destroy() : null
]);
this.destroyed_ = true;

this.video_ = null;
this.textTrack_ = null;
this.eventManager_ = null;
this.defaultAbrManager_ = null;
this.networkingEngine_ = null;
this.config_ = null;
var cancelation = Promise.resolve();
if (this.loadChain_) {
// A load is in progress. Cancel it.
cancelation = this.loadChain_.cancel(new shaka.util.Error(
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.LOAD_INTERRUPTED));
}

return p;
return cancelation.then(function() {
var p = Promise.all([
this.destroyStreaming_(),
this.eventManager_ ? this.eventManager_.destroy() : null,
this.networkingEngine_ ? this.networkingEngine_.destroy() : null
]);

this.video_ = null;
this.textTrack_ = null;
this.eventManager_ = null;
this.defaultAbrManager_ = null;
this.networkingEngine_ = null;
this.config_ = null;

return p;
}.bind(this));
};


Expand Down Expand Up @@ -317,54 +330,55 @@ shaka.Player.probeSupport = function() {
* unregistered parser.
* @return {!Promise} Resolved when the manifest has been loaded and playback
* has begun; rejected when an error occurs or the call was interrupted by
* unload() or another call to load().
* destroy(), unload() or another call to load().
* @export
*/
shaka.Player.prototype.load = function(manifestUri, opt_startTime,
opt_manifestParserFactory) {
var unloaded = this.unload();
var ready = this.loadInternal_(manifestUri, opt_manifestParserFactory);
var unloadPromise = this.unload();
var loadChain = new shaka.util.CancelableChain();
this.loadChain_ = loadChain;

this.loadInProgress_ = true;
return loadChain.then(function() {
return unloadPromise;
}).then(function() {

// If load(), unload(), or destroy() is called, abort this call to load().
var counter = ++this.loadCounter_;
function interrupt() {
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.LOAD_INTERRUPTED));
}

return Promise.all([ready, unloaded]).then(function(data) {
var parser = data[0].parser;
var manifest = data[0].manifest;
var drmEngine = data[0].drmEngine;

if (!this.video_ || (counter != this.loadCounter_)) {
// destroy() or unload() was called, so the temporary parser and
// drmEngine must be stopped and destroyed.
if (parser) parser.stop();
if (drmEngine) drmEngine.destroy();
return interrupt();
}
goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
return shaka.media.ManifestParser.getFactory(
manifestUri,
this.networkingEngine_,
this.config_.manifest.retryParameters,
opt_manifestParserFactory);
}.bind(this)).then(function(factory) {

goog.asserts.assert(parser, 'parser should not be null');
goog.asserts.assert(manifest, 'manifest should not be null');
goog.asserts.assert(drmEngine, 'drmEngine should not be null');
this.parser_ = new factory();
this.parser_.configure(this.config_.manifest);
goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
return this.parser_.start(
manifestUri,
this.networkingEngine_,
this.filterPeriod_.bind(this),
this.onError_.bind(this));
}.bind(this)).then(function(manifest) {

this.lastStatUpdateTimestamp_ = Date.now() / 1000;
this.parser_ = parser;
this.manifest_ = manifest;
this.manifestUri_ = manifestUri;
this.drmEngine_ = drmEngine;
this.drmEngine_ = this.createDrmEngine();
this.drmEngine_.configure(this.config_.drm);
return this.drmEngine_.init(manifest, false /* isOffline */);
}.bind(this)).then(function() {

// Re-filter the manifest after DRM has been initialized.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));

this.lastStatUpdateTimestamp_ = Date.now() / 1000;

// Wait for MediaSource to open before continuing.
return Promise.all([
this.drmEngine_.attach(this.video_),
this.mediaSourceOpen_
]);
}.bind(this)).then(function() {
if (!this.video_ || (counter != this.loadCounter_)) return interrupt();

// MediaSource is open, so create the Playhead, MediaSourceEngine, and
// StreamingEngine.
Expand All @@ -375,7 +389,6 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime,
this.streamingEngine_.configure(this.config_.streaming);
return this.streamingEngine_.init();
}.bind(this)).then(function() {
if (!this.video_ || (counter != this.loadCounter_)) return interrupt();

// Re-filter the manifest after streams have been chosen.
this.manifest_.periods.forEach(this.filterPeriod_.bind(this));
Expand All @@ -386,75 +399,21 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime,

this.config_.abr.manager.init(this.switch_.bind(this));

this.loadInProgress_ = false;
}.bind(this)).catch(function(error) {
this.loadChain_ = null;
}.bind(this)).finalize().catch(function(error) {
goog.asserts.assert(error instanceof shaka.util.Error,
'Wrong error type!');
shaka.log.debug('load() failed:', error);

if (this.video_ && (counter == this.loadCounter_)) {
// Not destroyed and not interrupted.
this.loadInProgress_ = false;
// If we haven't started another load, clear the loadChain_ member.
if (this.loadChain_ == loadChain) {
this.loadChain_ = null;
}

return Promise.reject(error);
}.bind(this));
};


/**
* Loads the given manifest and creates and initializes the manifest parser and
* DrmEngine. The caller takes ownership of the returned objects.
*
* @param {string} manifestUri
* @param {shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
* @return {!Promise.<{parser: shakaExtern.ManifestParser,
* manifest: ?shakaExtern.Manifest,
* drmEngine: shaka.media.DrmEngine}>}
* @private
*/
shaka.Player.prototype.loadInternal_ = function(
manifestUri, opt_manifestParserFactory) {
var ret = {
parser: null,
manifest: null,
drmEngine: null
};

goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
var getFactory = shaka.media.ManifestParser.getFactory(
manifestUri,
this.networkingEngine_,
this.config_.manifest.retryParameters,
opt_manifestParserFactory);

return getFactory.then(function(factory) {
if (!this.video_) return ret; // Destroyed. Caller must release objects.

ret.parser = new factory();
ret.parser.configure(this.config_.manifest);
return ret.parser.start(
manifestUri,
this.networkingEngine_,
this.filterPeriod_.bind(this),
this.onError_.bind(this));
}.bind(this)).then(function(manifest) {
if (!this.video_) return ret; // Destroyed.

ret.manifest = manifest;
ret.drmEngine = this.createDrmEngine();
ret.drmEngine.configure(this.config_.drm);
return ret.drmEngine.init(manifest, false /* isOffline */);
}.bind(this)).then(function() {
if (!this.video_) return ret; // Destroyed.

// Re-filter the manifest after DRM has been initialized.
ret.manifest.periods.forEach(this.filterPeriod_.bind(this));
return ret;
}.bind(this));
};


/**
* Creates a new instance of DrmEngine. This can be replaced by tests to
* create fake instances instead.
Expand Down Expand Up @@ -769,23 +728,19 @@ shaka.Player.prototype.isBuffering = function() {
* @export
*/
shaka.Player.prototype.unload = function() {
var loadInProgress = this.loadInProgress_;

this.loadInProgress_ = false;
++this.loadCounter_;
if (this.destroyed_) return Promise.resolve();

if (loadInProgress || this.manifest_) {
return this.destroyStreaming_().then(function() {
if (!this.video_) return; // Destroyed.

// Force an exit from the buffering state.
this.onBuffering_(false);

// Start the (potentially slow) process of opening MediaSource now.
this.mediaSourceOpen_ = this.createMediaSource();
}.bind(this));
if (this.loadChain_) {
// A load is in progress. Cancel it, then reset the streaming system.
var interrupt = new shaka.util.Error(
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.LOAD_INTERRUPTED);
return this.loadChain_.cancel(interrupt)
.then(this.resetStreaming_.bind(this));
} else {
return Promise.resolve();
// No loads or unloads are in progress.
// Just reset the streaming system if needed.
return this.resetStreaming_();
}
};

Expand Down Expand Up @@ -1029,7 +984,7 @@ shaka.Player.prototype.addTextTrack = function(
};

return this.streamingEngine_.notifyNewStream('text', stream).then(function() {
if (!this.video_) return; // Destroyed.
if (this.destroyed_) return;

// Only add the stream once it has been initialized. This ensures that
// calls to getTracks do not return the uninitialized stream.
Expand Down Expand Up @@ -1119,7 +1074,7 @@ shaka.Player.prototype.destroyStreaming_ = function() {
}

var p = Promise.all([
this.config_.abr.manager.stop(),
this.config_ ? this.config_.abr.manager.stop() : null,
this.drmEngine_ ? this.drmEngine_.destroy() : null,
this.mediaSourceEngine_ ? this.mediaSourceEngine_.destroy() : null,
this.playhead_ ? this.playhead_.destroy() : null,
Expand All @@ -1145,6 +1100,30 @@ shaka.Player.prototype.destroyStreaming_ = function() {
};


/**
* Reset the streaming system.
* @return {!Promise}
* @private
*/
shaka.Player.prototype.resetStreaming_ = function() {
if (!this.parser_) {
// Nothing is playing, so this is effectively a no-op.
return Promise.resolve();
}

// Destroy the streaming system before we recreate everything.
return this.destroyStreaming_().then(function() {
if (this.destroyed_) return;

// Force an exit from the buffering state.
this.onBuffering_(false);

// Start the (potentially slow) process of opening MediaSource now.
this.mediaSourceOpen_ = this.createMediaSource();
}.bind(this));
};


/**
* @const {string}
* @private
Expand Down Expand Up @@ -1245,7 +1224,7 @@ shaka.Player.prototype.filterPeriod_ = function(period) {

var tracksChanged = shaka.util.StreamUtils.applyRestrictions(
period, this.config_.restrictions);
if (tracksChanged && !this.loadInProgress_)
if (tracksChanged && !this.loadChain_)
this.onTracksChanged_();

var allStreamsRestricted =
Expand Down Expand Up @@ -1425,7 +1404,7 @@ shaka.Player.prototype.onChooseStreams_ = function(period) {

// If we are presently loading, we aren't done filtering streams just yet.
// Wait to send a 'trackschanged' event.
if (!this.loadInProgress_) {
if (!this.loadChain_) {
this.onTracksChanged_();
}

Expand Down Expand Up @@ -1495,11 +1474,7 @@ shaka.Player.prototype.onAdaptation_ = function() {
// This gives StreamingEngine time to absorb the changes before the user
// tries to query them.
Promise.resolve().then(function() {
if (!this.video_) {
// We've been destroyed! Do nothing.
return;
}

if (this.destroyed_) return;
var event = new shaka.util.FakeEvent('adaptation');
this.dispatchEvent(event);
}.bind(this));
Expand All @@ -1515,11 +1490,7 @@ shaka.Player.prototype.onTracksChanged_ = function() {
// This gives StreamingEngine time to absorb the changes before the user
// tries to query them.
Promise.resolve().then(function() {
if (!this.video_) {
// We've been destroyed! Do nothing.
return;
}

if (this.destroyed_) return;
var event = new shaka.util.FakeEvent('trackschanged');
this.dispatchEvent(event);
}.bind(this));
Expand Down
3 changes: 0 additions & 3 deletions shaka-player.uncompiled.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,3 @@ goog.require('shaka.polyfill.Promise');
goog.require('shaka.polyfill.VideoPlaybackQuality');

goog.require('shaka.offline.DBEngine');

// Temporary: this will be removed once shaka.Player requires CancelableChain:
goog.require('shaka.util.CancelableChain');
Loading

0 comments on commit a644900

Please sign in to comment.