Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / video_player / js / video_player.js
blobdff5801aac82190c65ee7ede9835ad2a974dce5b
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  * @param {!HTMLElement} playerContainer Main container.
7  * @param {!HTMLElement} videoContainer Container for the video element.
8  * @param {!HTMLElement} controlsContainer Container for video controls.
9  * @constructor
10  * @struct
11  * @extends {VideoControls}
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 (util.getKeyModifiers(e) + e.keyIdentifier) {
31       // Handle debug shortcut keys.
32       case 'Ctrl-Shift-U+0049': // Ctrl+Shift+I
33         chrome.fileManagerPrivate.openInspector('normal');
34         break;
35       case 'Ctrl-Shift-U+004A': // Ctrl+Shift+J
36         chrome.fileManagerPrivate.openInspector('console');
37         break;
38       case 'Ctrl-Shift-U+0043': // Ctrl+Shift+C
39         chrome.fileManagerPrivate.openInspector('element');
40         break;
41       case 'Ctrl-Shift-U+0042': // Ctrl+Shift+B
42         chrome.fileManagerPrivate.openInspector('background');
43         break;
45       case 'U+0020': // Space
46       case 'MediaPlayPause':
47         this.togglePlayStateWithFeedback();
48         break;
49       case 'U+001B': // Escape
50         util.toggleFullScreen(
51             chrome.app.window.current(),
52             false);  // Leave the full screen mode.
53         break;
54       case 'Right':
55       case 'MediaNextTrack':
56         player.advance_(1);
57         break;
58       case 'Left':
59       case 'MediaPreviousTrack':
60         player.advance_(0);
61         break;
62       case 'MediaStop':
63         // TODO: Define "Stop" behavior.
64         break;
65     }
66   }.wrap(this));
68   // TODO(mtomasz): Simplify. crbug.com/254318.
69   var clickInProgress = false;
70   videoContainer.addEventListener('click', function(e) {
71     if (clickInProgress)
72       return;
74     clickInProgress = true;
75     var togglePlayState = function() {
76       clickInProgress = false;
78       if (e.ctrlKey) {
79         this.toggleLoopedModeWithFeedback(true);
80         if (!this.isPlaying())
81           this.togglePlayStateWithFeedback();
82       } else {
83         this.togglePlayStateWithFeedback();
84       }
85     }.wrap(this);
87     if (!this.media_)
88       player.reloadCurrentVideo(togglePlayState);
89     else
90       setTimeout(togglePlayState, 0);
91   }.wrap(this));
93   /**
94    * @type {MouseInactivityWatcher}
95    * @private
96    */
97   this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
98   this.inactivityWatcher_.check();
101 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
104  * Gets inactivity watcher.
105  * @return {MouseInactivityWatcher} An inactivity watcher.
106  */
107 FullWindowVideoControls.prototype.getInactivityWatcher = function() {
108   return this.inactivityWatcher_;
112  * Displays error message.
114  * @param {string} message Message id.
115  */
116 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
117   var errorBanner = getRequiredElement('error');
118   errorBanner.textContent = loadTimeData.getString(message);
119   errorBanner.setAttribute('visible', 'true');
121   // The window is hidden if the video has not loaded yet.
122   chrome.app.window.current().show();
126  * Handles playback (decoder) errors.
127  * @param {MediaError} error Error object.
128  * @private
129  */
130 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
131   if (error.target && error.target.error &&
132       error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
133     if (this.casting)
134       this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
135     else
136       this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
137     this.decodeErrorOccured = false;
138   } else {
139     this.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
140     this.decodeErrorOccured = true;
141   }
143   // Disable inactivity watcher, and disable the ui, by hiding tools manually.
144   this.getInactivityWatcher().disabled = true;
145   getRequiredElement('video-player').setAttribute('disabled', 'true');
147   // Detach the video element, since it may be unreliable and reset stored
148   // current playback time.
149   this.cleanup();
150   this.clearState();
152   // Avoid reusing a video element.
153   player.unloadVideo();
157  * Toggles the full screen mode.
158  * @private
159  */
160 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
161   var appWindow = chrome.app.window.current();
162   util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
166  * Media completion handler.
167  */
168 FullWindowVideoControls.prototype.onMediaComplete = function() {
169   VideoControls.prototype.onMediaComplete.apply(this, arguments);
170   if (!this.getMedia().loop)
171     player.advance_(1);
175  * Video Player
177  * @constructor
178  * @struct
179  */
180 function VideoPlayer() {
181   this.controls_ = null;
182   this.videoElement_ = null;
184   /**
185    * @type {Array<!FileEntry>}
186    * @private
187    */
188   this.videos_ = null;
190   this.currentPos_ = 0;
192   this.currentSession_ = null;
193   this.currentCast_ = null;
195   this.loadQueue_ = new AsyncUtil.Queue();
197   this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
200 VideoPlayer.prototype = /** @struct */ {
201   /**
202    * @return {FullWindowVideoControls}
203    */
204   get controls() {
205     return this.controls_;
206   }
210  * Initializes the video player window. This method must be called after DOM
211  * initialization.
212  * @param {!Array<!FileEntry>} videos List of videos.
213  */
214 VideoPlayer.prototype.prepare = function(videos) {
215   this.videos_ = videos;
217   var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
219   document.ondragstart = preventDefault;
221   var maximizeButton = queryRequiredElement('.maximize-button');
222   maximizeButton.addEventListener(
223       'click',
224       function(event) {
225         var appWindow = chrome.app.window.current();
226         if (appWindow.isMaximized())
227           appWindow.restore();
228         else
229           appWindow.maximize();
230         event.stopPropagation();
231       }.wrap(null));
232   maximizeButton.addEventListener('mousedown', preventDefault);
234   var minimizeButton = queryRequiredElement('.minimize-button');
235   minimizeButton.addEventListener(
236       'click',
237       function(event) {
238         chrome.app.window.current().minimize();
239         event.stopPropagation();
240       }.wrap(null));
241   minimizeButton.addEventListener('mousedown', preventDefault);
243   var closeButton = queryRequiredElement('.close-button');
244   closeButton.addEventListener(
245       'click',
246       function(event) {
247         window.close();
248         event.stopPropagation();
249       }.wrap(null));
250   closeButton.addEventListener('mousedown', preventDefault);
252   cr.ui.decorate(getRequiredElement('cast-menu'), cr.ui.Menu);
254   this.controls_ = new FullWindowVideoControls(
255       getRequiredElement('video-player'),
256       getRequiredElement('video-container'),
257       getRequiredElement('controls'));
259   var reloadVideo = function(e) {
260     if (this.controls_.decodeErrorOccured &&
261         // Ignore shortcut keys
262         !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
263       this.reloadCurrentVideo(function() {
264         this.videoElement_.play();
265       }.wrap(this));
266       e.preventDefault();
267     }
268   }.wrap(this);
270   var arrowRight = queryRequiredElement('.arrow-box .arrow.right');
271   arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
272   var arrowLeft = queryRequiredElement('.arrow-box .arrow.left');
273   arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
275   var videoPlayerElement = getRequiredElement('video-player');
276   if (videos.length > 1)
277     videoPlayerElement.setAttribute('multiple', true);
278   else
279     videoPlayerElement.removeAttribute('multiple');
281   document.addEventListener('keydown', reloadVideo);
282   document.addEventListener('click', reloadVideo);
286  * Unloads the player.
287  */
288 function unload() {
289   // Releases keep awake just in case (should be released on unloading video).
290   chrome.power.releaseKeepAwake();
292   if (!player.controls || !player.controls.getMedia())
293     return;
295   player.controls.savePosition(true /* exiting */);
296   player.controls.cleanup();
300  * Loads the video file.
301  * @param {!FileEntry} video Entry of the video to be played.
302  * @param {function()=} opt_callback Completion callback.
303  * @private
304  */
305 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
306   this.unloadVideo(true);
308   this.loadQueue_.run(function(callback) {
309     document.title = video.name;
311     getRequiredElement('title').innerText = video.name;
313     var videoPlayerElement = getRequiredElement('video-player');
314     if (this.currentPos_ === (this.videos_.length - 1))
315       videoPlayerElement.setAttribute('last-video', true);
316     else
317       videoPlayerElement.removeAttribute('last-video');
319     if (this.currentPos_ === 0)
320       videoPlayerElement.setAttribute('first-video', true);
321     else
322       videoPlayerElement.removeAttribute('first-video');
324     // Re-enables ui and hides error message if already displayed.
325     getRequiredElement('video-player').removeAttribute('disabled');
326     getRequiredElement('error').removeAttribute('visible');
327     this.controls.detachMedia();
328     this.controls.getInactivityWatcher().disabled = true;
329     this.controls.decodeErrorOccured = false;
330     this.controls.casting = !!this.currentCast_;
332     videoPlayerElement.setAttribute('loading', true);
334     var media = new MediaManager(video);
336     Promise.all([media.getThumbnail(), media.getToken(false)])
337         .then(function(results) {
338           var url = results[0];
339           var token = results[1];
340           if (url && token) {
341             getRequiredElement('thumbnail').style.backgroundImage =
342                 'url(' + url + '&access_token=' + token + ')';
343           } else {
344             getRequiredElement('thumbnail').style.backgroundImage = '';
345           }
346         })
347         .catch(function() {
348           // Shows no image on error.
349           getRequiredElement('thumbnail').style.backgroundImage = '';
350         });
352     var videoElementInitializePromise;
353     if (this.currentCast_) {
354       metrics.recordPlayType(metrics.PLAY_TYPE.CAST);
356       videoPlayerElement.setAttribute('casting', true);
358       getRequiredElement('cast-name').textContent =
359           this.currentCast_.friendlyName;
361       videoPlayerElement.setAttribute('castable', true);
363       videoElementInitializePromise = media.isAvailableForCast()
364           .then(function(result) {
365             if (!result)
366               return Promise.reject('No casts are available.');
368             return new Promise(function(fulfill, reject) {
369               chrome.cast.requestSession(
370                   fulfill, reject, undefined, this.currentCast_.label);
371             }.bind(this)).then(function(session) {
372               session.addUpdateListener(this.onCastSessionUpdateBound_);
374               this.currentSession_ = session;
375               this.videoElement_ = new CastVideoElement(media, session);
376               this.controls.attachMedia(this.videoElement_);
377             }.bind(this));
378           }.bind(this));
379     } else {
380       metrics.recordPlayType(metrics.PLAY_TYPE.LOCAL);
381       videoPlayerElement.removeAttribute('casting');
383       this.videoElement_ = document.createElement('video');
384       getRequiredElement('video-container').appendChild(this.videoElement_);
386       this.controls.attachMedia(this.videoElement_);
387       this.videoElement_.src = video.toURL();
389       media.isAvailableForCast().then(function(result) {
390         if (result)
391           videoPlayerElement.setAttribute('castable', true);
392         else
393           videoPlayerElement.removeAttribute('castable');
394       }).catch(function() {
395         videoPlayerElement.setAttribute('castable', true);
396       });
398       videoElementInitializePromise = Promise.resolve();
399     }
401     videoElementInitializePromise
402         .then(function() {
403           var handler = function(currentPos) {
404             if (currentPos === this.currentPos_) {
405               if (opt_callback)
406                 opt_callback();
407               videoPlayerElement.removeAttribute('loading');
408               this.controls.getInactivityWatcher().disabled = false;
409             }
411             this.videoElement_.removeEventListener('loadedmetadata', handler);
412           }.wrap(this, this.currentPos_);
414           this.videoElement_.addEventListener('loadedmetadata', handler);
416           this.videoElement_.addEventListener('play', function() {
417             chrome.power.requestKeepAwake('display');
418           }.wrap());
419           this.videoElement_.addEventListener('pause', function() {
420             chrome.power.releaseKeepAwake();
421           }.wrap());
423           this.videoElement_.load();
424           callback();
425         }.bind(this))
426         // In case of error.
427         .catch(function(error) {
428           if (this.currentCast_)
429             metrics.recordCastVideoErrorAction();
431           videoPlayerElement.removeAttribute('loading');
432           console.error('Failed to initialize the video element.',
433                         error.stack || error);
434           this.controls_.showErrorMessage(
435               'VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
436           callback();
437         }.bind(this));
438   }.wrap(this));
442  * Plays the first video.
443  */
444 VideoPlayer.prototype.playFirstVideo = function() {
445   this.currentPos_ = 0;
446   this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
450  * Unloads the current video.
451  * @param {boolean=} opt_keepSession If true, keep using the current session.
452  *     Otherwise, discards the session.
453  */
454 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
455   this.loadQueue_.run(function(callback) {
456     chrome.power.releaseKeepAwake();
458     // Detaches the media from the control.
459     this.controls.detachMedia();
461     if (this.videoElement_) {
462       // If the element has dispose method, call it (CastVideoElement has it).
463       if (this.videoElement_.dispose)
464         this.videoElement_.dispose();
465       // Detach the previous video element, if exists.
466       if (this.videoElement_.parentNode)
467         this.videoElement_.parentNode.removeChild(this.videoElement_);
468     }
469     this.videoElement_ = null;
471     if (!opt_keepSession && this.currentSession_) {
472       this.currentSession_.stop(callback, callback);
473       this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
474       this.currentSession_ = null;
475     } else {
476       callback();
477     }
478   }.wrap(this));
482  * Called when the first video is ready after starting to load.
483  * @private
484  */
485 VideoPlayer.prototype.onFirstVideoReady_ = function() {
486   var videoWidth = this.videoElement_.videoWidth;
487   var videoHeight = this.videoElement_.videoHeight;
489   var aspect = videoWidth / videoHeight;
490   var newWidth = videoWidth;
491   var newHeight = videoHeight;
493   var shrinkX = newWidth / window.screen.availWidth;
494   var shrinkY = newHeight / window.screen.availHeight;
495   if (shrinkX > 1 || shrinkY > 1) {
496     if (shrinkY > shrinkX) {
497       newHeight = newHeight / shrinkY;
498       newWidth = newHeight * aspect;
499     } else {
500       newWidth = newWidth / shrinkX;
501       newHeight = newWidth / aspect;
502     }
503   }
505   var oldLeft = window.screenX;
506   var oldTop = window.screenY;
507   var oldWidth = window.outerWidth;
508   var oldHeight = window.outerHeight;
510   if (!oldWidth && !oldHeight) {
511     oldLeft = window.screen.availWidth / 2;
512     oldTop = window.screen.availHeight / 2;
513   }
515   var appWindow = chrome.app.window.current();
516   appWindow.resizeTo(newWidth, newHeight);
517   appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
518                    oldTop - (newHeight - oldHeight) / 2);
519   appWindow.show();
521   this.videoElement_.play();
525  * Advances to the next (or previous) track.
527  * @param {boolean} direction True to the next, false to the previous.
528  * @private
529  */
530 VideoPlayer.prototype.advance_ = function(direction) {
531   var newPos = this.currentPos_ + (direction ? 1 : -1);
532   if (0 <= newPos && newPos < this.videos_.length) {
533     this.currentPos_ = newPos;
534     this.reloadCurrentVideo(function() {
535       this.videoElement_.play();
536     }.wrap(this));
537   }
541  * Reloads the current video.
543  * @param {function()=} opt_callback Completion callback.
544  */
545 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
546   var currentVideo = this.videos_[this.currentPos_];
547   this.loadVideo_(currentVideo, opt_callback);
551  * Invokes when a menuitem in the cast menu is selected.
552  * @param {Object} cast Selected element in the list of casts.
553  * @private
554  */
555 VideoPlayer.prototype.onCastSelected_ = function(cast) {
556   // If the selected item is same as the current item, do nothing.
557   if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
558     return;
560   this.unloadVideo(false);
562   // Waits for unloading video.
563   this.loadQueue_.run(function(callback) {
564     this.currentCast_ = cast || null;
565     this.updateCheckOnCastMenu_();
566     this.reloadCurrentVideo();
567     callback();
568   }.wrap(this));
572  * Set the list of casts.
573  * @param {Array<Object>} casts List of casts.
574  */
575 VideoPlayer.prototype.setCastList = function(casts) {
576   var videoPlayerElement = getRequiredElement('video-player');
577   var menu = getRequiredElement('cast-menu');
578   menu.innerHTML = '';
580   // TODO(yoshiki): Handle the case that the current cast disappears.
582   if (casts.length === 0) {
583     videoPlayerElement.removeAttribute('cast-available');
584     if (this.currentCast_)
585       this.onCurrentCastDisappear_();
586     return;
587   }
589   if (this.currentCast_) {
590     var currentCastAvailable = casts.some(function(cast) {
591       return this.currentCast_.label === cast.label;
592     }.wrap(this));
594     if (!currentCastAvailable)
595       this.onCurrentCastDisappear_();
596   }
598   var item = new cr.ui.MenuItem();
599   item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
600   item.setAttribute('aria-label', item.label);
601   item.castLabel = '';
602   item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
603   menu.appendChild(item);
605   for (var i = 0; i < casts.length; i++) {
606     var item = new cr.ui.MenuItem();
607     item.label = casts[i].friendlyName;
608     item.setAttribute('aria-label', item.label);
609     item.castLabel = casts[i].label;
610     item.addEventListener('activate',
611                           this.onCastSelected_.wrap(this, casts[i]));
612     menu.appendChild(item);
613   }
614   this.updateCheckOnCastMenu_();
615   videoPlayerElement.setAttribute('cast-available', true);
619  * Updates the check status of the cast menu items.
620  * @private
621  */
622 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
623   var menuItems = getRequiredElement('cast-menu').menuItems;
624   for (var i = 0; i < menuItems.length; i++) {
625     var item = menuItems[i];
626     if (this.currentCast_ === null) {
627       // Playing on this computer.
628       if (item.castLabel === '')
629         item.checked = true;
630       else
631         item.checked = false;
632     } else {
633       // Playing on cast device.
634       if (item.castLabel === this.currentCast_.label)
635         item.checked = true;
636       else
637         item.checked = false;
638     }
639   }
643  * Called when the current cast is disappear from the cast list.
644  * @private
645  */
646 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
647   this.currentCast_ = null;
648   if (this.currentSession_) {
649     this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
650     this.currentSession_ = null;
651   }
652   this.controls.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
653   this.unloadVideo();
657  * This method should be called when the session is updated.
658  * @param {boolean} alive Whether the session is alive or not.
659  * @private
660  */
661 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
662   if (!alive)
663     this.unloadVideo();
666 var player = new VideoPlayer();
669  * Initializes the strings.
670  * @param {function()} callback Called when the sting data is ready.
671  */
672 function initStrings(callback) {
673   chrome.fileManagerPrivate.getStrings(function(strings) {
674     loadTimeData.data = strings;
675     i18nTemplate.process(document, loadTimeData);
676     callback();
677   }.wrap(null));
680 function initVolumeManager(callback) {
681   var volumeManager = new VolumeManagerWrapper(
682       VolumeManagerWrapper.NonNativeVolumeStatus.ENABLED);
683   volumeManager.ensureInitialized(callback);
686 var initPromise = Promise.all(
687     [new Promise(initStrings.wrap(null)),
688      new Promise(initVolumeManager.wrap(null)),
689      new Promise(util.addPageLoadHandler.wrap(null))]);
691 initPromise.then(function(unused) {
692   return new Promise(function(fulfill, reject) {
693     util.URLsToEntries(window.appState.items, function(entries) {
694       metrics.recordOpenVideoPlayerAction();
695       metrics.recordNumberOfOpenedFiles(entries.length);
697       player.prepare(entries);
698       player.playFirstVideo(player, fulfill);
699     }.wrap());
700   }.wrap());
701 }.wrap());