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
);