Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / video_player / js / video_player.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 /**
6  * @param {Element} playerContainer Main container.
7  * @param {Element} videoContainer Container for the video element.
8  * @param {Element} controlsContainer Container for video controls.
9  * @constructor
10  */
11 function FullWindowVideoControls(
12     playerContainer, videoContainer, controlsContainer) {
13   VideoControls.call(this,
14       controlsContainer,
15       this.onPlaybackError_.wrap(this),
16       loadTimeData.getString.wrap(loadTimeData),
17       this.toggleFullScreen_.wrap(this),
18       videoContainer);
19
20   this.playerContainer_ = playerContainer;
21   this.decodeErrorOccured = false;
22
23   this.casting = false;
24
25   this.updateStyle();
26   window.addEventListener('resize', this.updateStyle.wrap(this));
27   document.addEventListener('keydown', function(e) {
28     switch (e.keyIdentifier) {
29       case 'U+0020': // Space
30       case 'MediaPlayPause':
31         this.togglePlayStateWithFeedback();
32         break;
33       case 'U+001B': // Escape
34         util.toggleFullScreen(
35             chrome.app.window.current(),
36             false);  // Leave the full screen mode.
37         break;
38       case 'Right':
39       case 'MediaNextTrack':
40         player.advance_(1);
41         break;
42       case 'Left':
43       case 'MediaPreviousTrack':
44         player.advance_(0);
45         break;
46       case 'MediaStop':
47         // TODO: Define "Stop" behavior.
48         break;
49     }
50   }.wrap(this));
51
52   // TODO(mtomasz): Simplify. crbug.com/254318.
53   var clickInProgress = false;
54   videoContainer.addEventListener('click', function(e) {
55     if (clickInProgress)
56       return;
57
58     clickInProgress = true;
59     var togglePlayState = function() {
60       clickInProgress = false;
61
62       if (e.ctrlKey) {
63         this.toggleLoopedModeWithFeedback(true);
64         if (!this.isPlaying())
65           this.togglePlayStateWithFeedback();
66       } else {
67         this.togglePlayStateWithFeedback();
68       }
69     }.wrap(this);
70
71     if (!this.media_)
72       player.reloadCurrentVideo(togglePlayState);
73     else
74       setTimeout(togglePlayState);
75   }.wrap(this));
76
77   this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
78   this.__defineGetter__('inactivityWatcher', function() {
79     return this.inactivityWatcher_;
80   }.wrap(this));
81
82   this.inactivityWatcher_.check();
83 }
84
85 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
86
87 /**
88  * Displays error message.
89  *
90  * @param {string} message Message id.
91  */
92 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
93   var errorBanner = document.querySelector('#error');
94   errorBanner.textContent = loadTimeData.getString(message);
95   errorBanner.setAttribute('visible', 'true');
96
97   // The window is hidden if the video has not loaded yet.
98   chrome.app.window.current().show();
99 };
100
101 /**
102  * Handles playback (decoder) errors.
103  * @param {MediaError} error Error object.
104  * @private
105  */
106 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
107   if (error.target && error.target.error &&
108       error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
109     if (this.casting)
110       this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
111     else
112       this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
113     this.decodeErrorOccured = false;
114   } else {
115     this.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
116     this.decodeErrorOccured = true;
117   }
118
119   // Disable inactivity watcher, and disable the ui, by hiding tools manually.
120   this.inactivityWatcher.disabled = true;
121   document.querySelector('#video-player').setAttribute('disabled', 'true');
122
123   // Detach the video element, since it may be unreliable and reset stored
124   // current playback time.
125   this.cleanup();
126   this.clearState();
127
128   // Avoid reusing a video element.
129   player.unloadVideo();
130 };
131
132 /**
133  * Toggles the full screen mode.
134  * @private
135  */
136 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
137   var appWindow = chrome.app.window.current();
138   util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
139 };
140
141 /**
142  * Media completion handler.
143  */
144 FullWindowVideoControls.prototype.onMediaComplete = function() {
145   VideoControls.prototype.onMediaComplete.apply(this, arguments);
146   if (!this.getMedia().loop)
147     player.advance_(1);
148 };
149
150 /**
151  * @constructor
152  */
153 function VideoPlayer() {
154   this.controls_ = null;
155   this.videoElement_ = null;
156   this.videos_ = null;
157   this.currentPos_ = 0;
158
159   this.currentSession_ = null;
160   this.currentCast_ = null;
161
162   this.loadQueue_ = new AsyncUtil.Queue();
163
164   this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
165
166   Object.seal(this);
167 }
168
169 VideoPlayer.prototype = {
170   get controls() {
171     return this.controls_;
172   }
173 };
174
175 /**
176  * Initializes the video player window. This method must be called after DOM
177  * initialization.
178  * @param {Array.<Object.<string, Object>>} videos List of videos.
179  */
180 VideoPlayer.prototype.prepare = function(videos) {
181   this.videos_ = videos;
182
183   var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
184
185   document.ondragstart = preventDefault;
186
187   var maximizeButton = document.querySelector('.maximize-button');
188   maximizeButton.addEventListener(
189       'click',
190       function(event) {
191         var appWindow = chrome.app.window.current();
192         if (appWindow.isMaximized())
193           appWindow.restore();
194         else
195           appWindow.maximize();
196         event.stopPropagation();
197       }.wrap(null));
198   maximizeButton.addEventListener('mousedown', preventDefault);
199
200   var minimizeButton = document.querySelector('.minimize-button');
201   minimizeButton.addEventListener(
202       'click',
203       function(event) {
204         chrome.app.window.current().minimize();
205         event.stopPropagation();
206       }.wrap(null));
207   minimizeButton.addEventListener('mousedown', preventDefault);
208
209   var closeButton = document.querySelector('.close-button');
210   closeButton.addEventListener(
211       'click',
212       function(event) {
213         close();
214         event.stopPropagation();
215       }.wrap(null));
216   closeButton.addEventListener('mousedown', preventDefault);
217
218   var menu = document.querySelector('#cast-menu');
219   cr.ui.decorate(menu, cr.ui.Menu);
220
221   this.controls_ = new FullWindowVideoControls(
222       document.querySelector('#video-player'),
223       document.querySelector('#video-container'),
224       document.querySelector('#controls'));
225
226   var reloadVideo = function(e) {
227     if (this.controls_.decodeErrorOccured &&
228         // Ignore shortcut keys
229         !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
230       this.reloadCurrentVideo(function() {
231         this.videoElement_.play();
232       }.wrap(this));
233       e.preventDefault();
234     }
235   }.wrap(this);
236
237   var arrowRight = document.querySelector('.arrow-box .arrow.right');
238   arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
239   var arrowLeft = document.querySelector('.arrow-box .arrow.left');
240   arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
241
242   var videoPlayerElement = document.querySelector('#video-player');
243   if (videos.length > 1)
244     videoPlayerElement.setAttribute('multiple', true);
245   else
246     videoPlayerElement.removeAttribute('multiple');
247
248   document.addEventListener('keydown', reloadVideo);
249   document.addEventListener('click', reloadVideo);
250 };
251
252 /**
253  * Unloads the player.
254  */
255 function unload() {
256   // Releases keep awake just in case (should be released on unloading video).
257   chrome.power.releaseKeepAwake();
258
259   if (!player.controls || !player.controls.getMedia())
260     return;
261
262   player.controls.savePosition(true /* exiting */);
263   player.controls.cleanup();
264 }
265
266 /**
267  * Loads the video file.
268  * @param {Object} video Data of the video file.
269  * @param {function()=} opt_callback Completion callback.
270  * @private
271  */
272 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
273   this.unloadVideo(true);
274
275   this.loadQueue_.run(function(callback) {
276     document.title = video.title;
277
278     document.querySelector('#title').innerText = video.title;
279
280     var videoPlayerElement = document.querySelector('#video-player');
281     if (this.currentPos_ === (this.videos_.length - 1))
282       videoPlayerElement.setAttribute('last-video', true);
283     else
284       videoPlayerElement.removeAttribute('last-video');
285
286     if (this.currentPos_ === 0)
287       videoPlayerElement.setAttribute('first-video', true);
288     else
289       videoPlayerElement.removeAttribute('first-video');
290
291     // Re-enables ui and hides error message if already displayed.
292     document.querySelector('#video-player').removeAttribute('disabled');
293     document.querySelector('#error').removeAttribute('visible');
294     this.controls.detachMedia();
295     this.controls.inactivityWatcher.disabled = true;
296     this.controls.decodeErrorOccured = false;
297     this.controls.casting = !!this.currentCast_;
298
299     videoPlayerElement.setAttribute('loading', true);
300
301     var media = new MediaManager(video.entry);
302
303     Promise.all([media.getThumbnail(), media.getToken()])
304         .then(function(results) {
305           var url = results[0];
306           var token = results[1];
307           if (url && token) {
308             document.querySelector('#thumbnail').style.backgroundImage =
309                 'url(' + url + '&access_token=' + token + ')';
310           } else {
311             document.querySelector('#thumbnail').style.backgroundImage = '';
312           }
313         })
314         .catch(function() {
315           // Shows no image on error.
316           document.querySelector('#thumbnail').style.backgroundImage = '';
317         });
318
319     var videoElementInitializePromise;
320     if (this.currentCast_) {
321       videoPlayerElement.setAttribute('casting', true);
322
323       document.querySelector('#cast-name').textContent =
324           this.currentCast_.friendlyName;
325
326       videoPlayerElement.setAttribute('castable', true);
327
328       videoElementInitializePromise = media.isAvailableForCast()
329           .then(function(result) {
330             if (!result)
331               return Promise.reject('No casts are available.');
332
333             return new Promise(function(fulfill, reject) {
334               chrome.cast.requestSession(
335                   fulfill, reject, undefined, this.currentCast_.label);
336             }.bind(this)).then(function(session) {
337               session.addUpdateListener(this.onCastSessionUpdateBound_);
338
339               this.currentSession_ = session;
340               this.videoElement_ = new CastVideoElement(media, session);
341               this.controls.attachMedia(this.videoElement_);
342             }.bind(this));
343           }.bind(this));
344     } else {
345       videoPlayerElement.removeAttribute('casting');
346
347       this.videoElement_ = document.createElement('video');
348       document.querySelector('#video-container').appendChild(
349           this.videoElement_);
350
351       this.controls.attachMedia(this.videoElement_);
352       this.videoElement_.src = video.url;
353
354       media.isAvailableForCast().then(function(result) {
355         if (result)
356           videoPlayerElement.setAttribute('castable', true);
357         else
358           videoPlayerElement.removeAttribute('castable');
359       }).catch(function() {
360         videoPlayerElement.setAttribute('castable', true);
361       });
362
363       videoElementInitializePromise = Promise.resolve();
364     }
365
366     videoElementInitializePromise
367         .then(function() {
368           var handler = function(currentPos) {
369             if (currentPos === this.currentPos_) {
370               if (opt_callback)
371                 opt_callback();
372               videoPlayerElement.removeAttribute('loading');
373               this.controls.inactivityWatcher.disabled = false;
374             }
375
376             this.videoElement_.removeEventListener('loadedmetadata', handler);
377           }.wrap(this, this.currentPos_);
378
379           this.videoElement_.addEventListener('loadedmetadata', handler);
380
381           this.videoElement_.addEventListener('play', function() {
382             chrome.power.requestKeepAwake('display');
383           }.wrap());
384           this.videoElement_.addEventListener('pause', function() {
385             chrome.power.releaseKeepAwake();
386           }.wrap());
387
388           this.videoElement_.load();
389           callback();
390         }.bind(this))
391         // In case of error.
392         .catch(function(error) {
393           videoPlayerElement.removeAttribute('loading');
394           console.error('Failed to initialize the video element.',
395                         error.stack || error);
396           this.controls_.showErrorMessage(
397               'VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
398           callback();
399         }.bind(this));
400   }.wrap(this));
401 };
402
403 /**
404  * Plays the first video.
405  */
406 VideoPlayer.prototype.playFirstVideo = function() {
407   this.currentPos_ = 0;
408   this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
409 };
410
411 /**
412  * Unloads the current video.
413  * @param {boolean=} opt_keepSession If true, keep using the current session.
414  *     Otherwise, discards the session.
415  */
416 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
417   this.loadQueue_.run(function(callback) {
418     chrome.power.releaseKeepAwake();
419
420     // Detaches the media from the control.
421     this.controls.detachMedia();
422
423     if (this.videoElement_) {
424       // If the element has dispose method, call it (CastVideoElement has it).
425       if (this.videoElement_.dispose)
426         this.videoElement_.dispose();
427       // Detach the previous video element, if exists.
428       if (this.videoElement_.parentNode)
429         this.videoElement_.parentNode.removeChild(this.videoElement_);
430     }
431     this.videoElement_ = null;
432
433     if (!opt_keepSession && this.currentSession_) {
434       this.currentSession_.stop(callback, callback);
435       this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
436       this.currentSession_ = null;
437     } else {
438       callback();
439     }
440   }.wrap(this));
441 };
442
443 /**
444  * Called when the first video is ready after starting to load.
445  * @private
446  */
447 VideoPlayer.prototype.onFirstVideoReady_ = function() {
448   var videoWidth = this.videoElement_.videoWidth;
449   var videoHeight = this.videoElement_.videoHeight;
450
451   var aspect = videoWidth / videoHeight;
452   var newWidth = videoWidth;
453   var newHeight = videoHeight;
454
455   var shrinkX = newWidth / window.screen.availWidth;
456   var shrinkY = newHeight / window.screen.availHeight;
457   if (shrinkX > 1 || shrinkY > 1) {
458     if (shrinkY > shrinkX) {
459       newHeight = newHeight / shrinkY;
460       newWidth = newHeight * aspect;
461     } else {
462       newWidth = newWidth / shrinkX;
463       newHeight = newWidth / aspect;
464     }
465   }
466
467   var oldLeft = window.screenX;
468   var oldTop = window.screenY;
469   var oldWidth = window.outerWidth;
470   var oldHeight = window.outerHeight;
471
472   if (!oldWidth && !oldHeight) {
473     oldLeft = window.screen.availWidth / 2;
474     oldTop = window.screen.availHeight / 2;
475   }
476
477   var appWindow = chrome.app.window.current();
478   appWindow.resizeTo(newWidth, newHeight);
479   appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
480                    oldTop - (newHeight - oldHeight) / 2);
481   appWindow.show();
482
483   this.videoElement_.play();
484 };
485
486 /**
487  * Advances to the next (or previous) track.
488  *
489  * @param {boolean} direction True to the next, false to the previous.
490  * @private
491  */
492 VideoPlayer.prototype.advance_ = function(direction) {
493   var newPos = this.currentPos_ + (direction ? 1 : -1);
494   if (0 <= newPos && newPos < this.videos_.length) {
495     this.currentPos_ = newPos;
496     this.reloadCurrentVideo(function() {
497       this.videoElement_.play();
498     }.wrap(this));
499   }
500 };
501
502 /**
503  * Reloads the current video.
504  *
505  * @param {function()=} opt_callback Completion callback.
506  */
507 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
508   var currentVideo = this.videos_[this.currentPos_];
509   this.loadVideo_(currentVideo, opt_callback);
510 };
511
512 /**
513  * Invokes when a menuitem in the cast menu is selected.
514  * @param {Object} cast Selected element in the list of casts.
515  * @private
516  */
517 VideoPlayer.prototype.onCastSelected_ = function(cast) {
518   // If the selected item is same as the current item, do nothing.
519   if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
520     return;
521
522   this.unloadVideo(false);
523
524   // Waits for unloading video.
525   this.loadQueue_.run(function(callback) {
526     this.currentCast_ = cast || null;
527     this.updateCheckOnCastMenu_();
528     this.reloadCurrentVideo();
529     callback();
530   }.wrap(this));
531 };
532
533 /**
534  * Set the list of casts.
535  * @param {Array.<Object>} casts List of casts.
536  */
537 VideoPlayer.prototype.setCastList = function(casts) {
538   var videoPlayerElement = document.querySelector('#video-player');
539   var menu = document.querySelector('#cast-menu');
540   menu.innerHTML = '';
541
542   // TODO(yoshiki): Handle the case that the current cast disappears.
543
544   if (casts.length === 0) {
545     videoPlayerElement.removeAttribute('cast-available');
546     if (this.currentCast_)
547       this.onCurrentCastDisappear_();
548     return;
549   }
550
551   if (this.currentCast_) {
552     var currentCastAvailable = casts.some(function(cast) {
553       return this.currentCast_.label === cast.label;
554     }.wrap(this));
555
556     if (!currentCastAvailable)
557       this.onCurrentCastDisappear_();
558   }
559
560   var item = new cr.ui.MenuItem();
561   item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
562   item.setAttribute('aria-label', item.label);
563   item.castLabel = '';
564   item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
565   menu.appendChild(item);
566
567   for (var i = 0; i < casts.length; i++) {
568     var item = new cr.ui.MenuItem();
569     item.label = casts[i].friendlyName;
570     item.setAttribute('aria-label', item.label);
571     item.castLabel = casts[i].label;
572     item.addEventListener('activate',
573                           this.onCastSelected_.wrap(this, casts[i]));
574     menu.appendChild(item);
575   }
576   this.updateCheckOnCastMenu_();
577   videoPlayerElement.setAttribute('cast-available', true);
578 };
579
580 /**
581  * Updates the check status of the cast menu items.
582  * @private
583  */
584 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
585   var menu = document.querySelector('#cast-menu');
586   var menuItems = menu.menuItems;
587   for (var i = 0; i < menuItems.length; i++) {
588     var item = menuItems[i];
589     if (this.currentCast_ === null) {
590       // Playing on this computer.
591       if (item.castLabel === '')
592         item.checked = true;
593       else
594         item.checked = false;
595     } else {
596       // Playing on cast device.
597       if (item.castLabel === this.currentCast_.label)
598         item.checked = true;
599       else
600         item.checked = false;
601     }
602   }
603 };
604
605 /**
606  * Called when the current cast is disappear from the cast list.
607  * @private
608  */
609 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
610   this.currentCast_ = null;
611   if (this.currentSession_) {
612     this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
613     this.currentSession_ = null;
614   }
615   this.controls.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
616   this.unloadVideo();
617 };
618
619 /**
620  * This method should be called when the session is updated.
621  * @param {boolean} alive Whether the session is alive or not.
622  * @private
623  */
624 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
625   if (!alive)
626     this.unloadVideo();
627 };
628
629 /**
630  * Initialize the list of videos.
631  * @param {function(Array.<Object>)} callback Called with the video list when
632  *     it is ready.
633  */
634 function initVideos(callback) {
635   if (window.videos) {
636     var videos = window.videos;
637     window.videos = null;
638     callback(videos);
639     return;
640   }
641
642   chrome.runtime.onMessage.addListener(
643       function(request, sender, sendResponse) {
644         var videos = window.videos;
645         window.videos = null;
646         callback(videos);
647       }.wrap(null));
648 }
649
650 var player = new VideoPlayer();
651
652 /**
653  * Initializes the strings.
654  * @param {function()} callback Called when the sting data is ready.
655  */
656 function initStrings(callback) {
657   chrome.fileManagerPrivate.getStrings(function(strings) {
658     loadTimeData.data = strings;
659     i18nTemplate.process(document, loadTimeData);
660     callback();
661   }.wrap(null));
662 }
663
664 var initPromise = Promise.all(
665     [new Promise(initVideos.wrap(null)),
666      new Promise(initStrings.wrap(null)),
667      new Promise(util.addPageLoadHandler.wrap(null))]);
668
669 initPromise.then(function(results) {
670   var videos = results[0];
671   player.prepare(videos);
672   return new Promise(player.playFirstVideo.wrap(player));
673 }.wrap(null));