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.updateStyle(); 26 window.addEventListener('resize', this.updateStyle.wrap(this)); 27 document.addEventListener('keydown', function(e) { 28 if (e.keyIdentifier == 'U+0020') { // Space 29 this.togglePlayStateWithFeedback(); 30 e.preventDefault(); 31 } 32 if (e.keyIdentifier == 'U+001B') { // Escape 33 util.toggleFullScreen( 34 chrome.app.window.current(), 35 false); // Leave the full screen mode. 36 e.preventDefault(); 37 } 38 }.wrap(this)); 39 40 // TODO(mtomasz): Simplify. crbug.com/254318. 41 videoContainer.addEventListener('click', function(e) { 42 if (e.ctrlKey) { 43 this.toggleLoopedModeWithFeedback(true); 44 if (!this.isPlaying()) 45 this.togglePlayStateWithFeedback(); 46 } else { 47 this.togglePlayStateWithFeedback(); 48 } 49 }.wrap(this)); 50 51 this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer); 52 this.__defineGetter__('inactivityWatcher', function() { 53 return this.inactivityWatcher_; 54 }.wrap(this)); 55 56 this.inactivityWatcher_.check(); 57 } 58 59 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype }; 60 61 /** 62 * Displays error message. 63 * 64 * @param {string} message Message id. 65 * @private 66 */ 67 FullWindowVideoControls.prototype.showErrorMessage_ = function(message) { 68 var errorBanner = document.querySelector('#error'); 69 errorBanner.textContent = 70 loadTimeData.getString(message); 71 errorBanner.setAttribute('visible', 'true'); 72 73 // The window is hidden if the video has not loaded yet. 74 chrome.app.window.current().show(); 75 }; 76 77 /** 78 * Handles playback (decoder) errors. 79 * @private 80 */ 81 FullWindowVideoControls.prototype.onPlaybackError_ = function() { 82 this.showErrorMessage_('GALLERY_VIDEO_DECODING_ERROR'); 83 this.decodeErrorOccured = true; 84 85 // Disable inactivity watcher, and disable the ui, by hiding tools manually. 86 this.inactivityWatcher.disabled = true; 87 document.querySelector('#video-player').setAttribute('disabled', 'true'); 88 89 // Detach the video element, since it may be unreliable and reset stored 90 // current playback time. 91 this.cleanup(); 92 this.clearState(); 93 94 // Avoid reusing a video element. 95 player.unloadVideo(); 96 }; 97 98 /** 99 * Toggles the full screen mode. 100 * @private 101 */ 102 FullWindowVideoControls.prototype.toggleFullScreen_ = function() { 103 var appWindow = chrome.app.window.current(); 104 util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow)); 105 }; 106 107 /** 108 * @constructor 109 */ 110 function VideoPlayer() { 111 this.controls_ = null; 112 this.videoElement_ = null; 113 this.videos_ = null; 114 this.currentPos_ = 0; 115 116 Object.seal(this); 117 } 118 119 VideoPlayer.prototype = { 120 get controls() { 121 return this.controls_; 122 } 123 }; 124 125 /** 126 * Initializes the video player window. This method must be called after DOM 127 * initialization. 128 * @param {Array.<Object.<string, Object>>} videos List of videos. 129 */ 130 VideoPlayer.prototype.prepare = function(videos) { 131 this.videos_ = videos; 132 133 var preventDefault = function(event) { event.preventDefault(); }.wrap(null); 134 135 document.ondragstart = preventDefault; 136 137 var maximizeButton = document.querySelector('.maximize-button'); 138 maximizeButton.addEventListener( 139 'click', 140 function() { 141 var appWindow = chrome.app.window.current(); 142 if (appWindow.isMaximized()) 143 appWindow.restore(); 144 else 145 appWindow.maximize(); 146 }.wrap(null)); 147 maximizeButton.addEventListener('mousedown', preventDefault); 148 149 var minimizeButton = document.querySelector('.minimize-button'); 150 minimizeButton.addEventListener( 151 'click', 152 function() { 153 chrome.app.window.current().minimize() 154 }.wrap(null)); 155 minimizeButton.addEventListener('mousedown', preventDefault); 156 157 var closeButton = document.querySelector('.close-button'); 158 closeButton.addEventListener( 159 'click', 160 function() { close(); }.wrap(null)); 161 closeButton.addEventListener('mousedown', preventDefault); 162 163 this.controls_ = new FullWindowVideoControls( 164 document.querySelector('#video-player'), 165 document.querySelector('#video-container'), 166 document.querySelector('#controls')); 167 168 var reloadVideo = function(e) { 169 if (this.controls_.decodeErrorOccured && 170 // Ignore shortcut keys 171 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { 172 this.reloadCurrentVideo_(function() { 173 this.videoElement_.play(); 174 }.wrap(this)); 175 e.preventDefault(); 176 } 177 }.wrap(this); 178 179 var arrowRight = document.querySelector('.arrow-box .arrow.right'); 180 arrowRight.addEventListener('click', this.advance_.wrap(this, 1)); 181 var arrowLeft = document.querySelector('.arrow-box .arrow.left'); 182 arrowLeft.addEventListener('click', this.advance_.wrap(this, 0)); 183 184 var videoPlayerElement = document.querySelector('#video-player'); 185 if (videos.length > 1) 186 videoPlayerElement.setAttribute('multiple', true); 187 else 188 videoPlayerElement.removeAttribute('multiple'); 189 190 document.addEventListener('keydown', reloadVideo, true); 191 document.addEventListener('click', reloadVideo, true); 192 }; 193 194 /** 195 * Unloads the player. 196 */ 197 function unload() { 198 if (!player.controls || !player.controls.getMedia()) 199 return; 200 201 player.controls.savePosition(true /* exiting */); 202 player.controls.cleanup(); 203 } 204 205 /** 206 * Loads the video file. 207 * @param {string} url URL of the video file. 208 * @param {string} title Title of the video file. 209 * @param {function()=} opt_callback Completion callback. 210 * @private 211 */ 212 VideoPlayer.prototype.loadVideo_ = function(url, title, opt_callback) { 213 this.unloadVideo(); 214 215 document.title = title; 216 217 document.querySelector('#title').innerText = title; 218 219 var videoPlayerElement = document.querySelector('#video-player'); 220 if (this.currentPos_ === (this.videos_.length - 1)) 221 videoPlayerElement.setAttribute('last-video', true); 222 else 223 videoPlayerElement.removeAttribute('last-video'); 224 225 if (this.currentPos_ === 0) 226 videoPlayerElement.setAttribute('first-video', true); 227 else 228 videoPlayerElement.removeAttribute('first-video'); 229 230 // Re-enables ui and hides error message if already displayed. 231 document.querySelector('#video-player').removeAttribute('disabled'); 232 document.querySelector('#error').removeAttribute('visible'); 233 this.controls.inactivityWatcher.disabled = false; 234 this.controls.decodeErrorOccured = false; 235 236 this.videoElement_ = document.createElement('video'); 237 document.querySelector('#video-container').appendChild(this.videoElement_); 238 this.controls.attachMedia(this.videoElement_); 239 240 this.videoElement_.src = url; 241 this.videoElement_.load(); 242 243 if (opt_callback) { 244 var handler = function(currentPos, event) { 245 console.log('loaded: ', currentPos, this.currentPos_); 246 if (currentPos === this.currentPos_) 247 opt_callback(); 248 this.videoElement_.removeEventListener('loadedmetadata', handler); 249 }.wrap(this, this.currentPos_); 250 251 this.videoElement_.addEventListener('loadedmetadata', handler); 252 } 253 }; 254 255 /** 256 * Plays the first video. 257 */ 258 VideoPlayer.prototype.playFirstVideo = function() { 259 this.currentPos_ = 0; 260 this.reloadCurrentVideo_(this.onFirstVideoReady_.wrap(this)); 261 }; 262 263 /** 264 * Unloads the current video. 265 */ 266 VideoPlayer.prototype.unloadVideo = function() { 267 // Detach the previous video element, if exists. 268 if (this.videoElement_) 269 this.videoElement_.parentNode.removeChild(this.videoElement_); 270 this.videoElement_ = null; 271 }; 272 273 /** 274 * Called when the first video is ready after starting to load. 275 * @private 276 */ 277 VideoPlayer.prototype.onFirstVideoReady_ = function() { 278 // TODO: chrome.app.window soon will be able to resize the content area. 279 // Until then use approximate title bar height. 280 var TITLE_HEIGHT = 33; 281 282 var videoWidth = this.videoElement_.videoWidth; 283 var videoHeight = this.videoElement_.videoHeight; 284 285 var aspect = videoWidth / videoHeight; 286 var newWidth = videoWidth; 287 var newHeight = videoHeight + TITLE_HEIGHT; 288 289 var shrinkX = newWidth / window.screen.availWidth; 290 var shrinkY = newHeight / window.screen.availHeight; 291 if (shrinkX > 1 || shrinkY > 1) { 292 if (shrinkY > shrinkX) { 293 newHeight = newHeight / shrinkY; 294 newWidth = (newHeight - TITLE_HEIGHT) * aspect; 295 } else { 296 newWidth = newWidth / shrinkX; 297 newHeight = newWidth / aspect + TITLE_HEIGHT; 298 } 299 } 300 301 var oldLeft = window.screenX; 302 var oldTop = window.screenY; 303 var oldWidth = window.outerWidth; 304 var oldHeight = window.outerHeight; 305 306 if (!oldWidth && !oldHeight) { 307 oldLeft = window.screen.availWidth / 2; 308 oldTop = window.screen.availHeight / 2; 309 } 310 311 var appWindow = chrome.app.window.current(); 312 appWindow.resizeTo(newWidth, newHeight); 313 appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2, 314 oldTop - (newHeight - oldHeight) / 2); 315 appWindow.show(); 316 317 this.videoElement_.play(); 318 }; 319 320 /** 321 * Advances to the next (or previous) track. 322 * 323 * @param {boolean} direction True to the next, false to the previous. 324 * @private 325 */ 326 VideoPlayer.prototype.advance_ = function(direction) { 327 var newPos = this.currentPos_ + (direction ? 1 : -1); 328 if (0 <= newPos && newPos < this.videos_.length) { 329 this.currentPos_ = newPos; 330 this.reloadCurrentVideo_(function() { 331 this.videoElement_.play(); 332 }.wrap(this)); 333 } 334 }; 335 336 /** 337 * Reloads the current video. 338 * 339 * @param {function()=} opt_callback Completion callback. 340 * @private 341 */ 342 VideoPlayer.prototype.reloadCurrentVideo_ = function(opt_callback) { 343 var currentVideo = this.videos_[this.currentPos_]; 344 this.loadVideo_(currentVideo.fileUrl, currentVideo.entry.name, opt_callback); 345 }; 346 347 /** 348 * Initialize the list of videos. 349 * @param {function(Array.<Object>)} callback Called with the video list when 350 * it is ready. 351 */ 352 function initVideos(callback) { 353 if (window.videos) { 354 var videos = window.videos; 355 window.videos = null; 356 callback(videos); 357 return; 358 } 359 360 chrome.runtime.onMessage.addListener( 361 function(request, sender, sendResponse) { 362 var videos = window.videos; 363 window.videos = null; 364 callback(videos); 365 }.wrap(null)); 366 } 367 368 var player = new VideoPlayer(); 369 370 /** 371 * Initializes the strings. 372 * @param {function()} callback Called when the sting data is ready. 373 */ 374 function initStrings(callback) { 375 chrome.fileBrowserPrivate.getStrings(function(strings) { 376 loadTimeData.data = strings; 377 callback(); 378 }.wrap(null)); 379 } 380 381 var initPromise = Promise.all( 382 [new Promise(initVideos.wrap(null)), 383 new Promise(initStrings.wrap(null)), 384 new Promise(util.addPageLoadHandler.wrap(null))]); 385 386 initPromise.then(function(results) { 387 var videos = results[0]; 388 player.prepare(videos); 389 return new Promise(player.playFirstVideo.wrap(player)); 390 }.wrap(null)); 391