@silvermine/videojs-chromecast
Version:
video.js plugin for casting to chromecast
242 lines (212 loc) • 9.21 kB
JavaScript
/** @lends ChromecastSessionManager.prototype **/
class ChromecastSessionManager {
/**
* Stores the state of the current Chromecast session and its associated objects such
* as the
* [RemotePlayerController](https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayerController),
* and the
* [RemotePlayer](https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayer).
*
* WARNING: Do not instantiate this class until the
* [CastContext](https://developers.google.com/cast/docs/reference/chrome/cast.framework.CastContext)
* has been configured.
*
* For an undocumented (and thus unknown) reason, RemotePlayer and
* RemotePlayerController instances created before the cast context has been configured
* or after requesting a session or loading media will not stay in sync with media
* items that are loaded later.
*
* For example, the first item that you cast will work as expected: events on
* RemotePlayerController will fire and the state (currentTime, duration, etc) of the
* RemotePlayer instance will update as the media item plays. However, if a new media
* item is loaded via a `loadMedia` request, the media item will play, but the
* remotePlayer will be in a "media unloaded" state where the duration is 0, the
* currentTime does not update, and no change events are fired (except, strangely,
* displayStatus updates).
*
* @param player {object} Video.js Player
* @constructs ChromecastSessionManager
*/
constructor(player) {
this.player = player;
this._sessionListener = this._onSessionStateChange.bind(this);
this._castListener = this._onCastStateChange.bind(this);
this._addCastContextEventListeners();
// Remove global event listeners when this player instance is destroyed to prevent
// memory leaks.
this.player.on('dispose', this._removeCastContextEventListeners.bind(this));
this._notifyPlayerOfDevicesAvailabilityChange(this.getCastContext().getCastState());
this.remotePlayer = new cast.framework.RemotePlayer();
this.remotePlayerController = new cast.framework.RemotePlayerController(this.remotePlayer);
}
static hasConnected = false;
/**
* Add event listeners for events triggered on the current CastContext.
*
* @private
*/
_addCastContextEventListeners() {
var sessionStateChangedEvt = cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
castStateChangedEvt = cast.framework.CastContextEventType.CAST_STATE_CHANGED;
this.getCastContext().addEventListener(sessionStateChangedEvt, this._sessionListener);
this.getCastContext().addEventListener(castStateChangedEvt, this._castListener);
}
/**
* Remove event listeners that were added in {@link
* ChromecastSessionManager#_addCastContextEventListeners}.
*
* @private
*/
_removeCastContextEventListeners() {
var sessionStateChangedEvt = cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
castStateChangedEvt = cast.framework.CastContextEventType.CAST_STATE_CHANGED;
this.getCastContext().removeEventListener(sessionStateChangedEvt, this._sessionListener);
this.getCastContext().removeEventListener(castStateChangedEvt, this._castListener);
}
/**
* Handle the CastContext's SessionState change event.
*
* @private
*/
_onSessionStateChange(event) {
if (event.sessionState === cast.framework.SessionState.SESSION_ENDED) {
this.player.trigger('chromecastDisconnected');
this._reloadTech();
}
}
/**
* Handle the CastContext's CastState change event.
*
* @private
*/
_onCastStateChange(event) {
this._notifyPlayerOfDevicesAvailabilityChange(event.castState);
}
/**
* Triggers player events that notifies listeners that Chromecast devices are
* either available or unavailable.
*
* @private
*/
_notifyPlayerOfDevicesAvailabilityChange(castState) {
if (this.hasAvailableDevices(castState)) {
this.player.trigger('chromecastDevicesAvailable');
} else {
this.player.trigger('chromecastDevicesUnavailable');
}
}
/**
* Returns whether or not there are Chromecast devices available to cast to.
*
* @see https://developers.google.com/cast/docs/reference/chrome/cast.framework#.CastState
* @param {String} castState
* @return {boolean} true if there are Chromecast devices available to cast to.
*/
hasAvailableDevices(castState) {
castState = castState || this.getCastContext().getCastState();
return castState === cast.framework.CastState.NOT_CONNECTED ||
castState === cast.framework.CastState.CONNECTING ||
castState === cast.framework.CastState.CONNECTED;
}
/**
* Opens the Chromecast casting menu by requesting a CastSession. Does nothing if the
* Video.js player does not have a source.
*/
openCastMenu() {
var onSessionSuccess;
if (!this.player.currentSource()) {
// Do not cast if there is no media item loaded in the player
return;
}
onSessionSuccess = function() {
ChromecastSessionManager.hasConnected = true;
this.player.trigger('chromecastConnected');
this._reloadTech();
}.bind(this);
// It is the `requestSession` function call that actually causes the cast menu to
// open.
// The second parameter to `.then` is an error handler. We use a noop function here
// because we handle errors in the ChromecastTech class and we do not want an
// error to bubble up to the console. This error handler is also triggered when
// the user closes out of the chromecast selector pop-up without choosing a
// casting destination.
this.getCastContext().requestSession()
.then(onSessionSuccess, function() { /* noop */ });
}
/**
* Reloads the Video.js player's Tech. This causes the player to re-evaluate which
* Tech should be used for the current source by iterating over available Tech and
* calling `Tech.isSupported` and `Tech.canPlaySource`. Video.js uses the first
* Tech that returns true from both of those functions. This is what allows us to
* switch back and forth between the Chromecast Tech and other available Tech when a
* CastSession is connected or disconnected.
*
* @private
*/
_reloadTech() {
var player = this.player,
currentTime = player.currentTime(),
wasPaused = player.paused(),
sources = player.currentSources();
// Reload the current source(s) to re-lookup and use the currently available Tech.
// The chromecast Tech gets used if `ChromecastSessionManager.isChromecastConnected`
// is true (effectively, if a chromecast session is currently in progress),
// otherwise Video.js continues to search through the Tech list for other eligible
// Tech to use, such as the HTML5 player.
player.src(sources);
player.ready(function() {
if (wasPaused) {
player.pause();
} else {
player.play();
}
player.currentTime(currentTime || 0);
});
}
/**
* @see https://developers.google.com/cast/docs/reference/chrome/cast.framework.CastContext
* @returns {object} the current CastContext, if one exists
*/
getCastContext() {
return cast.framework.CastContext.getInstance();
}
/**
* @see https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayer
* @returns {object} the current RemotePlayer, if one exists
*/
getRemotePlayer() {
return this.remotePlayer;
}
/**
* @see https://developers.google.com/cast/docs/reference/chrome/cast.framework.RemotePlayerController
* @returns {object} the current RemotePlayerController, if one exists
*/
getRemotePlayerController() {
return this.remotePlayerController;
}
/**
* Returns whether or not the current Chromecast API is available (that is,
* `window.chrome`, `window.chrome.cast`, and `window.cast` exist).
*
* @static
* @returns {boolean} true if the Chromecast API is available
*/
static isChromecastAPIAvailable() {
return window.chrome && window.chrome.cast && window.cast;
}
/**
* Returns whether or not there is a current CastSession and it is connected.
*
* @static
* @returns {boolean} true if the current CastSession exists and is connected
*/
static isChromecastConnected() {
// We must also check the `hasConnected` flag because
// `getCastContext().getCastState()` returns `CONNECTED` even when the current
// casting session was initiated by another tab in the browser or by another process
return ChromecastSessionManager.isChromecastAPIAvailable() &&
(cast.framework.CastContext.getInstance().getCastState() === cast.framework.CastState.CONNECTED) &&
ChromecastSessionManager.hasConnected;
}
}
module.exports = ChromecastSessionManager;