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 {!HTMLElement} playerContainer Main container.
7 * @param {!HTMLElement} videoContainer Container for the video element.
8 * @param {!HTMLElement} controlsContainer Container for video controls.
11 * @extends {VideoControls}
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 (util.getKeyModifiers(e) + e.keyIdentifier) {
31 // Handle debug shortcut keys.
32 case 'Ctrl-Shift-U+0049': // Ctrl+Shift+I
33 chrome.fileManagerPrivate.openInspector('normal');
35 case 'Ctrl-Shift-U+004A': // Ctrl+Shift+J
36 chrome.fileManagerPrivate.openInspector('console');
38 case 'Ctrl-Shift-U+0043': // Ctrl+Shift+C
39 chrome.fileManagerPrivate.openInspector('element');
41 case 'Ctrl-Shift-U+0042': // Ctrl+Shift+B
42 chrome.fileManagerPrivate.openInspector('background');
45 case 'U+0020': // Space
46 case 'MediaPlayPause':
47 this.togglePlayStateWithFeedback();
49 case 'U+001B': // Escape
50 util.toggleFullScreen(
51 chrome.app.window.current(),
52 false); // Leave the full screen mode.
55 case 'MediaNextTrack':
59 case 'MediaPreviousTrack':
63 // TODO: Define "Stop" behavior.
68 // TODO(mtomasz): Simplify. crbug.com/254318.
69 var clickInProgress = false;
70 videoContainer.addEventListener('click', function(e) {
74 clickInProgress = true;
75 var togglePlayState = function() {
76 clickInProgress = false;
79 this.toggleLoopedModeWithFeedback(true);
80 if (!this.isPlaying())
81 this.togglePlayStateWithFeedback();
83 this.togglePlayStateWithFeedback();
88 player.reloadCurrentVideo(togglePlayState);
90 setTimeout(togglePlayState, 0);
94 * @type {MouseInactivityWatcher}
97 this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
98 this.inactivityWatcher_.check();
101 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
104 * Gets inactivity watcher.
105 * @return {MouseInactivityWatcher} An inactivity watcher.
107 FullWindowVideoControls.prototype.getInactivityWatcher = function() {
108 return this.inactivityWatcher_;
112 * Displays error message.
114 * @param {string} message Message id.
116 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
117 var errorBanner = getRequiredElement('error');
118 errorBanner.textContent = loadTimeData.getString(message);
119 errorBanner.setAttribute('visible', 'true');
121 // The window is hidden if the video has not loaded yet.
122 chrome.app.window.current().show();
126 * Handles playback (decoder) errors.
127 * @param {MediaError} error Error object.
130 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
131 if (error.target && error.target.error &&
132 error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
134 this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
136 this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
137 this.decodeErrorOccured = false;
139 this.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
140 this.decodeErrorOccured = true;
143 // Disable inactivity watcher, and disable the ui, by hiding tools manually.
144 this.getInactivityWatcher().disabled = true;
145 getRequiredElement('video-player').setAttribute('disabled', 'true');
147 // Detach the video element, since it may be unreliable and reset stored
148 // current playback time.
152 // Avoid reusing a video element.
153 player.unloadVideo();
157 * Toggles the full screen mode.
160 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
161 var appWindow = chrome.app.window.current();
162 util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
166 * Media completion handler.
168 FullWindowVideoControls.prototype.onMediaComplete = function() {
169 VideoControls.prototype.onMediaComplete.apply(this, arguments);
170 if (!this.getMedia().loop)
180 function VideoPlayer() {
181 this.controls_ = null;
182 this.videoElement_ = null;
185 * @type {Array<!FileEntry>}
190 this.currentPos_ = 0;
192 this.currentSession_ = null;
193 this.currentCast_ = null;
195 this.loadQueue_ = new AsyncUtil.Queue();
197 this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
200 VideoPlayer.prototype = /** @struct */ {
202 * @return {FullWindowVideoControls}
205 return this.controls_;
210 * Initializes the video player window. This method must be called after DOM
212 * @param {!Array<!FileEntry>} videos List of videos.
214 VideoPlayer.prototype.prepare = function(videos) {
215 this.videos_ = videos;
217 var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
219 document.ondragstart = preventDefault;
221 var maximizeButton = queryRequiredElement('.maximize-button');
222 maximizeButton.addEventListener(
225 var appWindow = chrome.app.window.current();
226 if (appWindow.isMaximized())
229 appWindow.maximize();
230 event.stopPropagation();
232 maximizeButton.addEventListener('mousedown', preventDefault);
234 var minimizeButton = queryRequiredElement('.minimize-button');
235 minimizeButton.addEventListener(
238 chrome.app.window.current().minimize();
239 event.stopPropagation();
241 minimizeButton.addEventListener('mousedown', preventDefault);
243 var closeButton = queryRequiredElement('.close-button');
244 closeButton.addEventListener(
248 event.stopPropagation();
250 closeButton.addEventListener('mousedown', preventDefault);
252 cr.ui.decorate(getRequiredElement('cast-menu'), cr.ui.Menu);
254 this.controls_ = new FullWindowVideoControls(
255 getRequiredElement('video-player'),
256 getRequiredElement('video-container'),
257 getRequiredElement('controls'));
259 var reloadVideo = function(e) {
260 if (this.controls_.decodeErrorOccured &&
261 // Ignore shortcut keys
262 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
263 this.reloadCurrentVideo(function() {
264 this.videoElement_.play();
270 var arrowRight = queryRequiredElement('.arrow-box .arrow.right');
271 arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
272 var arrowLeft = queryRequiredElement('.arrow-box .arrow.left');
273 arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
275 var videoPlayerElement = getRequiredElement('video-player');
276 if (videos.length > 1)
277 videoPlayerElement.setAttribute('multiple', true);
279 videoPlayerElement.removeAttribute('multiple');
281 document.addEventListener('keydown', reloadVideo);
282 document.addEventListener('click', reloadVideo);
286 * Unloads the player.
289 // Releases keep awake just in case (should be released on unloading video).
290 chrome.power.releaseKeepAwake();
292 if (!player.controls || !player.controls.getMedia())
295 player.controls.savePosition(true /* exiting */);
296 player.controls.cleanup();
300 * Loads the video file.
301 * @param {!FileEntry} video Entry of the video to be played.
302 * @param {function()=} opt_callback Completion callback.
305 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
306 this.unloadVideo(true);
308 this.loadQueue_.run(function(callback) {
309 document.title = video.name;
311 getRequiredElement('title').innerText = video.name;
313 var videoPlayerElement = getRequiredElement('video-player');
314 if (this.currentPos_ === (this.videos_.length - 1))
315 videoPlayerElement.setAttribute('last-video', true);
317 videoPlayerElement.removeAttribute('last-video');
319 if (this.currentPos_ === 0)
320 videoPlayerElement.setAttribute('first-video', true);
322 videoPlayerElement.removeAttribute('first-video');
324 // Re-enables ui and hides error message if already displayed.
325 getRequiredElement('video-player').removeAttribute('disabled');
326 getRequiredElement('error').removeAttribute('visible');
327 this.controls.detachMedia();
328 this.controls.getInactivityWatcher().disabled = true;
329 this.controls.decodeErrorOccured = false;
330 this.controls.casting = !!this.currentCast_;
332 videoPlayerElement.setAttribute('loading', true);
334 var media = new MediaManager(video);
336 Promise.all([media.getThumbnail(), media.getToken(false)])
337 .then(function(results) {
338 var url = results[0];
339 var token = results[1];
341 getRequiredElement('thumbnail').style.backgroundImage =
342 'url(' + url + '&access_token=' + token + ')';
344 getRequiredElement('thumbnail').style.backgroundImage = '';
348 // Shows no image on error.
349 getRequiredElement('thumbnail').style.backgroundImage = '';
352 var videoElementInitializePromise;
353 if (this.currentCast_) {
354 metrics.recordPlayType(metrics.PLAY_TYPE.CAST);
356 videoPlayerElement.setAttribute('casting', true);
358 getRequiredElement('cast-name').textContent =
359 this.currentCast_.friendlyName;
361 videoPlayerElement.setAttribute('castable', true);
363 videoElementInitializePromise = media.isAvailableForCast()
364 .then(function(result) {
366 return Promise.reject('No casts are available.');
368 return new Promise(function(fulfill, reject) {
369 chrome.cast.requestSession(
370 fulfill, reject, undefined, this.currentCast_.label);
371 }.bind(this)).then(function(session) {
372 session.addUpdateListener(this.onCastSessionUpdateBound_);
374 this.currentSession_ = session;
375 this.videoElement_ = new CastVideoElement(media, session);
376 this.controls.attachMedia(this.videoElement_);
380 metrics.recordPlayType(metrics.PLAY_TYPE.LOCAL);
381 videoPlayerElement.removeAttribute('casting');
383 this.videoElement_ = document.createElement('video');
384 getRequiredElement('video-container').appendChild(this.videoElement_);
386 this.controls.attachMedia(this.videoElement_);
387 this.videoElement_.src = video.toURL();
389 media.isAvailableForCast().then(function(result) {
391 videoPlayerElement.setAttribute('castable', true);
393 videoPlayerElement.removeAttribute('castable');
394 }).catch(function() {
395 videoPlayerElement.setAttribute('castable', true);
398 videoElementInitializePromise = Promise.resolve();
401 videoElementInitializePromise
403 var handler = function(currentPos) {
404 if (currentPos === this.currentPos_) {
407 videoPlayerElement.removeAttribute('loading');
408 this.controls.getInactivityWatcher().disabled = false;
411 this.videoElement_.removeEventListener('loadedmetadata', handler);
412 }.wrap(this, this.currentPos_);
414 this.videoElement_.addEventListener('loadedmetadata', handler);
416 this.videoElement_.addEventListener('play', function() {
417 chrome.power.requestKeepAwake('display');
419 this.videoElement_.addEventListener('pause', function() {
420 chrome.power.releaseKeepAwake();
423 this.videoElement_.load();
427 .catch(function(error) {
428 if (this.currentCast_)
429 metrics.recordCastVideoErrorAction();
431 videoPlayerElement.removeAttribute('loading');
432 console.error('Failed to initialize the video element.',
433 error.stack || error);
434 this.controls_.showErrorMessage(
435 'VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
442 * Plays the first video.
444 VideoPlayer.prototype.playFirstVideo = function() {
445 this.currentPos_ = 0;
446 this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
450 * Unloads the current video.
451 * @param {boolean=} opt_keepSession If true, keep using the current session.
452 * Otherwise, discards the session.
454 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
455 this.loadQueue_.run(function(callback) {
456 chrome.power.releaseKeepAwake();
458 // Detaches the media from the control.
459 this.controls.detachMedia();
461 if (this.videoElement_) {
462 // If the element has dispose method, call it (CastVideoElement has it).
463 if (this.videoElement_.dispose)
464 this.videoElement_.dispose();
465 // Detach the previous video element, if exists.
466 if (this.videoElement_.parentNode)
467 this.videoElement_.parentNode.removeChild(this.videoElement_);
469 this.videoElement_ = null;
471 if (!opt_keepSession && this.currentSession_) {
472 this.currentSession_.stop(callback, callback);
473 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
474 this.currentSession_ = null;
482 * Called when the first video is ready after starting to load.
485 VideoPlayer.prototype.onFirstVideoReady_ = function() {
486 var videoWidth = this.videoElement_.videoWidth;
487 var videoHeight = this.videoElement_.videoHeight;
489 var aspect = videoWidth / videoHeight;
490 var newWidth = videoWidth;
491 var newHeight = videoHeight;
493 var shrinkX = newWidth / window.screen.availWidth;
494 var shrinkY = newHeight / window.screen.availHeight;
495 if (shrinkX > 1 || shrinkY > 1) {
496 if (shrinkY > shrinkX) {
497 newHeight = newHeight / shrinkY;
498 newWidth = newHeight * aspect;
500 newWidth = newWidth / shrinkX;
501 newHeight = newWidth / aspect;
505 var oldLeft = window.screenX;
506 var oldTop = window.screenY;
507 var oldWidth = window.outerWidth;
508 var oldHeight = window.outerHeight;
510 if (!oldWidth && !oldHeight) {
511 oldLeft = window.screen.availWidth / 2;
512 oldTop = window.screen.availHeight / 2;
515 var appWindow = chrome.app.window.current();
516 appWindow.resizeTo(newWidth, newHeight);
517 appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
518 oldTop - (newHeight - oldHeight) / 2);
521 this.videoElement_.play();
525 * Advances to the next (or previous) track.
527 * @param {boolean} direction True to the next, false to the previous.
530 VideoPlayer.prototype.advance_ = function(direction) {
531 var newPos = this.currentPos_ + (direction ? 1 : -1);
532 if (0 <= newPos && newPos < this.videos_.length) {
533 this.currentPos_ = newPos;
534 this.reloadCurrentVideo(function() {
535 this.videoElement_.play();
541 * Reloads the current video.
543 * @param {function()=} opt_callback Completion callback.
545 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
546 var currentVideo = this.videos_[this.currentPos_];
547 this.loadVideo_(currentVideo, opt_callback);
551 * Invokes when a menuitem in the cast menu is selected.
552 * @param {Object} cast Selected element in the list of casts.
555 VideoPlayer.prototype.onCastSelected_ = function(cast) {
556 // If the selected item is same as the current item, do nothing.
557 if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
560 this.unloadVideo(false);
562 // Waits for unloading video.
563 this.loadQueue_.run(function(callback) {
564 this.currentCast_ = cast || null;
565 this.updateCheckOnCastMenu_();
566 this.reloadCurrentVideo();
572 * Set the list of casts.
573 * @param {Array<Object>} casts List of casts.
575 VideoPlayer.prototype.setCastList = function(casts) {
576 var videoPlayerElement = getRequiredElement('video-player');
577 var menu = getRequiredElement('cast-menu');
580 // TODO(yoshiki): Handle the case that the current cast disappears.
582 if (casts.length === 0) {
583 videoPlayerElement.removeAttribute('cast-available');
584 if (this.currentCast_)
585 this.onCurrentCastDisappear_();
589 if (this.currentCast_) {
590 var currentCastAvailable = casts.some(function(cast) {
591 return this.currentCast_.label === cast.label;
594 if (!currentCastAvailable)
595 this.onCurrentCastDisappear_();
598 var item = new cr.ui.MenuItem();
599 item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
600 item.setAttribute('aria-label', item.label);
602 item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
603 menu.appendChild(item);
605 for (var i = 0; i < casts.length; i++) {
606 var item = new cr.ui.MenuItem();
607 item.label = casts[i].friendlyName;
608 item.setAttribute('aria-label', item.label);
609 item.castLabel = casts[i].label;
610 item.addEventListener('activate',
611 this.onCastSelected_.wrap(this, casts[i]));
612 menu.appendChild(item);
614 this.updateCheckOnCastMenu_();
615 videoPlayerElement.setAttribute('cast-available', true);
619 * Updates the check status of the cast menu items.
622 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
623 var menuItems = getRequiredElement('cast-menu').menuItems;
624 for (var i = 0; i < menuItems.length; i++) {
625 var item = menuItems[i];
626 if (this.currentCast_ === null) {
627 // Playing on this computer.
628 if (item.castLabel === '')
631 item.checked = false;
633 // Playing on cast device.
634 if (item.castLabel === this.currentCast_.label)
637 item.checked = false;
643 * Called when the current cast is disappear from the cast list.
646 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
647 this.currentCast_ = null;
648 if (this.currentSession_) {
649 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
650 this.currentSession_ = null;
652 this.controls.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
657 * This method should be called when the session is updated.
658 * @param {boolean} alive Whether the session is alive or not.
661 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
666 var player = new VideoPlayer();
669 * Initializes the strings.
670 * @param {function()} callback Called when the sting data is ready.
672 function initStrings(callback) {
673 chrome.fileManagerPrivate.getStrings(function(strings) {
674 loadTimeData.data = strings;
675 i18nTemplate.process(document, loadTimeData);
680 function initVolumeManager(callback) {
681 var volumeManager = new VolumeManagerWrapper(
682 VolumeManagerWrapper.NonNativeVolumeStatus.ENABLED);
683 volumeManager.ensureInitialized(callback);
686 var initPromise = Promise.all(
687 [new Promise(initStrings.wrap(null)),
688 new Promise(initVolumeManager.wrap(null)),
689 new Promise(util.addPageLoadHandler.wrap(null))]);
691 initPromise.then(function(unused) {
692 return new Promise(function(fulfill, reject) {
693 util.URLsToEntries(window.appState.items, function(entries) {
694 metrics.recordOpenVideoPlayerAction();
695 metrics.recordNumberOfOpenedFiles(entries.length);
697 player.prepare(entries);
698 player.playFirstVideo(player, fulfill);