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 */ 13function 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 87FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype }; 88 89/** 90 * Displays error message. 91 * 92 * @param {string} message Message id. 93 */ 94FullWindowVideoControls.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 */ 108FullWindowVideoControls.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 */ 138FullWindowVideoControls.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 */ 146FullWindowVideoControls.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 */ 155function 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 171VideoPlayer.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 */ 182VideoPlayer.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 */ 257function 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 */ 274VideoPlayer.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 */ 407VideoPlayer.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 */ 417VideoPlayer.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 */ 448VideoPlayer.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 */ 493VideoPlayer.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 */ 508VideoPlayer.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 */ 518VideoPlayer.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 */ 538VideoPlayer.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 */ 585VideoPlayer.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 */ 610VideoPlayer.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 */ 625VideoPlayer.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 */ 635function 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 651var player = new VideoPlayer(); 652 653/** 654 * Initializes the strings. 655 * @param {function()} callback Called when the sting data is ready. 656 */ 657function initStrings(callback) { 658 chrome.fileManagerPrivate.getStrings(function(strings) { 659 loadTimeData.data = strings; 660 i18nTemplate.process(document, loadTimeData); 661 callback(); 662 }.wrap(null)); 663} 664 665var initPromise = Promise.all( 666 [new Promise(initVideos.wrap(null)), 667 new Promise(initStrings.wrap(null)), 668 new Promise(util.addPageLoadHandler.wrap(null))]); 669 670initPromise.then(function(results) { 671 var videos = results[0]; 672 player.prepare(videos); 673 return new Promise(player.playFirstVideo.wrap(player)); 674}.wrap(null)); 675