e1708101781aca64cbaeb89660492528c62a420e
[platform/framework/web/crosswalk.git] / src / ui / file_manager / video_player / js / video_player.js
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.
4
5 'use strict';
6
7 /**
8  * @param {Element} playerContainer Main container.
9  * @param {Element} videoContainer Container for the video element.
10  * @param {Element} controlsContainer Container for video controls.
11  * @constructor
12  */
13 function FullWindowVideoControls(
14     playerContainer, videoContainer, controlsContainer) {
15   VideoControls.call(this,
16       controlsContainer,
17       this.onPlaybackError_.wrap(this),
18       loadTimeData.getString.wrap(loadTimeData),
19       this.toggleFullScreen_.wrap(this),
20       videoContainer);
21
22   this.playerContainer_ = playerContainer;
23   this.decodeErrorOccured = false;
24
25   this.casting = false;
26
27   this.updateStyle();
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();
34         break;
35       case 'U+001B': // Escape
36         util.toggleFullScreen(
37             chrome.app.window.current(),
38             false);  // Leave the full screen mode.
39         break;
40       case 'Right':
41       case 'MediaNextTrack':
42         player.advance_(1);
43         break;
44       case 'Left':
45       case 'MediaPreviousTrack':
46         player.advance_(0);
47         break;
48       case 'MediaStop':
49         // TODO: Define "Stop" behavior.
50         break;
51     }
52   }.wrap(this));
53
54   // TODO(mtomasz): Simplify. crbug.com/254318.
55   var clickInProgress = false;
56   videoContainer.addEventListener('click', function(e) {
57     if (clickInProgress)
58       return;
59
60     clickInProgress = true;
61     var togglePlayState = function() {
62       clickInProgress = false;
63
64       if (e.ctrlKey) {
65         this.toggleLoopedModeWithFeedback(true);
66         if (!this.isPlaying())
67           this.togglePlayStateWithFeedback();
68       } else {
69         this.togglePlayStateWithFeedback();
70       }
71     }.wrap(this);
72
73     if (!this.media_)
74       player.reloadCurrentVideo(togglePlayState);
75     else
76       setTimeout(togglePlayState);
77   }.wrap(this));
78
79   this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
80   this.__defineGetter__('inactivityWatcher', function() {
81     return this.inactivityWatcher_;
82   }.wrap(this));
83
84   this.inactivityWatcher_.check();
85 }
86
87 FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
88
89 /**
90  * Displays error message.
91  *
92  * @param {string} message Message id.
93  */
94 FullWindowVideoControls.prototype.showErrorMessage = function(message) {
95   var errorBanner = document.querySelector('#error');
96   errorBanner.textContent = loadTimeData.getString(message);
97   errorBanner.setAttribute('visible', 'true');
98
99   // The window is hidden if the video has not loaded yet.
100   chrome.app.window.current().show();
101 };
102
103 /**
104  * Handles playback (decoder) errors.
105  * @param {MediaError} error Error object.
106  * @private
107  */
108 FullWindowVideoControls.prototype.onPlaybackError_ = function(error) {
109   if (error.target && error.target.error &&
110       error.target.error.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED) {
111     if (this.casting)
112       this.showErrorMessage('VIDEO_PLAYER_VIDEO_FILE_UNSUPPORTED_FOR_CAST');
113     else
114       this.showErrorMessage('GALLERY_VIDEO_ERROR');
115     this.decodeErrorOccured = false;
116   } else {
117     this.showErrorMessage('GALLERY_VIDEO_DECODING_ERROR');
118     this.decodeErrorOccured = true;
119   }
120
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');
124
125   // Detach the video element, since it may be unreliable and reset stored
126   // current playback time.
127   this.cleanup();
128   this.clearState();
129
130   // Avoid reusing a video element.
131   player.unloadVideo();
132 };
133
134 /**
135  * Toggles the full screen mode.
136  * @private
137  */
138 FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
139   var appWindow = chrome.app.window.current();
140   util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
141 };
142
143 /**
144  * Media completion handler.
145  */
146 FullWindowVideoControls.prototype.onMediaComplete = function() {
147   VideoControls.prototype.onMediaComplete.apply(this, arguments);
148   if (!this.getMedia().loop)
149     player.advance_(1);
150 };
151
152 /**
153  * @constructor
154  */
155 function VideoPlayer() {
156   this.controls_ = null;
157   this.videoElement_ = null;
158   this.videos_ = null;
159   this.currentPos_ = 0;
160
161   this.currentSession_ = null;
162   this.currentCast_ = null;
163
164   this.loadQueue_ = new AsyncUtil.Queue();
165
166   this.onCastSessionUpdateBound_ = this.onCastSessionUpdate_.wrap(this);
167
168   Object.seal(this);
169 }
170
171 VideoPlayer.prototype = {
172   get controls() {
173     return this.controls_;
174   }
175 };
176
177 /**
178  * Initializes the video player window. This method must be called after DOM
179  * initialization.
180  * @param {Array.<Object.<string, Object>>} videos List of videos.
181  */
182 VideoPlayer.prototype.prepare = function(videos) {
183   this.videos_ = videos;
184
185   var preventDefault = function(event) { event.preventDefault(); }.wrap(null);
186
187   document.ondragstart = preventDefault;
188
189   var maximizeButton = document.querySelector('.maximize-button');
190   maximizeButton.addEventListener(
191       'click',
192       function(event) {
193         var appWindow = chrome.app.window.current();
194         if (appWindow.isMaximized())
195           appWindow.restore();
196         else
197           appWindow.maximize();
198         event.stopPropagation();
199       }.wrap(null));
200   maximizeButton.addEventListener('mousedown', preventDefault);
201
202   var minimizeButton = document.querySelector('.minimize-button');
203   minimizeButton.addEventListener(
204       'click',
205       function(event) {
206         chrome.app.window.current().minimize();
207         event.stopPropagation();
208       }.wrap(null));
209   minimizeButton.addEventListener('mousedown', preventDefault);
210
211   var closeButton = document.querySelector('.close-button');
212   closeButton.addEventListener(
213       'click',
214       function(event) {
215         close();
216         event.stopPropagation();
217       }.wrap(null));
218   closeButton.addEventListener('mousedown', preventDefault);
219
220   var menu = document.querySelector('#cast-menu');
221   cr.ui.decorate(menu, cr.ui.Menu);
222
223   this.controls_ = new FullWindowVideoControls(
224       document.querySelector('#video-player'),
225       document.querySelector('#video-container'),
226       document.querySelector('#controls'));
227
228   var reloadVideo = function(e) {
229     if (this.controls_.decodeErrorOccured &&
230         // Ignore shortcut keys
231         !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
232       this.reloadCurrentVideo(function() {
233         this.videoElement_.play();
234       }.wrap(this));
235       e.preventDefault();
236     }
237   }.wrap(this);
238
239   var arrowRight = document.querySelector('.arrow-box .arrow.right');
240   arrowRight.addEventListener('click', this.advance_.wrap(this, 1));
241   var arrowLeft = document.querySelector('.arrow-box .arrow.left');
242   arrowLeft.addEventListener('click', this.advance_.wrap(this, 0));
243
244   var videoPlayerElement = document.querySelector('#video-player');
245   if (videos.length > 1)
246     videoPlayerElement.setAttribute('multiple', true);
247   else
248     videoPlayerElement.removeAttribute('multiple');
249
250   document.addEventListener('keydown', reloadVideo);
251   document.addEventListener('click', reloadVideo);
252 };
253
254 /**
255  * Unloads the player.
256  */
257 function unload() {
258   // Releases keep awake just in case (should be released on unloading video).
259   chrome.power.releaseKeepAwake();
260
261   if (!player.controls || !player.controls.getMedia())
262     return;
263
264   player.controls.savePosition(true /* exiting */);
265   player.controls.cleanup();
266 }
267
268 /**
269  * Loads the video file.
270  * @param {Object} video Data of the video file.
271  * @param {function()=} opt_callback Completion callback.
272  * @private
273  */
274 VideoPlayer.prototype.loadVideo_ = function(video, opt_callback) {
275   this.unloadVideo(true);
276
277   this.loadQueue_.run(function(callback) {
278     document.title = video.title;
279
280     document.querySelector('#title').innerText = video.title;
281
282     var videoPlayerElement = document.querySelector('#video-player');
283     if (this.currentPos_ === (this.videos_.length - 1))
284       videoPlayerElement.setAttribute('last-video', true);
285     else
286       videoPlayerElement.removeAttribute('last-video');
287
288     if (this.currentPos_ === 0)
289       videoPlayerElement.setAttribute('first-video', true);
290     else
291       videoPlayerElement.removeAttribute('first-video');
292
293     // Re-enables ui and hides error message if already displayed.
294     document.querySelector('#video-player').removeAttribute('disabled');
295     document.querySelector('#error').removeAttribute('visible');
296     this.controls.detachMedia();
297     this.controls.inactivityWatcher.disabled = true;
298     this.controls.decodeErrorOccured = false;
299     this.controls.casting = !!this.currentCast_;
300
301     videoPlayerElement.setAttribute('loading', true);
302
303     var media = new MediaManager(video.entry);
304
305     Promise.all([media.getThumbnail(), media.getToken()])
306         .then(function(results) {
307           var url = results[0];
308           var token = results[1];
309           if (url && token) {
310             document.querySelector('#thumbnail').style.backgroundImage =
311                 'url(' + url + '&access_token=' + token + ')';
312           } else {
313             document.querySelector('#thumbnail').style.backgroundImage = '';
314           }
315         })
316         .catch(function() {
317           // Shows no image on error.
318           document.querySelector('#thumbnail').style.backgroundImage = '';
319         });
320
321     var videoElementInitializePromise;
322     if (this.currentCast_) {
323       videoPlayerElement.setAttribute('casting', true);
324
325       document.querySelector('#cast-name').textContent =
326           this.currentCast_.friendlyName;
327
328       videoPlayerElement.setAttribute('castable', true);
329
330       videoElementInitializePromise = media.isAvailableForCast()
331           .then(function(result) {
332             if (!result)
333               return Promise.reject('No casts are available.');
334
335             return new Promise(function(fulfill, reject) {
336               chrome.cast.requestSession(
337                   fulfill, reject, undefined, this.currentCast_.label);
338             }.bind(this)).then(function(session) {
339               session.addUpdateListener(this.onCastSessionUpdateBound_);
340
341               this.currentSession_ = session;
342               this.videoElement_ = new CastVideoElement(media, session);
343               this.controls.attachMedia(this.videoElement_);
344             }.bind(this));
345           }.bind(this));
346     } else {
347       videoPlayerElement.removeAttribute('casting');
348
349       this.videoElement_ = document.createElement('video');
350       document.querySelector('#video-container').appendChild(
351           this.videoElement_);
352
353       this.controls.attachMedia(this.videoElement_);
354       this.videoElement_.src = video.url;
355
356       media.isAvailableForCast().then(function(result) {
357         if (result)
358           videoPlayerElement.setAttribute('castable', true);
359         else
360           videoPlayerElement.removeAttribute('castable');
361       }).catch(function() {
362         videoPlayerElement.setAttribute('castable', true);
363       });
364
365       videoElementInitializePromise = Promise.resolve();
366     }
367
368     videoElementInitializePromise
369         .then(function() {
370           var handler = function(currentPos) {
371             if (currentPos === this.currentPos_) {
372               if (opt_callback)
373                 opt_callback();
374               videoPlayerElement.removeAttribute('loading');
375               this.controls.inactivityWatcher.disabled = false;
376             }
377
378             this.videoElement_.removeEventListener('loadedmetadata', handler);
379           }.wrap(this, this.currentPos_);
380
381           this.videoElement_.addEventListener('loadedmetadata', handler);
382
383           this.videoElement_.addEventListener('play', function() {
384             chrome.power.requestKeepAwake('display');
385           }.wrap());
386           this.videoElement_.addEventListener('pause', function() {
387             chrome.power.releaseKeepAwake();
388           }.wrap());
389
390           this.videoElement_.load();
391           callback();
392         }.bind(this))
393         // In case of error.
394         .catch(function(error) {
395           videoPlayerElement.removeAttribute('loading');
396           console.error('Failed to initialize the video element.',
397                         error.stack || error);
398           this.controls_.showErrorMessage('GALLERY_VIDEO_ERROR');
399           callback();
400         }.bind(this));
401   }.wrap(this));
402 };
403
404 /**
405  * Plays the first video.
406  */
407 VideoPlayer.prototype.playFirstVideo = function() {
408   this.currentPos_ = 0;
409   this.reloadCurrentVideo(this.onFirstVideoReady_.wrap(this));
410 };
411
412 /**
413  * Unloads the current video.
414  * @param {boolean=} opt_keepSession If true, keep using the current session.
415  *     Otherwise, discards the session.
416  */
417 VideoPlayer.prototype.unloadVideo = function(opt_keepSession) {
418   this.loadQueue_.run(function(callback) {
419     chrome.power.releaseKeepAwake();
420
421     if (this.videoElement_) {
422       // If the element has dispose method, call it (CastVideoElement has it).
423       if (this.videoElement_.dispose)
424         this.videoElement_.dispose();
425       // Detach the previous video element, if exists.
426       if (this.videoElement_.parentNode)
427         this.videoElement_.parentNode.removeChild(this.videoElement_);
428     }
429     this.videoElement_ = null;
430
431     if (!opt_keepSession && this.currentSession_) {
432       this.currentSession_.stop(callback, callback);
433       this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
434       this.currentSession_ = null;
435     } else {
436       callback();
437     }
438   }.wrap(this));
439 };
440
441 /**
442  * Called when the first video is ready after starting to load.
443  * @private
444  */
445 VideoPlayer.prototype.onFirstVideoReady_ = function() {
446   var videoWidth = this.videoElement_.videoWidth;
447   var videoHeight = this.videoElement_.videoHeight;
448
449   var aspect = videoWidth / videoHeight;
450   var newWidth = videoWidth;
451   var newHeight = videoHeight;
452
453   var shrinkX = newWidth / window.screen.availWidth;
454   var shrinkY = newHeight / window.screen.availHeight;
455   if (shrinkX > 1 || shrinkY > 1) {
456     if (shrinkY > shrinkX) {
457       newHeight = newHeight / shrinkY;
458       newWidth = newHeight * aspect;
459     } else {
460       newWidth = newWidth / shrinkX;
461       newHeight = newWidth / aspect;
462     }
463   }
464
465   var oldLeft = window.screenX;
466   var oldTop = window.screenY;
467   var oldWidth = window.outerWidth;
468   var oldHeight = window.outerHeight;
469
470   if (!oldWidth && !oldHeight) {
471     oldLeft = window.screen.availWidth / 2;
472     oldTop = window.screen.availHeight / 2;
473   }
474
475   var appWindow = chrome.app.window.current();
476   appWindow.resizeTo(newWidth, newHeight);
477   appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
478                    oldTop - (newHeight - oldHeight) / 2);
479   appWindow.show();
480
481   this.videoElement_.play();
482 };
483
484 /**
485  * Advances to the next (or previous) track.
486  *
487  * @param {boolean} direction True to the next, false to the previous.
488  * @private
489  */
490 VideoPlayer.prototype.advance_ = function(direction) {
491   var newPos = this.currentPos_ + (direction ? 1 : -1);
492   if (0 <= newPos && newPos < this.videos_.length) {
493     this.currentPos_ = newPos;
494     this.reloadCurrentVideo(function() {
495       this.videoElement_.play();
496     }.wrap(this));
497   }
498 };
499
500 /**
501  * Reloads the current video.
502  *
503  * @param {function()=} opt_callback Completion callback.
504  */
505 VideoPlayer.prototype.reloadCurrentVideo = function(opt_callback) {
506   var currentVideo = this.videos_[this.currentPos_];
507   this.loadVideo_(currentVideo, opt_callback);
508 };
509
510 /**
511  * Invokes when a menuitem in the cast menu is selected.
512  * @param {Object} cast Selected element in the list of casts.
513  * @private
514  */
515 VideoPlayer.prototype.onCastSelected_ = function(cast) {
516   // If the selected item is same as the current item, do nothing.
517   if ((this.currentCast_ && this.currentCast_.label) === (cast && cast.label))
518     return;
519
520   this.unloadVideo(false);
521
522   // Waits for unloading video.
523   this.loadQueue_.run(function(callback) {
524     this.currentCast_ = cast || null;
525     this.updateCheckOnCastMenu_();
526     this.reloadCurrentVideo();
527     callback();
528   }.wrap(this));
529 };
530
531 /**
532  * Set the list of casts.
533  * @param {Array.<Object>} casts List of casts.
534  */
535 VideoPlayer.prototype.setCastList = function(casts) {
536   var videoPlayerElement = document.querySelector('#video-player');
537   var menu = document.querySelector('#cast-menu');
538   menu.innerHTML = '';
539
540   // TODO(yoshiki): Handle the case that the current cast disappears.
541
542   if (casts.length === 0) {
543     videoPlayerElement.removeAttribute('cast-available');
544     if (this.currentCast_)
545       this.onCurrentCastDisappear_();
546     return;
547   }
548
549   if (this.currentCast_) {
550     var currentCastAvailable = casts.some(function(cast) {
551       return this.currentCast_.label === cast.label;
552     }.wrap(this));
553
554     if (!currentCastAvailable)
555       this.onCurrentCastDisappear_();
556   }
557
558   var item = new cr.ui.MenuItem();
559   item.label = loadTimeData.getString('VIDEO_PLAYER_PLAY_THIS_COMPUTER');
560   item.setAttribute('aria-label', item.label);
561   item.castLabel = '';
562   item.addEventListener('activate', this.onCastSelected_.wrap(this, null));
563   menu.appendChild(item);
564
565   for (var i = 0; i < casts.length; i++) {
566     var item = new cr.ui.MenuItem();
567     item.label = casts[i].friendlyName;
568     item.setAttribute('aria-label', item.label);
569     item.castLabel = casts[i].label;
570     item.addEventListener('activate',
571                           this.onCastSelected_.wrap(this, casts[i]));
572     menu.appendChild(item);
573   }
574   this.updateCheckOnCastMenu_();
575   videoPlayerElement.setAttribute('cast-available', true);
576 };
577
578 /**
579  * Updates the check status of the cast menu items.
580  * @private
581  */
582 VideoPlayer.prototype.updateCheckOnCastMenu_ = function() {
583   var menu = document.querySelector('#cast-menu');
584   var menuItems = menu.menuItems;
585   for (var i = 0; i < menuItems.length; i++) {
586     var item = menuItems[i];
587     if (this.currentCast_ === null) {
588       // Playing on this computer.
589       if (item.castLabel === '')
590         item.checked = true;
591       else
592         item.checked = false;
593     } else {
594       // Playing on cast device.
595       if (item.castLabel === this.currentCast_.label)
596         item.checked = true;
597       else
598         item.checked = false;
599     }
600   }
601 };
602
603 /**
604  * Called when the current cast is disappear from the cast list.
605  * @private
606  */
607 VideoPlayer.prototype.onCurrentCastDisappear_ = function() {
608   this.currentCast_ = null;
609   if (this.currentSession_) {
610     this.currentSession_.removeUpdateListener(this.onCastSessionUpdateBound_);
611     this.currentSession_ = null;
612   }
613   this.controls.showErrorMessage('GALLERY_VIDEO_DECODING_ERROR');
614   this.unloadVideo();
615 };
616
617 /**
618  * This method should be called when the session is updated.
619  * @param {boolean} alive Whether the session is alive or not.
620  * @private
621  */
622 VideoPlayer.prototype.onCastSessionUpdate_ = function(alive) {
623   if (!alive)
624     this.unloadVideo();
625 };
626
627 /**
628  * Initialize the list of videos.
629  * @param {function(Array.<Object>)} callback Called with the video list when
630  *     it is ready.
631  */
632 function initVideos(callback) {
633   if (window.videos) {
634     var videos = window.videos;
635     window.videos = null;
636     callback(videos);
637     return;
638   }
639
640   chrome.runtime.onMessage.addListener(
641       function(request, sender, sendResponse) {
642         var videos = window.videos;
643         window.videos = null;
644         callback(videos);
645       }.wrap(null));
646 }
647
648 var player = new VideoPlayer();
649
650 /**
651  * Initializes the strings.
652  * @param {function()} callback Called when the sting data is ready.
653  */
654 function initStrings(callback) {
655   chrome.fileManagerPrivate.getStrings(function(strings) {
656     loadTimeData.data = strings;
657     i18nTemplate.process(document, loadTimeData);
658     callback();
659   }.wrap(null));
660 }
661
662 var initPromise = Promise.all(
663     [new Promise(initVideos.wrap(null)),
664      new Promise(initStrings.wrap(null)),
665      new Promise(util.addPageLoadHandler.wrap(null))]);
666
667 initPromise.then(function(results) {
668   var videos = results[0];
669   player.prepare(videos);
670   return new Promise(player.playFirstVideo.wrap(player));
671 }.wrap(null));