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