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.
6 * @fileoverview MediaControls class implements media playback controls
7 * that exist outside of the audio/video HTML element.
11 * @param {!HTMLElement} containerElement The container for the controls.
12 * @param {function(Event)} onMediaError Function to display an error message.
16 function MediaControls(containerElement, onMediaError) {
17 this.container_ = containerElement;
18 this.document_ = this.container_.ownerDocument;
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.
33 this.playButton_ = null;
36 * @type {MediaControls.Slider}
39 this.progressSlider_ = null;
45 this.duration_ = null;
48 * @type {MediaControls.AnimatedSlider}
57 this.textBanner_ = null;
63 this.soundButton_ = null;
69 this.resumeAfterDrag_ = false;
75 this.currentTime_ = null;
79 * Button's state types. Values are used as CSS class names.
82 MediaControls.ButtonStateType = {
89 * @return {HTMLAudioElement|HTMLVideoElement} The media element.
91 MediaControls.prototype.getMedia = function() { return this.media_ };
94 * Format the time in hh:mm:ss format (omitting redundant leading zeros).
96 * @param {number} timeInSec Time in seconds.
97 * @return {string} Formatted time string.
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);
105 if (hours) result += hours + ':';
106 if (hours && (minutes < 10)) result += '0';
107 result += minutes + ':';
108 if (seconds < 10) result += '0';
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.
120 MediaControls.prototype.createControl = function(className, opt_parent) {
121 var parent = opt_parent || this.container_;
122 var control = assertInstanceof(this.document_.createElement('div'),
124 control.className = className;
125 parent.appendChild(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.
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);
152 this.createControl('disabled', button);
154 button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT);
157 button.addEventListener('click', opt_handler);
163 * Enable/disable controls matching a given selector.
165 * @param {string} selector CSS selector.
166 * @param {boolean} on True if enable, false if disable.
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;
174 classList.remove('disabled');
176 classList.add('disabled');
187 MediaControls.prototype.play = function() {
189 return; // Media is detached.
197 MediaControls.prototype.pause = function() {
199 return; // Media is detached.
205 * @return {boolean} True if the media is currently playing.
207 MediaControls.prototype.isPlaying = function() {
208 return !!this.media_ && !this.media_.paused && !this.media_.ended;
214 MediaControls.prototype.togglePlayState = function() {
215 if (this.isPlaying())
222 * Toggles play/pause state on a mouse click on the play/pause button.
224 * @param {Event} event Mouse click event.
226 MediaControls.prototype.onPlayButtonClicked = function(event) {
227 this.togglePlayState();
231 * @param {HTMLElement=} opt_parent Parent container.
233 MediaControls.prototype.initPlayButton = function(opt_parent) {
234 this.playButton_ = this.createButton('play media-control',
235 this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */);
243 * The default range of 100 is too coarse for the media progress slider.
245 MediaControls.PROGRESS_RANGE = 5000;
248 * @param {boolean=} opt_seekMark True if the progress slider should have
250 * @param {HTMLElement=} opt_parent Parent container.
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),
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.
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].
289 MediaControls.prototype.onProgressChange_ = function(value) {
291 return; // Media is detached.
293 if (!this.media_.seekable || !this.media_.duration) {
294 console.error('Inconsistent media state');
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.
307 MediaControls.prototype.onProgressDrag_ = function(on) {
309 return; // Media is detached.
312 this.resumeAfterDrag_ = this.isPlaying();
313 this.media_.pause(true /* seeking */);
315 if (this.resumeAfterDrag_) {
316 if (this.media_.ended)
317 this.onMediaPlay_(false);
319 this.media_.play(true /* seeking */);
321 this.updatePlayButtonState_(this.isPlaying());
330 * @param {HTMLElement=} opt_parent Parent element for the controls.
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),
343 this.onVolumeChange_.bind(this),
344 this.onVolumeDrag_.bind(this));
348 * Click handler for the sound level button.
351 MediaControls.prototype.onSoundButtonClick_ = function() {
352 if (this.media_.volume == 0) {
353 this.volume_.setValue(this.savedVolume_ || 1);
355 this.savedVolume_ = this.media_.volume;
356 this.volume_.setValue(0);
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.
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;
374 * @param {number} value Volume [0..1].
377 MediaControls.prototype.onVolumeChange_ = function(value) {
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.
389 MediaControls.prototype.onVolumeDrag_ = function(on) {
390 if (on && (this.media_.volume != 0)) {
391 this.savedVolume_ = this.media_.volume;
396 * Media event handlers.
400 * Attach a media element.
402 * @param {!HTMLMediaElement} mediaElement The media element to control.
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_();
422 /* Copy the user selected volume to the new media element. */
423 this.savedVolume_ = this.media_.volume = this.volume_.getValue();
428 * Detach media event handlers.
430 MediaControls.prototype.detachMedia = function() {
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_);
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.
448 MediaControls.prototype.cleanup = function() {
452 this.media_.src = '';
458 * 'play' and 'pause' event handler.
459 * @param {boolean} playing True if playing.
462 MediaControls.prototype.onMediaPlay_ = function(playing) {
463 if (this.progressSlider_.isDragging())
466 this.updatePlayButtonState_(playing);
467 this.onPlayStateChanged();
471 * 'durationchange' event handler.
474 MediaControls.prototype.onMediaDuration_ = function() {
475 if (!this.media_ || !this.media_.duration) {
476 this.enableControls_('.media-control', false);
480 this.enableControls_('.media-control', true);
482 var sliderContainer = this.progressSlider_.getContainer();
483 if (this.media_.seekable)
484 sliderContainer.classList.remove('readonly');
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);
493 this.duration_.textContent = valueToString(1);
495 this.progressSlider_.setValueToStringFunction(valueToString);
497 if (this.media_.seekable)
498 this.restorePlayState();
502 * 'timeupdate' event handler.
505 MediaControls.prototype.onMediaProgress_ = function() {
506 if (!this.media_ || !this.media_.duration) {
507 this.displayProgress_(0, 1);
511 var current = this.media_.currentTime;
512 var duration = this.media_.duration;
514 if (this.progressSlider_.isDragging())
517 this.displayProgress_(current, duration);
519 if (current == duration) {
520 this.onMediaComplete();
522 this.onPlayStateChanged();
526 * Called when the media playback is complete.
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.
534 MediaControls.prototype.onPlayStateChanged = function() {};
537 * Updates the play button state.
538 * @param {boolean} playing If the video is playing.
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);
549 this.playButton_.setAttribute('state',
550 MediaControls.ButtonStateType.DEFAULT);
555 * Restore play state. Base implementation is empty.
557 MediaControls.prototype.restorePlayState = function() {};
560 * Encode current state into the page URL or the app state.
562 MediaControls.prototype.encodeState = function() {
563 if (!this.media_ || !this.media_.duration)
566 if (window.appState) {
567 window.appState.time = this.media_.currentTime;
574 * Decode current state from the page URL or the app state.
575 * @return {boolean} True if decode succeeded.
577 MediaControls.prototype.decodeState = function() {
578 if (!this.media_ || !window.appState || !('time' in window.appState))
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;
588 * Remove current state from the page URL or the app state.
590 MediaControls.prototype.clearState = function() {
591 if (!window.appState)
594 if ('time' in window.appState)
595 delete window.appState.time;
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.
612 MediaControls.Slider = function(container, value, range, onChange, onDrag) {
613 this.container_ = container;
614 this.onChange_ = onChange;
615 this.onDrag_ = onDrag;
621 this.isDragging_ = false;
623 var document = this.container_.ownerDocument;
625 this.container_.classList.add('custom-slider');
627 this.input_ = assertInstanceof(document.createElement('input'),
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);
659 this.setFilled_(value);
663 * @return {HTMLElement} The container element.
665 MediaControls.Slider.prototype.getContainer = function() {
666 return this.container_;
670 * @return {HTMLElement} The standard input element.
673 MediaControls.Slider.prototype.getInput_ = function() {
678 * @return {HTMLElement} The slider bar element.
680 MediaControls.Slider.prototype.getBar = function() {
685 * @return {number} [0..1] The current value.
687 MediaControls.Slider.prototype.getValue = function() {
692 * @param {number} value [0..1].
694 MediaControls.Slider.prototype.setValue = function(value) {
696 this.setValueToUI_(value);
700 * Fill the given proportion the slider bar (from the left).
702 * @param {number} proportion [0..1].
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].
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].
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.
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.
745 MediaControls.Slider.prototype.setValueToStringFunction = function(func) {};
748 * 'change' event handler.
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.
760 MediaControls.Slider.prototype.isDragging = function() {
761 return this.isDragging_;
765 * Mousedown/mouseup handler.
766 * @param {boolean} on True if the mouse is down.
769 MediaControls.Slider.prototype.onInputDrag_ = function(on) {
770 this.isDragging_ = 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.
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.
792 * @extends {MediaControls.Slider}
794 MediaControls.AnimatedSlider = function(
795 container, value, range, onChange, onDrag) {
796 MediaControls.Slider.apply(this, arguments);
802 this.animationInterval_ = 0;
805 MediaControls.AnimatedSlider.prototype = {
806 __proto__: MediaControls.Slider.prototype
810 * Number of animation steps.
812 MediaControls.AnimatedSlider.STEPS = 10;
815 * Animation duration.
817 MediaControls.AnimatedSlider.DURATION = 100;
820 * @param {number} value [0..1].
823 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
824 if (this.animationInterval_) {
825 clearInterval(this.animationInterval_);
827 var oldValue = this.getValueFromUI_();
829 this.animationInterval_ = setInterval(function() {
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_);
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.
854 * @extends {MediaControls.Slider}
856 MediaControls.PreciseSlider = function(
857 container, value, range, onChange, onDrag, formatFunction) {
858 MediaControls.Slider.apply(this, arguments);
860 var doc = this.container_.ownerDocument;
866 this.latestMouseUpTime_ = 0;
872 this.seekMarkTimer_ = 0;
878 this.latestSeekRatio_ = 0;
881 * @type {?function(number):string}
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.
907 MediaControls.PreciseSlider.SHOW_DELAY = 200;
910 * Hide the seek mark for this long after changing the position with a click.
912 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
915 * Hide the seek mark for this long after changing the position with a drag.
917 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
920 * Default hide timeout (no hiding).
922 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
927 MediaControls.PreciseSlider.prototype.setValueToStringFunction =
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.
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()))
952 this.seekMark_.style.left = ratio * 100 + '%';
954 if (ratio < this.getValue()) {
955 this.seekMark_.classList.remove('inverted');
957 this.seekMark_.classList.add('inverted');
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;
967 if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
968 this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
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.
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);
995 if (this.seekMark_.classList.contains('visible')) {
997 } else if (!this.seekMarkTimer_) {
998 this.seekMarkTimer_ =
999 setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
1004 * 'mouseout' event handler.
1005 * @param {Event} e Event.
1008 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
1009 for (var element = e.relatedTarget; element; element = element.parentNode) {
1010 if (element == this.getContainer())
1013 if (this.seekMarkTimer_) {
1014 clearTimeout(this.seekMarkTimer_);
1015 this.seekMarkTimer_ = 0;
1017 this.hideSeekMark_();
1021 * 'change' event handler.
1024 MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
1025 MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
1026 if (this.isDragging()) {
1028 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
1033 * Mousedown/mouseup handler.
1034 * @param {boolean} on True if the mouse is down.
1037 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) {
1038 MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
1041 // Dragging started, align the seek mark with the thumb position.
1043 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
1045 // Just finished dragging.
1046 // Show the label for the last time with a shorter timeout.
1048 this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
1049 this.latestMouseUpTime_ = Date.now();
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
1060 * @param {function(Event)=} opt_fullScreenToggle Function to toggle fullscreen
1062 * @param {HTMLElement=} opt_stateIconParent The parent for the icon that
1063 * gives visual feedback when the playback state changes.
1066 * @extends {MediaControls}
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);
1090 if (opt_stateIconParent) {
1091 this.stateIcon_ = this.createControl(
1092 'playback-state-icon', opt_stateIconParent);
1093 this.textBanner_ = this.createControl('text-banner', opt_stateIconParent);
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.
1107 VideoControls.RESUME_MARGIN = 0.03;
1110 * No resume for videos shorter than this.
1112 VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min.
1115 * When resuming rewind back this much.
1117 VideoControls.RESUME_REWIND = 5; // seconds.
1119 VideoControls.prototype = { __proto__: MediaControls.prototype };
1122 * Shows icon feedback for the current state of the video player.
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);
1146 * Shows a text banner.
1148 * @param {string} identifier String identifier.
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');
1161 this.textBanner_.addEventListener('webkitAnimationEnd', onAnimationEnd);
1163 this.textBanner_.setAttribute('visible', 'true');
1170 VideoControls.prototype.onPlayButtonClicked = function(event) {
1171 if (event.ctrlKey) {
1172 this.toggleLoopedModeWithFeedback(true);
1173 if (!this.isPlaying())
1174 this.togglePlayState();
1176 this.togglePlayState();
1181 * Media completion handler.
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.
1192 VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) {
1193 if (!this.getMedia().duration)
1195 this.toggleLoopedMode(on);
1197 // TODO(mtomasz): Simplify, crbug.com/254318.
1198 this.showTextBanner_('VIDEO_PLAYER_LOOPED_MODE');
1203 * Toggles the looped mode.
1204 * @param {boolean} on Whether enabled or not.
1206 VideoControls.prototype.toggleLoopedMode = function(on) {
1207 this.getMedia().loop = on;
1211 * Toggles play/pause state and flash an icon over the video.
1213 VideoControls.prototype.togglePlayStateWithFeedback = function() {
1214 if (!this.getMedia().duration)
1217 this.togglePlayState();
1218 this.showIconFeedback_();
1222 * Toggles play/pause state.
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();
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).
1238 VideoControls.prototype.savePosition = function(opt_sync) {
1240 !this.media_.duration ||
1241 this.media_.duration < VideoControls.RESUME_THRESHOLD) {
1245 var ratio = this.media_.currentTime / this.media_.duration;
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.
1253 position = Math.floor(
1254 Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND));
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 });
1264 util.AppCache.update(this.media_.src, position);
1269 * Resumes the playback position saved in the persistent storage.
1271 VideoControls.prototype.restorePlayState = function() {
1272 if (this.media_ && this.media_.duration >= VideoControls.RESUME_THRESHOLD) {
1273 util.AppCache.getValue(this.media_.src, function(position) {
1275 this.media_.currentTime = position;
1281 * Updates style to best fit the size of the container.
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';
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.
1312 * @extends {MediaControls}
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.
1344 AudioControls.prototype.onMediaComplete = function() {
1345 this.advanceTrack_(true);
1349 * The track position after which "previous" button acts as "restart".
1351 AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds.
1354 * @param {boolean} forward True if advancing forward.
1357 AudioControls.prototype.onAdvanceClick_ = function(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;
1364 this.advanceTrack_(forward);