Source: lib/cast/cast_proxy.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.cast.CastProxy');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.cast.CastSender');
  20. goog.require('shaka.cast.CastUtils');
  21. goog.require('shaka.log');
  22. goog.require('shaka.util.Error');
  23. goog.require('shaka.util.EventManager');
  24. goog.require('shaka.util.FakeEvent');
  25. goog.require('shaka.util.FakeEventTarget');
  26. goog.require('shaka.util.IDestroyable');
  27. /**
  28. * A proxy to switch between local and remote playback for Chromecast in a way
  29. * that is transparent to the app's controls.
  30. *
  31. * @constructor
  32. * @struct
  33. * @param {!HTMLMediaElement} video The local video element associated with the
  34. * local Player instance.
  35. * @param {!shaka.Player} player A local Player instance.
  36. * @param {string} receiverAppId The ID of the cast receiver application.
  37. * If blank, casting will not be available, but the proxy will still function
  38. * otherwise.
  39. * @implements {shaka.util.IDestroyable}
  40. * @extends {shaka.util.FakeEventTarget}
  41. * @export
  42. */
  43. shaka.cast.CastProxy = function(video, player, receiverAppId) {
  44. shaka.util.FakeEventTarget.call(this);
  45. /** @private {HTMLMediaElement} */
  46. this.localVideo_ = video;
  47. /** @private {shaka.Player} */
  48. this.localPlayer_ = player;
  49. /** @private {Object} */
  50. this.videoProxy_ = null;
  51. /** @private {Object} */
  52. this.playerProxy_ = null;
  53. /** @private {shaka.util.FakeEventTarget} */
  54. this.videoEventTarget_ = null;
  55. /** @private {shaka.util.FakeEventTarget} */
  56. this.playerEventTarget_ = null;
  57. /** @private {shaka.util.EventManager} */
  58. this.eventManager_ = null;
  59. /** @private {shaka.cast.CastSender} */
  60. this.sender_ = new shaka.cast.CastSender(
  61. receiverAppId,
  62. this.onCastStatusChanged_.bind(this),
  63. this.onFirstCastStateUpdate_.bind(this),
  64. this.onRemoteEvent_.bind(this),
  65. this.onResumeLocal_.bind(this),
  66. this.getInitState_.bind(this));
  67. this.init_();
  68. };
  69. goog.inherits(shaka.cast.CastProxy, shaka.util.FakeEventTarget);
  70. /**
  71. * Destroys the proxy and the underlying local Player.
  72. *
  73. * @param {boolean=} opt_forceDisconnect If true, force the receiver app to shut
  74. * down by disconnecting. Does nothing if not connected.
  75. * @override
  76. * @export
  77. */
  78. shaka.cast.CastProxy.prototype.destroy = function(opt_forceDisconnect) {
  79. if (opt_forceDisconnect && this.sender_) {
  80. this.sender_.forceDisconnect();
  81. }
  82. let async = [
  83. this.eventManager_ ? this.eventManager_.destroy() : null,
  84. this.localPlayer_ ? this.localPlayer_.destroy() : null,
  85. this.sender_ ? this.sender_.destroy() : null
  86. ];
  87. this.localVideo_ = null;
  88. this.localPlayer_ = null;
  89. this.videoProxy_ = null;
  90. this.playerProxy_ = null;
  91. this.eventManager_ = null;
  92. this.sender_ = null;
  93. return Promise.all(async);
  94. };
  95. /**
  96. * @event shaka.cast.CastProxy.CastStatusChangedEvent
  97. * @description Fired when cast status changes. The status change will be
  98. * reflected in canCast() and isCasting().
  99. * @property {string} type
  100. * 'caststatuschanged'
  101. * @exportDoc
  102. */
  103. /**
  104. * Get a proxy for the video element that delegates to local and remote video
  105. * elements as appropriate.
  106. *
  107. * @suppress {invalidCasts} to cast proxy Objects to unrelated types
  108. * @return {HTMLMediaElement}
  109. * @export
  110. */
  111. shaka.cast.CastProxy.prototype.getVideo = function() {
  112. return /** @type {HTMLMediaElement} */(this.videoProxy_);
  113. };
  114. /**
  115. * Get a proxy for the Player that delegates to local and remote Player objects
  116. * as appropriate.
  117. *
  118. * @suppress {invalidCasts} to cast proxy Objects to unrelated types
  119. * @return {shaka.Player}
  120. * @export
  121. */
  122. shaka.cast.CastProxy.prototype.getPlayer = function() {
  123. return /** @type {shaka.Player} */(this.playerProxy_);
  124. };
  125. /**
  126. * @return {boolean} True if the cast API is available and there are receivers.
  127. * @export
  128. */
  129. shaka.cast.CastProxy.prototype.canCast = function() {
  130. return this.sender_ ?
  131. this.sender_.apiReady() && this.sender_.hasReceivers() :
  132. false;
  133. };
  134. /**
  135. * @return {boolean} True if we are currently casting.
  136. * @export
  137. */
  138. shaka.cast.CastProxy.prototype.isCasting = function() {
  139. return this.sender_ ? this.sender_.isCasting() : false;
  140. };
  141. /**
  142. * @return {string} The name of the Cast receiver device, if isCasting().
  143. * @export
  144. */
  145. shaka.cast.CastProxy.prototype.receiverName = function() {
  146. return this.sender_ ? this.sender_.receiverName() : '';
  147. };
  148. /**
  149. * @return {!Promise} Resolved when connected to a receiver. Rejected if the
  150. * connection fails or is canceled by the user.
  151. * @export
  152. */
  153. shaka.cast.CastProxy.prototype.cast = function() {
  154. let initState = this.getInitState_();
  155. // TODO: transfer manually-selected tracks?
  156. // TODO: transfer side-loaded text tracks?
  157. return this.sender_.cast(initState).then(function() {
  158. if (!this.localPlayer_) {
  159. // We've already been destroyed.
  160. return;
  161. }
  162. // Unload the local manifest when casting succeeds.
  163. return this.localPlayer_.unload();
  164. }.bind(this));
  165. };
  166. /**
  167. * Set application-specific data.
  168. *
  169. * @param {Object} appData Application-specific data to relay to the receiver.
  170. * @export
  171. */
  172. shaka.cast.CastProxy.prototype.setAppData = function(appData) {
  173. this.sender_.setAppData(appData);
  174. };
  175. /**
  176. * Show a dialog where user can choose to disconnect from the cast connection.
  177. * @export
  178. */
  179. shaka.cast.CastProxy.prototype.suggestDisconnect = function() {
  180. this.sender_.showDisconnectDialog();
  181. };
  182. /**
  183. * Force the receiver app to shut down by disconnecting.
  184. * @export
  185. */
  186. shaka.cast.CastProxy.prototype.forceDisconnect = function() {
  187. this.sender_.forceDisconnect();
  188. };
  189. /**
  190. * Initialize the Proxies and the Cast sender.
  191. * @private
  192. */
  193. shaka.cast.CastProxy.prototype.init_ = function() {
  194. this.sender_.init();
  195. this.eventManager_ = new shaka.util.EventManager();
  196. shaka.cast.CastUtils.VideoEvents.forEach(function(name) {
  197. this.eventManager_.listen(this.localVideo_, name,
  198. this.videoProxyLocalEvent_.bind(this));
  199. }.bind(this));
  200. shaka.cast.CastUtils.PlayerEvents.forEach(function(name) {
  201. this.eventManager_.listen(this.localPlayer_, name,
  202. this.playerProxyLocalEvent_.bind(this));
  203. }.bind(this));
  204. // We would like to use Proxy here, but it is not supported on IE11 or Safari.
  205. this.videoProxy_ = {};
  206. for (let k in this.localVideo_) {
  207. Object.defineProperty(this.videoProxy_, k, {
  208. configurable: false,
  209. enumerable: true,
  210. get: this.videoProxyGet_.bind(this, k),
  211. set: this.videoProxySet_.bind(this, k)
  212. });
  213. }
  214. this.playerProxy_ = {};
  215. for (let k in /** @type {Object} */(this.localPlayer_)) {
  216. Object.defineProperty(this.playerProxy_, k, {
  217. configurable: false,
  218. enumerable: true,
  219. get: this.playerProxyGet_.bind(this, k)
  220. });
  221. }
  222. this.videoEventTarget_ = new shaka.util.FakeEventTarget();
  223. this.videoEventTarget_.dispatchTarget =
  224. /** @type {EventTarget} */(this.videoProxy_);
  225. this.playerEventTarget_ = new shaka.util.FakeEventTarget();
  226. this.playerEventTarget_.dispatchTarget =
  227. /** @type {EventTarget} */(this.playerProxy_);
  228. };
  229. /**
  230. * @return {shaka.cast.CastUtils.InitStateType} initState Video and player state
  231. * to be sent to the receiver.
  232. * @private
  233. */
  234. shaka.cast.CastProxy.prototype.getInitState_ = function() {
  235. let initState = {
  236. 'video': {},
  237. 'player': {},
  238. 'playerAfterLoad': {},
  239. 'manifest': this.localPlayer_.getManifestUri(),
  240. 'startTime': null
  241. };
  242. // Pause local playback before capturing state.
  243. this.localVideo_.pause();
  244. shaka.cast.CastUtils.VideoInitStateAttributes.forEach(function(name) {
  245. initState['video'][name] = this.localVideo_[name];
  246. }.bind(this));
  247. // If the video is still playing, set the startTime.
  248. // Has no effect if nothing is loaded.
  249. if (!this.localVideo_.ended) {
  250. initState['startTime'] = this.localVideo_.currentTime;
  251. }
  252. shaka.cast.CastUtils.PlayerInitState.forEach(function(pair) {
  253. let getter = pair[0];
  254. let setter = pair[1];
  255. let value = /** @type {Object} */(this.localPlayer_)[getter]();
  256. initState['player'][setter] = value;
  257. }.bind(this));
  258. shaka.cast.CastUtils.PlayerInitAfterLoadState.forEach(function(pair) {
  259. let getter = pair[0];
  260. let setter = pair[1];
  261. let value = /** @type {Object} */(this.localPlayer_)[getter]();
  262. initState['playerAfterLoad'][setter] = value;
  263. }.bind(this));
  264. return initState;
  265. };
  266. /**
  267. * Dispatch an event to notify the app that the status has changed.
  268. * @private
  269. */
  270. shaka.cast.CastProxy.prototype.onCastStatusChanged_ = function() {
  271. let event = new shaka.util.FakeEvent('caststatuschanged');
  272. this.dispatchEvent(event);
  273. };
  274. /**
  275. * Dispatch a synthetic play or pause event to ensure that the app correctly
  276. * knows that the player is playing, if joining an existing receiver.
  277. * @private
  278. */
  279. shaka.cast.CastProxy.prototype.onFirstCastStateUpdate_ = function() {
  280. let type = this.videoProxy_.paused ? 'pause' : 'play';
  281. let fakeEvent = new shaka.util.FakeEvent(type);
  282. this.videoEventTarget_.dispatchEvent(fakeEvent);
  283. };
  284. /**
  285. * Transfer remote state back and resume local playback.
  286. * @private
  287. */
  288. shaka.cast.CastProxy.prototype.onResumeLocal_ = function() {
  289. // Transfer back the player state.
  290. shaka.cast.CastUtils.PlayerInitState.forEach(function(pair) {
  291. let getter = pair[0];
  292. let setter = pair[1];
  293. let value = this.sender_.get('player', getter)();
  294. /** @type {Object} */(this.localPlayer_)[setter](value);
  295. }.bind(this));
  296. // Get the most recent manifest URI and ended state.
  297. let manifestUri = this.sender_.get('player', 'getManifestUri')();
  298. let ended = this.sender_.get('video', 'ended');
  299. let manifestReady = Promise.resolve();
  300. let autoplay = this.localVideo_.autoplay;
  301. let startTime = null;
  302. // If the video is still playing, set the startTime.
  303. // Has no effect if nothing is loaded.
  304. if (!ended) {
  305. startTime = this.sender_.get('video', 'currentTime');
  306. }
  307. // Now load the manifest, if present.
  308. if (manifestUri) {
  309. // Don't autoplay the content until we finish setting up initial state.
  310. this.localVideo_.autoplay = false;
  311. manifestReady = this.localPlayer_.load(manifestUri, startTime);
  312. }
  313. // Get the video state into a temp variable since we will apply it async.
  314. let videoState = {};
  315. shaka.cast.CastUtils.VideoInitStateAttributes.forEach(function(name) {
  316. videoState[name] = this.sender_.get('video', name);
  317. }.bind(this));
  318. // Finally, take on video state and player's "after load" state.
  319. manifestReady.then(() => {
  320. if (!this.localVideo_) {
  321. // We've already been destroyed.
  322. return;
  323. }
  324. shaka.cast.CastUtils.VideoInitStateAttributes.forEach(function(name) {
  325. this.localVideo_[name] = videoState[name];
  326. }.bind(this));
  327. shaka.cast.CastUtils.PlayerInitAfterLoadState.forEach(function(pair) {
  328. let getter = pair[0];
  329. let setter = pair[1];
  330. let value = this.sender_.get('player', getter)();
  331. /** @type {Object} */(this.localPlayer_)[setter](value);
  332. }.bind(this));
  333. // Restore the original autoplay setting.
  334. this.localVideo_.autoplay = autoplay;
  335. if (manifestUri) {
  336. // Resume playback with transferred state.
  337. this.localVideo_.play();
  338. }
  339. }, (error) => {
  340. // Pass any errors through to the app.
  341. goog.asserts.assert(error instanceof shaka.util.Error,
  342. 'Wrong error type!');
  343. let event = new shaka.util.FakeEvent('error', {'detail': error});
  344. this.localPlayer_.dispatchEvent(event);
  345. });
  346. };
  347. /**
  348. * @param {string} name
  349. * @return {?}
  350. * @private
  351. */
  352. shaka.cast.CastProxy.prototype.videoProxyGet_ = function(name) {
  353. if (name == 'addEventListener') {
  354. return this.videoEventTarget_.addEventListener.bind(
  355. this.videoEventTarget_);
  356. }
  357. if (name == 'removeEventListener') {
  358. return this.videoEventTarget_.removeEventListener.bind(
  359. this.videoEventTarget_);
  360. }
  361. // If we are casting, but the first update has not come in yet, use local
  362. // values, but not local methods.
  363. if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
  364. let value = this.localVideo_[name];
  365. if (typeof value != 'function') {
  366. return value;
  367. }
  368. }
  369. // Use local values and methods if we are not casting.
  370. if (!this.sender_.isCasting()) {
  371. let value = this.localVideo_[name];
  372. if (typeof value == 'function') {
  373. value = value.bind(this.localVideo_);
  374. }
  375. return value;
  376. }
  377. return this.sender_.get('video', name);
  378. };
  379. /**
  380. * @param {string} name
  381. * @param {?} value
  382. * @private
  383. */
  384. shaka.cast.CastProxy.prototype.videoProxySet_ = function(name, value) {
  385. if (!this.sender_.isCasting()) {
  386. this.localVideo_[name] = value;
  387. return;
  388. }
  389. this.sender_.set('video', name, value);
  390. };
  391. /**
  392. * @param {!Event} event
  393. * @private
  394. */
  395. shaka.cast.CastProxy.prototype.videoProxyLocalEvent_ = function(event) {
  396. if (this.sender_.isCasting()) {
  397. // Ignore any unexpected local events while casting. Events can still be
  398. // fired by the local video and Player when we unload() after the Cast
  399. // connection is complete.
  400. return;
  401. }
  402. // Convert this real Event into a FakeEvent for dispatch from our
  403. // FakeEventListener.
  404. let fakeEvent = new shaka.util.FakeEvent(event.type, event);
  405. this.videoEventTarget_.dispatchEvent(fakeEvent);
  406. };
  407. /**
  408. * @param {string} name
  409. * @return {?}
  410. * @private
  411. */
  412. shaka.cast.CastProxy.prototype.playerProxyGet_ = function(name) {
  413. if (name == 'addEventListener') {
  414. return this.playerEventTarget_.addEventListener.bind(
  415. this.playerEventTarget_);
  416. }
  417. if (name == 'removeEventListener') {
  418. return this.playerEventTarget_.removeEventListener.bind(
  419. this.playerEventTarget_);
  420. }
  421. if (name == 'getMediaElement') {
  422. return function() { return this.videoProxy_; }.bind(this);
  423. }
  424. if (name == 'getNetworkingEngine') {
  425. // Always returns a local instance, in case you need to make a request.
  426. // Issues a warning, in case you think you are making a remote request
  427. // or affecting remote filters.
  428. if (this.sender_.isCasting()) {
  429. shaka.log.warning('NOTE: getNetworkingEngine() is always local!');
  430. }
  431. return this.localPlayer_.getNetworkingEngine.bind(this.localPlayer_);
  432. }
  433. if (this.sender_.isCasting()) {
  434. // These methods are unavailable or otherwise stubbed during casting.
  435. if (name == 'getManifest' || name == 'drmInfo') {
  436. return function() {
  437. shaka.log.alwaysWarn(name + '() does not work while casting!');
  438. return null;
  439. };
  440. }
  441. if (name == 'attach' || name == 'detach') {
  442. return function() {
  443. shaka.log.alwaysWarn(name + '() does not work while casting!');
  444. return Promise.resolve();
  445. };
  446. }
  447. } // if (this.sender_.isCasting())
  448. // If we are casting, but the first update has not come in yet, use local
  449. // getters, but not local methods.
  450. if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
  451. if (shaka.cast.CastUtils.PlayerGetterMethods[name]) {
  452. let value = /** @type {Object} */(this.localPlayer_)[name];
  453. goog.asserts.assert(typeof value == 'function', 'only methods on Player');
  454. return value.bind(this.localPlayer_);
  455. }
  456. }
  457. // Use local getters and methods if we are not casting.
  458. if (!this.sender_.isCasting()) {
  459. let value = /** @type {Object} */(this.localPlayer_)[name];
  460. goog.asserts.assert(typeof value == 'function', 'only methods on Player');
  461. return value.bind(this.localPlayer_);
  462. }
  463. return this.sender_.get('player', name);
  464. };
  465. /**
  466. * @param {!Event} event
  467. * @private
  468. */
  469. shaka.cast.CastProxy.prototype.playerProxyLocalEvent_ = function(event) {
  470. if (this.sender_.isCasting()) {
  471. // Ignore any unexpected local events while casting.
  472. return;
  473. }
  474. this.playerEventTarget_.dispatchEvent(event);
  475. };
  476. /**
  477. * @param {string} targetName
  478. * @param {!shaka.util.FakeEvent} event
  479. * @private
  480. */
  481. shaka.cast.CastProxy.prototype.onRemoteEvent_ = function(targetName, event) {
  482. goog.asserts.assert(this.sender_.isCasting(),
  483. 'Should only receive remote events while casting');
  484. if (!this.sender_.isCasting()) {
  485. // Ignore any unexpected remote events.
  486. return;
  487. }
  488. if (targetName == 'video') {
  489. this.videoEventTarget_.dispatchEvent(event);
  490. } else if (targetName == 'player') {
  491. this.playerEventTarget_.dispatchEvent(event);
  492. }
  493. };