base: Change DCHECK_IS_ON to a macro DCHECK_IS_ON().
[chromium-blink-merge.git] / ui / file_manager / video_player / js / media_controls.js
blob830bb45ec86d10946078764f2c52cd1f7e14a01b
1 // Copyright (c) 2012 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.
5 /**
6  * @fileoverview MediaControls class implements media playback controls
7  * that exist outside of the audio/video HTML element.
8  */
10 /**
11  * @param {!HTMLElement} containerElement The container for the controls.
12  * @param {function(Event)} onMediaError Function to display an error message.
13  * @constructor
14  * @struct
15  */
16 function MediaControls(containerElement, onMediaError) {
17   this.container_ = containerElement;
18   this.document_ = this.container_.ownerDocument;
19   this.media_ = null;
21   this.onMediaPlayBound_ = this.onMediaPlay_.bind(this, true);
22   this.onMediaPauseBound_ = this.onMediaPlay_.bind(this, false);
23   this.onMediaDurationBound_ = this.onMediaDuration_.bind(this);
24   this.onMediaProgressBound_ = this.onMediaProgress_.bind(this);
25   this.onMediaError_ = onMediaError || function() {};
27   this.savedVolume_ = 1;  // 100% volume.
29   /**
30    * @type {HTMLElement}
31    * @private
32    */
33   this.playButton_ = null;
35   /**
36    * @type {MediaControls.Slider}
37    * @private
38    */
39   this.progressSlider_ = null;
41   /**
42    * @type {HTMLElement}
43    * @private
44    */
45   this.duration_ = null;
47   /**
48    * @type {MediaControls.AnimatedSlider}
49    * @private
50    */
51   this.volume_ = null;
53   /**
54    * @type {HTMLElement}
55    * @private
56    */
57   this.textBanner_ = null;
59   /**
60    * @type {HTMLElement}
61    * @private
62    */
63   this.soundButton_ = null;
65   /**
66    * @type {boolean}
67    * @private
68    */
69   this.resumeAfterDrag_ = false;
71   /**
72    * @type {HTMLElement}
73    * @private
74    */
75   this.currentTime_ = null;
78 /**
79  * Button's state types. Values are used as CSS class names.
80  * @enum {string}
81  */
82 MediaControls.ButtonStateType = {
83   DEFAULT: 'default',
84   PLAYING: 'playing',
85   ENDED: 'ended'
88 /**
89  * @return {HTMLAudioElement|HTMLVideoElement} The media element.
90  */
91 MediaControls.prototype.getMedia = function() { return this.media_ };
93 /**
94  * Format the time in hh:mm:ss format (omitting redundant leading zeros).
95  *
96  * @param {number} timeInSec Time in seconds.
97  * @return {string} Formatted time string.
98  * @private
99  */
100 MediaControls.formatTime_ = function(timeInSec) {
101   var seconds = Math.floor(timeInSec % 60);
102   var minutes = Math.floor((timeInSec / 60) % 60);
103   var hours = Math.floor(timeInSec / 60 / 60);
104   var result = '';
105   if (hours) result += hours + ':';
106   if (hours && (minutes < 10)) result += '0';
107   result += minutes + ':';
108   if (seconds < 10) result += '0';
109   result += seconds;
110   return result;
114  * Create a custom control.
116  * @param {string} className Class name.
117  * @param {HTMLElement=} opt_parent Parent element or container if undefined.
118  * @return {!HTMLElement} The new control element.
119  */
120 MediaControls.prototype.createControl = function(className, opt_parent) {
121   var parent = opt_parent || this.container_;
122   var control = assertInstanceof(this.document_.createElement('div'),
123       HTMLDivElement);
124   control.className = className;
125   parent.appendChild(control);
126   return control;
130  * Create a custom button.
132  * @param {string} className Class name.
133  * @param {function(Event)=} opt_handler Click handler.
134  * @param {HTMLElement=} opt_parent Parent element or container if undefined.
135  * @param {number=} opt_numStates Number of states, default: 1.
136  * @return {!HTMLElement} The new button element.
137  */
138 MediaControls.prototype.createButton = function(
139     className, opt_handler, opt_parent, opt_numStates) {
140   opt_numStates = opt_numStates || 1;
142   var button = this.createControl(className, opt_parent);
143   button.classList.add('media-button');
145   var stateTypes = Object.keys(MediaControls.ButtonStateType);
146   for (var state = 0; state != opt_numStates; state++) {
147     var stateClass = MediaControls.ButtonStateType[stateTypes[state]];
148     this.createControl('normal ' + stateClass, button);
149     this.createControl('hover ' + stateClass, button);
150     this.createControl('active ' + stateClass, button);
151   }
152   this.createControl('disabled', button);
154   button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT);
156   if (opt_handler)
157     button.addEventListener('click', opt_handler);
159   return button;
163  * Enable/disable controls matching a given selector.
165  * @param {string} selector CSS selector.
166  * @param {boolean} on True if enable, false if disable.
167  * @private
168  */
169 MediaControls.prototype.enableControls_ = function(selector, on) {
170   var controls = this.container_.querySelectorAll(selector);
171   for (var i = 0; i != controls.length; i++) {
172     var classList = controls[i].classList;
173     if (on)
174       classList.remove('disabled');
175     else
176       classList.add('disabled');
177   }
181  * Playback control.
182  */
185  * Play the media.
186  */
187 MediaControls.prototype.play = function() {
188   if (!this.media_)
189     return;  // Media is detached.
191   this.media_.play();
195  * Pause the media.
196  */
197 MediaControls.prototype.pause = function() {
198   if (!this.media_)
199     return;  // Media is detached.
201   this.media_.pause();
205  * @return {boolean} True if the media is currently playing.
206  */
207 MediaControls.prototype.isPlaying = function() {
208   return !!this.media_ && !this.media_.paused && !this.media_.ended;
212  * Toggle play/pause.
213  */
214 MediaControls.prototype.togglePlayState = function() {
215   if (this.isPlaying())
216     this.pause();
217   else
218     this.play();
222  * Toggles play/pause state on a mouse click on the play/pause button.
224  * @param {Event} event Mouse click event.
225  */
226 MediaControls.prototype.onPlayButtonClicked = function(event) {
227   this.togglePlayState();
231  * @param {HTMLElement=} opt_parent Parent container.
232  */
233 MediaControls.prototype.initPlayButton = function(opt_parent) {
234   this.playButton_ = this.createButton('play media-control',
235       this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */);
239  * Time controls
240  */
243  * The default range of 100 is too coarse for the media progress slider.
244  */
245 MediaControls.PROGRESS_RANGE = 5000;
248  * @param {boolean=} opt_seekMark True if the progress slider should have
249  *     a seek mark.
250  * @param {HTMLElement=} opt_parent Parent container.
251  */
252 MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) {
253   var timeControls = this.createControl('time-controls', opt_parent);
255   var sliderConstructor =
256       opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider;
258   this.progressSlider_ = new sliderConstructor(
259       this.createControl('progress media-control', timeControls),
260       0, /* value */
261       MediaControls.PROGRESS_RANGE,
262       this.onProgressChange_.bind(this),
263       this.onProgressDrag_.bind(this));
265   var timeBox = this.createControl('time media-control', timeControls);
267   this.duration_ = this.createControl('duration', timeBox);
268   // Set the initial width to the minimum to reduce the flicker.
269   this.duration_.textContent = MediaControls.formatTime_(0);
271   this.currentTime_ = this.createControl('current', timeBox);
275  * @param {number} current Current time is seconds.
276  * @param {number} duration Duration in seconds.
277  * @private
278  */
279 MediaControls.prototype.displayProgress_ = function(current, duration) {
280   var ratio = current / duration;
281   this.progressSlider_.setValue(ratio);
282   this.currentTime_.textContent = MediaControls.formatTime_(current);
286  * @param {number} value Progress [0..1].
287  * @private
288  */
289 MediaControls.prototype.onProgressChange_ = function(value) {
290   if (!this.media_)
291     return;  // Media is detached.
293   if (!this.media_.seekable || !this.media_.duration) {
294     console.error('Inconsistent media state');
295     return;
296   }
298   var current = this.media_.duration * value;
299   this.media_.currentTime = current;
300   this.currentTime_.textContent = MediaControls.formatTime_(current);
304  * @param {boolean} on True if dragging.
305  * @private
306  */
307 MediaControls.prototype.onProgressDrag_ = function(on) {
308   if (!this.media_)
309     return;  // Media is detached.
311   if (on) {
312     this.resumeAfterDrag_ = this.isPlaying();
313     this.media_.pause(true /* seeking */);
314   } else {
315     if (this.resumeAfterDrag_) {
316       if (this.media_.ended)
317         this.onMediaPlay_(false);
318       else
319         this.media_.play(true /* seeking */);
320     }
321     this.updatePlayButtonState_(this.isPlaying());
322   }
326  * Volume controls
327  */
330  * @param {HTMLElement=} opt_parent Parent element for the controls.
331  */
332 MediaControls.prototype.initVolumeControls = function(opt_parent) {
333   var volumeControls = this.createControl('volume-controls', opt_parent);
335   this.soundButton_ = this.createButton('sound media-control',
336       this.onSoundButtonClick_.bind(this), volumeControls);
337   this.soundButton_.setAttribute('level', 3);  // max level.
339   this.volume_ = new MediaControls.AnimatedSlider(
340       this.createControl('volume media-control', volumeControls),
341       1, /* value */
342       100 /* range */,
343       this.onVolumeChange_.bind(this),
344       this.onVolumeDrag_.bind(this));
348  * Click handler for the sound level button.
349  * @private
350  */
351 MediaControls.prototype.onSoundButtonClick_ = function() {
352   if (this.media_.volume == 0) {
353     this.volume_.setValue(this.savedVolume_ || 1);
354   } else {
355     this.savedVolume_ = this.media_.volume;
356     this.volume_.setValue(0);
357   }
358   this.onVolumeChange_(this.volume_.getValue());
362  * @param {number} value Volume [0..1].
363  * @return {number} The rough level [0..3] used to pick an icon.
364  * @private
365  */
366 MediaControls.getVolumeLevel_ = function(value) {
367   if (value == 0) return 0;
368   if (value <= 1 / 3) return 1;
369   if (value <= 2 / 3) return 2;
370   return 3;
374  * @param {number} value Volume [0..1].
375  * @private
376  */
377 MediaControls.prototype.onVolumeChange_ = function(value) {
378   if (!this.media_)
379     return;  // Media is detached.
381   this.media_.volume = value;
382   this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value));
386  * @param {boolean} on True if dragging is in progress.
387  * @private
388  */
389 MediaControls.prototype.onVolumeDrag_ = function(on) {
390   if (on && (this.media_.volume != 0)) {
391     this.savedVolume_ = this.media_.volume;
392   }
396  * Media event handlers.
397  */
400  * Attach a media element.
402  * @param {!HTMLMediaElement} mediaElement The media element to control.
403  */
404 MediaControls.prototype.attachMedia = function(mediaElement) {
405   this.media_ = mediaElement;
407   this.media_.addEventListener('play', this.onMediaPlayBound_);
408   this.media_.addEventListener('pause', this.onMediaPauseBound_);
409   this.media_.addEventListener('durationchange', this.onMediaDurationBound_);
410   this.media_.addEventListener('timeupdate', this.onMediaProgressBound_);
411   this.media_.addEventListener('error', this.onMediaError_);
413   // If the text banner is being displayed, hide it immediately, since it is
414   // related to the previous media.
415   this.textBanner_.removeAttribute('visible');
417   // Reflect the media state in the UI.
418   this.onMediaDuration_();
419   this.onMediaPlay_(this.isPlaying());
420   this.onMediaProgress_();
421   if (this.volume_) {
422     /* Copy the user selected volume to the new media element. */
423     this.savedVolume_ = this.media_.volume = this.volume_.getValue();
424   }
428  * Detach media event handlers.
429  */
430 MediaControls.prototype.detachMedia = function() {
431   if (!this.media_)
432     return;
434   this.media_.removeEventListener('play', this.onMediaPlayBound_);
435   this.media_.removeEventListener('pause', this.onMediaPauseBound_);
436   this.media_.removeEventListener('durationchange', this.onMediaDurationBound_);
437   this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_);
438   this.media_.removeEventListener('error', this.onMediaError_);
440   this.media_ = null;
444  * Force-empty the media pipeline. This is a workaround for crbug.com/149957.
445  * The document is not going to be GC-ed until the last Files app window closes,
446  * but we want the media pipeline to deinitialize ASAP to minimize leakage.
447  */
448 MediaControls.prototype.cleanup = function() {
449   if (!this.media_)
450     return;
452   this.media_.src = '';
453   this.media_.load();
454   this.detachMedia();
458  * 'play' and 'pause' event handler.
459  * @param {boolean} playing True if playing.
460  * @private
461  */
462 MediaControls.prototype.onMediaPlay_ = function(playing) {
463   if (this.progressSlider_.isDragging())
464     return;
466   this.updatePlayButtonState_(playing);
467   this.onPlayStateChanged();
471  * 'durationchange' event handler.
472  * @private
473  */
474 MediaControls.prototype.onMediaDuration_ = function() {
475   if (!this.media_ || !this.media_.duration) {
476     this.enableControls_('.media-control', false);
477     return;
478   }
480   this.enableControls_('.media-control', true);
482   var sliderContainer = this.progressSlider_.getContainer();
483   if (this.media_.seekable)
484     sliderContainer.classList.remove('readonly');
485   else
486     sliderContainer.classList.add('readonly');
488   var valueToString = function(value) {
489     var duration = this.media_ ? this.media_.duration : 0;
490     return MediaControls.formatTime_(this.media_.duration * value);
491   }.bind(this);
493   this.duration_.textContent = valueToString(1);
495   this.progressSlider_.setValueToStringFunction(valueToString);
497   if (this.media_.seekable)
498     this.restorePlayState();
502  * 'timeupdate' event handler.
503  * @private
504  */
505 MediaControls.prototype.onMediaProgress_ = function() {
506   if (!this.media_ || !this.media_.duration) {
507     this.displayProgress_(0, 1);
508     return;
509   }
511   var current = this.media_.currentTime;
512   var duration = this.media_.duration;
514   if (this.progressSlider_.isDragging())
515     return;
517   this.displayProgress_(current, duration);
519   if (current == duration) {
520     this.onMediaComplete();
521   }
522   this.onPlayStateChanged();
526  * Called when the media playback is complete.
527  */
528 MediaControls.prototype.onMediaComplete = function() {};
531  * Called when play/pause state is changed or on playback progress.
532  * This is the right moment to save the play state.
533  */
534 MediaControls.prototype.onPlayStateChanged = function() {};
537  * Updates the play button state.
538  * @param {boolean} playing If the video is playing.
539  * @private
540  */
541 MediaControls.prototype.updatePlayButtonState_ = function(playing) {
542   if (this.media_.ended && this.progressSlider_.isAtEnd()) {
543     this.playButton_.setAttribute('state',
544                                   MediaControls.ButtonStateType.ENDED);
545   } else if (playing) {
546     this.playButton_.setAttribute('state',
547                                   MediaControls.ButtonStateType.PLAYING);
548   } else {
549     this.playButton_.setAttribute('state',
550                                   MediaControls.ButtonStateType.DEFAULT);
551   }
555  * Restore play state. Base implementation is empty.
556  */
557 MediaControls.prototype.restorePlayState = function() {};
560  * Encode current state into the page URL or the app state.
561  */
562 MediaControls.prototype.encodeState = function() {
563   if (!this.media_ || !this.media_.duration)
564     return;
566   if (window.appState) {
567     window.appState.time = this.media_.currentTime;
568     util.saveAppState();
569   }
570   return;
574  * Decode current state from the page URL or the app state.
575  * @return {boolean} True if decode succeeded.
576  */
577 MediaControls.prototype.decodeState = function() {
578   if (!this.media_ || !window.appState || !('time' in window.appState))
579     return false;
580   // There is no page reload for apps v2, only app restart.
581   // Always restart in paused state.
582   this.media_.currentTime = window.appState.time;
583   this.pause();
584   return true;
588  * Remove current state from the page URL or the app state.
589  */
590 MediaControls.prototype.clearState = function() {
591   if (!window.appState)
592     return;
594   if ('time' in window.appState)
595     delete window.appState.time;
596   util.saveAppState();
597   return;
601  * Create a customized slider control.
603  * @param {!HTMLElement} container The containing div element.
604  * @param {number} value Initial value [0..1].
605  * @param {number} range Number of distinct slider positions to be supported.
606  * @param {function(number)} onChange Value change handler.
607  * @param {function(boolean)} onDrag Drag begin/end handler.
608  * @constructor
609  * @struct
610  */
612 MediaControls.Slider = function(container, value, range, onChange, onDrag) {
613   this.container_ = container;
614   this.onChange_ = onChange;
615   this.onDrag_ = onDrag;
617   /**
618    * @type {boolean}
619    * @private
620    */
621   this.isDragging_ = false;
623   var document = this.container_.ownerDocument;
625   this.container_.classList.add('custom-slider');
627   this.input_ = assertInstanceof(document.createElement('input'),
628       HTMLInputElement);
629   this.input_.type = 'range';
630   this.input_.min = (0).toString();
631   this.input_.max = range.toString();
632   this.input_.value = (value * range).toString();
633   this.container_.appendChild(this.input_);
635   this.input_.addEventListener(
636       'change', this.onInputChange_.bind(this));
637   this.input_.addEventListener(
638       'mousedown', this.onInputDrag_.bind(this, true));
639   this.input_.addEventListener(
640       'mouseup', this.onInputDrag_.bind(this, false));
642   this.bar_ = assertInstanceof(document.createElement('div'), HTMLDivElement);
643   this.bar_.className = 'bar';
644   this.container_.appendChild(this.bar_);
646   this.filled_ = document.createElement('div');
647   this.filled_.className = 'filled';
648   this.bar_.appendChild(this.filled_);
650   var leftCap = document.createElement('div');
651   leftCap.className = 'cap left';
652   this.bar_.appendChild(leftCap);
654   var rightCap = document.createElement('div');
655   rightCap.className = 'cap right';
656   this.bar_.appendChild(rightCap);
658   this.value_ = value;
659   this.setFilled_(value);
663  * @return {HTMLElement} The container element.
664  */
665 MediaControls.Slider.prototype.getContainer = function() {
666   return this.container_;
670  * @return {HTMLElement} The standard input element.
671  * @private
672  */
673 MediaControls.Slider.prototype.getInput_ = function() {
674   return this.input_;
678  * @return {HTMLElement} The slider bar element.
679  */
680 MediaControls.Slider.prototype.getBar = function() {
681   return this.bar_;
685  * @return {number} [0..1] The current value.
686  */
687 MediaControls.Slider.prototype.getValue = function() {
688   return this.value_;
692  * @param {number} value [0..1].
693  */
694 MediaControls.Slider.prototype.setValue = function(value) {
695   this.value_ = value;
696   this.setValueToUI_(value);
700  * Fill the given proportion the slider bar (from the left).
702  * @param {number} proportion [0..1].
703  * @private
704  */
705 MediaControls.Slider.prototype.setFilled_ = function(proportion) {
706   this.filled_.style.width = proportion * 100 + '%';
710  * Get the value from the input element.
712  * @return {number} Value [0..1].
713  * @private
714  */
715 MediaControls.Slider.prototype.getValueFromUI_ = function() {
716   return this.input_.value / this.input_.max;
720  * Update the UI with the current value.
722  * @param {number} value [0..1].
723  * @private
724  */
725 MediaControls.Slider.prototype.setValueToUI_ = function(value) {
726   this.input_.value = (value * this.input_.max).toString();
727   this.setFilled_(value);
731  * Compute the proportion in which the given position divides the slider bar.
733  * @param {number} position in pixels.
734  * @return {number} [0..1] proportion.
735  */
736 MediaControls.Slider.prototype.getProportion = function(position) {
737   var rect = this.bar_.getBoundingClientRect();
738   return Math.max(0, Math.min(1, (position - rect.left) / rect.width));
742  * Sets value formatting function.
743  * @param {function(number):string} func Value formatting function.
744  */
745 MediaControls.Slider.prototype.setValueToStringFunction = function(func) {};
748  * 'change' event handler.
749  * @private
750  */
751 MediaControls.Slider.prototype.onInputChange_ = function() {
752   this.value_ = this.getValueFromUI_();
753   this.setFilled_(this.value_);
754   this.onChange_(this.value_);
758  * @return {boolean} True if dragging is in progress.
759  */
760 MediaControls.Slider.prototype.isDragging = function() {
761   return this.isDragging_;
765  * Mousedown/mouseup handler.
766  * @param {boolean} on True if the mouse is down.
767  * @private
768  */
769 MediaControls.Slider.prototype.onInputDrag_ = function(on) {
770   this.isDragging_ = on;
771   this.onDrag_(on);
775  * Check if the slider position is at the end of the control.
776  * @return {boolean} True if the slider position is at the end.
777  */
778 MediaControls.Slider.prototype.isAtEnd = function() {
779   return this.input_.value === this.input_.max;
783  * Create a customized slider with animated thumb movement.
785  * @param {!HTMLElement} container The containing div element.
786  * @param {number} value Initial value [0..1].
787  * @param {number} range Number of distinct slider positions to be supported.
788  * @param {function(number)} onChange Value change handler.
789  * @param {function(boolean)} onDrag Drag begin/end handler.
790  * @constructor
791  * @struct
792  * @extends {MediaControls.Slider}
793  */
794 MediaControls.AnimatedSlider = function(
795     container, value, range, onChange, onDrag) {
796   MediaControls.Slider.apply(this, arguments);
798   /**
799    * @type {number}
800    * @private
801    */
802   this.animationInterval_ = 0;
805 MediaControls.AnimatedSlider.prototype = {
806   __proto__: MediaControls.Slider.prototype
810  * Number of animation steps.
811  */
812 MediaControls.AnimatedSlider.STEPS = 10;
815  * Animation duration.
816  */
817 MediaControls.AnimatedSlider.DURATION = 100;
820  * @param {number} value [0..1].
821  * @private
822  */
823 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
824   if (this.animationInterval_) {
825     clearInterval(this.animationInterval_);
826   }
827   var oldValue = this.getValueFromUI_();
828   var step = 0;
829   this.animationInterval_ = setInterval(function() {
830     step++;
831     var currentValue = oldValue +
832         (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS);
833     MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue);
834     if (step == MediaControls.AnimatedSlider.STEPS) {
835       clearInterval(this.animationInterval_);
836     }
837   }.bind(this),
838   MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS);
842  * Create a customized slider with a precise time feedback.
844  * The time value is shown above the slider bar at the mouse position.
846  * @param {!HTMLElement} container The containing div element.
847  * @param {number} value Initial value [0..1].
848  * @param {number} range Number of distinct slider positions to be supported.
849  * @param {function(number)} onChange Value change handler.
850  * @param {function(boolean)} onDrag Drag begin/end handler.
851  * @param {function(number):string} formatFunction Value formatting function.
852  * @constructor
853  * @struct
854  * @extends {MediaControls.Slider}
855  */
856 MediaControls.PreciseSlider = function(
857     container, value, range, onChange, onDrag, formatFunction) {
858   MediaControls.Slider.apply(this, arguments);
860   var doc = this.container_.ownerDocument;
862   /**
863    * @type {number}
864    * @private
865    */
866   this.latestMouseUpTime_ = 0;
868   /**
869    * @type {number}
870    * @private
871    */
872   this.seekMarkTimer_ = 0;
874   /**
875    * @type {number}
876    * @private
877    */
878   this.latestSeekRatio_ = 0;
880   /**
881    * @type {?function(number):string}
882    * @private
883    */
884   this.valueToString_ = null;
886   this.seekMark_ = doc.createElement('div');
887   this.seekMark_.className = 'seek-mark';
888   this.getBar().appendChild(this.seekMark_);
890   this.seekLabel_ = doc.createElement('div');
891   this.seekLabel_.className = 'seek-label';
892   this.seekMark_.appendChild(this.seekLabel_);
894   this.getContainer().addEventListener(
895       'mousemove', this.onMouseMove_.bind(this));
896   this.getContainer().addEventListener(
897       'mouseout', this.onMouseOut_.bind(this));
900 MediaControls.PreciseSlider.prototype = {
901   __proto__: MediaControls.Slider.prototype
905  * Show the seek mark after a delay.
906  */
907 MediaControls.PreciseSlider.SHOW_DELAY = 200;
910  * Hide the seek mark for this long after changing the position with a click.
911  */
912 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
915  * Hide the seek mark for this long after changing the position with a drag.
916  */
917 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
920  * Default hide timeout (no hiding).
921  */
922 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
925  * @override
926  */
927 MediaControls.PreciseSlider.prototype.setValueToStringFunction =
928     function(func) {
929   this.valueToString_ = func;
931   /* It is not completely accurate to assume that the max value corresponds
932    to the longest string, but generous CSS padding will compensate for that. */
933   var labelWidth = this.valueToString_(1).length / 2 + 1;
934   this.seekLabel_.style.width = labelWidth + 'em';
935   this.seekLabel_.style.marginLeft = -labelWidth / 2 + 'em';
939  * Show the time above the slider.
941  * @param {number} ratio [0..1] The proportion of the duration.
942  * @param {number} timeout Timeout in ms after which the label should be hidden.
943  *     MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call.
944  * @private
945  */
946 MediaControls.PreciseSlider.prototype.showSeekMark_ =
947     function(ratio, timeout) {
948   // Do not update the seek mark for the first 500ms after the drag is finished.
949   if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now()))
950     return;
952   this.seekMark_.style.left = ratio * 100 + '%';
954   if (ratio < this.getValue()) {
955     this.seekMark_.classList.remove('inverted');
956   } else {
957     this.seekMark_.classList.add('inverted');
958   }
959   this.seekLabel_.textContent = this.valueToString_(ratio);
961   this.seekMark_.classList.add('visible');
963   if (this.seekMarkTimer_) {
964     clearTimeout(this.seekMarkTimer_);
965     this.seekMarkTimer_ = 0;
966   }
967   if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
968     this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
969   }
973  * @private
974  */
975 MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() {
976   this.seekMarkTimer_ = 0;
977   this.seekMark_.classList.remove('visible');
981  * 'mouseout' event handler.
982  * @param {Event} e Event.
983  * @private
984  */
985 MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) {
986   this.latestSeekRatio_ = this.getProportion(e.clientX);
988   var showMark = function() {
989     if (!this.isDragging()) {
990       this.showSeekMark_(this.latestSeekRatio_,
991           MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY);
992     }
993   }.bind(this);
995   if (this.seekMark_.classList.contains('visible')) {
996     showMark();
997   } else if (!this.seekMarkTimer_) {
998     this.seekMarkTimer_ =
999         setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
1000   }
1004  * 'mouseout' event handler.
1005  * @param {Event} e Event.
1006  * @private
1007  */
1008 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
1009   for (var element = e.relatedTarget; element; element = element.parentNode) {
1010     if (element == this.getContainer())
1011       return;
1012   }
1013   if (this.seekMarkTimer_) {
1014     clearTimeout(this.seekMarkTimer_);
1015     this.seekMarkTimer_ = 0;
1016   }
1017   this.hideSeekMark_();
1021  * 'change' event handler.
1022  * @private
1023  */
1024 MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
1025   MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
1026   if (this.isDragging()) {
1027     this.showSeekMark_(
1028         this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
1029   }
1033  * Mousedown/mouseup handler.
1034  * @param {boolean} on True if the mouse is down.
1035  * @private
1036  */
1037 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) {
1038   MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
1040   if (on) {
1041     // Dragging started, align the seek mark with the thumb position.
1042     this.showSeekMark_(
1043         this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
1044   } else {
1045     // Just finished dragging.
1046     // Show the label for the last time with a shorter timeout.
1047     this.showSeekMark_(
1048         this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
1049     this.latestMouseUpTime_ = Date.now();
1050   }
1054  * Create video controls.
1056  * @param {!HTMLElement} containerElement The container for the controls.
1057  * @param {function(Event)} onMediaError Function to display an error message.
1058  * @param {function(string):string} stringFunction Function providing localized
1059  *     strings.
1060  * @param {function(Event)=} opt_fullScreenToggle Function to toggle fullscreen
1061  *     mode.
1062  * @param {HTMLElement=} opt_stateIconParent The parent for the icon that
1063  *     gives visual feedback when the playback state changes.
1064  * @constructor
1065  * @struct
1066  * @extends {MediaControls}
1067  */
1068 function VideoControls(containerElement, onMediaError, stringFunction,
1069     opt_fullScreenToggle, opt_stateIconParent) {
1070   MediaControls.call(this, containerElement, onMediaError);
1071   this.stringFunction_ = stringFunction;
1073   this.container_.classList.add('video-controls');
1074   this.initPlayButton();
1075   this.initTimeControls(true /* show seek mark */);
1076   this.initVolumeControls();
1078   // Create the cast button.
1079   this.castButton_ = this.createButton('cast menubutton');
1080   this.castButton_.setAttribute('menu', '#cast-menu');
1081   this.castButton_.setAttribute(
1082       'label', this.stringFunction_('VIDEO_PLAYER_PLAY_ON'));
1083   cr.ui.decorate(this.castButton_, cr.ui.MenuButton);
1085   if (opt_fullScreenToggle) {
1086     this.fullscreenButton_ =
1087         this.createButton('fullscreen', opt_fullScreenToggle);
1088   }
1090   if (opt_stateIconParent) {
1091     this.stateIcon_ = this.createControl(
1092         'playback-state-icon', opt_stateIconParent);
1093     this.textBanner_ = this.createControl('text-banner', opt_stateIconParent);
1094   }
1096   // Disables all controls at first.
1097   this.enableControls_('.media-control', false);
1099   var videoControls = this;
1100   chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
1101       function() { videoControls.togglePlayStateWithFeedback(); });
1105  * No resume if we are within this margin from the start or the end.
1106  */
1107 VideoControls.RESUME_MARGIN = 0.03;
1110  * No resume for videos shorter than this.
1111  */
1112 VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min.
1115  * When resuming rewind back this much.
1116  */
1117 VideoControls.RESUME_REWIND = 5;  // seconds.
1119 VideoControls.prototype = { __proto__: MediaControls.prototype };
1122  * Shows icon feedback for the current state of the video player.
1123  * @private
1124  */
1125 VideoControls.prototype.showIconFeedback_ = function() {
1126   var stateIcon = this.stateIcon_;
1127   stateIcon.removeAttribute('state');
1129   setTimeout(function() {
1130     var newState = this.isPlaying() ? 'play' : 'pause';
1132     var onAnimationEnd = function(state, event) {
1133       if (stateIcon.getAttribute('state') === state)
1134         stateIcon.removeAttribute('state');
1136       stateIcon.removeEventListener('webkitAnimationEnd', onAnimationEnd);
1137     }.bind(null, newState);
1138     stateIcon.addEventListener('webkitAnimationEnd', onAnimationEnd);
1140     // Shows the icon with animation.
1141     stateIcon.setAttribute('state', newState);
1142   }.bind(this), 0);
1146  * Shows a text banner.
1148  * @param {string} identifier String identifier.
1149  * @private
1150  */
1151 VideoControls.prototype.showTextBanner_ = function(identifier) {
1152   this.textBanner_.removeAttribute('visible');
1153   this.textBanner_.textContent = this.stringFunction_(identifier);
1155   setTimeout(function() {
1156     var onAnimationEnd = function(event) {
1157       this.textBanner_.removeEventListener(
1158           'webkitAnimationEnd', onAnimationEnd);
1159       this.textBanner_.removeAttribute('visible');
1160     }.bind(this);
1161     this.textBanner_.addEventListener('webkitAnimationEnd', onAnimationEnd);
1163     this.textBanner_.setAttribute('visible', 'true');
1164   }.bind(this), 0);
1168  * @override
1169  */
1170 VideoControls.prototype.onPlayButtonClicked = function(event) {
1171   if (event.ctrlKey) {
1172     this.toggleLoopedModeWithFeedback(true);
1173     if (!this.isPlaying())
1174       this.togglePlayState();
1175   } else {
1176     this.togglePlayState();
1177   }
1181  * Media completion handler.
1182  */
1183 VideoControls.prototype.onMediaComplete = function() {
1184   this.onMediaPlay_(false);  // Just update the UI.
1185   this.savePosition();  // This will effectively forget the position.
1189  * Toggles the looped mode with feedback.
1190  * @param {boolean} on Whether enabled or not.
1191  */
1192 VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) {
1193   if (!this.getMedia().duration)
1194     return;
1195   this.toggleLoopedMode(on);
1196   if (on) {
1197     // TODO(mtomasz): Simplify, crbug.com/254318.
1198     this.showTextBanner_('VIDEO_PLAYER_LOOPED_MODE');
1199   }
1203  * Toggles the looped mode.
1204  * @param {boolean} on Whether enabled or not.
1205  */
1206 VideoControls.prototype.toggleLoopedMode = function(on) {
1207   this.getMedia().loop = on;
1211  * Toggles play/pause state and flash an icon over the video.
1212  */
1213 VideoControls.prototype.togglePlayStateWithFeedback = function() {
1214   if (!this.getMedia().duration)
1215     return;
1217   this.togglePlayState();
1218   this.showIconFeedback_();
1222  * Toggles play/pause state.
1223  */
1224 VideoControls.prototype.togglePlayState = function() {
1225   if (this.isPlaying()) {
1226     // User gave the Pause command. Save the state and reset the loop mode.
1227     this.toggleLoopedMode(false);
1228     this.savePosition();
1229   }
1230   MediaControls.prototype.togglePlayState.apply(this, arguments);
1234  * Saves the playback position to the persistent storage.
1235  * @param {boolean=} opt_sync True if the position must be saved synchronously
1236  *     (required when closing app windows).
1237  */
1238 VideoControls.prototype.savePosition = function(opt_sync) {
1239   if (!this.media_ ||
1240       !this.media_.duration ||
1241       this.media_.duration < VideoControls.RESUME_THRESHOLD) {
1242     return;
1243   }
1245   var ratio = this.media_.currentTime / this.media_.duration;
1246   var position;
1247   if (ratio < VideoControls.RESUME_MARGIN ||
1248       ratio > (1 - VideoControls.RESUME_MARGIN)) {
1249     // We are too close to the beginning or the end.
1250     // Remove the resume position so that next time we start from the beginning.
1251     position = null;
1252   } else {
1253     position = Math.floor(
1254         Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND));
1255   }
1257   if (opt_sync) {
1258     // Packaged apps cannot save synchronously.
1259     // Pass the data to the background page.
1260     if (!window.saveOnExit)
1261       window.saveOnExit = [];
1262     window.saveOnExit.push({ key: this.media_.src, value: position });
1263   } else {
1264     util.AppCache.update(this.media_.src, position);
1265   }
1269  * Resumes the playback position saved in the persistent storage.
1270  */
1271 VideoControls.prototype.restorePlayState = function() {
1272   if (this.media_ && this.media_.duration >= VideoControls.RESUME_THRESHOLD) {
1273     util.AppCache.getValue(this.media_.src, function(position) {
1274       if (position)
1275         this.media_.currentTime = position;
1276     }.bind(this));
1277   }
1281  * Updates style to best fit the size of the container.
1282  */
1283 VideoControls.prototype.updateStyle = function() {
1284   // We assume that the video controls element fills the parent container.
1285   // This is easier than adding margins to this.container_.clientWidth.
1286   var width = this.container_.parentNode.clientWidth;
1288   // Set the margin to 5px for width >= 400, 0px for width < 160,
1289   // interpolate linearly in between.
1290   this.container_.style.margin =
1291       Math.ceil((Math.max(160, Math.min(width, 400)) - 160) / 48) + 'px';
1293   var hideBelow = function(selector, limit) {
1294     this.container_.querySelector(selector).style.display =
1295         width < limit ? 'none' : '-webkit-box';
1296   }.bind(this);
1298   hideBelow('.time', 350);
1299   hideBelow('.volume', 275);
1300   hideBelow('.volume-controls', 210);
1301   hideBelow('.fullscreen', 150);
1305  * Creates audio controls.
1307  * @param {!HTMLElement} container Parent container.
1308  * @param {function(boolean)} advanceTrack Parameter: true=forward.
1309  * @param {function(Event)} onError Error handler.
1310  * @constructor
1311  * @struct
1312  * @extends {MediaControls}
1313  */
1314 function AudioControls(container, advanceTrack, onError) {
1315   MediaControls.call(this, container, onError);
1317   this.container_.classList.add('audio-controls');
1319   this.advanceTrack_ = advanceTrack;
1321   this.initPlayButton();
1322   this.initTimeControls(false /* no seek mark */);
1323   /* No volume controls */
1324   this.createButton('previous', this.onAdvanceClick_.bind(this, false));
1325   this.createButton('next', this.onAdvanceClick_.bind(this, true));
1327   // Disables all controls at first.
1328   this.enableControls_('.media-control', false);
1330   var audioControls = this;
1331   chrome.mediaPlayerPrivate.onNextTrack.addListener(
1332       function() { audioControls.onAdvanceClick_(true); });
1333   chrome.mediaPlayerPrivate.onPrevTrack.addListener(
1334       function() { audioControls.onAdvanceClick_(false); });
1335   chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
1336       function() { audioControls.togglePlayState(); });
1339 AudioControls.prototype = { __proto__: MediaControls.prototype };
1342  * Media completion handler. Advances to the next track.
1343  */
1344 AudioControls.prototype.onMediaComplete = function() {
1345   this.advanceTrack_(true);
1349  * The track position after which "previous" button acts as "restart".
1350  */
1351 AudioControls.TRACK_RESTART_THRESHOLD = 5;  // seconds.
1354  * @param {boolean} forward True if advancing forward.
1355  * @private
1356  */
1357 AudioControls.prototype.onAdvanceClick_ = function(forward) {
1358   if (!forward &&
1359       (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) {
1360     // We are far enough from the beginning of the current track.
1361     // Restart it instead of than skipping to the previous one.
1362     this.getMedia().currentTime = 0;
1363   } else {
1364     this.advanceTrack_(forward);
1365   }