mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
1098 lines
32 KiB
JavaScript
1098 lines
32 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.cast.CastProxy');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.Player');
|
|
goog.require('shaka.ads.AbstractAd');
|
|
goog.require('shaka.cast.CastSender');
|
|
goog.require('shaka.cast.CastUtils');
|
|
goog.require('shaka.device.DeviceFactory');
|
|
goog.require('shaka.device.IDevice');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.util.ArrayUtils');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.util.EventManager');
|
|
goog.require('shaka.util.FakeEvent');
|
|
goog.require('shaka.util.FakeEventTarget');
|
|
goog.require('shaka.util.IDestroyable');
|
|
|
|
|
|
/**
|
|
* @event shaka.cast.CastProxy.CastStatusChangedEvent
|
|
* @description Fired when cast status changes. The status change will be
|
|
* reflected in canCast() and isCasting().
|
|
* @property {string} type
|
|
* 'caststatuschanged'
|
|
* @exportDoc
|
|
*/
|
|
|
|
|
|
/**
|
|
* @summary A proxy to switch between local and remote playback for Chromecast
|
|
* in a way that is transparent to the app's controls.
|
|
*
|
|
* @implements {shaka.util.IDestroyable}
|
|
* @export
|
|
*/
|
|
shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
|
|
/**
|
|
* @param {!HTMLMediaElement} video The local video element associated with
|
|
* the local Player instance.
|
|
* @param {!shaka.Player} player A local Player instance.
|
|
* @param {string} receiverAppId The ID of the cast receiver application.
|
|
* If blank, casting will not be available, but the proxy will still
|
|
* function otherwise.
|
|
* @param {boolean} androidReceiverCompatible Indicates if the app is
|
|
* compatible with an Android Receiver.
|
|
*/
|
|
constructor(video, player, receiverAppId,
|
|
androidReceiverCompatible = false) {
|
|
super();
|
|
|
|
/** @private {HTMLMediaElement} */
|
|
this.localVideo_ = video;
|
|
|
|
/** @private {shaka.Player} */
|
|
this.localPlayer_ = player;
|
|
|
|
/** @private {shaka.extern.IAdManager} */
|
|
this.localAdManager_ = this.localPlayer_.getAdManager();
|
|
|
|
/** @private {Object} */
|
|
this.videoProxy_ = null;
|
|
|
|
/** @private {Object} */
|
|
this.playerProxy_ = null;
|
|
|
|
/** @private {Object} */
|
|
this.adManagerProxy_ = null;
|
|
|
|
/** @private {Object} */
|
|
this.currentAdProxy_ = null;
|
|
|
|
/** @private {shaka.util.FakeEventTarget} */
|
|
this.videoEventTarget_ = null;
|
|
|
|
/** @private {shaka.util.FakeEventTarget} */
|
|
this.playerEventTarget_ = null;
|
|
|
|
/** @private {shaka.util.FakeEventTarget} */
|
|
this.adManagerEventTarget_ = null;
|
|
|
|
/** @private {shaka.util.EventManager} */
|
|
this.eventManager_ = null;
|
|
|
|
/** @private {string} */
|
|
this.receiverAppId_ = receiverAppId;
|
|
|
|
/** @private {boolean} */
|
|
this.androidReceiverCompatible_ = androidReceiverCompatible;
|
|
|
|
/** @private {!Array<?>} */
|
|
this.addThumbnailsTrackCalls_ = [];
|
|
|
|
/** @private {!Array<?>} */
|
|
this.addTextTrackAsyncCalls_ = [];
|
|
|
|
/** @private {!Array<?>} */
|
|
this.addChaptersTrackCalls_ = [];
|
|
|
|
/** @private {!Map} */
|
|
this.playerCompiledToExternNames_ = new Map();
|
|
|
|
/** @private {!Map} */
|
|
this.adManagerCompiledToExternNames_ = new Map();
|
|
|
|
/** @private {shaka.cast.CastSender} */
|
|
this.sender_ = null;
|
|
|
|
if (this.shouldInitCastSender_()) {
|
|
this.sender_ = new shaka.cast.CastSender(
|
|
receiverAppId,
|
|
() => this.onCastStatusChanged_(),
|
|
() => this.onFirstCastStateUpdate_(),
|
|
(targetName, event) => this.onRemoteEvent_(targetName, event),
|
|
() => this.onResumeLocal_(),
|
|
() => this.getInitState_(),
|
|
androidReceiverCompatible);
|
|
this.init_();
|
|
} else {
|
|
this.initWithoutSender_();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroys the proxy and the underlying local Player.
|
|
*
|
|
* @param {boolean=} forceDisconnect If true, force the receiver app to shut
|
|
* down by disconnecting. Does nothing if not connected.
|
|
* @override
|
|
* @export
|
|
*/
|
|
destroy(forceDisconnect = false) {
|
|
if (this.sender_ && forceDisconnect) {
|
|
this.sender_.forceDisconnect();
|
|
}
|
|
|
|
if (this.eventManager_) {
|
|
this.eventManager_.release();
|
|
this.eventManager_ = null;
|
|
}
|
|
|
|
const waitFor = [];
|
|
if (this.localPlayer_) {
|
|
waitFor.push(this.localPlayer_.destroy());
|
|
this.localPlayer_ = null;
|
|
}
|
|
|
|
if (this.sender_) {
|
|
waitFor.push(this.sender_.destroy());
|
|
this.sender_ = null;
|
|
}
|
|
|
|
this.localVideo_ = null;
|
|
this.localAdManager_ = null;
|
|
this.videoProxy_ = null;
|
|
this.playerProxy_ = null;
|
|
this.adManagerProxy_ = null;
|
|
this.currentAdProxy_ = null;
|
|
|
|
// FakeEventTarget implements IReleasable
|
|
super.release();
|
|
|
|
return Promise.all(waitFor);
|
|
}
|
|
|
|
/**
|
|
* Get a proxy for the video element that delegates to local and remote video
|
|
* elements as appropriate.
|
|
*
|
|
* @suppress {invalidCasts} to cast proxy Objects to unrelated types
|
|
* @return {!HTMLMediaElement}
|
|
* @export
|
|
*/
|
|
getVideo() {
|
|
return /** @type {!HTMLMediaElement} */(this.videoProxy_);
|
|
}
|
|
|
|
/**
|
|
* Get a proxy for the Player that delegates to local and remote Player
|
|
* objects as appropriate.
|
|
*
|
|
* @suppress {invalidCasts} to cast proxy Objects to unrelated types
|
|
* @return {!shaka.Player}
|
|
* @export
|
|
*/
|
|
getPlayer() {
|
|
return /** @type {!shaka.Player} */(this.playerProxy_);
|
|
}
|
|
|
|
/**
|
|
* Get a proxy for the AdManager that delegates to local and remote AdManager
|
|
* objects as appropriate.
|
|
*
|
|
* @suppress {invalidCasts} to cast proxy Objects to unrelated types
|
|
* @return {shaka.extern.IAdManager}
|
|
* @export
|
|
*/
|
|
getAdManager() {
|
|
return /** @type {shaka.extern.IAdManager} */(this.adManagerProxy_);
|
|
}
|
|
|
|
/**
|
|
* @return {boolean} True if the cast API is available and there are
|
|
* receivers.
|
|
* @export
|
|
*/
|
|
canCast() {
|
|
if (!this.sender_) {
|
|
return false;
|
|
}
|
|
return this.sender_.apiReady() && this.sender_.hasReceivers();
|
|
}
|
|
|
|
/**
|
|
* @return {boolean} True if we are currently casting.
|
|
* @export
|
|
*/
|
|
isCasting() {
|
|
if (!this.sender_) {
|
|
return false;
|
|
}
|
|
return this.sender_.isCasting();
|
|
}
|
|
|
|
/**
|
|
* @return {string} The name of the Cast receiver device, if isCasting().
|
|
* @export
|
|
*/
|
|
receiverName() {
|
|
if (!this.sender_) {
|
|
return '';
|
|
}
|
|
return this.sender_.receiverName();
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise} Resolved when connected to a receiver. Rejected if the
|
|
* connection fails or is canceled by the user.
|
|
* @export
|
|
*/
|
|
async cast() {
|
|
if (!this.sender_) {
|
|
return;
|
|
}
|
|
// TODO: transfer manually-selected tracks?
|
|
|
|
await this.sender_.cast();
|
|
if (!this.localPlayer_) {
|
|
// We've already been destroyed.
|
|
return;
|
|
}
|
|
|
|
// Unload the local manifest when casting succeeds.
|
|
await this.localPlayer_.unload();
|
|
}
|
|
|
|
/**
|
|
* Set application-specific data.
|
|
*
|
|
* @param {Object} appData Application-specific data to relay to the receiver.
|
|
* @export
|
|
*/
|
|
setAppData(appData) {
|
|
if (!this.sender_) {
|
|
return;
|
|
}
|
|
this.sender_.setAppData(appData);
|
|
}
|
|
|
|
/**
|
|
* Show a dialog where user can choose to disconnect from the cast connection.
|
|
* @export
|
|
*/
|
|
suggestDisconnect() {
|
|
if (!this.sender_) {
|
|
return;
|
|
}
|
|
this.sender_.showDisconnectDialog();
|
|
}
|
|
|
|
/**
|
|
* Force the receiver app to shut down by disconnecting.
|
|
* @export
|
|
*/
|
|
forceDisconnect() {
|
|
if (!this.sender_) {
|
|
return;
|
|
}
|
|
this.sender_.forceDisconnect();
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {string} newAppId
|
|
* @param {boolean=} newCastAndroidReceiver
|
|
* @export
|
|
*/
|
|
async changeReceiverId(newAppId, newCastAndroidReceiver = false) {
|
|
if (newAppId == this.receiverAppId_ &&
|
|
newCastAndroidReceiver == this.androidReceiverCompatible_) {
|
|
// Nothing to change
|
|
return;
|
|
}
|
|
|
|
this.receiverAppId_ = newAppId;
|
|
this.androidReceiverCompatible_ = newCastAndroidReceiver;
|
|
|
|
if (!this.sender_) {
|
|
return;
|
|
}
|
|
|
|
// Destroy the old sender
|
|
this.sender_.forceDisconnect();
|
|
await this.sender_.destroy();
|
|
this.sender_ = null;
|
|
|
|
|
|
// Create the new one
|
|
this.sender_ = new shaka.cast.CastSender(
|
|
newAppId,
|
|
() => this.onCastStatusChanged_(),
|
|
() => this.onFirstCastStateUpdate_(),
|
|
(targetName, event) => this.onRemoteEvent_(targetName, event),
|
|
() => this.onResumeLocal_(),
|
|
() => this.getInitState_(),
|
|
newCastAndroidReceiver);
|
|
|
|
this.sender_.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize the Proxies without Cast sender.
|
|
* @private
|
|
*/
|
|
initWithoutSender_() {
|
|
this.videoProxy_ = /** @type {Object} */(this.localVideo_);
|
|
this.playerProxy_ = /** @type {Object} */(this.localPlayer_);
|
|
this.adManagerProxy_ = /** @type {Object} */(this.localAdManager_);
|
|
}
|
|
|
|
/**
|
|
* Initialize the Proxies and the Cast sender.
|
|
* @private
|
|
*/
|
|
init_() {
|
|
this.sender_.init();
|
|
|
|
this.eventManager_ = new shaka.util.EventManager();
|
|
|
|
for (const name of shaka.cast.CastUtils.VideoEvents) {
|
|
this.eventManager_.listen(this.localVideo_, name,
|
|
(event) => this.videoProxyLocalEvent_(event));
|
|
}
|
|
|
|
for (const key in shaka.util.FakeEvent.EventName) {
|
|
const name = shaka.util.FakeEvent.EventName[key];
|
|
this.eventManager_.listen(this.localPlayer_, name,
|
|
(event) => this.playerProxyLocalEvent_(event));
|
|
}
|
|
|
|
if (this.localAdManager_) {
|
|
for (const name of shaka.cast.CastUtils.AdManagerEvents) {
|
|
this.eventManager_.listen(this.localAdManager_, name,
|
|
(event) => this.adManagerProxyLocalEvent_(event));
|
|
}
|
|
}
|
|
|
|
// We would like to use Proxy here, but it is not supported on Safari.
|
|
this.videoProxy_ = {};
|
|
for (const k in this.localVideo_) {
|
|
Object.defineProperty(this.videoProxy_, k, {
|
|
configurable: false,
|
|
enumerable: true,
|
|
get: () => this.videoProxyGet_(k),
|
|
set: (value) => { this.videoProxySet_(k, value); },
|
|
});
|
|
}
|
|
|
|
this.playerProxy_ = {};
|
|
this.iterateOverPlayerMethods_((name, method) => {
|
|
goog.asserts.assert(this.playerProxy_, 'Must have player proxy!');
|
|
Object.defineProperty(this.playerProxy_, name, {
|
|
configurable: false,
|
|
enumerable: true,
|
|
get: () => this.playerProxyGet_(name),
|
|
});
|
|
});
|
|
|
|
if (this.localAdManager_) {
|
|
this.adManagerProxy_ = {};
|
|
this.iterateOverAdManagerMethods_((name, method) => {
|
|
goog.asserts.assert(this.adManagerProxy_,
|
|
'Must have ad manager proxy!');
|
|
Object.defineProperty(this.adManagerProxy_, name, {
|
|
configurable: false,
|
|
enumerable: true,
|
|
get: () => this.adManagerProxyGet_(name),
|
|
});
|
|
});
|
|
}
|
|
|
|
if (COMPILED) {
|
|
this.mapCompiledToUncompiledPlayerMethodNames_();
|
|
this.mapCompiledToUncompiledAdManagerMethodNames_();
|
|
}
|
|
|
|
this.videoEventTarget_ = new shaka.util.FakeEventTarget();
|
|
this.videoEventTarget_.dispatchTarget =
|
|
/** @type {EventTarget} */(this.videoProxy_);
|
|
|
|
this.playerEventTarget_ = new shaka.util.FakeEventTarget();
|
|
this.playerEventTarget_.dispatchTarget =
|
|
/** @type {EventTarget} */(this.playerProxy_);
|
|
|
|
this.adManagerEventTarget_ = new shaka.util.FakeEventTarget();
|
|
this.adManagerEventTarget_.dispatchTarget =
|
|
/** @type {EventTarget} */(this.adManagerProxy_);
|
|
|
|
this.eventManager_.listen(this.localPlayer_,
|
|
shaka.util.FakeEvent.EventName.Unloading, () => {
|
|
if (this.sender_ && this.sender_.isCasting()) {
|
|
return;
|
|
}
|
|
this.resetExternalTracks();
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Maps compiled to uncompiled player names so we can figure out
|
|
* which method to call in compiled build, while casting.
|
|
* @private
|
|
*/
|
|
mapCompiledToUncompiledPlayerMethodNames_() {
|
|
// In compiled mode, UI tries to access player methods by their internal
|
|
// renamed names, but the proxy object doesn't know about those. See
|
|
// https://github.com/shaka-project/shaka-player/issues/2130 for details.
|
|
const methodsToNames = new Map();
|
|
this.iterateOverPlayerMethods_((name, method) => {
|
|
if (methodsToNames.has(method)) {
|
|
// If two method names, point to the same method, add them to the
|
|
// map as aliases of each other.
|
|
const name2 = methodsToNames.get(method);
|
|
// Assumes that the compiled name is shorter
|
|
if (name.length < name2.length) {
|
|
this.playerCompiledToExternNames_.set(name, name2);
|
|
} else {
|
|
this.playerCompiledToExternNames_.set(name2, name);
|
|
}
|
|
} else {
|
|
methodsToNames.set(method, name);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Maps compiled to uncompiled ad manager names so we can figure out
|
|
* which method to call in compiled build, while casting.
|
|
* @private
|
|
*/
|
|
mapCompiledToUncompiledAdManagerMethodNames_() {
|
|
// In compiled mode, UI tries to access ad methods by their internal
|
|
// renamed names, but the proxy object doesn't know about those. See
|
|
// https://github.com/shaka-project/shaka-player/issues/2130 for details.
|
|
const methodsToNames = new Map();
|
|
this.iterateOverAdManagerMethods_((name, method) => {
|
|
if (methodsToNames.has(method)) {
|
|
// If two method names, point to the same method, add them to the
|
|
// map as aliases of each other.
|
|
const name2 = methodsToNames.get(method);
|
|
// Assumes that the compiled name is shorter
|
|
if (name.length < name2.length) {
|
|
this.adManagerCompiledToExternNames_.set(name, name2);
|
|
} else {
|
|
this.adManagerCompiledToExternNames_.set(name2, name);
|
|
}
|
|
} else {
|
|
methodsToNames.set(method, name);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Iterates over all of the methods of the player, including inherited methods
|
|
* from FakeEventTarget.
|
|
* @param {function(string, function())} operation
|
|
* @private
|
|
*/
|
|
iterateOverPlayerMethods_(operation) {
|
|
goog.asserts.assert(this.localPlayer_, 'Must have player!');
|
|
const player = /** @type {!Object} */ (this.localPlayer_);
|
|
this.iterateOverClassMethods_(operation, player);
|
|
}
|
|
|
|
/**
|
|
* Iterates over all of the methods of the ad manager, including inherited
|
|
* methods from FakeEventTarget.
|
|
* @param {function(string, function())} operation
|
|
* @private
|
|
*/
|
|
iterateOverAdManagerMethods_(operation) {
|
|
goog.asserts.assert(this.localAdManager_, 'Must have ad manager!');
|
|
const adManager = /** @type {!Object} */ (this.localAdManager_);
|
|
this.iterateOverClassMethods_(operation, adManager);
|
|
}
|
|
|
|
/**
|
|
* Iterates over all of the methods of the current ad.
|
|
* @param {function(string, function())} operation
|
|
* @private
|
|
*/
|
|
iterateOverCurrentAdMethods_(operation) {
|
|
const ad = /** @type {!Object} */ (new shaka.cast.BasicAd());
|
|
this.iterateOverClassMethods_(operation, ad);
|
|
}
|
|
|
|
/**
|
|
* Iterates over all of the methods of an Object, including inherited methods
|
|
* from FakeEventTarget.
|
|
* @param {function(string, function())} operation
|
|
* @param {!Object} object
|
|
* @private
|
|
*/
|
|
iterateOverClassMethods_(operation, object) {
|
|
// Avoid accessing any over-written methods in the prototype chain.
|
|
const seenNames = new Set();
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @return {boolean}
|
|
*/
|
|
function shouldAddToTheMap(name) {
|
|
if (name == 'constructor') {
|
|
// Don't proxy the constructor.
|
|
return false;
|
|
}
|
|
|
|
const method = /** @type {Object} */(object)[name];
|
|
if (typeof method != 'function') {
|
|
// Don't proxy non-methods.
|
|
return false;
|
|
}
|
|
|
|
// Add if the map does not already have it
|
|
return !seenNames.has(name);
|
|
}
|
|
|
|
// First, look at the methods on the object itself, so this can properly
|
|
// proxy any methods not on the prototype (for example, in the mock player).
|
|
for (const key in object) {
|
|
if (shouldAddToTheMap(key)) {
|
|
seenNames.add(key);
|
|
operation(key, object[key]);
|
|
}
|
|
}
|
|
|
|
// The exact length of the prototype chain might vary; for resiliency, this
|
|
// will just look at the entire chain, rather than assuming a set length.
|
|
let proto = /** @type {!Object} */ (Object.getPrototypeOf(object));
|
|
const objProto = /** @type {!Object} */ (Object.getPrototypeOf({}));
|
|
while (proto && proto != objProto) { // Don't proxy Object methods.
|
|
for (const name of Object.getOwnPropertyNames(proto)) {
|
|
if (shouldAddToTheMap(name)) {
|
|
seenNames.add(name);
|
|
operation(name, (object)[name]);
|
|
}
|
|
}
|
|
proto = /** @type {!Object} */ (Object.getPrototypeOf(proto));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {shaka.cast.CastUtils.InitStateType} initState Video and player
|
|
* state to be sent to the receiver.
|
|
* @private
|
|
*/
|
|
getInitState_() {
|
|
const initState = {
|
|
'video': {},
|
|
'player': {},
|
|
'manifest': this.localPlayer_.getAssetUri(),
|
|
'startTime': null,
|
|
'mimeType': this.localPlayer_.getMimeType(),
|
|
'addThumbnailsTrackCalls': this.addThumbnailsTrackCalls_,
|
|
'addTextTrackAsyncCalls': this.addTextTrackAsyncCalls_,
|
|
'addChaptersTrackCalls': this.addChaptersTrackCalls_,
|
|
};
|
|
|
|
// Pause local playback before capturing state.
|
|
this.localVideo_.pause();
|
|
|
|
for (const name of shaka.cast.CastUtils.VideoInitStateAttributes) {
|
|
initState['video'][name] = this.localVideo_[name];
|
|
}
|
|
|
|
// If the video is still playing, set the startTime.
|
|
// Has no effect if nothing is loaded.
|
|
if (!this.localVideo_.ended) {
|
|
initState['startTime'] = this.localVideo_.currentTime;
|
|
}
|
|
|
|
for (const pair of shaka.cast.CastUtils.PlayerInitState) {
|
|
const getter = pair[0];
|
|
const setter = pair[1];
|
|
const value = /** @type {Object} */(this.localPlayer_)[getter]();
|
|
|
|
initState['player'][setter] = value;
|
|
}
|
|
|
|
return initState;
|
|
}
|
|
|
|
/**
|
|
* Dispatch an event to notify the app that the status has changed.
|
|
* @private
|
|
*/
|
|
onCastStatusChanged_() {
|
|
const event = new shaka.util.FakeEvent('caststatuschanged');
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Dispatch a synthetic play or pause event to ensure that the app correctly
|
|
* knows that the player is playing, if joining an existing receiver.
|
|
* @private
|
|
*/
|
|
onFirstCastStateUpdate_() {
|
|
const type = this.videoProxy_['paused'] ? 'pause' : 'play';
|
|
const fakeEvent = new shaka.util.FakeEvent(type);
|
|
this.videoEventTarget_.dispatchEvent(fakeEvent);
|
|
}
|
|
|
|
/**
|
|
* Transfer remote state back and resume local playback.
|
|
* @private
|
|
*/
|
|
onResumeLocal_() {
|
|
// Transfer back the player state.
|
|
for (const pair of shaka.cast.CastUtils.PlayerInitState) {
|
|
const getter = pair[0];
|
|
const setter = pair[1];
|
|
const value = this.sender_.get('player', getter)();
|
|
/** @type {Object} */(this.localPlayer_)[setter](value);
|
|
}
|
|
|
|
const addThumbnailsTrackCalls = this.addThumbnailsTrackCalls_;
|
|
const addTextTrackAsyncCalls = this.addTextTrackAsyncCalls_;
|
|
const addChaptersTrackCalls = this.addChaptersTrackCalls_;
|
|
|
|
this.resetExternalTracks();
|
|
|
|
// Get the most recent manifest URI and ended state.
|
|
const assetUri = this.sender_.get('player', 'getAssetUri')();
|
|
const ended = this.sender_.get('video', 'ended');
|
|
|
|
let manifestReady = Promise.resolve();
|
|
const autoplay = this.localVideo_.autoplay;
|
|
|
|
let startTime = null;
|
|
|
|
// If the video is still playing, set the startTime.
|
|
// Has no effect if nothing is loaded.
|
|
if (!ended) {
|
|
startTime = this.sender_.get('video', 'currentTime');
|
|
}
|
|
|
|
let activeTextTrack;
|
|
const textTracks = this.sender_.get('player', 'getTextTracks')();
|
|
|
|
if (textTracks && textTracks.length) {
|
|
activeTextTrack = textTracks.find((t) => t.active);
|
|
}
|
|
|
|
// Now load the manifest, if present.
|
|
if (assetUri) {
|
|
// Don't autoplay the content until we finish setting up initial state.
|
|
this.localVideo_.autoplay = false;
|
|
manifestReady = this.localPlayer_.load(assetUri, startTime);
|
|
}
|
|
|
|
// Get the video state into a temp variable since we will apply it async.
|
|
const videoState = {};
|
|
for (const name of shaka.cast.CastUtils.VideoInitStateAttributes) {
|
|
videoState[name] = this.sender_.get('video', name);
|
|
}
|
|
|
|
// Finally, take on video state and player's "after load" state.
|
|
manifestReady.then(() => {
|
|
if (!this.localVideo_) {
|
|
// We've already been destroyed.
|
|
return;
|
|
}
|
|
|
|
for (const args of addThumbnailsTrackCalls) {
|
|
this.getPlayer().addThumbnailsTrack(...args);
|
|
}
|
|
for (const args of addTextTrackAsyncCalls) {
|
|
this.getPlayer().addTextTrackAsync(...args);
|
|
}
|
|
for (const args of addChaptersTrackCalls) {
|
|
this.getPlayer().addChaptersTrack(...args);
|
|
}
|
|
|
|
for (const name of shaka.cast.CastUtils.VideoInitStateAttributes) {
|
|
this.localVideo_[name] = videoState[name];
|
|
}
|
|
|
|
if (activeTextTrack) {
|
|
const newTextTracks = this.localPlayer_.getTextTracks();
|
|
const trackToSelect = newTextTracks.find((t) => {
|
|
return t.language == activeTextTrack.language &&
|
|
shaka.util.ArrayUtils.equal(t.roles, activeTextTrack.roles) &&
|
|
t.forced == activeTextTrack.forced;
|
|
});
|
|
this.localPlayer_.selectTextTrack(trackToSelect);
|
|
} else {
|
|
this.localPlayer_.selectTextTrack();
|
|
}
|
|
|
|
// Restore the original autoplay setting.
|
|
this.localVideo_.autoplay = autoplay;
|
|
if (assetUri) {
|
|
// Resume playback with transferred state.
|
|
this.localVideo_.play();
|
|
}
|
|
}, (error) => {
|
|
// Pass any errors through to the app.
|
|
goog.asserts.assert(error instanceof shaka.util.Error,
|
|
'Wrong error type!');
|
|
const eventType = shaka.util.FakeEvent.EventName.Error;
|
|
const data = (new Map()).set('detail', error);
|
|
const event = new shaka.util.FakeEvent(eventType, data);
|
|
this.localPlayer_.dispatchEvent(event);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @return {?}
|
|
* @private
|
|
*/
|
|
videoProxyGet_(name) {
|
|
if (name == 'addEventListener') {
|
|
return (type, listener, options) => {
|
|
return this.videoEventTarget_.addEventListener(type, listener, options);
|
|
};
|
|
}
|
|
if (name == 'removeEventListener') {
|
|
return (type, listener, options) => {
|
|
return this.videoEventTarget_.removeEventListener(
|
|
type, listener, options);
|
|
};
|
|
}
|
|
|
|
// If we are casting, but the first update has not come in yet, use local
|
|
// values, but not local methods.
|
|
if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
|
|
const value = this.localVideo_[name];
|
|
if (typeof value != 'function') {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
// Use local values and methods if we are not casting.
|
|
if (!this.sender_.isCasting()) {
|
|
let value = this.localVideo_[name];
|
|
if (typeof value == 'function') {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
value = value.bind(this.localVideo_);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
return this.sender_.get('video', name);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {?} value
|
|
* @private
|
|
*/
|
|
videoProxySet_(name, value) {
|
|
if (!this.sender_.isCasting()) {
|
|
this.localVideo_[name] = value;
|
|
return;
|
|
}
|
|
|
|
this.sender_.set('video', name, value);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
videoProxyLocalEvent_(event) {
|
|
if (this.sender_.isCasting()) {
|
|
// Ignore any unexpected local events while casting. Events can still be
|
|
// fired by the local video and Player when we unload() after the Cast
|
|
// connection is complete.
|
|
return;
|
|
}
|
|
|
|
// Convert this real Event into a FakeEvent for dispatch from our
|
|
// FakeEventListener.
|
|
const fakeEvent = shaka.util.FakeEvent.fromRealEvent(event);
|
|
this.videoEventTarget_.dispatchEvent(fakeEvent);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {boolean} dontRecordCalls
|
|
* @return {?}
|
|
* @private
|
|
*/
|
|
playerProxyGet_(name, dontRecordCalls = false) {
|
|
// If name is a shortened compiled name, get the original version
|
|
// from our map.
|
|
if (this.playerCompiledToExternNames_.has(name)) {
|
|
name = this.playerCompiledToExternNames_.get(name);
|
|
}
|
|
|
|
if (name == 'addEventListener') {
|
|
return (type, listener, options) => {
|
|
return this.playerEventTarget_.addEventListener(
|
|
type, listener, options);
|
|
};
|
|
}
|
|
if (name == 'removeEventListener') {
|
|
return (type, listener, options) => {
|
|
return this.playerEventTarget_.removeEventListener(
|
|
type, listener, options);
|
|
};
|
|
}
|
|
|
|
if (name == 'getMediaElement') {
|
|
return () => this.videoProxy_;
|
|
}
|
|
|
|
if (name == 'getSharedConfiguration') {
|
|
shaka.log.warning(
|
|
'Can\'t share configuration across a network. Returning copy.');
|
|
return this.sender_.get('player', 'getConfiguration');
|
|
}
|
|
|
|
if (name == 'getNetworkingEngine') {
|
|
// Always returns a local instance, in case you need to make a request.
|
|
// Issues a warning, in case you think you are making a remote request
|
|
// or affecting remote filters.
|
|
if (this.sender_.isCasting()) {
|
|
shaka.log.warning('NOTE: getNetworkingEngine() is always local!');
|
|
}
|
|
return () => this.localPlayer_.getNetworkingEngine();
|
|
}
|
|
|
|
if (name == 'getDrmEngine') {
|
|
// Always returns a local instance.
|
|
if (this.sender_.isCasting()) {
|
|
shaka.log.warning('NOTE: getDrmEngine() is always local!');
|
|
}
|
|
return () => this.localPlayer_.getDrmEngine();
|
|
}
|
|
|
|
if (name == 'getAdManager') {
|
|
return () => this.adManagerProxy_;
|
|
}
|
|
|
|
if (name == 'getQueueManager') {
|
|
// Always returns a local instance.
|
|
return () => this.localPlayer_.getQueueManager();
|
|
}
|
|
|
|
if (name == 'setVideoContainer') {
|
|
// Always returns a local instance.
|
|
if (this.sender_.isCasting()) {
|
|
shaka.log.warning('NOTE: setVideoContainer() is always local!');
|
|
}
|
|
return (container) => this.localPlayer_.setVideoContainer(container);
|
|
}
|
|
|
|
if (!dontRecordCalls) {
|
|
if (name == 'addThumbnailsTrack') {
|
|
return (...args) => {
|
|
this.addThumbnailsTrackCalls_.push(args);
|
|
return this.playerProxyGet_(
|
|
name, /* dontRecordCalls= */ true)(...args);
|
|
};
|
|
}
|
|
if (name == 'addTextTrackAsync') {
|
|
return (...args) => {
|
|
this.addTextTrackAsyncCalls_.push(args);
|
|
return this.playerProxyGet_(
|
|
name, /* dontRecordCalls= */ true)(...args);
|
|
};
|
|
}
|
|
if (name == 'addChaptersTrack') {
|
|
return (...args) => {
|
|
this.addChaptersTrackCalls_.push(args);
|
|
return this.playerProxyGet_(
|
|
name, /* dontRecordCalls= */ true)(...args);
|
|
};
|
|
}
|
|
}
|
|
|
|
if (this.sender_.isCasting()) {
|
|
// These methods are unavailable or otherwise stubbed during casting.
|
|
if (name == 'getManifest' || name == 'drmInfo') {
|
|
return () => {
|
|
shaka.log.alwaysWarn(name + '() does not work while casting!');
|
|
return null;
|
|
};
|
|
}
|
|
|
|
if (name == 'attach' || name == 'detach') {
|
|
return () => {
|
|
shaka.log.alwaysWarn(name + '() does not work while casting!');
|
|
return Promise.resolve();
|
|
};
|
|
}
|
|
} // if (this.sender_.isCasting())
|
|
|
|
// If we are casting, but the first update has not come in yet, use local
|
|
// getters, but not local methods.
|
|
if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
|
|
if (shaka.cast.CastUtils.PlayerGetterMethods.has(name) ||
|
|
shaka.cast.CastUtils.LargePlayerGetterMethods.has(name)) {
|
|
const value = /** @type {Object} */(this.localPlayer_)[name];
|
|
goog.asserts.assert(typeof value == 'function',
|
|
'only methods on Player');
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
return value.bind(this.localPlayer_);
|
|
}
|
|
}
|
|
|
|
// Use local getters and methods if we are not casting.
|
|
if (!this.sender_.isCasting()) {
|
|
const value = /** @type {Object} */(this.localPlayer_)[name];
|
|
goog.asserts.assert(typeof value == 'function',
|
|
'only methods on Player');
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
return value.bind(this.localPlayer_);
|
|
}
|
|
|
|
return this.sender_.get('player', name);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
playerProxyLocalEvent_(event) {
|
|
if (this.sender_.isCasting()) {
|
|
// Ignore any unexpected local events while casting.
|
|
return;
|
|
}
|
|
|
|
this.playerEventTarget_.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @return {?}
|
|
* @private
|
|
*/
|
|
adManagerProxyGet_(name) {
|
|
// If name is a shortened compiled name, get the original version
|
|
// from our map.
|
|
if (this.adManagerCompiledToExternNames_.has(name)) {
|
|
name = this.adManagerCompiledToExternNames_.get(name);
|
|
}
|
|
|
|
if (name == 'addEventListener') {
|
|
return (type, listener, options) => {
|
|
return this.adManagerEventTarget_.addEventListener(
|
|
type, listener, options);
|
|
};
|
|
}
|
|
if (name == 'removeEventListener') {
|
|
return (type, listener, options) => {
|
|
return this.adManagerEventTarget_.removeEventListener(
|
|
type, listener, options);
|
|
};
|
|
}
|
|
|
|
// Use local getters and methods if we are not casting.
|
|
if (!this.sender_.isCasting()) {
|
|
const value = /** @type {Object} */(this.localAdManager_)[name];
|
|
goog.asserts.assert(typeof value == 'function',
|
|
'only methods on Ad Manager');
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
return value.bind(this.localAdManager_);
|
|
}
|
|
|
|
if (name == 'getCurrentAd') {
|
|
return () => this.currentAdProxy_;
|
|
}
|
|
|
|
return this.sender_.get('adManager', name);
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
adManagerProxyLocalEvent_(event) {
|
|
if (this.sender_.isCasting()) {
|
|
// Ignore any unexpected local events while casting.
|
|
return;
|
|
}
|
|
|
|
this.adManagerEventTarget_.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @return {?}
|
|
* @private
|
|
*/
|
|
currentAdProxyGet_(name) {
|
|
// This function should not be called if we are not casting.
|
|
if (!this.sender_.isCasting()) {
|
|
return null;
|
|
}
|
|
|
|
return this.sender_.get('currentAd', name);
|
|
}
|
|
|
|
/**
|
|
* @param {string} targetName
|
|
* @param {!shaka.util.FakeEvent} event
|
|
* @private
|
|
*/
|
|
onRemoteEvent_(targetName, event) {
|
|
goog.asserts.assert(this.sender_.isCasting(),
|
|
'Should only receive remote events while casting');
|
|
if (!this.sender_.isCasting()) {
|
|
// Ignore any unexpected remote events.
|
|
return;
|
|
}
|
|
|
|
if (targetName == 'video') {
|
|
this.videoEventTarget_.dispatchEvent(event);
|
|
} else if (targetName == 'player') {
|
|
if (event.type == shaka.util.FakeEvent.EventName.Unloading) {
|
|
this.resetExternalTracks();
|
|
}
|
|
this.playerEventTarget_.dispatchEvent(event);
|
|
} else if (targetName == 'adManager') {
|
|
if (event.type == 'ad-started') {
|
|
this.currentAdProxy_ = {};
|
|
this.iterateOverCurrentAdMethods_((name, method) => {
|
|
goog.asserts.assert(this.currentAdProxy_,
|
|
'Must have current ad proxy!');
|
|
Object.defineProperty(this.currentAdProxy_, name, {
|
|
configurable: false,
|
|
enumerable: true,
|
|
get: () => this.currentAdProxyGet_(name),
|
|
});
|
|
});
|
|
} else if (event.type == 'ad-stopped') {
|
|
this.currentAdProxy_ = null;
|
|
}
|
|
this.adManagerEventTarget_.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset external tracks
|
|
*/
|
|
resetExternalTracks() {
|
|
this.addThumbnailsTrackCalls_ = [];
|
|
this.addTextTrackAsyncCalls_ = [];
|
|
this.addChaptersTrackCalls_ = [];
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
* @private
|
|
*/
|
|
shouldInitCastSender_() {
|
|
if (!window.chrome) {
|
|
return false;
|
|
}
|
|
const device = shaka.device.DeviceFactory.getDevice();
|
|
if (device.getDeviceType() == shaka.device.IDevice.DeviceType.CAST) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
shaka.cast.BasicAd = class extends shaka.ads.AbstractAd {};
|