Show reload button only when the slider is at the end.
[chromium-blink-merge.git] / ui / file_manager / video_player / js / media_controls.js
blob5bbbd9eca439db8895c7f892efb4ce2d84d2598e
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} onMediaError Function to display an error message.
13  * @constructor
14  */
15 function MediaControls(containerElement, onMediaError) {
16   this.container_ = containerElement;
17   this.document_ = this.container_.ownerDocument;
18   this.media_ = null;
20   this.onMediaPlayBound_ = this.onMediaPlay_.bind(this, true);
21   this.onMediaPauseBound_ = this.onMediaPlay_.bind(this, false);
22   this.onMediaDurationBound_ = this.onMediaDuration_.bind(this);
23   this.onMediaProgressBound_ = this.onMediaProgress_.bind(this);
24   this.onMediaError_ = onMediaError || function() {};
26   this.savedVolume_ = 1;  // 100% volume.
29 /**
30  * Button's state types. Values are used as CSS class names.
31  * @enum {string}
32  */
33 MediaControls.ButtonStateType = {
34   DEFAULT: 'default',
35   PLAYING: 'playing',
36   ENDED: 'ended'
39 /**
40  * @return {HTMLAudioElement|HTMLVideoElement} The media element.
41  */
42 MediaControls.prototype.getMedia = function() { return this.media_ };
44 /**
45  * Format the time in hh:mm:ss format (omitting redundant leading zeros).
46  *
47  * @param {number} timeInSec Time in seconds.
48  * @return {string} Formatted time string.
49  * @private
50  */
51 MediaControls.formatTime_ = function(timeInSec) {
52   var seconds = Math.floor(timeInSec % 60);
53   var minutes = Math.floor((timeInSec / 60) % 60);
54   var hours = Math.floor(timeInSec / 60 / 60);
55   var result = '';
56   if (hours) result += hours + ':';
57   if (hours && (minutes < 10)) result += '0';
58   result += minutes + ':';
59   if (seconds < 10) result += '0';
60   result += seconds;
61   return result;
64 /**
65  * Create a custom control.
66  *
67  * @param {string} className Class name.
68  * @param {HTMLElement=} opt_parent Parent element or container if undefined.
69  * @return {HTMLElement} The new control element.
70  */
71 MediaControls.prototype.createControl = function(className, opt_parent) {
72   var parent = opt_parent || this.container_;
73   var control = this.document_.createElement('div');
74   control.className = className;
75   parent.appendChild(control);
76   return control;
79 /**
80  * Create a custom button.
81  *
82  * @param {string} className Class name.
83  * @param {function(Event)} handler Click handler.
84  * @param {HTMLElement=} opt_parent Parent element or container if undefined.
85  * @param {number=} opt_numStates Number of states, default: 1.
86  * @return {HTMLElement} The new button element.
87  */
88 MediaControls.prototype.createButton = function(
89     className, handler, opt_parent, opt_numStates) {
90   opt_numStates = opt_numStates || 1;
92   var button = this.createControl(className, opt_parent);
93   button.classList.add('media-button');
94   button.addEventListener('click', handler);
96   var stateTypes = Object.keys(MediaControls.ButtonStateType);
97   for (var state = 0; state != opt_numStates; state++) {
98     var stateClass = MediaControls.ButtonStateType[stateTypes[state]];
99     this.createControl('normal ' + stateClass, button);
100     this.createControl('hover ' + stateClass, button);
101     this.createControl('active ' + stateClass, button);
102   }
103   this.createControl('disabled', button);
105   button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT);
106   button.addEventListener('click', handler);
107   return button;
111  * Enable/disable controls matching a given selector.
113  * @param {string} selector CSS selector.
114  * @param {boolean} on True if enable, false if disable.
115  * @private
116  */
117 MediaControls.prototype.enableControls_ = function(selector, on) {
118   var controls = this.container_.querySelectorAll(selector);
119   for (var i = 0; i != controls.length; i++) {
120     var classList = controls[i].classList;
121     if (on)
122       classList.remove('disabled');
123     else
124       classList.add('disabled');
125   }
129  * Playback control.
130  */
133  * Play the media.
134  */
135 MediaControls.prototype.play = function() {
136   if (!this.media_)
137     return;  // Media is detached.
139   this.media_.play();
143  * Pause the media.
144  */
145 MediaControls.prototype.pause = function() {
146   if (!this.media_)
147     return;  // Media is detached.
149   this.media_.pause();
153  * @return {boolean} True if the media is currently playing.
154  */
155 MediaControls.prototype.isPlaying = function() {
156   return this.media_ && !this.media_.paused && !this.media_.ended;
160  * Toggle play/pause.
161  */
162 MediaControls.prototype.togglePlayState = function() {
163   if (this.isPlaying())
164     this.pause();
165   else
166     this.play();
170  * Toggle play/pause state on a mouse click on the play/pause button. Can be
171  * called externally. TODO(mtomasz): Remove it. http://www.crbug.com/254318.
173  * @param {Event=} opt_event Mouse click event.
174  */
175 MediaControls.prototype.onPlayButtonClicked = function(opt_event) {
176   this.togglePlayState();
180  * @param {HTMLElement=} opt_parent Parent container.
181  */
182 MediaControls.prototype.initPlayButton = function(opt_parent) {
183   this.playButton_ = this.createButton('play media-control',
184       this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */);
188  * Time controls
189  */
192  * The default range of 100 is too coarse for the media progress slider.
193  */
194 MediaControls.PROGRESS_RANGE = 5000;
197  * @param {boolean=} opt_seekMark True if the progress slider should have
198  *     a seek mark.
199  * @param {HTMLElement=} opt_parent Parent container.
200  */
201 MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) {
202   var timeControls = this.createControl('time-controls', opt_parent);
204   var sliderConstructor =
205       opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider;
207   this.progressSlider_ = new sliderConstructor(
208       this.createControl('progress media-control', timeControls),
209       0, /* value */
210       MediaControls.PROGRESS_RANGE,
211       this.onProgressChange_.bind(this),
212       this.onProgressDrag_.bind(this));
214   var timeBox = this.createControl('time media-control', timeControls);
216   this.duration_ = this.createControl('duration', timeBox);
217   // Set the initial width to the minimum to reduce the flicker.
218   this.duration_.textContent = MediaControls.formatTime_(0);
220   this.currentTime_ = this.createControl('current', timeBox);
224  * @param {number} current Current time is seconds.
225  * @param {number} duration Duration in seconds.
226  * @private
227  */
228 MediaControls.prototype.displayProgress_ = function(current, duration) {
229   var ratio = current / duration;
230   this.progressSlider_.setValue(ratio);
231   this.currentTime_.textContent = MediaControls.formatTime_(current);
235  * @param {number} value Progress [0..1].
236  * @private
237  */
238 MediaControls.prototype.onProgressChange_ = function(value) {
239   if (!this.media_)
240     return;  // Media is detached.
242   if (!this.media_.seekable || !this.media_.duration) {
243     console.error('Inconsistent media state');
244     return;
245   }
247   var current = this.media_.duration * value;
248   this.media_.currentTime = current;
249   this.currentTime_.textContent = MediaControls.formatTime_(current);
253  * @param {boolean} on True if dragging.
254  * @private
255  */
256 MediaControls.prototype.onProgressDrag_ = function(on) {
257   if (!this.media_)
258     return;  // Media is detached.
260   if (on) {
261     this.resumeAfterDrag_ = this.isPlaying();
262     this.media_.pause(true /* seeking */);
263   } else {
264     if (this.resumeAfterDrag_) {
265       if (this.media_.ended)
266         this.onMediaPlay_(false);
267       else
268         this.media_.play(true /* seeking */);
269     }
270     this.updatePlayButtonState_(this.isPlaying());
271   }
275  * Volume controls
276  */
279  * @param {HTMLElement=} opt_parent Parent element for the controls.
280  */
281 MediaControls.prototype.initVolumeControls = function(opt_parent) {
282   var volumeControls = this.createControl('volume-controls', opt_parent);
284   this.soundButton_ = this.createButton('sound media-control',
285       this.onSoundButtonClick_.bind(this), volumeControls);
286   this.soundButton_.setAttribute('level', 3);  // max level.
288   this.volume_ = new MediaControls.AnimatedSlider(
289       this.createControl('volume media-control', volumeControls),
290       1, /* value */
291       100 /* range */,
292       this.onVolumeChange_.bind(this),
293       this.onVolumeDrag_.bind(this));
297  * Click handler for the sound level button.
298  * @private
299  */
300 MediaControls.prototype.onSoundButtonClick_ = function() {
301   if (this.media_.volume == 0) {
302     this.volume_.setValue(this.savedVolume_ || 1);
303   } else {
304     this.savedVolume_ = this.media_.volume;
305     this.volume_.setValue(0);
306   }
307   this.onVolumeChange_(this.volume_.getValue());
311  * @param {number} value Volume [0..1].
312  * @return {number} The rough level [0..3] used to pick an icon.
313  * @private
314  */
315 MediaControls.getVolumeLevel_ = function(value) {
316   if (value == 0) return 0;
317   if (value <= 1 / 3) return 1;
318   if (value <= 2 / 3) return 2;
319   return 3;
323  * @param {number} value Volume [0..1].
324  * @private
325  */
326 MediaControls.prototype.onVolumeChange_ = function(value) {
327   if (!this.media_)
328     return;  // Media is detached.
330   this.media_.volume = value;
331   this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value));
335  * @param {boolean} on True if dragging is in progress.
336  * @private
337  */
338 MediaControls.prototype.onVolumeDrag_ = function(on) {
339   if (on && (this.media_.volume != 0)) {
340     this.savedVolume_ = this.media_.volume;
341   }
345  * Media event handlers.
346  */
349  * Attach a media element.
351  * @param {HTMLMediaElement} mediaElement The media element to control.
352  */
353 MediaControls.prototype.attachMedia = function(mediaElement) {
354   this.media_ = mediaElement;
356   this.media_.addEventListener('play', this.onMediaPlayBound_);
357   this.media_.addEventListener('pause', this.onMediaPauseBound_);
358   this.media_.addEventListener('durationchange', this.onMediaDurationBound_);
359   this.media_.addEventListener('timeupdate', this.onMediaProgressBound_);
360   this.media_.addEventListener('error', this.onMediaError_);
362   // If the text banner is being displayed, hide it immediately, since it is
363   // related to the previous media.
364   this.textBanner_.removeAttribute('visible');
366   // Reflect the media state in the UI.
367   this.onMediaDuration_();
368   this.onMediaPlay_(this.isPlaying());
369   this.onMediaProgress_();
370   if (this.volume_) {
371     /* Copy the user selected volume to the new media element. */
372     this.savedVolume_ = this.media_.volume = this.volume_.getValue();
373   }
377  * Detach media event handlers.
378  */
379 MediaControls.prototype.detachMedia = function() {
380   if (!this.media_)
381     return;
383   this.media_.removeEventListener('play', this.onMediaPlayBound_);
384   this.media_.removeEventListener('pause', this.onMediaPauseBound_);
385   this.media_.removeEventListener('durationchange', this.onMediaDurationBound_);
386   this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_);
387   this.media_.removeEventListener('error', this.onMediaError_);
389   this.media_ = null;
393  * Force-empty the media pipeline. This is a workaround for crbug.com/149957.
394  * The document is not going to be GC-ed until the last Files app window closes,
395  * but we want the media pipeline to deinitialize ASAP to minimize leakage.
396  */
397 MediaControls.prototype.cleanup = function() {
398   if (!this.media_)
399     return;
401   this.media_.src = '';
402   this.media_.load();
403   this.detachMedia();
407  * 'play' and 'pause' event handler.
408  * @param {boolean} playing True if playing.
409  * @private
410  */
411 MediaControls.prototype.onMediaPlay_ = function(playing) {
412   if (this.progressSlider_.isDragging())
413     return;
415   this.updatePlayButtonState_(playing);
416   this.onPlayStateChanged();
420  * 'durationchange' event handler.
421  * @private
422  */
423 MediaControls.prototype.onMediaDuration_ = function() {
424   if (!this.media_ || !this.media_.duration) {
425     this.enableControls_('.media-control', false);
426     return;
427   }
429   this.enableControls_('.media-control', true);
431   var sliderContainer = this.progressSlider_.getContainer();
432   if (this.media_.seekable)
433     sliderContainer.classList.remove('readonly');
434   else
435     sliderContainer.classList.add('readonly');
437   var valueToString = function(value) {
438     var duration = this.media_ ? this.media_.duration : 0;
439     return MediaControls.formatTime_(this.media_.duration * value);
440   }.bind(this);
442   this.duration_.textContent = valueToString(1);
444   if (this.progressSlider_.setValueToStringFunction)
445     this.progressSlider_.setValueToStringFunction(valueToString);
447   if (this.media_.seekable)
448     this.restorePlayState();
452  * 'timeupdate' event handler.
453  * @private
454  */
455 MediaControls.prototype.onMediaProgress_ = function() {
456   if (!this.media_ || !this.media_.duration) {
457     this.displayProgress_(0, 1);
458     return;
459   }
461   var current = this.media_.currentTime;
462   var duration = this.media_.duration;
464   if (this.progressSlider_.isDragging())
465     return;
467   this.displayProgress_(current, duration);
469   if (current == duration) {
470     this.onMediaComplete();
471   }
472   this.onPlayStateChanged();
476  * Called when the media playback is complete.
477  */
478 MediaControls.prototype.onMediaComplete = function() {};
481  * Called when play/pause state is changed or on playback progress.
482  * This is the right moment to save the play state.
483  */
484 MediaControls.prototype.onPlayStateChanged = function() {};
487  * Updates the play button state.
488  * @param {boolean} playing If the video is playing.
489  * @private
490  */
491 MediaControls.prototype.updatePlayButtonState_ = function(playing) {
492   if (this.media_.ended && this.progressSlider_.isAtEnd()) {
493     this.playButton_.setAttribute('state',
494                                   MediaControls.ButtonStateType.ENDED);
495   } else if (playing) {
496     this.playButton_.setAttribute('state',
497                                   MediaControls.ButtonStateType.PLAYING);
498   } else {
499     this.playButton_.setAttribute('state',
500                                   MediaControls.ButtonStateType.DEFAULT);
501   }
505  * Restore play state. Base implementation is empty.
506  */
507 MediaControls.prototype.restorePlayState = function() {};
510  * Encode current state into the page URL or the app state.
511  */
512 MediaControls.prototype.encodeState = function() {
513   if (!this.media_ || !this.media_.duration)
514     return;
516   if (window.appState) {
517     window.appState.time = this.media_.currentTime;
518     util.saveAppState();
519   }
520   return;
524  * Decode current state from the page URL or the app state.
525  * @return {boolean} True if decode succeeded.
526  */
527 MediaControls.prototype.decodeState = function() {
528   if (!this.media_ || !window.appState || !('time' in window.appState))
529     return false;
530   // There is no page reload for apps v2, only app restart.
531   // Always restart in paused state.
532   this.media_.currentTime = window.appState.time;
533   this.pause();
534   return true;
538  * Remove current state from the page URL or the app state.
539  */
540 MediaControls.prototype.clearState = function() {
541   if (!window.appState)
542     return;
544   if ('time' in window.appState)
545     delete window.appState.time;
546   util.saveAppState();
547   return;
551  * Create a customized slider control.
553  * @param {HTMLElement} container The containing div element.
554  * @param {number} value Initial value [0..1].
555  * @param {number} range Number of distinct slider positions to be supported.
556  * @param {function(number)} onChange Value change handler.
557  * @param {function(boolean)} onDrag Drag begin/end handler.
558  * @constructor
559  */
561 MediaControls.Slider = function(container, value, range, onChange, onDrag) {
562   this.container_ = container;
563   this.onChange_ = onChange;
564   this.onDrag_ = onDrag;
566   var document = this.container_.ownerDocument;
568   this.container_.classList.add('custom-slider');
570   this.input_ = document.createElement('input');
571   this.input_.type = 'range';
572   this.input_.min = 0;
573   this.input_.max = range;
574   this.input_.value = value * range;
575   this.container_.appendChild(this.input_);
577   this.input_.addEventListener(
578       'change', this.onInputChange_.bind(this));
579   this.input_.addEventListener(
580       'mousedown', this.onInputDrag_.bind(this, true));
581   this.input_.addEventListener(
582       'mouseup', this.onInputDrag_.bind(this, false));
584   this.bar_ = document.createElement('div');
585   this.bar_.className = 'bar';
586   this.container_.appendChild(this.bar_);
588   this.filled_ = document.createElement('div');
589   this.filled_.className = 'filled';
590   this.bar_.appendChild(this.filled_);
592   var leftCap = document.createElement('div');
593   leftCap.className = 'cap left';
594   this.bar_.appendChild(leftCap);
596   var rightCap = document.createElement('div');
597   rightCap.className = 'cap right';
598   this.bar_.appendChild(rightCap);
600   this.value_ = value;
601   this.setFilled_(value);
605  * @return {HTMLElement} The container element.
606  */
607 MediaControls.Slider.prototype.getContainer = function() {
608   return this.container_;
612  * @return {HTMLElement} The standard input element.
613  * @private
614  */
615 MediaControls.Slider.prototype.getInput_ = function() {
616   return this.input_;
620  * @return {HTMLElement} The slider bar element.
621  */
622 MediaControls.Slider.prototype.getBar = function() {
623   return this.bar_;
627  * @return {number} [0..1] The current value.
628  */
629 MediaControls.Slider.prototype.getValue = function() {
630   return this.value_;
634  * @param {number} value [0..1].
635  */
636 MediaControls.Slider.prototype.setValue = function(value) {
637   this.value_ = value;
638   this.setValueToUI_(value);
642  * Fill the given proportion the slider bar (from the left).
644  * @param {number} proportion [0..1].
645  * @private
646  */
647 MediaControls.Slider.prototype.setFilled_ = function(proportion) {
648   this.filled_.style.width = proportion * 100 + '%';
652  * Get the value from the input element.
654  * @return {number} Value [0..1].
655  * @private
656  */
657 MediaControls.Slider.prototype.getValueFromUI_ = function() {
658   return this.input_.value / this.input_.max;
662  * Update the UI with the current value.
664  * @param {number} value [0..1].
665  * @private
666  */
667 MediaControls.Slider.prototype.setValueToUI_ = function(value) {
668   this.input_.value = value * this.input_.max;
669   this.setFilled_(value);
673  * Compute the proportion in which the given position divides the slider bar.
675  * @param {number} position in pixels.
676  * @return {number} [0..1] proportion.
677  */
678 MediaControls.Slider.prototype.getProportion = function(position) {
679   var rect = this.bar_.getBoundingClientRect();
680   return Math.max(0, Math.min(1, (position - rect.left) / rect.width));
684  * 'change' event handler.
685  * @private
686  */
687 MediaControls.Slider.prototype.onInputChange_ = function() {
688   this.value_ = this.getValueFromUI_();
689   this.setFilled_(this.value_);
690   this.onChange_(this.value_);
694  * @return {boolean} True if dragging is in progress.
695  */
696 MediaControls.Slider.prototype.isDragging = function() {
697   return this.isDragging_;
701  * Mousedown/mouseup handler.
702  * @param {boolean} on True if the mouse is down.
703  * @private
704  */
705 MediaControls.Slider.prototype.onInputDrag_ = function(on) {
706   this.isDragging_ = on;
707   this.onDrag_(on);
711  * Check if the slider position is at the end of the control.
712  * @return {boolean} True if the slider position is at the end.
713  */
714 MediaControls.Slider.prototype.isAtEnd = function() {
715   return this.input_.value === this.input_.max;
719  * Create a customized slider with animated thumb movement.
721  * @param {HTMLElement} container The containing div element.
722  * @param {number} value Initial value [0..1].
723  * @param {number} range Number of distinct slider positions to be supported.
724  * @param {function(number)} onChange Value change handler.
725  * @param {function(boolean)} onDrag Drag begin/end handler.
726  * @param {function(number):string} formatFunction Value formatting function.
727  * @constructor
728  */
729 MediaControls.AnimatedSlider = function(
730     container, value, range, onChange, onDrag, formatFunction) {
731   MediaControls.Slider.apply(this, arguments);
734 MediaControls.AnimatedSlider.prototype = {
735   __proto__: MediaControls.Slider.prototype
739  * Number of animation steps.
740  */
741 MediaControls.AnimatedSlider.STEPS = 10;
744  * Animation duration.
745  */
746 MediaControls.AnimatedSlider.DURATION = 100;
749  * @param {number} value [0..1].
750  * @private
751  */
752 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
753   if (this.animationInterval_) {
754     clearInterval(this.animationInterval_);
755   }
756   var oldValue = this.getValueFromUI_();
757   var step = 0;
758   this.animationInterval_ = setInterval(function() {
759     step++;
760     var currentValue = oldValue +
761         (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS);
762     MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue);
763     if (step == MediaControls.AnimatedSlider.STEPS) {
764       clearInterval(this.animationInterval_);
765     }
766   }.bind(this),
767   MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS);
771  * Create a customized slider with a precise time feedback.
773  * The time value is shown above the slider bar at the mouse position.
775  * @param {HTMLElement} container The containing div element.
776  * @param {number} value Initial value [0..1].
777  * @param {number} range Number of distinct slider positions to be supported.
778  * @param {function(number)} onChange Value change handler.
779  * @param {function(boolean)} onDrag Drag begin/end handler.
780  * @param {function(number):string} formatFunction Value formatting function.
781  * @constructor
782  */
783 MediaControls.PreciseSlider = function(
784     container, value, range, onChange, onDrag, formatFunction) {
785   MediaControls.Slider.apply(this, arguments);
787   var doc = this.container_.ownerDocument;
789   /**
790    * @type {function(number):string}
791    * @private
792    */
793   this.valueToString_ = null;
795   this.seekMark_ = doc.createElement('div');
796   this.seekMark_.className = 'seek-mark';
797   this.getBar().appendChild(this.seekMark_);
799   this.seekLabel_ = doc.createElement('div');
800   this.seekLabel_.className = 'seek-label';
801   this.seekMark_.appendChild(this.seekLabel_);
803   this.getContainer().addEventListener(
804       'mousemove', this.onMouseMove_.bind(this));
805   this.getContainer().addEventListener(
806       'mouseout', this.onMouseOut_.bind(this));
809 MediaControls.PreciseSlider.prototype = {
810   __proto__: MediaControls.Slider.prototype
814  * Show the seek mark after a delay.
815  */
816 MediaControls.PreciseSlider.SHOW_DELAY = 200;
819  * Hide the seek mark for this long after changing the position with a click.
820  */
821 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
824  * Hide the seek mark for this long after changing the position with a drag.
825  */
826 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
829  * Default hide timeout (no hiding).
830  */
831 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
834  * @param {function(number):string} func Value formatting function.
835  */
836 MediaControls.PreciseSlider.prototype.setValueToStringFunction =
837     function(func) {
838   this.valueToString_ = func;
840   /* It is not completely accurate to assume that the max value corresponds
841    to the longest string, but generous CSS padding will compensate for that. */
842   var labelWidth = this.valueToString_(1).length / 2 + 1;
843   this.seekLabel_.style.width = labelWidth + 'em';
844   this.seekLabel_.style.marginLeft = -labelWidth / 2 + 'em';
848  * Show the time above the slider.
850  * @param {number} ratio [0..1] The proportion of the duration.
851  * @param {number} timeout Timeout in ms after which the label should be hidden.
852  *     MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call.
853  * @private
854  */
855 MediaControls.PreciseSlider.prototype.showSeekMark_ =
856     function(ratio, timeout) {
857   // Do not update the seek mark for the first 500ms after the drag is finished.
858   if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now()))
859     return;
861   this.seekMark_.style.left = ratio * 100 + '%';
863   if (ratio < this.getValue()) {
864     this.seekMark_.classList.remove('inverted');
865   } else {
866     this.seekMark_.classList.add('inverted');
867   }
868   this.seekLabel_.textContent = this.valueToString_(ratio);
870   this.seekMark_.classList.add('visible');
872   if (this.seekMarkTimer_) {
873     clearTimeout(this.seekMarkTimer_);
874     this.seekMarkTimer_ = null;
875   }
876   if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
877     this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
878   }
882  * @private
883  */
884 MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() {
885   this.seekMarkTimer_ = null;
886   this.seekMark_.classList.remove('visible');
890  * 'mouseout' event handler.
891  * @param {Event} e Event.
892  * @private
893  */
894 MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) {
895   this.latestSeekRatio_ = this.getProportion(e.clientX);
897   var self = this;
898   function showMark() {
899     if (!self.isDragging()) {
900       self.showSeekMark_(self.latestSeekRatio_,
901           MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY);
902     }
903   }
905   if (this.seekMark_.classList.contains('visible')) {
906     showMark();
907   } else if (!this.seekMarkTimer_) {
908     this.seekMarkTimer_ =
909         setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
910   }
914  * 'mouseout' event handler.
915  * @param {Event} e Event.
916  * @private
917  */
918 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
919   for (var element = e.relatedTarget; element; element = element.parentNode) {
920     if (element == this.getContainer())
921       return;
922   }
923   if (this.seekMarkTimer_) {
924     clearTimeout(this.seekMarkTimer_);
925     this.seekMarkTimer_ = null;
926   }
927   this.hideSeekMark_();
931  * 'change' event handler.
932  * @private
933  */
934 MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
935   MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
936   if (this.isDragging()) {
937     this.showSeekMark_(
938         this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
939   }
943  * Mousedown/mouseup handler.
944  * @param {boolean} on True if the mouse is down.
945  * @private
946  */
947 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) {
948   MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
950   if (on) {
951     // Dragging started, align the seek mark with the thumb position.
952     this.showSeekMark_(
953         this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
954   } else {
955     // Just finished dragging.
956     // Show the label for the last time with a shorter timeout.
957     this.showSeekMark_(
958         this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
959     this.latestMouseUpTime_ = Date.now();
960   }
964  * Create video controls.
966  * @param {HTMLElement} containerElement The container for the controls.
967  * @param {function} onMediaError Function to display an error message.
968  * @param {function(string):string} stringFunction Function providing localized
969  *     strings.
970  * @param {function=} opt_fullScreenToggle Function to toggle fullscreen mode.
971  * @param {HTMLElement=} opt_stateIconParent The parent for the icon that
972  *     gives visual feedback when the playback state changes.
973  * @constructor
974  */
975 function VideoControls(containerElement, onMediaError, stringFunction,
976     opt_fullScreenToggle, opt_stateIconParent) {
977   MediaControls.call(this, containerElement, onMediaError);
978   this.stringFunction_ = stringFunction;
980   this.container_.classList.add('video-controls');
981   this.initPlayButton();
982   this.initTimeControls(true /* show seek mark */);
983   this.initVolumeControls();
985   // Create the cast button.
986   this.castButton_ = this.createButton('cast menubutton');
987   this.castButton_.setAttribute('menu', '#cast-menu');
988   this.castButton_.setAttribute(
989       'label', this.stringFunction_('VIDEO_PLAYER_PLAY_ON'));
990   cr.ui.decorate(this.castButton_, cr.ui.MenuButton);
992   if (opt_fullScreenToggle) {
993     this.fullscreenButton_ =
994         this.createButton('fullscreen', opt_fullScreenToggle);
995   }
997   if (opt_stateIconParent) {
998     this.stateIcon_ = this.createControl(
999         'playback-state-icon', opt_stateIconParent);
1000     this.textBanner_ = this.createControl('text-banner', opt_stateIconParent);
1001   }
1003   // Disables all controls at first.
1004   this.enableControls_('.media-control', false);
1006   var videoControls = this;
1007   chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
1008       function() { videoControls.togglePlayStateWithFeedback(); });
1012  * No resume if we are within this margin from the start or the end.
1013  */
1014 VideoControls.RESUME_MARGIN = 0.03;
1017  * No resume for videos shorter than this.
1018  */
1019 VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min.
1022  * When resuming rewind back this much.
1023  */
1024 VideoControls.RESUME_REWIND = 5;  // seconds.
1026 VideoControls.prototype = { __proto__: MediaControls.prototype };
1029  * Shows icon feedback for the current state of the video player.
1030  * @private
1031  */
1032 VideoControls.prototype.showIconFeedback_ = function() {
1033   var stateIcon = this.stateIcon_;
1034   stateIcon.removeAttribute('state');
1036   setTimeout(function() {
1037     var newState = this.isPlaying() ? 'play' : 'pause';
1039     var onAnimationEnd = function(state, event) {
1040       if (stateIcon.getAttribute('state') === state)
1041         stateIcon.removeAttribute('state');
1043       stateIcon.removeEventListener('webkitAnimationEnd', onAnimationEnd);
1044     }.bind(null, newState);
1045     stateIcon.addEventListener('webkitAnimationEnd', onAnimationEnd);
1047     // Shows the icon with animation.
1048     stateIcon.setAttribute('state', newState);
1049   }.bind(this), 0);
1053  * Shows a text banner.
1055  * @param {string} identifier String identifier.
1056  * @private
1057  */
1058 VideoControls.prototype.showTextBanner_ = function(identifier) {
1059   this.textBanner_.removeAttribute('visible');
1060   this.textBanner_.textContent = this.stringFunction_(identifier);
1062   setTimeout(function() {
1063     var onAnimationEnd = function(event) {
1064       this.textBanner_.removeEventListener(
1065           'webkitAnimationEnd', onAnimationEnd);
1066       this.textBanner_.removeAttribute('visible');
1067     }.bind(this);
1068     this.textBanner_.addEventListener('webkitAnimationEnd', onAnimationEnd);
1070     this.textBanner_.setAttribute('visible', 'true');
1071   }.bind(this), 0);
1075  * Toggle play/pause state on a mouse click on the play/pause button. Can be
1076  * called externally.
1078  * @param {Event} event Mouse click event.
1079  */
1080 VideoControls.prototype.onPlayButtonClicked = function(event) {
1081   if (event.ctrlKey) {
1082     this.toggleLoopedModeWithFeedback(true);
1083     if (!this.isPlaying())
1084       this.togglePlayState();
1085   } else {
1086     this.togglePlayState();
1087   }
1091  * Media completion handler.
1092  */
1093 VideoControls.prototype.onMediaComplete = function() {
1094   this.onMediaPlay_(false);  // Just update the UI.
1095   this.savePosition();  // This will effectively forget the position.
1099  * Toggles the looped mode with feedback.
1100  * @param {boolean} on Whether enabled or not.
1101  */
1102 VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) {
1103   if (!this.getMedia().duration)
1104     return;
1105   this.toggleLoopedMode(on);
1106   if (on) {
1107     // TODO(mtomasz): Simplify, crbug.com/254318.
1108     this.showTextBanner_('VIDEO_PLAYER_LOOPED_MODE');
1109   }
1113  * Toggles the looped mode.
1114  * @param {boolean} on Whether enabled or not.
1115  */
1116 VideoControls.prototype.toggleLoopedMode = function(on) {
1117   this.getMedia().loop = on;
1121  * Toggles play/pause state and flash an icon over the video.
1122  */
1123 VideoControls.prototype.togglePlayStateWithFeedback = function() {
1124   if (!this.getMedia().duration)
1125     return;
1127   this.togglePlayState();
1128   this.showIconFeedback_();
1132  * Toggles play/pause state.
1133  */
1134 VideoControls.prototype.togglePlayState = function() {
1135   if (this.isPlaying()) {
1136     // User gave the Pause command. Save the state and reset the loop mode.
1137     this.toggleLoopedMode(false);
1138     this.savePosition();
1139   }
1140   MediaControls.prototype.togglePlayState.apply(this, arguments);
1144  * Saves the playback position to the persistent storage.
1145  * @param {boolean=} opt_sync True if the position must be saved synchronously
1146  *     (required when closing app windows).
1147  */
1148 VideoControls.prototype.savePosition = function(opt_sync) {
1149   if (!this.media_ ||
1150       !this.media_.duration ||
1151       this.media_.duration < VideoControls.RESUME_THRESHOLD) {
1152     return;
1153   }
1155   var ratio = this.media_.currentTime / this.media_.duration;
1156   var position;
1157   if (ratio < VideoControls.RESUME_MARGIN ||
1158       ratio > (1 - VideoControls.RESUME_MARGIN)) {
1159     // We are too close to the beginning or the end.
1160     // Remove the resume position so that next time we start from the beginning.
1161     position = null;
1162   } else {
1163     position = Math.floor(
1164         Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND));
1165   }
1167   if (opt_sync) {
1168     // Packaged apps cannot save synchronously.
1169     // Pass the data to the background page.
1170     if (!window.saveOnExit)
1171       window.saveOnExit = [];
1172     window.saveOnExit.push({ key: this.media_.src, value: position });
1173   } else {
1174     util.AppCache.update(this.media_.src, position);
1175   }
1179  * Resumes the playback position saved in the persistent storage.
1180  */
1181 VideoControls.prototype.restorePlayState = function() {
1182   if (this.media_ && this.media_.duration >= VideoControls.RESUME_THRESHOLD) {
1183     util.AppCache.getValue(this.media_.src, function(position) {
1184       if (position)
1185         this.media_.currentTime = position;
1186     }.bind(this));
1187   }
1191  * Updates style to best fit the size of the container.
1192  */
1193 VideoControls.prototype.updateStyle = function() {
1194   // We assume that the video controls element fills the parent container.
1195   // This is easier than adding margins to this.container_.clientWidth.
1196   var width = this.container_.parentNode.clientWidth;
1198   // Set the margin to 5px for width >= 400, 0px for width < 160,
1199   // interpolate linearly in between.
1200   this.container_.style.margin =
1201       Math.ceil((Math.max(160, Math.min(width, 400)) - 160) / 48) + 'px';
1203   var hideBelow = function(selector, limit) {
1204     this.container_.querySelector(selector).style.display =
1205         width < limit ? 'none' : '-webkit-box';
1206   }.bind(this);
1208   hideBelow('.time', 350);
1209   hideBelow('.volume', 275);
1210   hideBelow('.volume-controls', 210);
1211   hideBelow('.fullscreen', 150);
1215  * Creates audio controls.
1217  * @param {HTMLElement} container Parent container.
1218  * @param {function(boolean)} advanceTrack Parameter: true=forward.
1219  * @param {function} onError Error handler.
1220  * @constructor
1221  */
1222 function AudioControls(container, advanceTrack, onError) {
1223   MediaControls.call(this, container, onError);
1225   this.container_.classList.add('audio-controls');
1227   this.advanceTrack_ = advanceTrack;
1229   this.initPlayButton();
1230   this.initTimeControls(false /* no seek mark */);
1231   /* No volume controls */
1232   this.createButton('previous', this.onAdvanceClick_.bind(this, false));
1233   this.createButton('next', this.onAdvanceClick_.bind(this, true));
1235   // Disables all controls at first.
1236   this.enableControls_('.media-control', false);
1238   var audioControls = this;
1239   chrome.mediaPlayerPrivate.onNextTrack.addListener(
1240       function() { audioControls.onAdvanceClick_(true); });
1241   chrome.mediaPlayerPrivate.onPrevTrack.addListener(
1242       function() { audioControls.onAdvanceClick_(false); });
1243   chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
1244       function() { audioControls.togglePlayState(); });
1247 AudioControls.prototype = { __proto__: MediaControls.prototype };
1250  * Media completion handler. Advances to the next track.
1251  */
1252 AudioControls.prototype.onMediaComplete = function() {
1253   this.advanceTrack_(true);
1257  * The track position after which "previous" button acts as "restart".
1258  */
1259 AudioControls.TRACK_RESTART_THRESHOLD = 5;  // seconds.
1262  * @param {boolean} forward True if advancing forward.
1263  * @private
1264  */
1265 AudioControls.prototype.onAdvanceClick_ = function(forward) {
1266   if (!forward &&
1267       (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) {
1268     // We are far enough from the beginning of the current track.
1269     // Restart it instead of than skipping to the previous one.
1270     this.getMedia().currentTime = 0;
1271   } else {
1272     this.advanceTrack_(forward);
1273   }