Skip to content

Commit

Permalink
Add Media Source Engine Step in Load Graph
Browse files Browse the repository at this point in the history
This change takes the part of |onLoad| that creates media source
engine and makes it into its own state in the graph.

This allows us to pre-initialize media source engine and will allow for
the load-process to be aborted after initializing media source engine
but before load.

This ensure that we only initialize media source engine once per load
(fixing #1570) by modeling "initialized media source" as its own
node in the graph.

The tests that appear to be removed from "player_integration.js" have
been moved to be with the other load graph tests and re-written to
make use of the load-graph events for testing.

The stats test that appears to be removed from "player_unit.js" was
moved to "player_integration.js". This test was failing before the
player was not overriding media source (like the other tests were) since
this one had to do with load order, it was moved to be closer to the
other load order tests.

Issue #816
Issue #997
Closes #1570

Change-Id: I646f0559e3878f28374a3fc56f3d70f6d8a462a4
  • Loading branch information
vaage committed Feb 26, 2019
1 parent b978ae8 commit 1752f1d
Show file tree
Hide file tree
Showing 3 changed files with 335 additions and 136 deletions.
195 changes: 152 additions & 43 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,17 @@ shaka.Player = function(mediaElement, dependencyInjector) {
/** @private {shaka.routing.Node} */
this.unloadNode_ = {name: 'unload'};
/** @private {shaka.routing.Node} */
this.mediaSourceNode_ = {name: 'media-source'};
/** @private {shaka.routing.Node} */
this.loadNode_ = {name: 'load'};

const actions = new Map();
actions.set(this.attachNode_, (has, wants) => this.onAttach_(has, wants));
actions.set(this.detachNode_, (has, wants) => this.onDetach_(has, wants));
actions.set(this.unloadNode_, (has, wants) => this.onUnload_(has, wants));
actions.set(this.mediaSourceNode_, (has, wants) => {
return this.onInitializeMediaSourceEngine_(has, wants);
});
actions.set(this.loadNode_, (has, wants) => this.onLoad_(has, wants));

/** @private {shaka.routing.Walker.Implementation} */
Expand Down Expand Up @@ -254,7 +259,7 @@ shaka.Player = function(mediaElement, dependencyInjector) {
// the LAST thing we do in the constructor because conceptually it relies on
// player having been initialized.
if (mediaElement) {
this.attach(mediaElement);
this.attach(mediaElement, /* initializeMediaSource= */ true);
}
};

Expand Down Expand Up @@ -634,10 +639,6 @@ shaka.Player.probeSupport = function() {
* Calls to |attach| will interrupt any in-progress calls to |load| but cannot
* interrupt calls to |attach|, |detach|, or |unload|.
*
* TODO(vaage): Restore functionality for pre-initializing media source engine.
* It should default to pre-initializing media source when the
* optional param is not provided.
*
* @param {!HTMLMediaElement} mediaElement
* @param {boolean=} initializeMediaSource
* @return {!Promise}
Expand All @@ -652,12 +653,25 @@ shaka.Player.prototype.attach = function(mediaElement, initializeMediaSource) {
const payload = this.createEmptyPayload_();
payload.mediaElement = mediaElement;

let destination = initializeMediaSource ?
this.mediaSourceNode_ :
this.attachNode_;

// Because |initializeMediaSource| is optional, it can be |undefined| which
// will get evaluated to |false|. However, we want to default to |true| when
// the value is not provided. To play well with closure, and to handle the
// default value on its own, we will override our destination if the value was
// not provided.
if (initializeMediaSource === undefined) {
destination = this.mediaSourceNode_;
}

// Tell the walker to go to "attached", but do not let this request be
// interrupted. If it could be interrupted, it could allow |onLoad| to be
// called with no media element.
const events = this.walker_.startNewRoute((currentPayload) => {
return {
node: this.attachNode_,
node: destination,
payload: payload,
interruptible: false,
};
Expand Down Expand Up @@ -735,16 +749,37 @@ shaka.Player.prototype.unload = function(initializeMediaSource) {
const payload = this.createEmptyPayload_();

const events = this.walker_.startNewRoute((currentPayload) => {
const node = currentPayload.mediaElement ?
this.attachNode_ :
this.detachNode_;
// When someone calls |unload| we can either be before attached or detached
// (there is nothing stopping someone from calling |detach| when we are
// already detached).
//
// If we are attached to the correct element, we can tear down the previous
// playback components and go to the attached media source node depending
// on whether or not the caller wants to pre-init media source.
//
// If we don't have a media element, we assume that we are already at the
// detached node - but only the walker knows that. To ensure we are actually
// there, we tell the walker to go to detach. While this is technically
// unnecessary, it ensures that we are in the state we want to be in and
// ready for the next request.
let destination = null;

if (currentPayload.mediaElement && initializeMediaSource) {
destination = this.mediaSourceNode_;
} else if (currentPayload.mediaElement) {
destination = this.attachNode_;
} else {
destination = this.detachNode_;
}

goog.asserts.assert(destination, 'We should have picked a destination.');

// Copy over the media element because we want to keep using the same
// element - the other values don't matter.
payload.mediaElement = currentPayload.mediaElement;

return {
node: node,
node: destination,
payload: payload,
interruptible: false,
};
Expand Down Expand Up @@ -1010,6 +1045,53 @@ shaka.Player.prototype.onUnload_ = async function(has, wants) {
};


/**
* This should only be called by the load graph when it is time to initialize
* media source engine. The only time this may be called is when we are attached
* to the same media element as in the request.
*
* This method assumes that it is safe for it to execute, the load-graph is
* responsible for ensuring all assumptions are true.
*
* @param {shaka.routing.Payload} has
* @param {shaka.routing.Payload} wants
*
* @return {!Promise}
* @private
*/
shaka.Player.prototype.onInitializeMediaSourceEngine_ = async function(
has, wants) {
goog.asserts.assert(
has.mediaElement,
'We should have a media element when loading.');
goog.asserts.assert(
has.mediaElement == wants.mediaElement,
'|has| and |wants| should have the same media element when loading.');
goog.asserts.assert(
this.mediaSourceEngine_ == null,
'We should not have a media source engine yet.');

const closedCaptionsParser =
shaka.media.MuxJSClosedCaptionParser.isSupported() ?
new shaka.media.MuxJSClosedCaptionParser() :
new shaka.media.NoopCaptionParser();

const TextDisplayerFactory = this.config_.textDisplayFactory;
const textDisplayer = new TextDisplayerFactory();
textDisplayer.setTextVisibility(this.textVisibility_);

const mediaSourceEngine = this.createMediaSourceEngine(
has.mediaElement, closedCaptionsParser, textDisplayer);

// Wait for media source engine to finish opening. This promise should
// NEVER be rejected as per the media source engine implementation.
await mediaSourceEngine.open();

// Wait until it is ready to actually store the reference.
this.mediaSourceEngine_ = mediaSourceEngine;
};


/**
* This should only be called by the load graph when it is time to load all
* playback components needed for playback. The only times this may be called
Expand Down Expand Up @@ -1115,8 +1197,6 @@ shaka.Player.prototype.onLoad_ = async function(has, wants) {
return this.switch_(variant, clearBuffer, safeMargin);
});

this.mediaSourceEngine_ = this.createMediaSourceEngine();

this.playhead_ = this.createPlayhead(has.startTime);
this.playheadObservers_ = this.createPlayheadObservers_();

Expand Down Expand Up @@ -1496,27 +1576,19 @@ shaka.Player.prototype.createPlayheadObservers_ = function() {


/**
* Creates a new instance of MediaSourceEngine. This can be replaced by tests
* to create fake instances instead.
* Create a new media source engine. This will ONLY be replaced by tests as a
* way to inject fake media source engine instances.
*
* @param {!HTMLMediaElement} mediaElement
* @param {!shaka.media.IClosedCaptionParser} closedCaptionsParser
* @param {!shaka.extern.TextDisplayer} textDisplayer
*
* @return {!shaka.media.MediaSourceEngine}
*/
shaka.Player.prototype.createMediaSourceEngine = function() {
goog.asserts.assert(this.video_, 'video should be valid');

const closedCaptionsParser =
shaka.media.MuxJSClosedCaptionParser.isSupported() ?
new shaka.media.MuxJSClosedCaptionParser() :
new shaka.media.NoopCaptionParser();

const TextDisplayerFactory = this.config_.textDisplayFactory;
const textDisplayer = new TextDisplayerFactory();
textDisplayer.setTextVisibility(this.textVisibility_);

shaka.Player.prototype.createMediaSourceEngine = function(
mediaElement, closedCaptionsParser, textDisplayer) {
return new shaka.media.MediaSourceEngine(
this.video_,
closedCaptionsParser,
textDisplayer);
mediaElement, closedCaptionsParser, textDisplayer);
};


Expand Down Expand Up @@ -3941,17 +4013,18 @@ shaka.Player.prototype.createAbortLoadError_ = function() {
/**
* Key
* ----------------------
* - D : Detach Node
* - A : Attach Node
* - L : Load Node
* - U : Unloading Node
* D : Detach Node
* A : Attach Node
* MS : Media Source Node
* L : Load Node
* U : Unloading Node
*
* Graph Topology
* ----------------------
* [D]<-->[A]--->[L]
* ^ |
* | |
* [U]<----/
* [D]<-->[A]--->[MS]-->[L]
* ^ | |
* | | |
* [U]<----/------/
*
* @param {!shaka.routing.Node} currentlyAt
* @param {shaka.routing.Payload} currentlyWith
Expand All @@ -3977,6 +4050,11 @@ shaka.Player.prototype.getNextStep_ = function(
next = this.getNextAfterAttach_(wantsToBeAt, currentlyWith, wantsToHave);
}

if (currentlyAt == this.mediaSourceNode_) {
next = this.getNextAfterMediaSource_(
wantsToBeAt, currentlyWith, wantsToHave);
}

// Load is very simple, always go to unload next because after we have started
// playing content, we need to tear-down everything before loading anything
// else.
Expand Down Expand Up @@ -4014,17 +4092,45 @@ shaka.Player.prototype.getNextAfterAttach_ = function(goingTo, has, wants) {
// our current state.
if (goingTo == this.attachNode_) { return this.attachNode_; }

// TODO(vaage): Right now we can go directly to load, but when we add the
// partial load states (e.g. media source) this will need to
// change.
if (goingTo == this.loadNode_) { return this.loadNode_; }
// The next step from attached to loaded is through media source.
if (goingTo == this.mediaSourceNode_ || goingTo == this.loadNode_) {
return this.mediaSourceNode_;
}

// We are missing a rule, the null will get caught by a common check in
// the routing system.
return null;
};


/**
* @param {!shaka.routing.Node} goingTo
* @param {shaka.routing.Payload} has
* @param {shaka.routing.Payload} wants
* @return {?shaka.routing.Node}
* @private
*/
shaka.Player.prototype.getNextAfterMediaSource_ = function(
goingTo, has, wants) {
// We can only go to load or unload. If we are wanting to go to load and we
// have the right media element, we can go there. If we don't, no matter
// where we want to go, we must go through unload.
if (goingTo == this.loadNode_ && has.mediaElement == wants.mediaElement) {
return this.loadNode_;
} else {
// Right now the unload node is responsible for tearing down all playback
// components (including media source). So since we have created media
// source, we need to unload since our dependencies are not compatible.
//
// TODO: We are structured this way to maintain a historic structure. Going
// forward, there is no reason to restrict ourselves to this. Going
// forward we should explore breaking apart |onUnload| and develop
// more meaningful terminology around tearing down playback resources.
return this.unloadNode_;
}
};


/**
* @param {!shaka.routing.Node} goingTo
* @param {shaka.routing.Payload} has
Expand All @@ -4039,8 +4145,11 @@ shaka.Player.prototype.getNextAfterUnload_ = function(goingTo, has, wants) {
// to match, if they don't match, we need to go through detach first.
if (has.mediaElement != wants.mediaElement) { return this.detachNode_; }

if (goingTo == this.attachNode_) { return this.attachNode_; }
if (goingTo == this.loadNode_) { return this.attachNode_; }
if (goingTo == this.mediaSourceNode_ ||
goingTo == this.attachNode_ ||
goingTo == this.loadNode_) {
return this.attachNode_;
}

// We are missing a rule, the null will get caught by a common check in
// the routing system.
Expand Down
Loading

0 comments on commit 1752f1d

Please sign in to comment.