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