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.
8 * @param {Element} playerContainer Main container.
9 * @param {Element} videoContainer Container for the video element.
10 * @param {Element} controlsContainer Container for video controls.
13 function FullWindowVideoControls(
14 playerContainer, videoContainer, controlsContainer) {
15 VideoControls.call(this,
17 this.onPlaybackError_.wrap(this),
18 loadTimeData.getString.wrap(loadTimeData),
19 this.toggleFullScreen_.wrap(this),
22 this.playerContainer_ = playerContainer;
23 this.decodeErrorOccured = false;
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();
35 case 'U+001B': // Escape
36 util.toggleFullScreen(
37 chrome.app.window.current(),
38 false); // Leave the full screen mode.
41 case 'MediaNextTrack':
45 case 'MediaPreviousTrack':
49 // TODO: Define "Stop" behavior.
54 // TODO(mtomasz): Simplify. crbug.com/254318.
55 var clickInProgress = false;
56 videoContainer.addEventListener('click', function(e) {
60 clickInProgress = true;
61 var togglePlayState = function() {
62 clickInProgress = false;
65 this.toggleLoopedModeWithFeedback(true);
66 if (!this.isPlaying())
67 this.togglePlayStateWithFeedback();
69 this.togglePlayStateWithFeedback();
74 player.reloadCurrentVideo(togglePlayState);
76 setTimeout(togglePlayState);
79 this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
80 this.__defineGetter__('inactivityWatcher', function() {
81 return this.inactivityWatcher_;
84 this.inactivityWatcher_.check();
87 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
90 * Displays error message.
92 * @param {string} message Message id.
94 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
95 var errorBanner = document.querySelector('#error');
96 errorBanner.textContent = loadTimeData.getString(message);
97 errorBanner.setAttribute('visible', 'true');
99 // The window is hidden if the video has not loaded yet.
100 chrome.app.window.current().show();
104 * Handles playback (decoder) errors.
105 * @param {MediaError} error Error object.
108 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
109 if (error.target && error.target.error &&
110 error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
112 this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
114 this.showErrorMessage('GALLERY_VIDEO_ERROR');
115 this.decodeErrorOccured = false;
117 this.showErrorMessage('GALLERY_VIDEO_DECODING_ERROR');
118 this.decodeErrorOccured = true;
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');
125 // Detach the video element, since it may be unreliable and reset stored
126 // current playback time.
130 // Avoid reusing a video element.
131 player.unloadVideo();
135 * Toggles the full screen mode.
138 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
139 var appWindow = chrome.app.window.current();
140 util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
144 * Media completion handler.
146 FullWindowVideoControls.prototype.onMediaComplete = function() {
147 VideoControls.prototype.onMediaComplete.apply(this, arguments);
148 if (!this.getMedia().loop)
155 function VideoPlayer() {
156 this.controls_ = null;
157 this.videoElement_ = null;
159 this.currentPos_ = 0;
161 this.currentSession_ = null;
162 this.currentCast_ = null;
164 this.loadQueue_ = new AsyncUtil.Queue();
166 this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
171 VideoPlayer.prototype = {
173 return this.controls_;
178 * Initializes the video player window. This method must be called after DOM
180 * @param {Array.<Object.<string, Object>>} videos List of videos.
182 VideoPlayer.prototype.prepare = function(videos) {
183 this.videos_ = videos;
185 var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
187 document.ondragstart = preventDefault;
189 var maximizeButton = document.querySelector('.maximize-button');
190 maximizeButton.addEventListener(
193 var appWindow = chrome.app.window.current();
194 if (appWindow.isMaximized())
197 appWindow.maximize();
198 event.stopPropagation();
200 maximizeButton.addEventListener('mousedown', preventDefault);
202 var minimizeButton = document.querySelector('.minimize-button');
203 minimizeButton.addEventListener(
206 chrome.app.window.current().minimize();
207 event.stopPropagation();
209 minimizeButton.addEventListener('mousedown', preventDefault);
211 var closeButton = document.querySelector('.close-button');
212 closeButton.addEventListener(
216 event.stopPropagation();
218 closeButton.addEventListener('mousedown', preventDefault);
220 var castButton = document.querySelector('.cast-button');
221 cr.ui.decorate(castButton, cr.ui.MenuButton);
222 castButton.addEventListener(
225 event.stopPropagation();
227 castButton.addEventListener('mousedown', preventDefault);
229 var menu = document.querySelector('#cast-menu');
230 cr.ui.decorate(menu, cr.ui.Menu);
232 this.controls_ = new FullWindowVideoControls(
233 document.querySelector('#video-player'),
234 document.querySelector('#video-container'),
235 document.querySelector('#controls'));
237 var reloadVideo = function(e) {
238 if (this.controls_.decodeErrorOccured &&
239 // Ignore shortcut keys
240 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
241 this.reloadCurrentVideo(function() {
242 this.videoElement_.play();
248 var arrowRight = document.querySelector('.arrow-box .arrow.right');
249 arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
250 var arrowLeft = document.querySelector('.arrow-box .arrow.left');
251 arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
253 var videoPlayerElement = document.querySelector('#video-player');
254 if (videos.length > 1)
255 videoPlayerElement.setAttribute('multiple', true);
257 videoPlayerElement.removeAttribute('multiple');
259 document.addEventListener('keydown', reloadVideo);
260 document.addEventListener('click', reloadVideo);
264 * Unloads the player.
267 // Releases keep awake just in case (should be released on unloading video).
268 chrome.power.releaseKeepAwake();
270 if (!player.controls || !player.controls.getMedia())
273 player.controls.savePosition(true /* exiting */);
274 player.controls.cleanup();
278 * Loads the video file.
279 * @param {Object} video Data of the video file.
280 * @param {function()=} opt_callback Completion callback.
283 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
284 this.unloadVideo(true);
286 this.loadQueue_.run(function(callback) {
287 document.title = video.title;
289 document.querySelector('#title').innerText = video.title;
291 var videoPlayerElement = document.querySelector('#video-player');
292 if (this.currentPos_ === (this.videos_.length - 1))
293 videoPlayerElement.setAttribute('last-video', true);
295 videoPlayerElement.removeAttribute('last-video');
297 if (this.currentPos_ === 0)
298 videoPlayerElement.setAttribute('first-video', true);
300 videoPlayerElement.removeAttribute('first-video');
302 // Re-enables ui and hides error message if already displayed.
303 document.querySelector('#video-player').removeAttribute('disabled');
304 document.querySelector('#error').removeAttribute('visible');
305 this.controls.detachMedia();
306 this.controls.inactivityWatcher.disabled = true;
307 this.controls.decodeErrorOccured = false;
308 this.controls.casting = !!this.currentCast_;
310 videoPlayerElement.setAttribute('loading', true);
312 var media = new MediaManager(video.entry);
314 Promise.all([media.getThumbnail(), media.getToken()])
315 .then(function(results) {
316 var url = results[0];
317 var token = results[1];
318 document.querySelector('#thumbnail').style.backgroundImage =
319 'url(' + url + '&access_token=' + token + ')';
322 // Shows no image on error.
323 document.querySelector('#thumbnail').style.backgroundImage = '';
326 var videoElementInitializePromise;
327 if (this.currentCast_) {
328 videoPlayerElement.setAttribute('casting', true);
330 document.querySelector('#cast-name').textContent =
331 this.currentCast_.friendlyName;
333 videoPlayerElement.setAttribute('castable', true);
335 videoElementInitializePromise = media.isAvailableForCast()
336 .then(function(result) {
338 return Promise.reject('No casts are available.');
340 return new Promise(function(fulfill, reject) {
341 chrome.cast.requestSession(
342 fulfill, reject, undefined, this.currentCast_.label);
343 }.bind(this)).then(function(session) {
344 session.addUpdateListener(this.onCastSessionUpdateBound_);
346 this.currentSession_ = session;
347 this.videoElement_ = new CastVideoElement(media, session);
348 this.controls.attachMedia(this.videoElement_);
352 videoPlayerElement.removeAttribute('casting');
354 this.videoElement_ = document.createElement('video');
355 document.querySelector('#video-container').appendChild(
358 this.controls.attachMedia(this.videoElement_);
359 this.videoElement_.src = video.url;
361 media.isAvailableForCast().then(function(result) {
363 videoPlayerElement.setAttribute('castable', true);
365 videoPlayerElement.removeAttribute('castable');
366 }).catch(function() {
367 videoPlayerElement.setAttribute('castable', true);
370 videoElementInitializePromise = Promise.resolve();
373 videoElementInitializePromise
375 var handler = function(currentPos) {
376 if (currentPos === this.currentPos_) {
379 videoPlayerElement.removeAttribute('loading');
380 this.controls.inactivityWatcher.disabled = false;
383 this.videoElement_.removeEventListener('loadedmetadata', handler);
384 }.wrap(this, this.currentPos_);
386 this.videoElement_.addEventListener('loadedmetadata', handler);
388 this.videoElement_.addEventListener('play', function() {
389 chrome.power.requestKeepAwake('display');
391 this.videoElement_.addEventListener('pause', function() {
392 chrome.power.releaseKeepAwake();
395 this.videoElement_.load();
399 .catch(function(error) {
400 videoPlayerElement.removeAttribute('loading');
401 console.error('Failed to initialize the video element.',
402 error.stack || error);
403 this.controls_.showErrorMessage('GALLERY_VIDEO_ERROR');
410 * Plays the first video.
412 VideoPlayer.prototype.playFirstVideo = function() {
413 this.currentPos_ = 0;
414 this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
418 * Unloads the current video.
419 * @param {boolean=} opt_keepSession If true, keep using the current session.
420 * Otherwise, discards the session.
422 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
423 this.loadQueue_.run(function(callback) {
424 chrome.power.releaseKeepAwake();
426 if (this.videoElement_) {
427 // If the element has dispose method, call it (CastVideoElement has it).
428 if (this.videoElement_.dispose)
429 this.videoElement_.dispose();
430 // Detach the previous video element, if exists.
431 if (this.videoElement_.parentNode)
432 this.videoElement_.parentNode.removeChild(this.videoElement_);
434 this.videoElement_ = null;
436 if (!opt_keepSession && this.currentSession_) {
437 this.currentSession_.stop(callback, callback);
438 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
439 this.currentSession_ = null;
447 * Called when the first video is ready after starting to load.
450 VideoPlayer.prototype.onFirstVideoReady_ = function() {
451 var videoWidth = this.videoElement_.videoWidth;
452 var videoHeight = this.videoElement_.videoHeight;
454 var aspect = videoWidth / videoHeight;
455 var newWidth = videoWidth;
456 var newHeight = videoHeight;
458 var shrinkX = newWidth / window.screen.availWidth;
459 var shrinkY = newHeight / window.screen.availHeight;
460 if (shrinkX > 1 || shrinkY > 1) {
461 if (shrinkY > shrinkX) {
462 newHeight = newHeight / shrinkY;
463 newWidth = newHeight * aspect;
465 newWidth = newWidth / shrinkX;
466 newHeight = newWidth / aspect;
470 var oldLeft = window.screenX;
471 var oldTop = window.screenY;
472 var oldWidth = window.outerWidth;
473 var oldHeight = window.outerHeight;
475 if (!oldWidth && !oldHeight) {
476 oldLeft = window.screen.availWidth / 2;
477 oldTop = window.screen.availHeight / 2;
480 var appWindow = chrome.app.window.current();
481 appWindow.resizeTo(newWidth, newHeight);
482 appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
483 oldTop - (newHeight - oldHeight) / 2);
486 this.videoElement_.play();
490 * Advances to the next (or previous) track.
492 * @param {boolean} direction True to the next, false to the previous.
495 VideoPlayer.prototype.advance_ = function(direction) {
496 var newPos = this.currentPos_ + (direction ? 1 : -1);
497 if (0 <= newPos && newPos < this.videos_.length) {
498 this.currentPos_ = newPos;
499 this.reloadCurrentVideo(function() {
500 this.videoElement_.play();
506 * Reloads the current video.
508 * @param {function()=} opt_callback Completion callback.
510 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
511 var currentVideo = this.videos_[this.currentPos_];
512 this.loadVideo_(currentVideo, opt_callback);
516 * Invokes when a menuitem in the cast menu is selected.
517 * @param {Object} cast Selected element in the list of casts.
520 VideoPlayer.prototype.onCastSelected_ = function(cast) {
521 // If the selected item is same as the current item, do nothing.
522 if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
525 this.unloadVideo(false);
527 // Waits for unloading video.
528 this.loadQueue_.run(function(callback) {
529 this.currentCast_ = cast || null;
530 this.updateCheckOnCastMenu_();
531 this.reloadCurrentVideo();
537 * Set the list of casts.
538 * @param {Array.<Object>} casts List of casts.
540 VideoPlayer.prototype.setCastList = function(casts) {
541 var videoPlayerElement = document.querySelector('#video-player');
542 var menu = document.querySelector('#cast-menu');
545 // TODO(yoshiki): Handle the case that the current cast disappears.
547 if (casts.length === 0) {
548 videoPlayerElement.removeAttribute('cast-available');
549 if (this.currentCast_)
550 this.onCurrentCastDisappear_();
554 if (this.currentCast_) {
555 var currentCastAvailable = casts.some(function(cast) {
556 return this.currentCast_.label === cast.label;
559 if (!currentCastAvailable)
560 this.onCurrentCastDisappear_();
563 var item = new cr.ui.MenuItem();
564 item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
565 item.setAttribute('aria-label', item.label);
567 item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
568 menu.appendChild(item);
570 for (var i = 0; i < casts.length; i++) {
571 var item = new cr.ui.MenuItem();
572 item.label = casts[i].friendlyName;
573 item.setAttribute('aria-label', item.label);
574 item.castLabel = casts[i].label;
575 item.addEventListener('activate',
576 this.onCastSelected_.wrap(this, casts[i]));
577 menu.appendChild(item);
579 this.updateCheckOnCastMenu_();
580 videoPlayerElement.setAttribute('cast-available', true);
584 * Updates the check status of the cast menu items.
587 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
588 var menu = document.querySelector('#cast-menu');
589 var menuItems = menu.menuItems;
590 for (var i = 0; i < menuItems.length; i++) {
591 var item = menuItems[i];
592 if (this.currentCast_ === null) {
593 // Playing on this computer.
594 if (item.castLabel === '')
597 item.checked = false;
599 // Playing on cast device.
600 if (item.castLabel === this.currentCast_.label)
603 item.checked = false;
609 * Called when the current cast is disappear from the cast list.
612 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
613 this.currentCast_ = null;
614 if (this.currentSession_) {
615 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
616 this.currentSession_ = null;
618 this.controls.showErrorMessage('GALLERY_VIDEO_DECODING_ERROR');
623 * This method should be called when the session is updated.
624 * @param {boolean} alive Whether the session is alive or not.
627 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
633 * Initialize the list of videos.
634 * @param {function(Array.<Object>)} callback Called with the video list when
637 function initVideos(callback) {
639 var videos = window.videos;
640 window.videos = null;
645 chrome.runtime.onMessage.addListener(
646 function(request, sender, sendResponse) {
647 var videos = window.videos;
648 window.videos = null;
653 var player = new VideoPlayer();
656 * Initializes the strings.
657 * @param {function()} callback Called when the sting data is ready.
659 function initStrings(callback) {
660 chrome.fileManagerPrivate.getStrings(function(strings) {
661 loadTimeData.data = strings;
662 i18nTemplate.process(document, loadTimeData);
667 var initPromise = Promise.all(
668 [new Promise(initVideos.wrap(null)),
669 new Promise(initStrings.wrap(null)),
670 new Promise(util.addPageLoadHandler.wrap(null))]);
672 initPromise.then(function(results) {
673 var videos = results[0];
674 player.prepare(videos);
675 return new Promise(player.playFirstVideo.wrap(player));