base: Change DCHECK_IS_ON to a macro DCHECK_IS_ON().
[chromium-blink-merge.git] / ui / file_manager / video_player / js / video_player.js
blob650947835102889532425fdf2319264a0f32ccde
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 (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, 0);
77   }.wrap(this));
79   /**
80    * @type {MouseInactivityWatcher}
81    * @private
82    */
83   this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
84   this.inactivityWatcher_.check();
87 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
89 /**
90  * Gets inactivity watcher.
91  * @return {MouseInactivityWatcher} An inactivity watcher.
92  */
93 FullWindowVideoControls.prototype.getInactivityWatcher = function() {
94   return this.inactivityWatcher_;
97 /**
98  * Displays error message.
99  *
100  * @param {string} message Message id.
101  */
102 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
103   var errorBanner = queryRequiredElement(document, '#error');
104   errorBanner.textContent = loadTimeData.getString(message);
105   errorBanner.setAttribute('visible', 'true');
107   // The window is hidden if the video has not loaded yet.
108   chrome.app.window.current().show();
112  * Handles playback (decoder) errors.
113  * @param {MediaError} error Error object.
114  * @private
115  */
116 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
117   if (error.target && error.target.error &&
118       error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
119     if (this.casting)
120       this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
121     else
122       this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
123     this.decodeErrorOccured = false;
124   } else {
125     this.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
126     this.decodeErrorOccured = true;
127   }
129   // Disable inactivity watcher, and disable the ui, by hiding tools manually.
130   this.getInactivityWatcher().disabled = true;
131   queryRequiredElement(document, '#video-player')
132       .setAttribute('disabled', 'true');
134   // Detach the video element, since it may be unreliable and reset stored
135   // current playback time.
136   this.cleanup();
137   this.clearState();
139   // Avoid reusing a video element.
140   player.unloadVideo();
144  * Toggles the full screen mode.
145  * @private
146  */
147 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
148   var appWindow = chrome.app.window.current();
149   util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
153  * Media completion handler.
154  */
155 FullWindowVideoControls.prototype.onMediaComplete = function() {
156   VideoControls.prototype.onMediaComplete.apply(this, arguments);
157   if (!this.getMedia().loop)
158     player.advance_(1);
162  * Video Player
164  * @constructor
165  * @struct
166  */
167 function VideoPlayer() {
168   this.controls_ = null;
169   this.videoElement_ = null;
171   /**
172    * @type {Array.<!FileEntry>}
173    * @private
174    */
175   this.videos_ = null;
177   this.currentPos_ = 0;
179   this.currentSession_ = null;
180   this.currentCast_ = null;
182   this.loadQueue_ = new AsyncUtil.Queue();
184   this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
187 VideoPlayer.prototype = /** @struct */ {
188   /**
189    * @return {FullWindowVideoControls}
190    */
191   get controls() {
192     return this.controls_;
193   }
197  * Initializes the video player window. This method must be called after DOM
198  * initialization.
199  * @param {!Array.<!FileEntry>} videos List of videos.
200  */
201 VideoPlayer.prototype.prepare = function(videos) {
202   this.videos_ = videos;
204   var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
206   document.ondragstart = preventDefault;
208   var maximizeButton = queryRequiredElement(document, '.maximize-button');
209   maximizeButton.addEventListener(
210       'click',
211       function(event) {
212         var appWindow = chrome.app.window.current();
213         if (appWindow.isMaximized())
214           appWindow.restore();
215         else
216           appWindow.maximize();
217         event.stopPropagation();
218       }.wrap(null));
219   maximizeButton.addEventListener('mousedown', preventDefault);
221   var minimizeButton = queryRequiredElement(document, '.minimize-button');
222   minimizeButton.addEventListener(
223       'click',
224       function(event) {
225         chrome.app.window.current().minimize();
226         event.stopPropagation();
227       }.wrap(null));
228   minimizeButton.addEventListener('mousedown', preventDefault);
230   var closeButton = queryRequiredElement(document, '.close-button');
231   closeButton.addEventListener(
232       'click',
233       function(event) {
234         window.close();
235         event.stopPropagation();
236       }.wrap(null));
237   closeButton.addEventListener('mousedown', preventDefault);
239   var menu = queryRequiredElement(document, '#cast-menu');
240   cr.ui.decorate(menu, cr.ui.Menu);
242   this.controls_ = new FullWindowVideoControls(
243       queryRequiredElement(document, '#video-player'),
244       queryRequiredElement(document, '#video-container'),
245       queryRequiredElement(document, '#controls'));
247   var reloadVideo = function(e) {
248     if (this.controls_.decodeErrorOccured &&
249         // Ignore shortcut keys
250         !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
251       this.reloadCurrentVideo(function() {
252         this.videoElement_.play();
253       }.wrap(this));
254       e.preventDefault();
255     }
256   }.wrap(this);
258   var arrowRight = queryRequiredElement(document, '.arrow-box .arrow.right');
259   arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
260   var arrowLeft = queryRequiredElement(document, '.arrow-box .arrow.left');
261   arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
263   var videoPlayerElement = queryRequiredElement(document, '#video-player');
264   if (videos.length > 1)
265     videoPlayerElement.setAttribute('multiple', true);
266   else
267     videoPlayerElement.removeAttribute('multiple');
269   document.addEventListener('keydown', reloadVideo);
270   document.addEventListener('click', reloadVideo);
274  * Unloads the player.
275  */
276 function unload() {
277   // Releases keep awake just in case (should be released on unloading video).
278   chrome.power.releaseKeepAwake();
280   if (!player.controls || !player.controls.getMedia())
281     return;
283   player.controls.savePosition(true /* exiting */);
284   player.controls.cleanup();
288  * Loads the video file.
289  * @param {!FileEntry} video Entry of the video to be played.
290  * @param {function()=} opt_callback Completion callback.
291  * @private
292  */
293 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
294   this.unloadVideo(true);
296   this.loadQueue_.run(function(callback) {
297     document.title = video.name;
299     queryRequiredElement(document, '#title').innerText = video.name;
301     var videoPlayerElement = queryRequiredElement(document, '#video-player');
302     if (this.currentPos_ === (this.videos_.length - 1))
303       videoPlayerElement.setAttribute('last-video', true);
304     else
305       videoPlayerElement.removeAttribute('last-video');
307     if (this.currentPos_ === 0)
308       videoPlayerElement.setAttribute('first-video', true);
309     else
310       videoPlayerElement.removeAttribute('first-video');
312     // Re-enables ui and hides error message if already displayed.
313     queryRequiredElement(document, '#video-player').removeAttribute('disabled');
314     queryRequiredElement(document, '#error').removeAttribute('visible');
315     this.controls.detachMedia();
316     this.controls.getInactivityWatcher().disabled = true;
317     this.controls.decodeErrorOccured = false;
318     this.controls.casting = !!this.currentCast_;
320     videoPlayerElement.setAttribute('loading', true);
322     var media = new MediaManager(video);
324     Promise.all([media.getThumbnail(), media.getToken(false)])
325         .then(function(results) {
326           var url = results[0];
327           var token = results[1];
328           if (url && token) {
329             queryRequiredElement(document, '#thumbnail').style.backgroundImage =
330                 'url(' + url + '&access_token=' + token + ')';
331           } else {
332             queryRequiredElement(document, '#thumbnail').style.backgroundImage =
333                 '';
334           }
335         })
336         .catch(function() {
337           // Shows no image on error.
338           queryRequiredElement(document, '#thumbnail').style.backgroundImage =
339               '';
340         });
342     var videoElementInitializePromise;
343     if (this.currentCast_) {
344       metrics.recordPlayType(metrics.PLAY_TYPE.CAST);
346       videoPlayerElement.setAttribute('casting', true);
348       queryRequiredElement(document, '#cast-name').textContent =
349           this.currentCast_.friendlyName;
351       videoPlayerElement.setAttribute('castable', true);
353       videoElementInitializePromise = media.isAvailableForCast()
354           .then(function(result) {
355             if (!result)
356               return Promise.reject('No casts are available.');
358             return new Promise(function(fulfill, reject) {
359               chrome.cast.requestSession(
360                   fulfill, reject, undefined, this.currentCast_.label);
361             }.bind(this)).then(function(session) {
362               session.addUpdateListener(this.onCastSessionUpdateBound_);
364               this.currentSession_ = session;
365               this.videoElement_ = new CastVideoElement(media, session);
366               this.controls.attachMedia(this.videoElement_);
367             }.bind(this));
368           }.bind(this));
369     } else {
370       metrics.recordPlayType(metrics.PLAY_TYPE.LOCAL);
371       videoPlayerElement.removeAttribute('casting');
373       this.videoElement_ = document.createElement('video');
374       queryRequiredElement(document, '#video-container').appendChild(
375           this.videoElement_);
377       this.controls.attachMedia(this.videoElement_);
378       this.videoElement_.src = video.toURL();
380       media.isAvailableForCast().then(function(result) {
381         if (result)
382           videoPlayerElement.setAttribute('castable', true);
383         else
384           videoPlayerElement.removeAttribute('castable');
385       }).catch(function() {
386         videoPlayerElement.setAttribute('castable', true);
387       });
389       videoElementInitializePromise = Promise.resolve();
390     }
392     videoElementInitializePromise
393         .then(function() {
394           var handler = function(currentPos) {
395             if (currentPos === this.currentPos_) {
396               if (opt_callback)
397                 opt_callback();
398               videoPlayerElement.removeAttribute('loading');
399               this.controls.getInactivityWatcher().disabled = false;
400             }
402             this.videoElement_.removeEventListener('loadedmetadata', handler);
403           }.wrap(this, this.currentPos_);
405           this.videoElement_.addEventListener('loadedmetadata', handler);
407           this.videoElement_.addEventListener('play', function() {
408             chrome.power.requestKeepAwake('display');
409           }.wrap());
410           this.videoElement_.addEventListener('pause', function() {
411             chrome.power.releaseKeepAwake();
412           }.wrap());
414           this.videoElement_.load();
415           callback();
416         }.bind(this))
417         // In case of error.
418         .catch(function(error) {
419           if (this.currentCast_)
420             metrics.recordCastVideoErrorAction();
422           videoPlayerElement.removeAttribute('loading');
423           console.error('Failed to initialize the video element.',
424                         error.stack || error);
425           this.controls_.showErrorMessage(
426               'VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
427           callback();
428         }.bind(this));
429   }.wrap(this));
433  * Plays the first video.
434  */
435 VideoPlayer.prototype.playFirstVideo = function() {
436   this.currentPos_ = 0;
437   this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
441  * Unloads the current video.
442  * @param {boolean=} opt_keepSession If true, keep using the current session.
443  *     Otherwise, discards the session.
444  */
445 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
446   this.loadQueue_.run(function(callback) {
447     chrome.power.releaseKeepAwake();
449     // Detaches the media from the control.
450     this.controls.detachMedia();
452     if (this.videoElement_) {
453       // If the element has dispose method, call it (CastVideoElement has it).
454       if (this.videoElement_.dispose)
455         this.videoElement_.dispose();
456       // Detach the previous video element, if exists.
457       if (this.videoElement_.parentNode)
458         this.videoElement_.parentNode.removeChild(this.videoElement_);
459     }
460     this.videoElement_ = null;
462     if (!opt_keepSession && this.currentSession_) {
463       this.currentSession_.stop(callback, callback);
464       this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
465       this.currentSession_ = null;
466     } else {
467       callback();
468     }
469   }.wrap(this));
473  * Called when the first video is ready after starting to load.
474  * @private
475  */
476 VideoPlayer.prototype.onFirstVideoReady_ = function() {
477   var videoWidth = this.videoElement_.videoWidth;
478   var videoHeight = this.videoElement_.videoHeight;
480   var aspect = videoWidth / videoHeight;
481   var newWidth = videoWidth;
482   var newHeight = videoHeight;
484   var shrinkX = newWidth / window.screen.availWidth;
485   var shrinkY = newHeight / window.screen.availHeight;
486   if (shrinkX > 1 || shrinkY > 1) {
487     if (shrinkY > shrinkX) {
488       newHeight = newHeight / shrinkY;
489       newWidth = newHeight * aspect;
490     } else {
491       newWidth = newWidth / shrinkX;
492       newHeight = newWidth / aspect;
493     }
494   }
496   var oldLeft = window.screenX;
497   var oldTop = window.screenY;
498   var oldWidth = window.outerWidth;
499   var oldHeight = window.outerHeight;
501   if (!oldWidth && !oldHeight) {
502     oldLeft = window.screen.availWidth / 2;
503     oldTop = window.screen.availHeight / 2;
504   }
506   var appWindow = chrome.app.window.current();
507   appWindow.resizeTo(newWidth, newHeight);
508   appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
509                    oldTop - (newHeight - oldHeight) / 2);
510   appWindow.show();
512   this.videoElement_.play();
516  * Advances to the next (or previous) track.
518  * @param {boolean} direction True to the next, false to the previous.
519  * @private
520  */
521 VideoPlayer.prototype.advance_ = function(direction) {
522   var newPos = this.currentPos_ + (direction ? 1 : -1);
523   if (0 <= newPos && newPos < this.videos_.length) {
524     this.currentPos_ = newPos;
525     this.reloadCurrentVideo(function() {
526       this.videoElement_.play();
527     }.wrap(this));
528   }
532  * Reloads the current video.
534  * @param {function()=} opt_callback Completion callback.
535  */
536 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
537   var currentVideo = this.videos_[this.currentPos_];
538   this.loadVideo_(currentVideo, opt_callback);
542  * Invokes when a menuitem in the cast menu is selected.
543  * @param {Object} cast Selected element in the list of casts.
544  * @private
545  */
546 VideoPlayer.prototype.onCastSelected_ = function(cast) {
547   // If the selected item is same as the current item, do nothing.
548   if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
549     return;
551   this.unloadVideo(false);
553   // Waits for unloading video.
554   this.loadQueue_.run(function(callback) {
555     this.currentCast_ = cast || null;
556     this.updateCheckOnCastMenu_();
557     this.reloadCurrentVideo();
558     callback();
559   }.wrap(this));
563  * Set the list of casts.
564  * @param {Array.<Object>} casts List of casts.
565  */
566 VideoPlayer.prototype.setCastList = function(casts) {
567   var videoPlayerElement = queryRequiredElement(document, '#video-player');
568   var menu = queryRequiredElement(document, '#cast-menu');
569   menu.innerHTML = '';
571   // TODO(yoshiki): Handle the case that the current cast disappears.
573   if (casts.length === 0) {
574     videoPlayerElement.removeAttribute('cast-available');
575     if (this.currentCast_)
576       this.onCurrentCastDisappear_();
577     return;
578   }
580   if (this.currentCast_) {
581     var currentCastAvailable = casts.some(function(cast) {
582       return this.currentCast_.label === cast.label;
583     }.wrap(this));
585     if (!currentCastAvailable)
586       this.onCurrentCastDisappear_();
587   }
589   var item = new cr.ui.MenuItem();
590   item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
591   item.setAttribute('aria-label', item.label);
592   item.castLabel = '';
593   item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
594   menu.appendChild(item);
596   for (var i = 0; i < casts.length; i++) {
597     var item = new cr.ui.MenuItem();
598     item.label = casts[i].friendlyName;
599     item.setAttribute('aria-label', item.label);
600     item.castLabel = casts[i].label;
601     item.addEventListener('activate',
602                           this.onCastSelected_.wrap(this, casts[i]));
603     menu.appendChild(item);
604   }
605   this.updateCheckOnCastMenu_();
606   videoPlayerElement.setAttribute('cast-available', true);
610  * Updates the check status of the cast menu items.
611  * @private
612  */
613 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
614   var menu = queryRequiredElement(document, '#cast-menu');
615   var menuItems = menu.menuItems;
616   for (var i = 0; i < menuItems.length; i++) {
617     var item = menuItems[i];
618     if (this.currentCast_ === null) {
619       // Playing on this computer.
620       if (item.castLabel === '')
621         item.checked = true;
622       else
623         item.checked = false;
624     } else {
625       // Playing on cast device.
626       if (item.castLabel === this.currentCast_.label)
627         item.checked = true;
628       else
629         item.checked = false;
630     }
631   }
635  * Called when the current cast is disappear from the cast list.
636  * @private
637  */
638 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
639   this.currentCast_ = null;
640   if (this.currentSession_) {
641     this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
642     this.currentSession_ = null;
643   }
644   this.controls.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
645   this.unloadVideo();
649  * This method should be called when the session is updated.
650  * @param {boolean} alive Whether the session is alive or not.
651  * @private
652  */
653 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
654   if (!alive)
655     this.unloadVideo();
658 var player = new VideoPlayer();
661  * Initializes the strings.
662  * @param {function()} callback Called when the sting data is ready.
663  */
664 function initStrings(callback) {
665   chrome.fileManagerPrivate.getStrings(function(strings) {
666     loadTimeData.data = strings;
667     i18nTemplate.process(document, loadTimeData);
668     callback();
669   }.wrap(null));
672 function initVolumeManager(callback) {
673   var volumeManager = new VolumeManagerWrapper(
674       VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
675   volumeManager.ensureInitialized(callback);
678 var initPromise = Promise.all(
679     [new Promise(initStrings.wrap(null)),
680      new Promise(initVolumeManager.wrap(null)),
681      new Promise(util.addPageLoadHandler.wrap(null))]);
683 initPromise.then(function(unused) {
684   return new Promise(function(fulfill, reject) {
685     util.URLsToEntries(window.appState.items, function(entries) {
686       metrics.recordOpenVideoPlayerAction();
687       metrics.recordNumberOfOpenedFiles(entries.length);
689       player.prepare(entries);
690       player.playFirstVideo(player, fulfill);
691     }.wrap());
692   }.wrap());
693 }.wrap());