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