- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / media / media_controls.js
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.
4
5 'use strict';
6
7 /**
8  * @fileoverview MediaControls class implements media playback controls
9  * that exist outside of the audio/video HTML element.
10  */
11
12 /**
13  * @param {HTMLElement} containerElement The container for the controls.
14  * @param {function} onMediaError Function to display an error message.
15  * @constructor
16  */
17 function MediaControls(containerElement, onMediaError) {
18   this.container_ = containerElement;
19   this.document_ = this.container_.ownerDocument;
20   this.media_ = null;
21
22   this.onMediaPlayBound_ = this.onMediaPlay_.bind(this, true);
23   this.onMediaPauseBound_ = this.onMediaPlay_.bind(this, false);
24   this.onMediaDurationBound_ = this.onMediaDuration_.bind(this);
25   this.onMediaProgressBound_ = this.onMediaProgress_.bind(this);
26   this.onMediaError_ = onMediaError || function() {};
27 }
28
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'
37 };
38
39 /**
40  * @return {HTMLAudioElement|HTMLVideoElement} The media element.
41  */
42 MediaControls.prototype.getMedia = function() { return this.media_ };
43
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;
62 };
63
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;
77 };
78
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;
91
92   var button = this.createControl(className, opt_parent);
93   button.classList.add('media-button');
94   button.addEventListener('click', handler);
95
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);
104
105   button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT);
106   button.addEventListener('click', handler);
107   return button;
108 };
109
110 /**
111  * Enable/disable controls matching a given selector.
112  *
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   }
126 };
127
128 /*
129  * Playback control.
130  */
131
132 /**
133  * Play the media.
134  */
135 MediaControls.prototype.play = function() {
136   this.media_.play();
137 };
138
139 /**
140  * Pause the media.
141  */
142 MediaControls.prototype.pause = function() {
143   this.media_.pause();
144 };
145
146 /**
147  * @return {boolean} True if the media is currently playing.
148  */
149 MediaControls.prototype.isPlaying = function() {
150   return !this.media_.paused && !this.media_.ended;
151 };
152
153 /**
154  * Toggle play/pause.
155  */
156 MediaControls.prototype.togglePlayState = function() {
157   if (this.isPlaying())
158     this.pause();
159   else
160     this.play();
161 };
162
163 /**
164  * Toggle play/pause state on a mouse click on the play/pause button. Can be
165  * called externally. TODO(mtomasz): Remove it. http://www.crbug.com/254318.
166  *
167  * @param {Event=} opt_event Mouse click event.
168  */
169 MediaControls.prototype.onPlayButtonClicked = function(opt_event) {
170   this.togglePlayState();
171 };
172
173 /**
174  * @param {HTMLElement=} opt_parent Parent container.
175  */
176 MediaControls.prototype.initPlayButton = function(opt_parent) {
177   this.playButton_ = this.createButton('play media-control',
178       this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */);
179 };
180
181 /*
182  * Time controls
183  */
184
185 /**
186  * The default range of 100 is too coarse for the media progress slider.
187  */
188 MediaControls.PROGRESS_RANGE = 5000;
189
190 /**
191  * @param {boolean=} opt_seekMark True if the progress slider should have
192  *     a seek mark.
193  * @param {HTMLElement=} opt_parent Parent container.
194  */
195 MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) {
196   var timeControls = this.createControl('time-controls', opt_parent);
197
198   var sliderConstructor =
199       opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider;
200
201   this.progressSlider_ = new sliderConstructor(
202       this.createControl('progress media-control', timeControls),
203       0, /* value */
204       MediaControls.PROGRESS_RANGE,
205       this.onProgressChange_.bind(this),
206       this.onProgressDrag_.bind(this));
207
208   var timeBox = this.createControl('time media-control', timeControls);
209
210   this.duration_ = this.createControl('duration', timeBox);
211   // Set the initial width to the minimum to reduce the flicker.
212   this.duration_.textContent = MediaControls.formatTime_(0);
213
214   this.currentTime_ = this.createControl('current', timeBox);
215 };
216
217 /**
218  * @param {number} current Current time is seconds.
219  * @param {number} duration Duration in seconds.
220  * @private
221  */
222 MediaControls.prototype.displayProgress_ = function(current, duration) {
223   var ratio = current / duration;
224   this.progressSlider_.setValue(ratio);
225   this.currentTime_.textContent = MediaControls.formatTime_(current);
226 };
227
228 /**
229  * @param {number} value Progress [0..1].
230  * @private
231  */
232 MediaControls.prototype.onProgressChange_ = function(value) {
233   if (!this.media_.seekable || !this.media_.duration) {
234     console.error('Inconsistent media state');
235     return;
236   }
237
238   var current = this.media_.duration * value;
239   this.media_.currentTime = current;
240   this.currentTime_.textContent = MediaControls.formatTime_(current);
241 };
242
243 /**
244  * @param {boolean} on True if dragging.
245  * @private
246  */
247 MediaControls.prototype.onProgressDrag_ = function(on) {
248   if (on) {
249     this.resumeAfterDrag_ = this.isPlaying();
250     this.media_.pause();
251   } else {
252     if (this.resumeAfterDrag_) {
253       if (this.media_.ended)
254         this.onMediaPlay_(false);
255       else
256         this.media_.play();
257     }
258     this.updatePlayButtonState_(this.isPlaying());
259   }
260 };
261
262 /*
263  * Volume controls
264  */
265
266 /**
267  * @param {HTMLElement=} opt_parent Parent element for the controls.
268  */
269 MediaControls.prototype.initVolumeControls = function(opt_parent) {
270   var volumeControls = this.createControl('volume-controls', opt_parent);
271
272   this.soundButton_ = this.createButton('sound media-control',
273       this.onSoundButtonClick_.bind(this), volumeControls);
274   this.soundButton_.setAttribute('level', 3);  // max level.
275
276   this.volume_ = new MediaControls.AnimatedSlider(
277       this.createControl('volume media-control', volumeControls),
278       1, /* value */
279       100 /* range */,
280       this.onVolumeChange_.bind(this),
281       this.onVolumeDrag_.bind(this));
282 };
283
284 /**
285  * Click handler for the sound level button.
286  * @private
287  */
288 MediaControls.prototype.onSoundButtonClick_ = function() {
289   if (this.media_.volume == 0) {
290     this.volume_.setValue(this.savedVolume_ || 1);
291   } else {
292     this.savedVolume_ = this.media_.volume;
293     this.volume_.setValue(0);
294   }
295   this.onVolumeChange_(this.volume_.getValue());
296 };
297
298 /**
299  * @param {number} value Volume [0..1].
300  * @return {number} The rough level [0..3] used to pick an icon.
301  * @private
302  */
303 MediaControls.getVolumeLevel_ = function(value) {
304   if (value == 0) return 0;
305   if (value <= 1 / 3) return 1;
306   if (value <= 2 / 3) return 2;
307   return 3;
308 };
309
310 /**
311  * @param {number} value Volume [0..1].
312  * @private
313  */
314 MediaControls.prototype.onVolumeChange_ = function(value) {
315   this.media_.volume = value;
316   this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value));
317 };
318
319 /**
320  * @param {boolean} on True if dragging is in progress.
321  * @private
322  */
323 MediaControls.prototype.onVolumeDrag_ = function(on) {
324   if (on && (this.media_.volume != 0)) {
325     this.savedVolume_ = this.media_.volume;
326   }
327 };
328
329 /*
330  * Media event handlers.
331  */
332
333 /**
334  * Attach a media element.
335  *
336  * @param {HTMLMediaElement} mediaElement The media element to control.
337  */
338 MediaControls.prototype.attachMedia = function(mediaElement) {
339   this.media_ = mediaElement;
340
341   this.media_.addEventListener('play', this.onMediaPlayBound_);
342   this.media_.addEventListener('pause', this.onMediaPauseBound_);
343   this.media_.addEventListener('durationchange', this.onMediaDurationBound_);
344   this.media_.addEventListener('timeupdate', this.onMediaProgressBound_);
345   this.media_.addEventListener('error', this.onMediaError_);
346
347   // Reflect the media state in the UI.
348   this.onMediaDuration_();
349   this.onMediaPlay_(this.isPlaying());
350   this.onMediaProgress_();
351   if (this.volume_) {
352     /* Copy the user selected volume to the new media element. */
353     this.media_.volume = this.volume_.getValue();
354   }
355 };
356
357 /**
358  * Detach media event handlers.
359  */
360 MediaControls.prototype.detachMedia = function() {
361   if (!this.media_)
362     return;
363
364   this.media_.removeEventListener('play', this.onMediaPlayBound_);
365   this.media_.removeEventListener('pause', this.onMediaPauseBound_);
366   this.media_.removeEventListener('durationchange', this.onMediaDurationBound_);
367   this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_);
368   this.media_.removeEventListener('error', this.onMediaError_);
369
370   this.media_ = null;
371 };
372
373 /**
374  * Force-empty the media pipeline. This is a workaround for crbug.com/149957.
375  * The document is not going to be GC-ed until the last Files app window closes,
376  * but we want the media pipeline to deinitialize ASAP to minimize leakage.
377  */
378 MediaControls.prototype.cleanup = function() {
379   this.media_.src = '';
380   this.media_.load();
381   this.detachMedia();
382 };
383
384 /**
385  * 'play' and 'pause' event handler.
386  * @param {boolean} playing True if playing.
387  * @private
388  */
389 MediaControls.prototype.onMediaPlay_ = function(playing) {
390   if (this.progressSlider_.isDragging())
391     return;
392
393   this.updatePlayButtonState_(playing);
394   this.onPlayStateChanged();
395 };
396
397 /**
398  * 'durationchange' event handler.
399  * @private
400  */
401 MediaControls.prototype.onMediaDuration_ = function() {
402   if (!this.media_.duration) {
403     this.enableControls_('.media-control', false);
404     return;
405   }
406
407   this.enableControls_('.media-control', true);
408
409   var sliderContainer = this.progressSlider_.getContainer();
410   if (this.media_.seekable)
411     sliderContainer.classList.remove('readonly');
412   else
413     sliderContainer.classList.add('readonly');
414
415   var valueToString = function(value) {
416     return MediaControls.formatTime_(this.media_.duration * value);
417   }.bind(this);
418
419   this.duration_.textContent = valueToString(1);
420
421   if (this.progressSlider_.setValueToStringFunction)
422     this.progressSlider_.setValueToStringFunction(valueToString);
423
424   if (this.media_.seekable)
425     this.restorePlayState();
426 };
427
428 /**
429  * 'timeupdate' event handler.
430  * @private
431  */
432 MediaControls.prototype.onMediaProgress_ = function() {
433   if (!this.media_.duration) {
434     this.displayProgress_(0, 1);
435     return;
436   }
437
438   var current = this.media_.currentTime;
439   var duration = this.media_.duration;
440
441   if (this.progressSlider_.isDragging())
442     return;
443
444   this.displayProgress_(current, duration);
445
446   if (current == duration) {
447     this.onMediaComplete();
448   }
449   this.onPlayStateChanged();
450 };
451
452 /**
453  * Called when the media playback is complete.
454  */
455 MediaControls.prototype.onMediaComplete = function() {};
456
457 /**
458  * Called when play/pause state is changed or on playback progress.
459  * This is the right moment to save the play state.
460  */
461 MediaControls.prototype.onPlayStateChanged = function() {};
462
463 /**
464  * Updates the play button state.
465  * @param {boolean} playing If the video is playing.
466  * @private
467  */
468 MediaControls.prototype.updatePlayButtonState_ = function(playing) {
469   if (playing) {
470     this.playButton_.setAttribute('state',
471                                   MediaControls.ButtonStateType.PLAYING);
472   } else if (!this.media_.ended) {
473     this.playButton_.setAttribute('state',
474                                   MediaControls.ButtonStateType.DEFAULT);
475   } else {
476     this.playButton_.setAttribute('state',
477                                   MediaControls.ButtonStateType.ENDED);
478   }
479 };
480
481 /**
482  * Restore play state. Base implementation is empty.
483  */
484 MediaControls.prototype.restorePlayState = function() {};
485
486 /**
487  * Encode current state into the page URL or the app state.
488  */
489 MediaControls.prototype.encodeState = function() {
490   if (!this.media_.duration)
491     return;
492
493   if (window.appState) {
494     window.appState.time = this.media_.currentTime;
495     util.saveAppState();
496     return;
497   }
498
499   var playState = JSON.stringify({
500       play: this.isPlaying(),
501       time: this.media_.currentTime
502     });
503
504   var newLocation = document.location.origin + document.location.pathname +
505       document.location.search + '#' + playState;
506
507   document.location.href = newLocation;
508 };
509
510 /**
511  * Decode current state from the page URL or the app state.
512  * @return {boolean} True if decode succeeded.
513  */
514 MediaControls.prototype.decodeState = function() {
515   if (window.appState) {
516     if (!('time' in window.appState))
517       return false;
518     // There is no page reload for apps v2, only app restart.
519     // Always restart in paused state.
520     this.media_.currentTime = appState.time;
521     this.pause();
522     return true;
523   }
524
525   var hash = document.location.hash.substring(1);
526   if (hash) {
527     try {
528       var playState = JSON.parse(hash);
529       if (!('time' in playState))
530         return false;
531
532       this.media_.currentTime = playState.time;
533
534       if (playState.play)
535         this.play();
536       else
537         this.pause();
538
539       return true;
540     } catch (e) {
541       console.warn('Cannot decode play state');
542     }
543   }
544   return false;
545 };
546
547 /**
548  * Remove current state from the page URL or the app state.
549  */
550 MediaControls.prototype.clearState = function() {
551   if (window.appState) {
552     if ('time' in window.appState)
553       delete window.appState.time;
554     util.saveAppState();
555     return;
556   }
557
558   var newLocation = document.location.origin + document.location.pathname +
559       document.location.search + '#';
560
561   document.location.href = newLocation;
562 };
563
564 /**
565  * Create a customized slider control.
566  *
567  * @param {HTMLElement} container The containing div element.
568  * @param {number} value Initial value [0..1].
569  * @param {number} range Number of distinct slider positions to be supported.
570  * @param {function(number)} onChange Value change handler.
571  * @param {function(boolean)} onDrag Drag begin/end handler.
572  * @constructor
573  */
574
575 MediaControls.Slider = function(container, value, range, onChange, onDrag) {
576   this.container_ = container;
577   this.onChange_ = onChange;
578   this.onDrag_ = onDrag;
579
580   var document = this.container_.ownerDocument;
581
582   this.container_.classList.add('custom-slider');
583
584   this.input_ = document.createElement('input');
585   this.input_.type = 'range';
586   this.input_.min = 0;
587   this.input_.max = range;
588   this.input_.value = value * range;
589   this.container_.appendChild(this.input_);
590
591   this.input_.addEventListener(
592       'change', this.onInputChange_.bind(this));
593   this.input_.addEventListener(
594       'mousedown', this.onInputDrag_.bind(this, true));
595   this.input_.addEventListener(
596       'mouseup', this.onInputDrag_.bind(this, false));
597
598   this.bar_ = document.createElement('div');
599   this.bar_.className = 'bar';
600   this.container_.appendChild(this.bar_);
601
602   this.filled_ = document.createElement('div');
603   this.filled_.className = 'filled';
604   this.bar_.appendChild(this.filled_);
605
606   var leftCap = document.createElement('div');
607   leftCap.className = 'cap left';
608   this.bar_.appendChild(leftCap);
609
610   var rightCap = document.createElement('div');
611   rightCap.className = 'cap right';
612   this.bar_.appendChild(rightCap);
613
614   this.value_ = value;
615   this.setFilled_(value);
616 };
617
618 /**
619  * @return {HTMLElement} The container element.
620  */
621 MediaControls.Slider.prototype.getContainer = function() {
622   return this.container_;
623 };
624
625 /**
626  * @return {HTMLElement} The standard input element.
627  * @private
628  */
629 MediaControls.Slider.prototype.getInput_ = function() {
630   return this.input_;
631 };
632
633 /**
634  * @return {HTMLElement} The slider bar element.
635  */
636 MediaControls.Slider.prototype.getBar = function() {
637   return this.bar_;
638 };
639
640 /**
641  * @return {number} [0..1] The current value.
642  */
643 MediaControls.Slider.prototype.getValue = function() {
644   return this.value_;
645 };
646
647 /**
648  * @param {number} value [0..1].
649  */
650 MediaControls.Slider.prototype.setValue = function(value) {
651   this.value_ = value;
652   this.setValueToUI_(value);
653 };
654
655 /**
656  * Fill the given proportion the slider bar (from the left).
657  *
658  * @param {number} proportion [0..1].
659  * @private
660  */
661 MediaControls.Slider.prototype.setFilled_ = function(proportion) {
662   this.filled_.style.width = proportion * 100 + '%';
663 };
664
665 /**
666  * Get the value from the input element.
667  *
668  * @return {number} Value [0..1].
669  * @private
670  */
671 MediaControls.Slider.prototype.getValueFromUI_ = function() {
672   return this.input_.value / this.input_.max;
673 };
674
675 /**
676  * Update the UI with the current value.
677  *
678  * @param {number} value [0..1].
679  * @private
680  */
681 MediaControls.Slider.prototype.setValueToUI_ = function(value) {
682   this.input_.value = value * this.input_.max;
683   this.setFilled_(value);
684 };
685
686 /**
687  * Compute the proportion in which the given position divides the slider bar.
688  *
689  * @param {number} position in pixels.
690  * @return {number} [0..1] proportion.
691  */
692 MediaControls.Slider.prototype.getProportion = function(position) {
693   var rect = this.bar_.getBoundingClientRect();
694   return Math.max(0, Math.min(1, (position - rect.left) / rect.width));
695 };
696
697 /**
698  * 'change' event handler.
699  * @private
700  */
701 MediaControls.Slider.prototype.onInputChange_ = function() {
702   this.value_ = this.getValueFromUI_();
703   this.setFilled_(this.value_);
704   this.onChange_(this.value_);
705 };
706
707 /**
708  * @return {boolean} True if dragging is in progress.
709  */
710 MediaControls.Slider.prototype.isDragging = function() {
711   return this.isDragging_;
712 };
713
714 /**
715  * Mousedown/mouseup handler.
716  * @param {boolean} on True if the mouse is down.
717  * @private
718  */
719 MediaControls.Slider.prototype.onInputDrag_ = function(on) {
720   this.isDragging_ = on;
721   this.onDrag_(on);
722 };
723
724 /**
725  * Create a customized slider with animated thumb movement.
726  *
727  * @param {HTMLElement} container The containing div element.
728  * @param {number} value Initial value [0..1].
729  * @param {number} range Number of distinct slider positions to be supported.
730  * @param {function(number)} onChange Value change handler.
731  * @param {function(boolean)} onDrag Drag begin/end handler.
732  * @param {function(number):string} formatFunction Value formatting function.
733  * @constructor
734  */
735 MediaControls.AnimatedSlider = function(
736     container, value, range, onChange, onDrag, formatFunction) {
737   MediaControls.Slider.apply(this, arguments);
738 };
739
740 MediaControls.AnimatedSlider.prototype = {
741   __proto__: MediaControls.Slider.prototype
742 };
743
744 /**
745  * Number of animation steps.
746  */
747 MediaControls.AnimatedSlider.STEPS = 10;
748
749 /**
750  * Animation duration.
751  */
752 MediaControls.AnimatedSlider.DURATION = 100;
753
754 /**
755  * @param {number} value [0..1].
756  * @private
757  */
758 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
759   if (this.animationInterval_) {
760     clearInterval(this.animationInterval_);
761   }
762   var oldValue = this.getValueFromUI_();
763   var step = 0;
764   this.animationInterval_ = setInterval(function() {
765       step++;
766       var currentValue = oldValue +
767           (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS);
768       MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue);
769       if (step == MediaControls.AnimatedSlider.STEPS) {
770         clearInterval(this.animationInterval_);
771       }
772     }.bind(this),
773     MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS);
774 };
775
776 /**
777  * Create a customized slider with a precise time feedback.
778  *
779  * The time value is shown above the slider bar at the mouse position.
780  *
781  * @param {HTMLElement} container The containing div element.
782  * @param {number} value Initial value [0..1].
783  * @param {number} range Number of distinct slider positions to be supported.
784  * @param {function(number)} onChange Value change handler.
785  * @param {function(boolean)} onDrag Drag begin/end handler.
786  * @param {function(number):string} formatFunction Value formatting function.
787  * @constructor
788  */
789 MediaControls.PreciseSlider = function(
790     container, value, range, onChange, onDrag, formatFunction) {
791   MediaControls.Slider.apply(this, arguments);
792
793   var doc = this.container_.ownerDocument;
794
795   /**
796    * @type {function(number):string}
797    * @private
798    */
799   this.valueToString_ = null;
800
801   this.seekMark_ = doc.createElement('div');
802   this.seekMark_.className = 'seek-mark';
803   this.getBar().appendChild(this.seekMark_);
804
805   this.seekLabel_ = doc.createElement('div');
806   this.seekLabel_.className = 'seek-label';
807   this.seekMark_.appendChild(this.seekLabel_);
808
809   this.getContainer().addEventListener(
810       'mousemove', this.onMouseMove_.bind(this));
811   this.getContainer().addEventListener(
812       'mouseout', this.onMouseOut_.bind(this));
813 };
814
815 MediaControls.PreciseSlider.prototype = {
816   __proto__: MediaControls.Slider.prototype
817 };
818
819 /**
820  * Show the seek mark after a delay.
821  */
822 MediaControls.PreciseSlider.SHOW_DELAY = 200;
823
824 /**
825  * Hide the seek mark for this long after changing the position with a click.
826  */
827 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
828
829 /**
830  * Hide the seek mark for this long after changing the position with a drag.
831  */
832 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
833
834 /**
835  * Default hide timeout (no hiding).
836  */
837 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
838
839 /**
840  * @param {function(number):string} func Value formatting function.
841  */
842 MediaControls.PreciseSlider.prototype.setValueToStringFunction =
843     function(func) {
844   this.valueToString_ = func;
845
846   /* It is not completely accurate to assume that the max value corresponds
847    to the longest string, but generous CSS padding will compensate for that. */
848   var labelWidth = this.valueToString_(1).length / 2 + 1;
849   this.seekLabel_.style.width = labelWidth + 'em';
850   this.seekLabel_.style.marginLeft = -labelWidth / 2 + 'em';
851 };
852
853 /**
854  * Show the time above the slider.
855  *
856  * @param {number} ratio [0..1] The proportion of the duration.
857  * @param {number} timeout Timeout in ms after which the label should be hidden.
858  *     MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call.
859  * @private
860  */
861 MediaControls.PreciseSlider.prototype.showSeekMark_ =
862     function(ratio, timeout) {
863   // Do not update the seek mark for the first 500ms after the drag is finished.
864   if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now()))
865     return;
866
867   this.seekMark_.style.left = ratio * 100 + '%';
868
869   if (ratio < this.getValue()) {
870     this.seekMark_.classList.remove('inverted');
871   } else {
872     this.seekMark_.classList.add('inverted');
873   }
874   this.seekLabel_.textContent = this.valueToString_(ratio);
875
876   this.seekMark_.classList.add('visible');
877
878   if (this.seekMarkTimer_) {
879     clearTimeout(this.seekMarkTimer_);
880     this.seekMarkTimer_ = null;
881   }
882   if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
883     this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
884   }
885 };
886
887 /**
888  * @private
889  */
890 MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() {
891   this.seekMarkTimer_ = null;
892   this.seekMark_.classList.remove('visible');
893 };
894
895 /**
896  * 'mouseout' event handler.
897  * @param {Event} e Event.
898  * @private
899  */
900 MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) {
901   this.latestSeekRatio_ = this.getProportion(e.clientX);
902
903   var self = this;
904   function showMark() {
905     if (!self.isDragging()) {
906       self.showSeekMark_(self.latestSeekRatio_,
907           MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY);
908     }
909   }
910
911   if (this.seekMark_.classList.contains('visible')) {
912     showMark();
913   } else if (!this.seekMarkTimer_) {
914     this.seekMarkTimer_ =
915         setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
916   }
917 };
918
919 /**
920  * 'mouseout' event handler.
921  * @param {Event} e Event.
922  * @private
923  */
924 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
925   for (var element = e.relatedTarget; element; element = element.parentNode) {
926     if (element == this.getContainer())
927       return;
928   }
929   if (this.seekMarkTimer_) {
930     clearTimeout(this.seekMarkTimer_);
931     this.seekMarkTimer_ = null;
932   }
933   this.hideSeekMark_();
934 };
935
936 /**
937  * 'change' event handler.
938  * @private
939  */
940 MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
941   MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
942   if (this.isDragging()) {
943     this.showSeekMark_(
944         this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
945   }
946 };
947
948 /**
949  * Mousedown/mouseup handler.
950  * @param {boolean} on True if the mouse is down.
951  * @private
952  */
953 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) {
954   MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
955
956   if (on) {
957     // Dragging started, align the seek mark with the thumb position.
958     this.showSeekMark_(
959         this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
960   } else {
961     // Just finished dragging.
962     // Show the label for the last time with a shorter timeout.
963     this.showSeekMark_(
964         this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
965     this.latestMouseUpTime_ = Date.now();
966   }
967 };
968
969 /**
970  * Create video controls.
971  *
972  * @param {HTMLElement} containerElement The container for the controls.
973  * @param {function} onMediaError Function to display an error message.
974  * @param {function(string):string} stringFunction Function providing localized
975  *     strings.
976  * @param {function=} opt_fullScreenToggle Function to toggle fullscreen mode.
977  * @param {HTMLElement=} opt_stateIconParent The parent for the icon that
978  *     gives visual feedback when the playback state changes.
979  * @constructor
980  */
981 function VideoControls(containerElement, onMediaError, stringFunction,
982    opt_fullScreenToggle, opt_stateIconParent) {
983   MediaControls.call(this, containerElement, onMediaError);
984   this.stringFunction_ = stringFunction;
985
986   this.container_.classList.add('video-controls');
987   this.initPlayButton();
988   this.initTimeControls(true /* show seek mark */);
989   this.initVolumeControls();
990
991   if (opt_fullScreenToggle) {
992     this.fullscreenButton_ =
993         this.createButton('fullscreen', opt_fullScreenToggle);
994   }
995
996   if (opt_stateIconParent) {
997     this.stateIcon_ = this.createControl(
998         'playback-state-icon', opt_stateIconParent);
999     this.textBanner_ = this.createControl('text-banner', opt_stateIconParent);
1000   }
1001
1002   var videoControls = this;
1003   chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
1004       function() { videoControls.togglePlayStateWithFeedback(); });
1005 }
1006
1007 /**
1008  * No resume if we are within this margin from the start or the end.
1009  */
1010 VideoControls.RESUME_MARGIN = 0.03;
1011
1012 /**
1013  * No resume for videos shorter than this.
1014  */
1015 VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min.
1016
1017 /**
1018  * When resuming rewind back this much.
1019  */
1020 VideoControls.RESUME_REWIND = 5;  // seconds.
1021
1022 VideoControls.prototype = { __proto__: MediaControls.prototype };
1023
1024 /**
1025  * Shows icon feedback for the current state of the video player.
1026  * @private
1027  */
1028 VideoControls.prototype.showIconFeedback_ = function() {
1029   this.stateIcon_.removeAttribute('state');
1030   setTimeout(function() {
1031     this.stateIcon_.setAttribute('state', this.isPlaying() ? 'play' : 'pause');
1032   }.bind(this), 0);
1033 };
1034
1035 /**
1036  * Shows a text banner.
1037  *
1038  * @param {string} identifier String identifier.
1039  * @private
1040  */
1041 VideoControls.prototype.showTextBanner_ = function(identifier) {
1042   this.textBanner_.removeAttribute('visible');
1043   this.textBanner_.textContent = this.stringFunction_(identifier);
1044   setTimeout(function() {
1045     this.textBanner_.setAttribute('visible', 'true');
1046   }.bind(this), 0);
1047 };
1048
1049 /**
1050  * Toggle play/pause state on a mouse click on the play/pause button. Can be
1051  * called externally.
1052  *
1053  * @param {Event} event Mouse click event.
1054  */
1055 VideoControls.prototype.onPlayButtonClicked = function(event) {
1056   if (event.ctrlKey) {
1057     this.toggleLoopedModeWithFeedback(true);
1058     if (!this.isPlaying())
1059       this.togglePlayState();
1060   } else {
1061     this.togglePlayState();
1062   }
1063 };
1064
1065 /**
1066  * Media completion handler.
1067  */
1068 VideoControls.prototype.onMediaComplete = function() {
1069   this.onMediaPlay_(false);  // Just update the UI.
1070   this.savePosition();  // This will effectively forget the position.
1071 };
1072
1073 /**
1074  * Toggles the looped mode with feedback.
1075  * @param {boolean} on Whether enabled or not.
1076  */
1077 VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) {
1078   if (!this.getMedia().duration)
1079     return;
1080   this.toggleLoopedMode(on);
1081   if (on) {
1082     // TODO(mtomasz): Simplify, crbug.com/254318.
1083     this.showTextBanner_('GALLERY_VIDEO_LOOPED_MODE');
1084   }
1085 };
1086
1087 /**
1088  * Toggles the looped mode.
1089  * @param {boolean} on Whether enabled or not.
1090  */
1091 VideoControls.prototype.toggleLoopedMode = function(on) {
1092   this.getMedia().loop = on;
1093 };
1094
1095 /**
1096  * Toggles play/pause state and flash an icon over the video.
1097  */
1098 VideoControls.prototype.togglePlayStateWithFeedback = function() {
1099   if (!this.getMedia().duration)
1100     return;
1101
1102   this.togglePlayState();
1103   this.showIconFeedback_();
1104 };
1105
1106 /**
1107  * Toggles play/pause state.
1108  */
1109 VideoControls.prototype.togglePlayState = function() {
1110   if (this.isPlaying()) {
1111     // User gave the Pause command. Save the state and reset the loop mode.
1112     this.toggleLoopedMode(false);
1113     this.savePosition();
1114   }
1115   MediaControls.prototype.togglePlayState.apply(this, arguments);
1116 };
1117
1118 /**
1119  * Saves the playback position to the persistent storage.
1120  * @param {boolean=} opt_sync True if the position must be saved synchronously
1121  *     (required when closing app windows).
1122  */
1123 VideoControls.prototype.savePosition = function(opt_sync) {
1124   if (!this.media_.duration ||
1125       this.media_.duration < VideoControls.RESUME_THRESHOLD) {
1126     return;
1127   }
1128
1129   var ratio = this.media_.currentTime / this.media_.duration;
1130   var position;
1131   if (ratio < VideoControls.RESUME_MARGIN ||
1132       ratio > (1 - VideoControls.RESUME_MARGIN)) {
1133     // We are too close to the beginning or the end.
1134     // Remove the resume position so that next time we start from the beginning.
1135     position = null;
1136   } else {
1137     position = Math.floor(
1138         Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND));
1139   }
1140
1141   if (opt_sync) {
1142     // Packaged apps cannot save synchronously.
1143     // Pass the data to the background page.
1144     if (!window.saveOnExit)
1145       window.saveOnExit = [];
1146     window.saveOnExit.push({ key: this.media_.src, value: position });
1147   } else {
1148     util.AppCache.update(this.media_.src, position);
1149   }
1150 };
1151
1152 /**
1153  * Resumes the playback position saved in the persistent storage.
1154  */
1155 VideoControls.prototype.restorePlayState = function() {
1156   if (this.media_.duration >= VideoControls.RESUME_THRESHOLD) {
1157     util.AppCache.getValue(this.media_.src, function(position) {
1158       if (position)
1159         this.media_.currentTime = position;
1160     }.bind(this));
1161   }
1162 };
1163
1164 /**
1165  * Updates style to best fit the size of the container.
1166  */
1167 VideoControls.prototype.updateStyle = function() {
1168   // We assume that the video controls element fills the parent container.
1169   // This is easier than adding margins to this.container_.clientWidth.
1170   var width = this.container_.parentNode.clientWidth;
1171
1172   // Set the margin to 5px for width >= 400, 0px for width < 160,
1173   // interpolate linearly in between.
1174   this.container_.style.margin =
1175       Math.ceil((Math.max(160, Math.min(width, 400)) - 160) / 48) + 'px';
1176
1177   var hideBelow = function(selector, limit) {
1178     this.container_.querySelector(selector).style.display =
1179         width < limit ? 'none' : '-webkit-box';
1180   }.bind(this);
1181
1182   hideBelow('.time', 350);
1183   hideBelow('.volume', 275);
1184   hideBelow('.volume-controls', 210);
1185   hideBelow('.fullscreen', 150);
1186 };
1187
1188 /**
1189  * Creates audio controls.
1190  *
1191  * @param {HTMLElement} container Parent container.
1192  * @param {function(boolean)} advanceTrack Parameter: true=forward.
1193  * @param {function} onError Error handler.
1194  * @constructor
1195  */
1196 function AudioControls(container, advanceTrack, onError) {
1197   MediaControls.call(this, container, onError);
1198
1199   this.container_.classList.add('audio-controls');
1200
1201   this.advanceTrack_ = advanceTrack;
1202
1203   this.initPlayButton();
1204   this.initTimeControls(false /* no seek mark */);
1205   /* No volume controls */
1206   this.createButton('previous', this.onAdvanceClick_.bind(this, false));
1207   this.createButton('next', this.onAdvanceClick_.bind(this, true));
1208
1209   var audioControls = this;
1210   chrome.mediaPlayerPrivate.onNextTrack.addListener(
1211       function() { audioControls.onAdvanceClick_(true); });
1212   chrome.mediaPlayerPrivate.onPrevTrack.addListener(
1213       function() { audioControls.onAdvanceClick_(false); });
1214   chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
1215       function() { audioControls.togglePlayState(); });
1216 }
1217
1218 AudioControls.prototype = { __proto__: MediaControls.prototype };
1219
1220 /**
1221  * Media completion handler. Advances to the next track.
1222  */
1223 AudioControls.prototype.onMediaComplete = function() {
1224   this.advanceTrack_(true);
1225 };
1226
1227 /**
1228  * The track position after which "previous" button acts as "restart".
1229  */
1230 AudioControls.TRACK_RESTART_THRESHOLD = 5;  // seconds.
1231
1232 /**
1233  * @param {boolean} forward True if advancing forward.
1234  * @private
1235  */
1236 AudioControls.prototype.onAdvanceClick_ = function(forward) {
1237   if (!forward &&
1238       (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) {
1239     // We are far enough from the beginning of the current track.
1240     // Restart it instead of than skipping to the previous one.
1241     this.getMedia().currentTime = 0;
1242   } else {
1243     this.advanceTrack_(forward);
1244   }
1245 };