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 (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, 0);
80 * @type {MouseInactivityWatcher}
83 this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
84 this.inactivityWatcher_.check();
87 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
90 * Gets inactivity watcher.
91 * @return {MouseInactivityWatcher} An inactivity watcher.
93 FullWindowVideoControls.prototype.getInactivityWatcher = function() {
94 return this.inactivityWatcher_;
98 * Displays error message.
100 * @param {string} message Message id.
102 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
103 var errorBanner = queryRequiredElement(document, '#error');
104 errorBanner.textContent = loadTimeData.getString(message);
105 errorBanner.setAttribute('visible', 'true');
107 // The window is hidden if the video has not loaded yet.
108 chrome.app.window.current().show();
112 * Handles playback (decoder) errors.
113 * @param {MediaError} error Error object.
116 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
117 if (error.target && error.target.error &&
118 error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
120 this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
122 this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
123 this.decodeErrorOccured = false;
125 this.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
126 this.decodeErrorOccured = true;
129 // Disable inactivity watcher, and disable the ui, by hiding tools manually.
130 this.getInactivityWatcher().disabled = true;
131 queryRequiredElement(document, '#video-player')
132 .setAttribute('disabled', 'true');
134 // Detach the video element, since it may be unreliable and reset stored
135 // current playback time.
139 // Avoid reusing a video element.
140 player.unloadVideo();
144 * Toggles the full screen mode.
147 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
148 var appWindow = chrome.app.window.current();
149 util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
153 * Media completion handler.
155 FullWindowVideoControls.prototype.onMediaComplete = function() {
156 VideoControls.prototype.onMediaComplete.apply(this, arguments);
157 if (!this.getMedia().loop)
167 function VideoPlayer() {
168 this.controls_ = null;
169 this.videoElement_ = null;
172 * @type {Array.<!FileEntry>}
177 this.currentPos_ = 0;
179 this.currentSession_ = null;
180 this.currentCast_ = null;
182 this.loadQueue_ = new AsyncUtil.Queue();
184 this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
187 VideoPlayer.prototype = /** @struct */ {
189 * @return {FullWindowVideoControls}
192 return this.controls_;
197 * Initializes the video player window. This method must be called after DOM
199 * @param {!Array.<!FileEntry>} videos List of videos.
201 VideoPlayer.prototype.prepare = function(videos) {
202 this.videos_ = videos;
204 var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
206 document.ondragstart = preventDefault;
208 var maximizeButton = queryRequiredElement(document, '.maximize-button');
209 maximizeButton.addEventListener(
212 var appWindow = chrome.app.window.current();
213 if (appWindow.isMaximized())
216 appWindow.maximize();
217 event.stopPropagation();
219 maximizeButton.addEventListener('mousedown', preventDefault);
221 var minimizeButton = queryRequiredElement(document, '.minimize-button');
222 minimizeButton.addEventListener(
225 chrome.app.window.current().minimize();
226 event.stopPropagation();
228 minimizeButton.addEventListener('mousedown', preventDefault);
230 var closeButton = queryRequiredElement(document, '.close-button');
231 closeButton.addEventListener(
235 event.stopPropagation();
237 closeButton.addEventListener('mousedown', preventDefault);
239 var menu = queryRequiredElement(document, '#cast-menu');
240 cr.ui.decorate(menu, cr.ui.Menu);
242 this.controls_ = new FullWindowVideoControls(
243 queryRequiredElement(document, '#video-player'),
244 queryRequiredElement(document, '#video-container'),
245 queryRequiredElement(document, '#controls'));
247 var reloadVideo = function(e) {
248 if (this.controls_.decodeErrorOccured &&
249 // Ignore shortcut keys
250 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
251 this.reloadCurrentVideo(function() {
252 this.videoElement_.play();
258 var arrowRight = queryRequiredElement(document, '.arrow-box .arrow.right');
259 arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
260 var arrowLeft = queryRequiredElement(document, '.arrow-box .arrow.left');
261 arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
263 var videoPlayerElement = queryRequiredElement(document, '#video-player');
264 if (videos.length > 1)
265 videoPlayerElement.setAttribute('multiple', true);
267 videoPlayerElement.removeAttribute('multiple');
269 document.addEventListener('keydown', reloadVideo);
270 document.addEventListener('click', reloadVideo);
274 * Unloads the player.
277 // Releases keep awake just in case (should be released on unloading video).
278 chrome.power.releaseKeepAwake();
280 if (!player.controls || !player.controls.getMedia())
283 player.controls.savePosition(true /* exiting */);
284 player.controls.cleanup();
288 * Loads the video file.
289 * @param {!FileEntry} video Entry of the video to be played.
290 * @param {function()=} opt_callback Completion callback.
293 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
294 this.unloadVideo(true);
296 this.loadQueue_.run(function(callback) {
297 document.title = video.name;
299 queryRequiredElement(document, '#title').innerText = video.name;
301 var videoPlayerElement = queryRequiredElement(document, '#video-player');
302 if (this.currentPos_ === (this.videos_.length - 1))
303 videoPlayerElement.setAttribute('last-video', true);
305 videoPlayerElement.removeAttribute('last-video');
307 if (this.currentPos_ === 0)
308 videoPlayerElement.setAttribute('first-video', true);
310 videoPlayerElement.removeAttribute('first-video');
312 // Re-enables ui and hides error message if already displayed.
313 queryRequiredElement(document, '#video-player').removeAttribute('disabled');
314 queryRequiredElement(document, '#error').removeAttribute('visible');
315 this.controls.detachMedia();
316 this.controls.getInactivityWatcher().disabled = true;
317 this.controls.decodeErrorOccured = false;
318 this.controls.casting = !!this.currentCast_;
320 videoPlayerElement.setAttribute('loading', true);
322 var media = new MediaManager(video);
324 Promise.all([media.getThumbnail(), media.getToken(false)])
325 .then(function(results) {
326 var url = results[0];
327 var token = results[1];
329 queryRequiredElement(document, '#thumbnail').style.backgroundImage =
330 'url(' + url + '&access_token=' + token + ')';
332 queryRequiredElement(document, '#thumbnail').style.backgroundImage =
337 // Shows no image on error.
338 queryRequiredElement(document, '#thumbnail').style.backgroundImage =
342 var videoElementInitializePromise;
343 if (this.currentCast_) {
344 metrics.recordPlayType(metrics.PLAY_TYPE.CAST);
346 videoPlayerElement.setAttribute('casting', true);
348 queryRequiredElement(document, '#cast-name').textContent =
349 this.currentCast_.friendlyName;
351 videoPlayerElement.setAttribute('castable', true);
353 videoElementInitializePromise = media.isAvailableForCast()
354 .then(function(result) {
356 return Promise.reject('No casts are available.');
358 return new Promise(function(fulfill, reject) {
359 chrome.cast.requestSession(
360 fulfill, reject, undefined, this.currentCast_.label);
361 }.bind(this)).then(function(session) {
362 session.addUpdateListener(this.onCastSessionUpdateBound_);
364 this.currentSession_ = session;
365 this.videoElement_ = new CastVideoElement(media, session);
366 this.controls.attachMedia(this.videoElement_);
370 metrics.recordPlayType(metrics.PLAY_TYPE.LOCAL);
371 videoPlayerElement.removeAttribute('casting');
373 this.videoElement_ = document.createElement('video');
374 queryRequiredElement(document, '#video-container').appendChild(
377 this.controls.attachMedia(this.videoElement_);
378 this.videoElement_.src = video.toURL();
380 media.isAvailableForCast().then(function(result) {
382 videoPlayerElement.setAttribute('castable', true);
384 videoPlayerElement.removeAttribute('castable');
385 }).catch(function() {
386 videoPlayerElement.setAttribute('castable', true);
389 videoElementInitializePromise = Promise.resolve();
392 videoElementInitializePromise
394 var handler = function(currentPos) {
395 if (currentPos === this.currentPos_) {
398 videoPlayerElement.removeAttribute('loading');
399 this.controls.getInactivityWatcher().disabled = false;
402 this.videoElement_.removeEventListener('loadedmetadata', handler);
403 }.wrap(this, this.currentPos_);
405 this.videoElement_.addEventListener('loadedmetadata', handler);
407 this.videoElement_.addEventListener('play', function() {
408 chrome.power.requestKeepAwake('display');
410 this.videoElement_.addEventListener('pause', function() {
411 chrome.power.releaseKeepAwake();
414 this.videoElement_.load();
418 .catch(function(error) {
419 if (this.currentCast_)
420 metrics.recordCastVideoErrorAction();
422 videoPlayerElement.removeAttribute('loading');
423 console.error('Failed to initialize the video element.',
424 error.stack || error);
425 this.controls_.showErrorMessage(
426 'VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED');
433 * Plays the first video.
435 VideoPlayer.prototype.playFirstVideo = function() {
436 this.currentPos_ = 0;
437 this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
441 * Unloads the current video.
442 * @param {boolean=} opt_keepSession If true, keep using the current session.
443 * Otherwise, discards the session.
445 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
446 this.loadQueue_.run(function(callback) {
447 chrome.power.releaseKeepAwake();
449 // Detaches the media from the control.
450 this.controls.detachMedia();
452 if (this.videoElement_) {
453 // If the element has dispose method, call it (CastVideoElement has it).
454 if (this.videoElement_.dispose)
455 this.videoElement_.dispose();
456 // Detach the previous video element, if exists.
457 if (this.videoElement_.parentNode)
458 this.videoElement_.parentNode.removeChild(this.videoElement_);
460 this.videoElement_ = null;
462 if (!opt_keepSession && this.currentSession_) {
463 this.currentSession_.stop(callback, callback);
464 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
465 this.currentSession_ = null;
473 * Called when the first video is ready after starting to load.
476 VideoPlayer.prototype.onFirstVideoReady_ = function() {
477 var videoWidth = this.videoElement_.videoWidth;
478 var videoHeight = this.videoElement_.videoHeight;
480 var aspect = videoWidth / videoHeight;
481 var newWidth = videoWidth;
482 var newHeight = videoHeight;
484 var shrinkX = newWidth / window.screen.availWidth;
485 var shrinkY = newHeight / window.screen.availHeight;
486 if (shrinkX > 1 || shrinkY > 1) {
487 if (shrinkY > shrinkX) {
488 newHeight = newHeight / shrinkY;
489 newWidth = newHeight * aspect;
491 newWidth = newWidth / shrinkX;
492 newHeight = newWidth / aspect;
496 var oldLeft = window.screenX;
497 var oldTop = window.screenY;
498 var oldWidth = window.outerWidth;
499 var oldHeight = window.outerHeight;
501 if (!oldWidth && !oldHeight) {
502 oldLeft = window.screen.availWidth / 2;
503 oldTop = window.screen.availHeight / 2;
506 var appWindow = chrome.app.window.current();
507 appWindow.resizeTo(newWidth, newHeight);
508 appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
509 oldTop - (newHeight - oldHeight) / 2);
512 this.videoElement_.play();
516 * Advances to the next (or previous) track.
518 * @param {boolean} direction True to the next, false to the previous.
521 VideoPlayer.prototype.advance_ = function(direction) {
522 var newPos = this.currentPos_ + (direction ? 1 : -1);
523 if (0 <= newPos && newPos < this.videos_.length) {
524 this.currentPos_ = newPos;
525 this.reloadCurrentVideo(function() {
526 this.videoElement_.play();
532 * Reloads the current video.
534 * @param {function()=} opt_callback Completion callback.
536 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
537 var currentVideo = this.videos_[this.currentPos_];
538 this.loadVideo_(currentVideo, opt_callback);
542 * Invokes when a menuitem in the cast menu is selected.
543 * @param {Object} cast Selected element in the list of casts.
546 VideoPlayer.prototype.onCastSelected_ = function(cast) {
547 // If the selected item is same as the current item, do nothing.
548 if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
551 this.unloadVideo(false);
553 // Waits for unloading video.
554 this.loadQueue_.run(function(callback) {
555 this.currentCast_ = cast || null;
556 this.updateCheckOnCastMenu_();
557 this.reloadCurrentVideo();
563 * Set the list of casts.
564 * @param {Array.<Object>} casts List of casts.
566 VideoPlayer.prototype.setCastList = function(casts) {
567 var videoPlayerElement = queryRequiredElement(document, '#video-player');
568 var menu = queryRequiredElement(document, '#cast-menu');
571 // TODO(yoshiki): Handle the case that the current cast disappears.
573 if (casts.length === 0) {
574 videoPlayerElement.removeAttribute('cast-available');
575 if (this.currentCast_)
576 this.onCurrentCastDisappear_();
580 if (this.currentCast_) {
581 var currentCastAvailable = casts.some(function(cast) {
582 return this.currentCast_.label === cast.label;
585 if (!currentCastAvailable)
586 this.onCurrentCastDisappear_();
589 var item = new cr.ui.MenuItem();
590 item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
591 item.setAttribute('aria-label', item.label);
593 item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
594 menu.appendChild(item);
596 for (var i = 0; i < casts.length; i++) {
597 var item = new cr.ui.MenuItem();
598 item.label = casts[i].friendlyName;
599 item.setAttribute('aria-label', item.label);
600 item.castLabel = casts[i].label;
601 item.addEventListener('activate',
602 this.onCastSelected_.wrap(this, casts[i]));
603 menu.appendChild(item);
605 this.updateCheckOnCastMenu_();
606 videoPlayerElement.setAttribute('cast-available', true);
610 * Updates the check status of the cast menu items.
613 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
614 var menu = queryRequiredElement(document, '#cast-menu');
615 var menuItems = menu.menuItems;
616 for (var i = 0; i < menuItems.length; i++) {
617 var item = menuItems[i];
618 if (this.currentCast_ === null) {
619 // Playing on this computer.
620 if (item.castLabel === '')
623 item.checked = false;
625 // Playing on cast device.
626 if (item.castLabel === this.currentCast_.label)
629 item.checked = false;
635 * Called when the current cast is disappear from the cast list.
638 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
639 this.currentCast_ = null;
640 if (this.currentSession_) {
641 this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
642 this.currentSession_ = null;
644 this.controls.showErrorMessage('VIDEO_PLAYER_PLAYBACK_ERROR');
649 * This method should be called when the session is updated.
650 * @param {boolean} alive Whether the session is alive or not.
653 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
658 var player = new VideoPlayer();
661 * Initializes the strings.
662 * @param {function()} callback Called when the sting data is ready.
664 function initStrings(callback) {
665 chrome.fileManagerPrivate.getStrings(function(strings) {
666 loadTimeData.data = strings;
667 i18nTemplate.process(document, loadTimeData);
672 function initVolumeManager(callback) {
673 var volumeManager = new VolumeManagerWrapper(
674 VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
675 volumeManager.ensureInitialized(callback);
678 var initPromise = Promise.all(
679 [new Promise(initStrings.wrap(null)),
680 new Promise(initVolumeManager.wrap(null)),
681 new Promise(util.addPageLoadHandler.wrap(null))]);
683 initPromise.then(function(unused) {
684 return new Promise(function(fulfill, reject) {
685 util.URLsToEntries(window.appState.items, function(entries) {
686 metrics.recordOpenVideoPlayerAction();
687 metrics.recordNumberOfOpenedFiles(entries.length);
689 player.prepare(entries);
690 player.playFirstVideo(player, fulfill);