384474f78c595ee5f94a3d775a171edd51fd7695
[platform/framework/web/crosswalk.git] / src / ui / file_manager / video_player / js / cast / cast_video_element.js
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.
4
5 'use strict';
6
7 /**
8  * Interval for updating media info (in ms).
9  * @type {number}
10  * @const
11  */
12 var MEDIA_UPDATE_INTERVAL = 250;
13
14 /**
15  * The namespace for communication between the cast and the player.
16  * @type {string}
17  * @const
18  */
19 var CAST_MESSAGE_NAMESPACE = 'urn:x-cast:com.google.chromeos.videoplayer';
20
21 /**
22  * This class is the dummy class which has same interface as VideoElement. This
23  * behaves like VideoElement, and is used for making Chromecast player
24  * controlled instead of the true Video Element tag.
25  *
26  * @param {MediaManager} media Media manager with the media to play.
27  * @param {chrome.cast.Session} session Session to play a video on.
28  * @constructor
29  */
30 function CastVideoElement(media, session) {
31   this.mediaManager_ = media;
32   this.mediaInfo_ = null;
33
34   this.castMedia_ = null;
35   this.castSession_ = session;
36   this.currentTime_ = null;
37   this.src_ = '';
38   this.volume_ = 100;
39   this.currentMediaPlayerState_ = null;
40   this.currentMediaCurrentTime_ = null;
41   this.currentMediaDuration_ = null;
42   this.playInProgress_ = false;
43   this.pauseInProgress_ = false;
44
45   this.onMessageBound_ = this.onMessage_.bind(this);
46   this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
47   this.castSession_.addMessageListener(
48       CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
49 }
50
51 CastVideoElement.prototype = {
52   __proto__: cr.EventTarget.prototype,
53
54   /**
55    * Prepares for unloading this objects.
56    */
57   dispose: function() {
58     this.unloadMedia_();
59     this.castSession_.removeMessageListener(
60         CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
61   },
62
63   /**
64    * Returns a parent node. This must always be null.
65    * @type {Element}
66    */
67   get parentNode() {
68     return null;
69   },
70
71   /**
72    * The total time of the video (in sec).
73    * @type {?number}
74    */
75   get duration() {
76     return this.currentMediaDuration_;
77   },
78
79   /**
80    * The current timestamp of the video (in sec).
81    * @type {?number}
82    */
83   get currentTime() {
84     if (this.castMedia_) {
85       if (this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED)
86         return this.currentMediaDuration_;  // Returns the duration.
87       else
88         return this.castMedia_.getEstimatedTime();
89     } else {
90       return null;
91     }
92   },
93   set currentTime(currentTime) {
94     // TODO(yoshiki): Support seek.
95   },
96
97   /**
98    * If this video is pauses or not.
99    * @type {boolean}
100    */
101   get paused() {
102     if (!this.castMedia_)
103       return false;
104
105     return !this.playInProgress_ &&
106         (this.pauseInProgress_ ||
107          this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED);
108   },
109
110   /**
111    * If this video is ended or not.
112    * @type {boolean}
113    */
114   get ended() {
115     if (!this.castMedia_)
116       return true;
117
118    return this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
119   },
120
121   /**
122    * If this video is seekable or not.
123    * @type {boolean}
124    */
125   get seekable() {
126     // TODO(yoshiki): Support seek.
127     return false;
128   },
129
130   /**
131    * Value of the volume
132    * @type {number}
133    */
134   get volume() {
135     return this.castSession_.receiver.volume.muted ?
136                0 :
137                this.castSession_.receiver.volume.level;
138   },
139   set volume(volume) {
140     var VOLUME_EPS = 0.01;  // Threshold for ignoring a small change.
141
142     // Ignores < 1% change.
143     if (Math.abs(this.castSession_.receiver.volume.level - volume) < VOLUME_EPS)
144       return;
145
146     if (this.castSession_.receiver.volume.muted) {
147       if (volume < VOLUME_EPS)
148         return;
149
150       // Unmute before setting volume.
151       this.castSession_.setReceiverMuted(false,
152           function() {},
153           this.onCastCommandError_.wrap(this));
154
155       this.castSession_.setReceiverVolumeLevel(volume,
156           function() {},
157           this.onCastCommandError_.wrap(this));
158     } else {
159       if (volume < VOLUME_EPS) {
160         this.castSession_.setReceiverMuted(true,
161             function() {},
162             this.onCastCommandError_.wrap(this));
163         return;
164       }
165
166       this.castSession_.setReceiverVolumeLevel(volume,
167           function() {},
168           this.onCastCommandError_.wrap(this));
169     }
170   },
171
172   /**
173    * Returns the source of the current video.
174    * @type {?string}
175    */
176   get src() {
177     return null;
178   },
179   set src(value) {
180     // Do nothing.
181   },
182
183   /**
184    * Plays the video.
185    */
186   play: function() {
187     var play = function() {
188       this.castMedia_.play(null,
189           function() {
190             this.playInProgress_ = false;
191           }.wrap(this),
192           function(error) {
193             this.playInProgress_ = false;
194             this.onCastCommandError_(error);
195           }.wrap(this));
196     }.wrap(this);
197
198     this.playInProgress_ = true;
199
200     if (!this.castMedia_)
201       this.load(play);
202     else
203       play();
204   },
205
206   /**
207    * Pauses the video.
208    */
209   pause: function() {
210     if (!this.castMedia_)
211       return;
212
213     this.pauseInProgress_ = true;
214     this.castMedia_.pause(null,
215         function() {
216           this.pauseInProgress_ = false;
217         }.wrap(this),
218         function(error) {
219           this.pauseInProgress_ = false;
220           this.onCastCommandError_(error);
221         }.wrap(this));
222   },
223
224   /**
225    * Loads the video.
226    */
227   load: function(opt_callback) {
228     var sendTokenPromise = this.mediaManager_.getToken().then(function(token) {
229       this.token_ = token;
230       this.sendMessage_({message: 'push-token', token: token});
231     }.bind(this));
232
233     Promise.all([
234       sendTokenPromise,
235       this.mediaManager_.getUrl(),
236       this.mediaManager_.getMime(),
237       this.mediaManager_.getThumbnail()]).
238         then(function(results) {
239           var url = results[1];
240           var mime = results[2];
241           var thumbnailUrl = results[3];
242
243           this.mediaInfo_ = new chrome.cast.media.MediaInfo(url);
244           this.mediaInfo_.contentType = mime;
245           this.mediaInfo_.customData = {
246             tokenRequired: true,
247             thumbnailUrl: thumbnailUrl,
248           };
249
250           var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
251           return new Promise(
252               this.castSession_.loadMedia.bind(this.castSession_, request)).
253               then(function(media) {
254                 this.onMediaDiscovered_(media);
255                 if (opt_callback)
256                   opt_callback();
257               }.bind(this));
258         }.bind(this)).catch(function(error) {
259           this.unloadMedia_();
260           this.dispatchEvent(new Event('error'));
261           console.error('Cast failed.', error.stack || error);
262         }.bind(this));
263   },
264
265   /**
266    * Unloads the video.
267    * @private
268    */
269   unloadMedia_: function() {
270     if (this.castMedia_) {
271       this.castMedia_.stop(null,
272           function() {},
273           function(error) {
274             // Ignores session error, since session may already be closed.
275             if (error.code !== chrome.cast.ErrorCode.SESSION_ERROR)
276               this.onCastCommandError_(error);
277           }.wrap(this));
278
279       this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
280       this.castMedia_ = null;
281     }
282     clearInterval(this.updateTimerId_);
283   },
284
285   /**
286    * Sends the message to cast.
287    * @param {Object} message Message to be sent (Must be JSON-able object).
288    * @private
289    */
290   sendMessage_: function(message) {
291     this.castSession_.sendMessage(CAST_MESSAGE_NAMESPACE, message);
292   },
293
294   /**
295    * Invoked when receiving a message from the cast.
296    * @param {string} namespace Namespace of the message.
297    * @param {string} messageAsJson Content of message as json format.
298    * @private
299    */
300   onMessage_: function(namespace, messageAsJson) {
301     if (namespace !== CAST_MESSAGE_NAMESPACE || !messageAsJson)
302       return;
303
304     var message = JSON.parse(messageAsJson);
305     if (message['message'] === 'request-token') {
306       if (message['previousToken'] === this.token_) {
307           this.mediaManager_.getToken().then(function(token) {
308             this.sendMessage_({message: 'push-token', token: token});
309             // TODO(yoshiki): Revokes the previous token.
310           }.bind(this)).catch(function(error) {
311             // Send an empty token as an error.
312             this.sendMessage_({message: 'push-token', token: ''});
313             // TODO(yoshiki): Revokes the previous token.
314             console.error(error.stack || error);
315           });
316       } else {
317         console.error(
318             'New token is requested, but the previous token mismatches.');
319       }
320     }
321   },
322
323   /**
324    * This method is called periodically to update media information while the
325    * media is loaded.
326    * @private
327    */
328   onPeriodicalUpdateTimer_: function() {
329     if (!this.castMedia_)
330       return;
331
332     if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING)
333       this.onCastMediaUpdated_(true);
334   },
335
336   /**
337    * This method should be called when a media file is loaded.
338    * @param {chrome.cast.Media} media Media object which was discovered.
339    * @private
340    */
341   onMediaDiscovered_: function(media) {
342     if (this.castMedia_ !== null) {
343       this.unloadMedia_();
344       console.info('New media is found and the old media is overridden.');
345     }
346
347     this.castMedia_ = media;
348     this.onCastMediaUpdated_(true);
349     // Notify that the metadata of the video is ready.
350     this.dispatchEvent(new Event('loadedmetadata'));
351
352     media.addUpdateListener(this.onCastMediaUpdatedBound_);
353     this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this),
354                                       MEDIA_UPDATE_INTERVAL);
355   },
356
357   /**
358    * This method should be called when a media command to cast is failed.
359    * @param {Object} error Object representing the error.
360    * @private
361    */
362   onCastCommandError_: function(error) {
363     this.unloadMedia_();
364     this.dispatchEvent(new Event('error'));
365     console.error('Error on sending command to cast.', error.stack || error);
366   },
367
368   /**
369    * This is called when any media data is updated and by the periodical timer
370    * is fired.
371    *
372    * @param {boolean} alive Media availability. False if it's unavailable.
373    * @private
374    */
375   onCastMediaUpdated_: function(alive) {
376     if (!this.castMedia_)
377       return;
378
379     var media = this.castMedia_;
380     if (this.currentMediaPlayerState_ !== media.playerState) {
381       var oldPlayState = false;
382       var oldState = this.currentMediaPlayerState_;
383       if (oldState === chrome.cast.media.PlayerState.BUFFERING ||
384           oldState === chrome.cast.media.PlayerState.PLAYING) {
385         oldPlayState = true;
386       }
387       var newPlayState = false;
388       var newState = media.playerState;
389       if (newState === chrome.cast.media.PlayerState.BUFFERING ||
390           newState === chrome.cast.media.PlayerState.PLAYING) {
391         newPlayState = true;
392       }
393       if (!oldPlayState && newPlayState)
394         this.dispatchEvent(new Event('play'));
395       if (oldPlayState && !newPlayState)
396         this.dispatchEvent(new Event('pause'));
397
398       this.currentMediaPlayerState_ = newState;
399     }
400     if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) {
401       this.currentMediaCurrentTime_ = media.getEstimatedTime();
402       this.dispatchEvent(new Event('timeupdate'));
403     }
404
405     if (this.currentMediaDuration_ !== media.media.duration) {
406       this.currentMediaDuration_ = media.media.duration;
407       this.dispatchEvent(new Event('durationchange'));
408     }
409
410     // Media is being unloaded.
411     if (!alive) {
412       this.unloadMedia_();
413       return;
414     }
415   },
416 };