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} onMediaError Function to display an error message.
15 function MediaControls(containerElement, onMediaError) {
16 this.container_ = containerElement;
17 this.document_ = this.container_.ownerDocument;
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.
30 * Button's state types. Values are used as CSS class names.
33 MediaControls.ButtonStateType = {
40 * @return {HTMLAudioElement|HTMLVideoElement} The media element.
42 MediaControls.prototype.getMedia = function() { return this.media_ };
45 * Format the time in hh:mm:ss format (omitting redundant leading zeros).
47 * @param {number} timeInSec Time in seconds.
48 * @return {string} Formatted time string.
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);
56 if (hours) result += hours + ':';
57 if (hours && (minutes < 10)) result += '0';
58 result += minutes + ':';
59 if (seconds < 10) result += '0';
65 * Create a custom control.
67 * @param {string} className Class name.
68 * @param {HTMLElement=} opt_parent Parent element or container if undefined.
69 * @return {HTMLElement} The new control element.
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);
80 * Create a custom button.
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.
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);
103 this.createControl('disabled', button);
105 button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT);
106 button.addEventListener('click', handler);
111 * Enable/disable controls matching a given selector.
113 * @param {string} selector CSS selector.
114 * @param {boolean} on True if enable, false if disable.
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;
122 classList.remove('disabled');
124 classList.add('disabled');
135 MediaControls.prototype.play = function() {
137 return; // Media is detached.
145 MediaControls.prototype.pause = function() {
147 return; // Media is detached.
153 * @return {boolean} True if the media is currently playing.
155 MediaControls.prototype.isPlaying = function() {
156 return this.media_ && !this.media_.paused && !this.media_.ended;
162 MediaControls.prototype.togglePlayState = function() {
163 if (this.isPlaying())
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.
175 MediaControls.prototype.onPlayButtonClicked = function(opt_event) {
176 this.togglePlayState();
180 * @param {HTMLElement=} opt_parent Parent container.
182 MediaControls.prototype.initPlayButton = function(opt_parent) {
183 this.playButton_ = this.createButton('play media-control',
184 this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */);
192 * The default range of 100 is too coarse for the media progress slider.
194 MediaControls.PROGRESS_RANGE = 5000;
197 * @param {boolean=} opt_seekMark True if the progress slider should have
199 * @param {HTMLElement=} opt_parent Parent container.
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),
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.
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].
238 MediaControls.prototype.onProgressChange_ = function(value) {
240 return; // Media is detached.
242 if (!this.media_.seekable || !this.media_.duration) {
243 console.error('Inconsistent media state');
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.
256 MediaControls.prototype.onProgressDrag_ = function(on) {
258 return; // Media is detached.
261 this.resumeAfterDrag_ = this.isPlaying();
262 this.media_.pause(true /* seeking */);
264 if (this.resumeAfterDrag_) {
265 if (this.media_.ended)
266 this.onMediaPlay_(false);
268 this.media_.play(true /* seeking */);
270 this.updatePlayButtonState_(this.isPlaying());
279 * @param {HTMLElement=} opt_parent Parent element for the controls.
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),
292 this.onVolumeChange_.bind(this),
293 this.onVolumeDrag_.bind(this));
297 * Click handler for the sound level button.
300 MediaControls.prototype.onSoundButtonClick_ = function() {
301 if (this.media_.volume == 0) {
302 this.volume_.setValue(this.savedVolume_ || 1);
304 this.savedVolume_ = this.media_.volume;
305 this.volume_.setValue(0);
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.
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;
323 * @param {number} value Volume [0..1].
326 MediaControls.prototype.onVolumeChange_ = function(value) {
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.
338 MediaControls.prototype.onVolumeDrag_ = function(on) {
339 if (on && (this.media_.volume != 0)) {
340 this.savedVolume_ = this.media_.volume;
345 * Media event handlers.
349 * Attach a media element.
351 * @param {HTMLMediaElement} mediaElement The media element to control.
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_();
371 /* Copy the user selected volume to the new media element. */
372 this.savedVolume_ = this.media_.volume = this.volume_.getValue();
377 * Detach media event handlers.
379 MediaControls.prototype.detachMedia = function() {
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_);
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.
397 MediaControls.prototype.cleanup = function() {
401 this.media_.src = '';
407 * 'play' and 'pause' event handler.
408 * @param {boolean} playing True if playing.
411 MediaControls.prototype.onMediaPlay_ = function(playing) {
412 if (this.progressSlider_.isDragging())
415 this.updatePlayButtonState_(playing);
416 this.onPlayStateChanged();
420 * 'durationchange' event handler.
423 MediaControls.prototype.onMediaDuration_ = function() {
424 if (!this.media_ || !this.media_.duration) {
425 this.enableControls_('.media-control', false);
429 this.enableControls_('.media-control', true);
431 var sliderContainer = this.progressSlider_.getContainer();
432 if (this.media_.seekable)
433 sliderContainer.classList.remove('readonly');
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);
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.
455 MediaControls.prototype.onMediaProgress_ = function() {
456 if (!this.media_ || !this.media_.duration) {
457 this.displayProgress_(0, 1);
461 var current = this.media_.currentTime;
462 var duration = this.media_.duration;
464 if (this.progressSlider_.isDragging())
467 this.displayProgress_(current, duration);
469 if (current == duration) {
470 this.onMediaComplete();
472 this.onPlayStateChanged();
476 * Called when the media playback is complete.
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.
484 MediaControls.prototype.onPlayStateChanged = function() {};
487 * Updates the play button state.
488 * @param {boolean} playing If the video is playing.
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);
499 this.playButton_.setAttribute('state',
500 MediaControls.ButtonStateType.DEFAULT);
505 * Restore play state. Base implementation is empty.
507 MediaControls.prototype.restorePlayState = function() {};
510 * Encode current state into the page URL or the app state.
512 MediaControls.prototype.encodeState = function() {
513 if (!this.media_ || !this.media_.duration)
516 if (window.appState) {
517 window.appState.time = this.media_.currentTime;
524 * Decode current state from the page URL or the app state.
525 * @return {boolean} True if decode succeeded.
527 MediaControls.prototype.decodeState = function() {
528 if (!this.media_ || !window.appState || !('time' in window.appState))
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;
538 * Remove current state from the page URL or the app state.
540 MediaControls.prototype.clearState = function() {
541 if (!window.appState)
544 if ('time' in window.appState)
545 delete window.appState.time;
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.
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';
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);
601 this.setFilled_(value);
605 * @return {HTMLElement} The container element.
607 MediaControls.Slider.prototype.getContainer = function() {
608 return this.container_;
612 * @return {HTMLElement} The standard input element.
615 MediaControls.Slider.prototype.getInput_ = function() {
620 * @return {HTMLElement} The slider bar element.
622 MediaControls.Slider.prototype.getBar = function() {
627 * @return {number} [0..1] The current value.
629 MediaControls.Slider.prototype.getValue = function() {
634 * @param {number} value [0..1].
636 MediaControls.Slider.prototype.setValue = function(value) {
638 this.setValueToUI_(value);
642 * Fill the given proportion the slider bar (from the left).
644 * @param {number} proportion [0..1].
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].
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].
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.
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.
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.
696 MediaControls.Slider.prototype.isDragging = function() {
697 return this.isDragging_;
701 * Mousedown/mouseup handler.
702 * @param {boolean} on True if the mouse is down.
705 MediaControls.Slider.prototype.onInputDrag_ = function(on) {
706 this.isDragging_ = 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.
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.
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.
741 MediaControls.AnimatedSlider.STEPS = 10;
744 * Animation duration.
746 MediaControls.AnimatedSlider.DURATION = 100;
749 * @param {number} value [0..1].
752 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
753 if (this.animationInterval_) {
754 clearInterval(this.animationInterval_);
756 var oldValue = this.getValueFromUI_();
758 this.animationInterval_ = setInterval(function() {
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_);
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.
783 MediaControls.PreciseSlider = function(
784 container, value, range, onChange, onDrag, formatFunction) {
785 MediaControls.Slider.apply(this, arguments);
787 var doc = this.container_.ownerDocument;
790 * @type {function(number):string}
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.
816 MediaControls.PreciseSlider.SHOW_DELAY = 200;
819 * Hide the seek mark for this long after changing the position with a click.
821 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
824 * Hide the seek mark for this long after changing the position with a drag.
826 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
829 * Default hide timeout (no hiding).
831 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
834 * @param {function(number):string} func Value formatting function.
836 MediaControls.PreciseSlider.prototype.setValueToStringFunction =
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.
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()))
861 this.seekMark_.style.left = ratio * 100 + '%';
863 if (ratio < this.getValue()) {
864 this.seekMark_.classList.remove('inverted');
866 this.seekMark_.classList.add('inverted');
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;
876 if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
877 this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
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.
894 MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) {
895 this.latestSeekRatio_ = this.getProportion(e.clientX);
898 function showMark() {
899 if (!self.isDragging()) {
900 self.showSeekMark_(self.latestSeekRatio_,
901 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY);
905 if (this.seekMark_.classList.contains('visible')) {
907 } else if (!this.seekMarkTimer_) {
908 this.seekMarkTimer_ =
909 setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
914 * 'mouseout' event handler.
915 * @param {Event} e Event.
918 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
919 for (var element = e.relatedTarget; element; element = element.parentNode) {
920 if (element == this.getContainer())
923 if (this.seekMarkTimer_) {
924 clearTimeout(this.seekMarkTimer_);
925 this.seekMarkTimer_ = null;
927 this.hideSeekMark_();
931 * 'change' event handler.
934 MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
935 MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
936 if (this.isDragging()) {
938 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
943 * Mousedown/mouseup handler.
944 * @param {boolean} on True if the mouse is down.
947 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) {
948 MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
951 // Dragging started, align the seek mark with the thumb position.
953 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
955 // Just finished dragging.
956 // Show the label for the last time with a shorter timeout.
958 this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
959 this.latestMouseUpTime_ = Date.now();
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
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.
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);
997 if (opt_stateIconParent) {
998 this.stateIcon_ = this.createControl(
999 'playback-state-icon', opt_stateIconParent);
1000 this.textBanner_ = this.createControl('text-banner', opt_stateIconParent);
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.
1014 VideoControls.RESUME_MARGIN = 0.03;
1017 * No resume for videos shorter than this.
1019 VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min.
1022 * When resuming rewind back this much.
1024 VideoControls.RESUME_REWIND = 5; // seconds.
1026 VideoControls.prototype = { __proto__: MediaControls.prototype };
1029 * Shows icon feedback for the current state of the video player.
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);
1053 * Shows a text banner.
1055 * @param {string} identifier String identifier.
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');
1068 this.textBanner_.addEventListener('webkitAnimationEnd', onAnimationEnd);
1070 this.textBanner_.setAttribute('visible', 'true');
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.
1080 VideoControls.prototype.onPlayButtonClicked = function(event) {
1081 if (event.ctrlKey) {
1082 this.toggleLoopedModeWithFeedback(true);
1083 if (!this.isPlaying())
1084 this.togglePlayState();
1086 this.togglePlayState();
1091 * Media completion handler.
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.
1102 VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) {
1103 if (!this.getMedia().duration)
1105 this.toggleLoopedMode(on);
1107 // TODO(mtomasz): Simplify, crbug.com/254318.
1108 this.showTextBanner_('VIDEO_PLAYER_LOOPED_MODE');
1113 * Toggles the looped mode.
1114 * @param {boolean} on Whether enabled or not.
1116 VideoControls.prototype.toggleLoopedMode = function(on) {
1117 this.getMedia().loop = on;
1121 * Toggles play/pause state and flash an icon over the video.
1123 VideoControls.prototype.togglePlayStateWithFeedback = function() {
1124 if (!this.getMedia().duration)
1127 this.togglePlayState();
1128 this.showIconFeedback_();
1132 * Toggles play/pause state.
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();
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).
1148 VideoControls.prototype.savePosition = function(opt_sync) {
1150 !this.media_.duration ||
1151 this.media_.duration < VideoControls.RESUME_THRESHOLD) {
1155 var ratio = this.media_.currentTime / this.media_.duration;
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.
1163 position = Math.floor(
1164 Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND));
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 });
1174 util.AppCache.update(this.media_.src, position);
1179 * Resumes the playback position saved in the persistent storage.
1181 VideoControls.prototype.restorePlayState = function() {
1182 if (this.media_ && this.media_.duration >= VideoControls.RESUME_THRESHOLD) {
1183 util.AppCache.getValue(this.media_.src, function(position) {
1185 this.media_.currentTime = position;
1191 * Updates style to best fit the size of the container.
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';
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.
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.
1252 AudioControls.prototype.onMediaComplete = function() {
1253 this.advanceTrack_(true);
1257 * The track position after which "previous" button acts as "restart".
1259 AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds.
1262 * @param {boolean} forward True if advancing forward.
1265 AudioControls.prototype.onAdvanceClick_ = function(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;
1272 this.advanceTrack_(forward);