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.
6 * @param {Element} playerContainer Main container.
7 * @param {Element} videoContainer Container for the video element.
8 * @param {Element} controlsContainer Container for video controls.
11 function FullWindowVideoControls(
12 playerContainer, videoContainer, controlsContainer) {
13 VideoControls.call(this,
15 this.onPlaybackError_.wrap(this),
16 loadTimeData.getString.wrap(loadTimeData),
17 this.toggleFullScreen_.wrap(this),
20 this.playerContainer_ = playerContainer;
21 this.decodeErrorOccured = false;
26 window.addEventListener('resize', this.updateStyle.wrap(this));
27 document.addEventListener('keydown', function(e) {
28 switch (e.keyIdentifier) {
29 case 'U+0020': // Space
30 case 'MediaPlayPause':
31 this.togglePlayStateWithFeedback();
33 case 'U+001B': // Escape
34 util.toggleFullScreen(
35 chrome.app.window.current(),
36 false); // Leave the full screen mode.
39 case 'MediaNextTrack':
43 case 'MediaPreviousTrack':
47 // TODO: Define "Stop" behavior.
52 // TODO(mtomasz): Simplify. crbug.com/254318.
53 var clickInProgress = false;
54 videoContainer.addEventListener('click', function(e) {
58 clickInProgress = true;
59 var togglePlayState = function() {
60 clickInProgress = false;
63 this.toggleLoopedModeWithFeedback(true);
64 if (!this.isPlaying())
65 this.togglePlayStateWithFeedback();
67 this.togglePlayStateWithFeedback();
72 player.reloadCurrentVideo(togglePlayState);
74 setTimeout(togglePlayState);
77 this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
78 this.__defineGetter__('inactivityWatcher', function() {
79 return this.inactivityWatcher_;
82 this.inactivityWatcher_.check();
85 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
88 * Displays error message.
90 * @param {string} message Message id.
92 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
93 var errorBanner = document.querySelector('#error');
94 errorBanner.textContent = loadTimeData.getString(message);
95 errorBanner.setAttribute('visible', 'true');
97 // The window is hidden if the video has not loaded yet.
98 chrome.app.window.current().show();
102 * Handles playback (decoder) errors.
103 * @param {MediaError} error Error object.
106 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
107 if (error.target && error.target.error &&
108 error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
110 this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
112 this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
113 this.decodeErrorOccured = false;
115 this.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
116 this.decodeErrorOccured = true;
119 // Disable inactivity watcher, and disable the ui, by hiding tools manually.
120 this.inactivityWatcher.disabled = true;
121 document.querySelector('#video-player').setAttribute('disabled', 'true');
123 // Detach the video element, since it may be unreliable and reset stored
124 // current playback time.
128 // Avoid reusing a video element.
129 player.unloadVideo();
133 * Toggles the full screen mode.
136 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
137 var appWindow = chrome.app.window.current();
138 util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
142 * Media completion handler.
144 FullWindowVideoControls.prototype.onMediaComplete = function() {
145 VideoControls.prototype.onMediaComplete.apply(this, arguments);
146 if (!this.getMedia().loop)
153 function VideoPlayer() {
154 this.controls_ = null;
155 this.videoElement_ = null;
157 this.currentPos_ = 0;
159 this.currentSession_ = null;
160 this.currentCast_ = null;
162 this.loadQueue_ = new AsyncUtil.Queue();
164 this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
169 VideoPlayer.prototype = {
171 return this.controls_;
176 * Initializes the video player window. This method must be called after DOM
178 * @param {Array.<Object.<string, Object>>} videos List of videos.
180 VideoPlayer.prototype.prepare = function(videos) {
181 this.videos_ = videos;
183 var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
185 document.ondragstart = preventDefault;
187 var maximizeButton = document.querySelector('.maximize-button');
188 maximizeButton.addEventListener(
191 var appWindow = chrome.app.window.current();
192 if (appWindow.isMaximized())
195 appWindow.maximize();
196 event.stopPropagation();
198 maximizeButton.addEventListener('mousedown', preventDefault);
200 var minimizeButton = document.querySelector('.minimize-button');
201 minimizeButton.addEventListener(
204 chrome.app.window.current().minimize();
205 event.stopPropagation();
207 minimizeButton.addEventListener('mousedown', preventDefault);
209 var closeButton = document.querySelector('.close-button');
210 closeButton.addEventListener(
214 event.stopPropagation();
216 closeButton.addEventListener('mousedown', preventDefault);
218 var menu = document.querySelector('#cast-menu');
219 cr.ui.decorate(menu, cr.ui.Menu);
221 this.controls_ = new FullWindowVideoControls(
222 document.querySelector('#video-player'),
223 document.querySelector('#video-container'),
224 document.querySelector('#controls'));
226 var reloadVideo = function(e) {
227 if (this.controls_.decodeErrorOccured &&
228 // Ignore shortcut keys
229 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
230 this.reloadCurrentVideo(function() {
231 this.videoElement_.play();
237 var arrowRight = document.querySelector('.arrow-box .arrow.right');
238 arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
239 var arrowLeft = document.querySelector('.arrow-box .arrow.left');
240 arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
242 var videoPlayerElement = document.querySelector('#video-player');
243 if (videos.length > 1)
244 videoPlayerElement.setAttribute('multiple', true);
246 videoPlayerElement.removeAttribute('multiple');
248 document.addEventListener('keydown', reloadVideo);
249 document.addEventListener('click', reloadVideo);
253 * Unloads the player.
256 // Releases keep awake just in case (should be released on unloading video).
257 chrome.power.releaseKeepAwake();
259 if (!player.controls || !player.controls.getMedia())
262 player.controls.savePosition(true /* exiting */);
263 player.controls.cleanup();
267 * Loads the video file.
268 * @param {Object} video Data of the video file.
269 * @param {function()=} opt_callback Completion callback.
272 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
273 this.unloadVideo(true);
275 this.loadQueue_.run(function(callback) {
276 document.title = video.title;
278 document.querySelector('#title').innerText = video.title;
280 var videoPlayerElement = document.querySelector('#video-player');
281 if (this.currentPos_ === (this.videos_.length - 1))
282 videoPlayerElement.setAttribute('last-video', true);
284 videoPlayerElement.removeAttribute('last-video');
286 if (this.currentPos_ === 0)
287 videoPlayerElement.setAttribute('first-video', true);
289 videoPlayerElement.removeAttribute('first-video');
291 // Re-enables ui and hides error message if already displayed.
292 document.querySelector('#video-player').removeAttribute('disabled');
293 document.querySelector('#error').removeAttribute('visible');
294 this.controls.detachMedia();
295 this.controls.inactivityWatcher.disabled = true;
296 this.controls.decodeErrorOccured = false;
297 this.controls.casting = !!this.currentCast_;
299 videoPlayerElement.setAttribute('loading', true);
301 var media = new MediaManager(video.entry);
303 Promise.all([media.getThumbnail(), media.getToken()])
304 .then(function(results) {
305 var url = results[0];
306 var token = results[1];
308 document.querySelector('#thumbnail').style.backgroundImage =
309 'url(' + url + '&access_token=' + token + ')';
311 document.querySelector('#thumbnail').style.backgroundImage = '';
315 // Shows no image on error.
316 document.querySelector('#thumbnail').style.backgroundImage = '';
319 var videoElementInitializePromise;
320 if (this.currentCast_) {
321 videoPlayerElement.setAttribute('casting', true);
323 document.querySelector('#cast-name').textContent =
324 this.currentCast_.friendlyName;
326 videoPlayerElement.setAttribute('castable', true);
328 videoElementInitializePromise = media.isAvailableForCast()
329 .then(function(result) {
331 return Promise.reject('No casts are available.');
333 return new Promise(function(fulfill, reject) {
334 chrome.cast.requestSession(
335 fulfill, reject, undefined, this.currentCast_.label);
336 }.bind(this)).then(function(session) {
337 session.addUpdateListener(this.onCastSessionUpdateBound_);
339 this.currentSession_ = session;
340 this.videoElement_ = new CastVideoElement(media, session);
341 this.controls.attachMedia(this.videoElement_);
345 videoPlayerElement.removeAttribute('casting');
347 this.videoElement_ = document.createElement('video');
348 document.querySelector('#video-container').appendChild(
351 this.controls.attachMedia(this.videoElement_);
352 this.videoElement_.src = video.url;
354 media.isAvailableForCast().then(function(result) {
356 videoPlayerElement.setAttribute('castable', true);
358 videoPlayerElement.removeAttribute('castable');
359 }).catch(function() {
360 videoPlayerElement.setAttribute('castable', true);
363 videoElementInitializePromise = Promise.resolve();
366 videoElementInitializePromise
368 var handler = function(currentPos) {
369 if (currentPos === this.currentPos_) {
372 videoPlayerElement.removeAttribute('loading');
373 this.controls.inactivityWatcher.disabled = false;
376 this.videoElement_.removeEventListener('loadedmetadata', handler);
377 }.wrap(this, this.currentPos_);
379 this.videoElement_.addEventListener('loadedmetadata', handler);
381 this.videoElement_.addEventListener('play', function() {
382 chrome.power.requestKeepAwake('display');
384 this.videoElement_.addEventListener('pause', function() {
385 chrome.power.releaseKeepAwake();
388 this.videoElement_.load();
392 .catch(function(error) {
393 videoPlayerElement.removeAttribute('loading');
394 console.error('Failed to initialize the video element.',
395 error.stack || error);
396 this.controls_.showErrorMessage(
397 'VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
404 * Plays the first video.
406 VideoPlayer.prototype.playFirstVideo = function() {
407 this.currentPos_ = 0;
408 this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
412 * Unloads the current video.
413 * @param {boolean=} opt_keepSession If true, keep using the current session.
414 * Otherwise, discards the session.
416 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
417 this.loadQueue_.run(function(callback) {
418 chrome.power.releaseKeepAwake();
420 // Detaches the media from the control.
421 this.controls.detachMedia();
423 if (this.videoElement_) {
424 // If the element has dispose method, call it (CastVideoElement has it).
425 if (this.videoElement_.dispose)
426 this.videoElement_.dispose();
427 // Detach the previous video element, if exists.
428 if (this.videoElement_.parentNode)
429 this.videoElement_.parentNode.removeChild(this.videoElement_);
431 this.videoElement_ = null;
433 if (!opt_keepSession && this.currentSession_) {
434 this.currentSession_.stop(callback, callback);
435 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
436 this.currentSession_ = null;
444 * Called when the first video is ready after starting to load.
447 VideoPlayer.prototype.onFirstVideoReady_ = function() {
448 var videoWidth = this.videoElement_.videoWidth;
449 var videoHeight = this.videoElement_.videoHeight;
451 var aspect = videoWidth / videoHeight;
452 var newWidth = videoWidth;
453 var newHeight = videoHeight;
455 var shrinkX = newWidth / window.screen.availWidth;
456 var shrinkY = newHeight / window.screen.availHeight;
457 if (shrinkX > 1 || shrinkY > 1) {
458 if (shrinkY > shrinkX) {
459 newHeight = newHeight / shrinkY;
460 newWidth = newHeight * aspect;
462 newWidth = newWidth / shrinkX;
463 newHeight = newWidth / aspect;
467 var oldLeft = window.screenX;
468 var oldTop = window.screenY;
469 var oldWidth = window.outerWidth;
470 var oldHeight = window.outerHeight;
472 if (!oldWidth && !oldHeight) {
473 oldLeft = window.screen.availWidth / 2;
474 oldTop = window.screen.availHeight / 2;
477 var appWindow = chrome.app.window.current();
478 appWindow.resizeTo(newWidth, newHeight);
479 appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
480 oldTop - (newHeight - oldHeight) / 2);
483 this.videoElement_.play();
487 * Advances to the next (or previous) track.
489 * @param {boolean} direction True to the next, false to the previous.
492 VideoPlayer.prototype.advance_ = function(direction) {
493 var newPos = this.currentPos_ + (direction ? 1 : -1);
494 if (0 <= newPos && newPos < this.videos_.length) {
495 this.currentPos_ = newPos;
496 this.reloadCurrentVideo(function() {
497 this.videoElement_.play();
503 * Reloads the current video.
505 * @param {function()=} opt_callback Completion callback.
507 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
508 var currentVideo = this.videos_[this.currentPos_];
509 this.loadVideo_(currentVideo, opt_callback);
513 * Invokes when a menuitem in the cast menu is selected.
514 * @param {Object} cast Selected element in the list of casts.
517 VideoPlayer.prototype.onCastSelected_ = function(cast) {
518 // If the selected item is same as the current item, do nothing.
519 if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
522 this.unloadVideo(false);
524 // Waits for unloading video.
525 this.loadQueue_.run(function(callback) {
526 this.currentCast_ = cast || null;
527 this.updateCheckOnCastMenu_();
528 this.reloadCurrentVideo();
534 * Set the list of casts.
535 * @param {Array.<Object>} casts List of casts.
537 VideoPlayer.prototype.setCastList = function(casts) {
538 var videoPlayerElement = document.querySelector('#video-player');
539 var menu = document.querySelector('#cast-menu');
542 // TODO(yoshiki): Handle the case that the current cast disappears.
544 if (casts.length === 0) {
545 videoPlayerElement.removeAttribute('cast-available');
546 if (this.currentCast_)
547 this.onCurrentCastDisappear_();
551 if (this.currentCast_) {
552 var currentCastAvailable = casts.some(function(cast) {
553 return this.currentCast_.label === cast.label;
556 if (!currentCastAvailable)
557 this.onCurrentCastDisappear_();
560 var item = new cr.ui.MenuItem();
561 item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
562 item.setAttribute('aria-label', item.label);
564 item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
565 menu.appendChild(item);
567 for (var i = 0; i < casts.length; i++) {
568 var item = new cr.ui.MenuItem();
569 item.label = casts[i].friendlyName;
570 item.setAttribute('aria-label', item.label);
571 item.castLabel = casts[i].label;
572 item.addEventListener('activate',
573 this.onCastSelected_.wrap(this, casts[i]));
574 menu.appendChild(item);
576 this.updateCheckOnCastMenu_();
577 videoPlayerElement.setAttribute('cast-available', true);
581 * Updates the check status of the cast menu items.
584 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
585 var menu = document.querySelector('#cast-menu');
586 var menuItems = menu.menuItems;
587 for (var i = 0; i < menuItems.length; i++) {
588 var item = menuItems[i];
589 if (this.currentCast_ === null) {
590 // Playing on this computer.
591 if (item.castLabel === '')
594 item.checked = false;
596 // Playing on cast device.
597 if (item.castLabel === this.currentCast_.label)
600 item.checked = false;
606 * Called when the current cast is disappear from the cast list.
609 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
610 this.currentCast_ = null;
611 if (this.currentSession_) {
612 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
613 this.currentSession_ = null;
615 this.controls.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
620 * This method should be called when the session is updated.
621 * @param {boolean} alive Whether the session is alive or not.
624 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
630 * Initialize the list of videos.
631 * @param {function(Array.<Object>)} callback Called with the video list when
634 function initVideos(callback) {
636 var videos = window.videos;
637 window.videos = null;
642 chrome.runtime.onMessage.addListener(
643 function(request, sender, sendResponse) {
644 var videos = window.videos;
645 window.videos = null;
650 var player = new VideoPlayer();
653 * Initializes the strings.
654 * @param {function()} callback Called when the sting data is ready.
656 function initStrings(callback) {
657 chrome.fileManagerPrivate.getStrings(function(strings) {
658 loadTimeData.data = strings;
659 i18nTemplate.process(document, loadTimeData);
664 var initPromise = Promise.all(
665 [new Promise(initVideos.wrap(null)),
666 new Promise(initStrings.wrap(null)),
667 new Promise(util.addPageLoadHandler.wrap(null))]);
669 initPromise.then(function(results) {
670 var videos = results[0];
671 player.prepare(videos);
672 return new Promise(player.playFirstVideo.wrap(player));