Add type annotations to video_player/js/cast/*.js.
[chromium-blink-merge.git] / ui / file_manager / video_player / js / cast / cast_video_element.js
blob0aa1f3fc5c18c4961a2a8550c6fef9a1c65398ad
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * Interval for updating media info (in ms).
7  * @type {number}
8  * @const
9  */
10 var MEDIA_UPDATE_INTERVAL = 250;
12 /**
13  * The namespace for communication between the cast and the player.
14  * @type {string}
15  * @const
16  */
17 var CAST_MESSAGE_NAMESPACE = 'urn:x-cast:com.google.chromeos.videoplayer';
19 /**
20  * This class is the dummy class which has same interface as VideoElement. This
21  * behaves like VideoElement, and is used for making Chromecast player
22  * controlled instead of the true Video Element tag.
23  *
24  * @param {MediaManager} media Media manager with the media to play.
25  * @param {chrome.cast.Session} session Session to play a video on.
26  * @constructor
27  * @struct
28  * @extends {cr.EventTarget}
29  */
30 function CastVideoElement(media, session) {
31   this.mediaManager_ = media;
32   this.mediaInfo_ = null;
34   this.castMedia_ = null;
35   this.castSession_ = session;
36   this.currentTime_ = null;
37   this.src_ = '';
38   this.volume_ = 100;
39   this.loop_ = false;
40   this.currentMediaPlayerState_ = null;
41   this.currentMediaCurrentTime_ = null;
42   this.currentMediaDuration_ = null;
43   this.playInProgress_ = false;
44   this.pauseInProgress_ = false;
45   this.errorCode_ = 0;
47   /**
48    * @type {number}
49    * @private
50    */
51   this.updateTimerId_ = 0;
53   /**
54    * @type {?string}
55    * @private
56    */
57   this.token_ = null;
59   this.onMessageBound_ = this.onMessage_.bind(this);
60   this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
61   this.castSession_.addMessageListener(
62       CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
65 CastVideoElement.prototype = /** @struct */ {
66   __proto__: cr.EventTarget.prototype,
68   /**
69    * Prepares for unloading this objects.
70    */
71   dispose: function() {
72     this.unloadMedia_();
73     this.castSession_.removeMessageListener(
74         CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
75   },
77   /**
78    * Returns a parent node. This must always be null.
79    * @type {Element}
80    */
81   get parentNode() {
82     return null;
83   },
85   /**
86    * The total time of the video (in sec).
87    * @type {?number}
88    */
89   get duration() {
90     return this.currentMediaDuration_;
91   },
93   /**
94    * The current timestamp of the video (in sec).
95    * @type {?number}
96    */
97   get currentTime() {
98     if (this.castMedia_) {
99       if (this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED)
100         return this.currentMediaDuration_;  // Returns the duration.
101       else
102         return this.castMedia_.getEstimatedTime();
103     } else {
104       return null;
105     }
106   },
107   set currentTime(currentTime) {
108     var seekRequest = new chrome.cast.media.SeekRequest();
109     seekRequest.currentTime = currentTime;
110     this.castMedia_.seek(seekRequest,
111         function() {},
112         this.onCastCommandError_.wrap(this));
113   },
115   /**
116    * If this video is pauses or not.
117    * @type {boolean}
118    */
119   get paused() {
120     if (!this.castMedia_)
121       return false;
123     return !this.playInProgress_ &&
124         (this.pauseInProgress_ ||
125          this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED);
126   },
128   /**
129    * If this video is ended or not.
130    * @type {boolean}
131    */
132   get ended() {
133     if (!this.castMedia_)
134       return true;
136     return !this.playInProgress_ &&
137            this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
138   },
140   /**
141    * TimeRange object that represents the seekable ranges of the media
142    * resource.
143    * @type {TimeRanges}
144    */
145   get seekable() {
146     return {
147       length: 1,
148       start: function(index) { return 0; },
149       end: function(index) { return this.currentMediaDuration_; },
150     };
151   },
153   /**
154    * Value of the volume
155    * @type {number}
156    */
157   get volume() {
158     return this.castSession_.receiver.volume.muted ?
159                0 :
160                this.castSession_.receiver.volume.level;
161   },
162   set volume(volume) {
163     var VOLUME_EPS = 0.01;  // Threshold for ignoring a small change.
166     if (this.castSession_.receiver.volume.muted) {
167       if (volume < VOLUME_EPS)
168         return;
170       // Unmute before setting volume.
171       this.castSession_.setReceiverMuted(false,
172           function() {},
173           this.onCastCommandError_.wrap(this));
175       this.castSession_.setReceiverVolumeLevel(volume,
176           function() {},
177           this.onCastCommandError_.wrap(this));
178     } else {
179       // Ignores < 1% change.
180       var diff = this.castSession_.receiver.volume.level - volume;
181       if (Math.abs(diff) < VOLUME_EPS)
182         return;
184       if (volume < VOLUME_EPS) {
185         this.castSession_.setReceiverMuted(true,
186             function() {},
187             this.onCastCommandError_.wrap(this));
188         return;
189       }
191       this.castSession_.setReceiverVolumeLevel(volume,
192           function() {},
193           this.onCastCommandError_.wrap(this));
194     }
195   },
197   /**
198    * Returns the source of the current video.
199    * @type {?string}
200    */
201   get src() {
202     return null;
203   },
204   set src(value) {
205     // Do nothing.
206   },
208   /**
209    * Returns the flag if the video loops at end or not.
210    * @type {boolean}
211    */
212   get loop() {
213     return this.loop_;
214   },
215   set loop(value) {
216     this.loop_ = !!value;
217   },
219   /**
220    * Returns the error object if available.
221    * @type {?Object}
222    */
223   get error() {
224     if (this.errorCode_ === 0)
225       return null;
227     return {code: this.errorCode_};
228   },
230   /**
231    * Plays the video.
232    * @param {boolean=} opt_seeking True when seeking. False otherwise.
233    */
234   play: function(opt_seeking) {
235     if (this.playInProgress_)
236       return;
238     var play = function() {
239       // If the casted media is already playing and a pause request is not in
240       // progress, we can skip this play request.
241       if (this.castMedia_.playerState ===
242               chrome.cast.media.PlayerState.PLAYING &&
243           !this.pauseInProgress_) {
244         this.playInProgress_ = false;
245         return;
246       }
248       var playRequest = new chrome.cast.media.PlayRequest();
249       playRequest.customData = {seeking: !!opt_seeking};
251       this.castMedia_.play(
252           playRequest,
253           function() {
254             this.playInProgress_ = false;
255           }.wrap(this),
256           function(error) {
257             this.playInProgress_ = false;
258             this.onCastCommandError_(error);
259           }.wrap(this));
260     }.wrap(this);
262     this.playInProgress_ = true;
264     if (!this.castMedia_)
265       this.load(play);
266     else
267       play();
268   },
270   /**
271    * Pauses the video.
272    * @param {boolean=} opt_seeking True when seeking. False otherwise.
273    */
274   pause: function(opt_seeking) {
275     if (!this.castMedia_)
276       return;
278     if (this.pauseInProgress_ ||
279         this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED) {
280       return;
281     }
283     var pauseRequest = new chrome.cast.media.PauseRequest();
284     pauseRequest.customData = {seeking: !!opt_seeking};
286     this.pauseInProgress_ = true;
287     this.castMedia_.pause(
288         pauseRequest,
289         function() {
290           this.pauseInProgress_ = false;
291         }.wrap(this),
292         function(error) {
293           this.pauseInProgress_ = false;
294           this.onCastCommandError_(error);
295         }.wrap(this));
296   },
298   /**
299    * Loads the video.
300    */
301   load: function(opt_callback) {
302     var sendTokenPromise = this.mediaManager_.getToken(false).then(
303         function(token) {
304           this.token_ = token;
305           this.sendMessage_({message: 'push-token', token: token});
306         }.bind(this));
308     // Resets the error code.
309     this.errorCode_ = 0;
311     Promise.all([
312       sendTokenPromise,
313       this.mediaManager_.getUrl(),
314       this.mediaManager_.getMime(),
315       this.mediaManager_.getThumbnail()]).
316         then(function(results) {
317           var url = results[1];
318           var mime = results[2];  // maybe empty
319           var thumbnailUrl = results[3];  // maybe empty
321           this.mediaInfo_ = new chrome.cast.media.MediaInfo(url, mime);
322           this.mediaInfo_.customData = {
323             tokenRequired: true,
324             thumbnailUrl: thumbnailUrl,
325           };
327           var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
328           return new Promise(
329               this.castSession_.loadMedia.bind(this.castSession_, request)).
330               then(function(media) {
331                 this.onMediaDiscovered_(media);
332                 if (opt_callback)
333                   opt_callback();
334               }.bind(this));
335         }.bind(this)).catch(function(error) {
336           this.unloadMedia_();
337           this.dispatchEvent(new Event('error'));
338           console.error('Cast failed.', error.stack || error);
339         }.bind(this));
340   },
342   /**
343    * Unloads the video.
344    * @private
345    */
346   unloadMedia_: function() {
347     if (this.castMedia_) {
348       this.castMedia_.stop(null,
349           function() {},
350           function(error) {
351             // Ignores session error, since session may already be closed.
352             if (error.code !== chrome.cast.ErrorCode.SESSION_ERROR)
353               this.onCastCommandError_(error);
354           }.wrap(this));
356       this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
357       this.castMedia_ = null;
358     }
360     clearInterval(this.updateTimerId_);
361   },
363   /**
364    * Sends the message to cast.
365    * @param {(!Object|string)} message Message to be sent (Must be JSON-able
366    *     object).
367    * @private
368    */
369   sendMessage_: function(message) {
370     this.castSession_.sendMessage(CAST_MESSAGE_NAMESPACE, message,
371         function() {}, function(error) {});
372   },
374   /**
375    * Invoked when receiving a message from the cast.
376    * @param {string} namespace Namespace of the message.
377    * @param {string} messageAsJson Content of message as json format.
378    * @private
379    */
380   onMessage_: function(namespace, messageAsJson) {
381     if (namespace !== CAST_MESSAGE_NAMESPACE || !messageAsJson)
382       return;
384     var message = JSON.parse(messageAsJson);
385     if (message['message'] === 'request-token') {
386       if (message['previousToken'] === this.token_) {
387         this.mediaManager_.getToken(true).then(function(token) {
388           this.token_ = token;
389           this.sendMessage_({message: 'push-token', token: token});
390           // TODO(yoshiki): Revokes the previous token.
391         }.bind(this)).catch(function(error) {
392           // Send an empty token as an error.
393           this.sendMessage_({message: 'push-token', token: ''});
394           // TODO(yoshiki): Revokes the previous token.
395           console.error(error.stack || error);
396         });
397       } else {
398         console.error(
399             'New token is requested, but the previous token mismatches.');
400       }
401     } else if (message['message'] === 'playback-error') {
402       if (message['detail'] === 'src-not-supported')
403         this.errorCode_ = MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
404     }
405   },
407   /**
408    * This method is called periodically to update media information while the
409    * media is loaded.
410    * @private
411    */
412   onPeriodicalUpdateTimer_: function() {
413     if (!this.castMedia_)
414       return;
416     if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING)
417       this.onCastMediaUpdated_(true);
418   },
420   /**
421    * This method should be called when a media file is loaded.
422    * @param {chrome.cast.media.Media} media Media object which was discovered.
423    * @private
424    */
425   onMediaDiscovered_: function(media) {
426     if (this.castMedia_ !== null) {
427       this.unloadMedia_();
428       console.info('New media is found and the old media is overridden.');
429     }
431     this.castMedia_ = media;
432     this.onCastMediaUpdated_(true);
433     // Notify that the metadata of the video is ready.
434     this.dispatchEvent(new Event('loadedmetadata'));
436     media.addUpdateListener(this.onCastMediaUpdatedBound_);
437     this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this),
438                                       MEDIA_UPDATE_INTERVAL);
439   },
441   /**
442    * This method should be called when a media command to cast is failed.
443    * @param {Object} error Object representing the error.
444    * @private
445    */
446   onCastCommandError_: function(error) {
447     this.unloadMedia_();
448     this.dispatchEvent(new Event('error'));
449     console.error('Error on sending command to cast.', error.stack || error);
450   },
452   /**
453    * This is called when any media data is updated and by the periodical timer
454    * is fired.
455    *
456    * @param {boolean} alive Media availability. False if it's unavailable.
457    * @private
458    */
459   onCastMediaUpdated_: function(alive) {
460     if (!this.castMedia_)
461       return;
463     var media = this.castMedia_;
464     if (this.loop_ &&
465         media.idleReason === chrome.cast.media.IdleReason.FINISHED &&
466         !alive) {
467       // Resets the previous media silently.
468       this.castMedia_ = null;
470       // Replay the current media.
471       this.currentMediaPlayerState_ = chrome.cast.media.PlayerState.BUFFERING;
472       this.currentMediaCurrentTime_ = 0;
473       this.dispatchEvent(new Event('play'));
474       this.dispatchEvent(new Event('timeupdate'));
475       this.play();
476       return;
477     }
479     if (this.currentMediaPlayerState_ !== media.playerState) {
480       var oldPlayState = false;
481       var oldState = this.currentMediaPlayerState_;
482       if (oldState === chrome.cast.media.PlayerState.BUFFERING ||
483           oldState === chrome.cast.media.PlayerState.PLAYING) {
484         oldPlayState = true;
485       }
486       var newPlayState = false;
487       var newState = media.playerState;
488       if (newState === chrome.cast.media.PlayerState.BUFFERING ||
489           newState === chrome.cast.media.PlayerState.PLAYING) {
490         newPlayState = true;
491       }
492       if (!oldPlayState && newPlayState)
493         this.dispatchEvent(new Event('play'));
494       if (oldPlayState && !newPlayState)
495         this.dispatchEvent(new Event('pause'));
497       this.currentMediaPlayerState_ = newState;
498     }
499     if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) {
500       this.currentMediaCurrentTime_ = media.getEstimatedTime();
501       this.dispatchEvent(new Event('timeupdate'));
502     }
504     if (this.currentMediaDuration_ !== media.media.duration) {
505       // Since recordMediumCount which is called inside recordCastedVideoLangth
506       // can take a value ranges from 1 to 10,000, we don't allow to pass 0
507       // here. i.e. length 0 is not recorded.
508       if (this.currentMediaDuration_)
509         metrics.recordCastedVideoLength(this.currentMediaDuration_);
511       this.currentMediaDuration_ = media.media.duration;
512       this.dispatchEvent(new Event('durationchange'));
513     }
515     // Media is being unloaded.
516     if (!alive) {
517       this.unloadMedia_();
518       return;
519     }
520   },