Upstream version 10.39.225.0
[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.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;
46
47   this.onMessageBound_ = this.onMessage_.bind(this);
48   this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this);
49   this.castSession_.addMessageListener(
50       CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
51 }
52
53 CastVideoElement.prototype = {
54   __proto__: cr.EventTarget.prototype,
55
56   /**
57    * Prepares for unloading this objects.
58    */
59   dispose: function() {
60     this.unloadMedia_();
61     this.castSession_.removeMessageListener(
62         CAST_MESSAGE_NAMESPACE, this.onMessageBound_);
63   },
64
65   /**
66    * Returns a parent node. This must always be null.
67    * @type {Element}
68    */
69   get parentNode() {
70     return null;
71   },
72
73   /**
74    * The total time of the video (in sec).
75    * @type {?number}
76    */
77   get duration() {
78     return this.currentMediaDuration_;
79   },
80
81   /**
82    * The current timestamp of the video (in sec).
83    * @type {?number}
84    */
85   get currentTime() {
86     if (this.castMedia_) {
87       if (this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED)
88         return this.currentMediaDuration_;  // Returns the duration.
89       else
90         return this.castMedia_.getEstimatedTime();
91     } else {
92       return null;
93     }
94   },
95   set currentTime(currentTime) {
96     var seekRequest = new chrome.cast.media.SeekRequest();
97     seekRequest.currentTime = currentTime;
98     this.castMedia_.seek(seekRequest,
99         function() {},
100         this.onCastCommandError_.wrap(this));
101   },
102
103   /**
104    * If this video is pauses or not.
105    * @type {boolean}
106    */
107   get paused() {
108     if (!this.castMedia_)
109       return false;
110
111     return !this.playInProgress_ &&
112         (this.pauseInProgress_ ||
113          this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED);
114   },
115
116   /**
117    * If this video is ended or not.
118    * @type {boolean}
119    */
120   get ended() {
121     if (!this.castMedia_)
122       return true;
123
124     return !this.playInProgress &&
125            this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED;
126   },
127
128   /**
129    * TimeRange object that represents the seekable ranges of the media
130    * resource.
131    * @type {TimeRanges}
132    */
133   get seekable() {
134     return {
135       length: 1,
136       start: function(index) { return 0; },
137       end: function(index) { return this.currentMediaDuration_; },
138     };
139   },
140
141   /**
142    * Value of the volume
143    * @type {number}
144    */
145   get volume() {
146     return this.castSession_.receiver.volume.muted ?
147                0 :
148                this.castSession_.receiver.volume.level;
149   },
150   set volume(volume) {
151     var VOLUME_EPS = 0.01;  // Threshold for ignoring a small change.
152
153
154     if (this.castSession_.receiver.volume.muted) {
155       if (volume < VOLUME_EPS)
156         return;
157
158       // Unmute before setting volume.
159       this.castSession_.setReceiverMuted(false,
160           function() {},
161           this.onCastCommandError_.wrap(this));
162
163       this.castSession_.setReceiverVolumeLevel(volume,
164           function() {},
165           this.onCastCommandError_.wrap(this));
166     } else {
167       // Ignores < 1% change.
168       var diff = this.castSession_.receiver.volume.level - volume;
169       if (Math.abs(diff) < VOLUME_EPS)
170         return;
171
172       if (volume < VOLUME_EPS) {
173         this.castSession_.setReceiverMuted(true,
174             function() {},
175             this.onCastCommandError_.wrap(this));
176         return;
177       }
178
179       this.castSession_.setReceiverVolumeLevel(volume,
180           function() {},
181           this.onCastCommandError_.wrap(this));
182     }
183   },
184
185   /**
186    * Returns the source of the current video.
187    * @type {?string}
188    */
189   get src() {
190     return null;
191   },
192   set src(value) {
193     // Do nothing.
194   },
195
196   /**
197    * Returns the flag if the video loops at end or not.
198    * @type {boolean}
199    */
200   get loop() {
201     return this.loop_;
202   },
203   set loop(value) {
204     this.loop_ = !!value;
205   },
206
207   /**
208    * Returns the error object if available.
209    * @type {?Object}
210    */
211   get error() {
212     if (this.errorCode_ === 0)
213       return null;
214
215     return {code: this.errorCode_};
216   },
217
218   /**
219    * Plays the video.
220    * @param {boolean=} opt_seeking True when seeking. False otherwise.
221    */
222   play: function(opt_seeking) {
223     if (this.playInProgress_)
224       return;
225
226     var play = function() {
227       // If the casted media is already playing and a pause request is not in
228       // progress, we can skip this play request.
229       if (this.castMedia_.playerState ===
230               chrome.cast.media.PlayerState.PLAYING &&
231           !this.pauseInProgress_) {
232         this.playInProgress_ = false;
233         return;
234       }
235
236       var playRequest = new chrome.cast.media.PlayRequest();
237       playRequest.customData = {seeking: !!opt_seeking};
238
239       this.castMedia_.play(
240           playRequest,
241           function() {
242             this.playInProgress_ = false;
243           }.wrap(this),
244           function(error) {
245             this.playInProgress_ = false;
246             this.onCastCommandError_(error);
247           }.wrap(this));
248     }.wrap(this);
249
250     this.playInProgress_ = true;
251
252     if (!this.castMedia_)
253       this.load(play);
254     else
255       play();
256   },
257
258   /**
259    * Pauses the video.
260    * @param {boolean=} opt_seeking True when seeking. False otherwise.
261    */
262   pause: function(opt_seeking) {
263     if (!this.castMedia_)
264       return;
265
266     if (this.pauseInProgress_ ||
267         this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED) {
268       return;
269     }
270
271     var pauseRequest = new chrome.cast.media.PauseRequest();
272     pauseRequest.customData = {seeking: !!opt_seeking};
273
274     this.pauseInProgress_ = true;
275     this.castMedia_.pause(
276         pauseRequest,
277         function() {
278           this.pauseInProgress_ = false;
279         }.wrap(this),
280         function(error) {
281           this.pauseInProgress_ = false;
282           this.onCastCommandError_(error);
283         }.wrap(this));
284   },
285
286   /**
287    * Loads the video.
288    */
289   load: function(opt_callback) {
290     var sendTokenPromise = this.mediaManager_.getToken().then(function(token) {
291       this.token_ = token;
292       this.sendMessage_({message: 'push-token', token: token});
293     }.bind(this));
294
295     // Resets the error code.
296     this.errorCode_ = 0;
297
298     Promise.all([
299       sendTokenPromise,
300       this.mediaManager_.getUrl(),
301       this.mediaManager_.getMime(),
302       this.mediaManager_.getThumbnail()]).
303         then(function(results) {
304           var url = results[1];
305           var mime = results[2];  // maybe empty
306           var thumbnailUrl = results[3];  // maybe empty
307
308           this.mediaInfo_ = new chrome.cast.media.MediaInfo(url);
309           this.mediaInfo_.contentType = mime;
310           this.mediaInfo_.customData = {
311             tokenRequired: true,
312             thumbnailUrl: thumbnailUrl,
313           };
314
315           var request = new chrome.cast.media.LoadRequest(this.mediaInfo_);
316           return new Promise(
317               this.castSession_.loadMedia.bind(this.castSession_, request)).
318               then(function(media) {
319                 this.onMediaDiscovered_(media);
320                 if (opt_callback)
321                   opt_callback();
322               }.bind(this));
323         }.bind(this)).catch(function(error) {
324           this.unloadMedia_();
325           this.dispatchEvent(new Event('error'));
326           console.error('Cast failed.', error.stack || error);
327         }.bind(this));
328   },
329
330   /**
331    * Unloads the video.
332    * @private
333    */
334   unloadMedia_: function() {
335     if (this.castMedia_) {
336       this.castMedia_.stop(null,
337           function() {},
338           function(error) {
339             // Ignores session error, since session may already be closed.
340             if (error.code !== chrome.cast.ErrorCode.SESSION_ERROR)
341               this.onCastCommandError_(error);
342           }.wrap(this));
343
344       this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_);
345       this.castMedia_ = null;
346     }
347
348     clearInterval(this.updateTimerId_);
349   },
350
351   /**
352    * Sends the message to cast.
353    * @param {Object} message Message to be sent (Must be JSON-able object).
354    * @private
355    */
356   sendMessage_: function(message) {
357     this.castSession_.sendMessage(CAST_MESSAGE_NAMESPACE, message);
358   },
359
360   /**
361    * Invoked when receiving a message from the cast.
362    * @param {string} namespace Namespace of the message.
363    * @param {string} messageAsJson Content of message as json format.
364    * @private
365    */
366   onMessage_: function(namespace, messageAsJson) {
367     if (namespace !== CAST_MESSAGE_NAMESPACE || !messageAsJson)
368       return;
369
370     var message = JSON.parse(messageAsJson);
371     if (message['message'] === 'request-token') {
372       if (message['previousToken'] === this.token_) {
373         this.mediaManager_.getToken(true).then(function(token) {
374           this.token_ = token;
375           this.sendMessage_({message: 'push-token', token: token});
376           // TODO(yoshiki): Revokes the previous token.
377         }.bind(this)).catch(function(error) {
378           // Send an empty token as an error.
379           this.sendMessage_({message: 'push-token', token: ''});
380           // TODO(yoshiki): Revokes the previous token.
381           console.error(error.stack || error);
382         });
383       } else {
384         console.error(
385             'New token is requested, but the previous token mismatches.');
386       }
387     } else if (message['message'] === 'playback-error') {
388       if (message['detail'] === 'src-not-supported')
389         this.errorCode_ = MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
390     }
391   },
392
393   /**
394    * This method is called periodically to update media information while the
395    * media is loaded.
396    * @private
397    */
398   onPeriodicalUpdateTimer_: function() {
399     if (!this.castMedia_)
400       return;
401
402     if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING)
403       this.onCastMediaUpdated_(true);
404   },
405
406   /**
407    * This method should be called when a media file is loaded.
408    * @param {chrome.cast.Media} media Media object which was discovered.
409    * @private
410    */
411   onMediaDiscovered_: function(media) {
412     if (this.castMedia_ !== null) {
413       this.unloadMedia_();
414       console.info('New media is found and the old media is overridden.');
415     }
416
417     this.castMedia_ = media;
418     this.onCastMediaUpdated_(true);
419     // Notify that the metadata of the video is ready.
420     this.dispatchEvent(new Event('loadedmetadata'));
421
422     media.addUpdateListener(this.onCastMediaUpdatedBound_);
423     this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this),
424                                       MEDIA_UPDATE_INTERVAL);
425   },
426
427   /**
428    * This method should be called when a media command to cast is failed.
429    * @param {Object} error Object representing the error.
430    * @private
431    */
432   onCastCommandError_: function(error) {
433     this.unloadMedia_();
434     this.dispatchEvent(new Event('error'));
435     console.error('Error on sending command to cast.', error.stack || error);
436   },
437
438   /**
439    * This is called when any media data is updated and by the periodical timer
440    * is fired.
441    *
442    * @param {boolean} alive Media availability. False if it's unavailable.
443    * @private
444    */
445   onCastMediaUpdated_: function(alive) {
446     if (!this.castMedia_)
447       return;
448
449     var media = this.castMedia_;
450     if (this.loop_ &&
451         media.idleReason === chrome.cast.media.IdleReason.FINISHED &&
452         !alive) {
453       // Resets the previous media silently.
454       this.castMedia_ = null;
455
456       // Replay the current media.
457       this.currentMediaPlayerState_ = chrome.cast.media.PlayerState.BUFFERING;
458       this.currentMediaCurrentTime_ = 0;
459       this.dispatchEvent(new Event('play'));
460       this.dispatchEvent(new Event('timeupdate'));
461       this.play();
462       return;
463     }
464
465     if (this.currentMediaPlayerState_ !== media.playerState) {
466       var oldPlayState = false;
467       var oldState = this.currentMediaPlayerState_;
468       if (oldState === chrome.cast.media.PlayerState.BUFFERING ||
469           oldState === chrome.cast.media.PlayerState.PLAYING) {
470         oldPlayState = true;
471       }
472       var newPlayState = false;
473       var newState = media.playerState;
474       if (newState === chrome.cast.media.PlayerState.BUFFERING ||
475           newState === chrome.cast.media.PlayerState.PLAYING) {
476         newPlayState = true;
477       }
478       if (!oldPlayState && newPlayState)
479         this.dispatchEvent(new Event('play'));
480       if (oldPlayState && !newPlayState)
481         this.dispatchEvent(new Event('pause'));
482
483       this.currentMediaPlayerState_ = newState;
484     }
485     if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) {
486       this.currentMediaCurrentTime_ = media.getEstimatedTime();
487       this.dispatchEvent(new Event('timeupdate'));
488     }
489
490     if (this.currentMediaDuration_ !== media.media.duration) {
491       this.currentMediaDuration_ = media.media.duration;
492       this.dispatchEvent(new Event('durationchange'));
493     }
494
495     // Media is being unloaded.
496     if (!alive) {
497       this.unloadMedia_();
498       return;
499     }
500   },
501 };