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.
8 * @fileoverview MediaControls class implements media playback controls
9 * that exist outside of the audio/video HTML element.
13 * @param {HTMLElement} containerElement The container for the controls.
14 * @param {function} onMediaError Function to display an error message.
17 function MediaControls(containerElement, onMediaError) {
18 this.container_ = containerElement;
19 this.document_ = this.container_.ownerDocument;
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() {};
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() {
142 MediaControls.prototype.pause = function() {
147 * @return {boolean} True if the media is currently playing.
149 MediaControls.prototype.isPlaying = function() {
150 return this.media_ && !this.media_.paused && !this.media_.ended;
156 MediaControls.prototype.togglePlayState = function() {
157 if (this.isPlaying())
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.
167 * @param {Event=} opt_event Mouse click event.
169 MediaControls.prototype.onPlayButtonClicked = function(opt_event) {
170 this.togglePlayState();
174 * @param {HTMLElement=} opt_parent Parent container.
176 MediaControls.prototype.initPlayButton = function(opt_parent) {
177 this.playButton_ = this.createButton('play media-control',
178 this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */);
186 * The default range of 100 is too coarse for the media progress slider.
188 MediaControls.PROGRESS_RANGE = 5000;
191 * @param {boolean=} opt_seekMark True if the progress slider should have
193 * @param {HTMLElement=} opt_parent Parent container.
195 MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) {
196 var timeControls = this.createControl('time-controls', opt_parent);
198 var sliderConstructor =
199 opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider;
201 this.progressSlider_ = new sliderConstructor(
202 this.createControl('progress media-control', timeControls),
204 MediaControls.PROGRESS_RANGE,
205 this.onProgressChange_.bind(this),
206 this.onProgressDrag_.bind(this));
208 var timeBox = this.createControl('time media-control', timeControls);
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);
214 this.currentTime_ = this.createControl('current', timeBox);
218 * @param {number} current Current time is seconds.
219 * @param {number} duration Duration in seconds.
222 MediaControls.prototype.displayProgress_ = function(current, duration) {
223 var ratio = current / duration;
224 this.progressSlider_.setValue(ratio);
225 this.currentTime_.textContent = MediaControls.formatTime_(current);
229 * @param {number} value Progress [0..1].
232 MediaControls.prototype.onProgressChange_ = function(value) {
233 if (!this.media_.seekable || !this.media_.duration) {
234 console.error('Inconsistent media state');
238 var current = this.media_.duration * value;
239 this.media_.currentTime = current;
240 this.currentTime_.textContent = MediaControls.formatTime_(current);
244 * @param {boolean} on True if dragging.
247 MediaControls.prototype.onProgressDrag_ = function(on) {
252 this.resumeAfterDrag_ = this.isPlaying();
255 if (this.resumeAfterDrag_) {
256 if (this.media_.ended)
257 this.onMediaPlay_(false);
261 this.updatePlayButtonState_(this.isPlaying());
270 * @param {HTMLElement=} opt_parent Parent element for the controls.
272 MediaControls.prototype.initVolumeControls = function(opt_parent) {
273 var volumeControls = this.createControl('volume-controls', opt_parent);
275 this.soundButton_ = this.createButton('sound media-control',
276 this.onSoundButtonClick_.bind(this), volumeControls);
277 this.soundButton_.setAttribute('level', 3); // max level.
279 this.volume_ = new MediaControls.AnimatedSlider(
280 this.createControl('volume media-control', volumeControls),
283 this.onVolumeChange_.bind(this),
284 this.onVolumeDrag_.bind(this));
288 * Click handler for the sound level button.
291 MediaControls.prototype.onSoundButtonClick_ = function() {
292 if (this.media_.volume == 0) {
293 this.volume_.setValue(this.savedVolume_ || 1);
295 this.savedVolume_ = this.media_.volume;
296 this.volume_.setValue(0);
298 this.onVolumeChange_(this.volume_.getValue());
302 * @param {number} value Volume [0..1].
303 * @return {number} The rough level [0..3] used to pick an icon.
306 MediaControls.getVolumeLevel_ = function(value) {
307 if (value == 0) return 0;
308 if (value <= 1 / 3) return 1;
309 if (value <= 2 / 3) return 2;
314 * @param {number} value Volume [0..1].
317 MediaControls.prototype.onVolumeChange_ = function(value) {
318 this.media_.volume = value;
319 this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value));
323 * @param {boolean} on True if dragging is in progress.
326 MediaControls.prototype.onVolumeDrag_ = function(on) {
327 if (on && (this.media_.volume != 0)) {
328 this.savedVolume_ = this.media_.volume;
333 * Media event handlers.
337 * Attach a media element.
339 * @param {HTMLMediaElement} mediaElement The media element to control.
341 MediaControls.prototype.attachMedia = function(mediaElement) {
342 this.media_ = mediaElement;
344 this.media_.addEventListener('play', this.onMediaPlayBound_);
345 this.media_.addEventListener('pause', this.onMediaPauseBound_);
346 this.media_.addEventListener('durationchange', this.onMediaDurationBound_);
347 this.media_.addEventListener('timeupdate', this.onMediaProgressBound_);
348 this.media_.addEventListener('error', this.onMediaError_);
350 // Reflect the media state in the UI.
351 this.onMediaDuration_();
352 this.onMediaPlay_(this.isPlaying());
353 this.onMediaProgress_();
355 /* Copy the user selected volume to the new media element. */
356 this.media_.volume = this.volume_.getValue();
361 * Detach media event handlers.
363 MediaControls.prototype.detachMedia = function() {
367 this.media_.removeEventListener('play', this.onMediaPlayBound_);
368 this.media_.removeEventListener('pause', this.onMediaPauseBound_);
369 this.media_.removeEventListener('durationchange', this.onMediaDurationBound_);
370 this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_);
371 this.media_.removeEventListener('error', this.onMediaError_);
377 * Force-empty the media pipeline. This is a workaround for crbug.com/149957.
378 * The document is not going to be GC-ed until the last Files app window closes,
379 * but we want the media pipeline to deinitialize ASAP to minimize leakage.
381 MediaControls.prototype.cleanup = function() {
385 this.media_.src = '';
391 * 'play' and 'pause' event handler.
392 * @param {boolean} playing True if playing.
395 MediaControls.prototype.onMediaPlay_ = function(playing) {
396 if (this.progressSlider_.isDragging())
399 this.updatePlayButtonState_(playing);
400 this.onPlayStateChanged();
404 * 'durationchange' event handler.
407 MediaControls.prototype.onMediaDuration_ = function() {
408 if (!this.media_ || !this.media_.duration) {
409 this.enableControls_('.media-control', false);
413 this.enableControls_('.media-control', true);
415 var sliderContainer = this.progressSlider_.getContainer();
416 if (this.media_.seekable)
417 sliderContainer.classList.remove('readonly');
419 sliderContainer.classList.add('readonly');
421 var valueToString = function(value) {
422 return MediaControls.formatTime_(this.media_.duration * value);
425 this.duration_.textContent = valueToString(1);
427 if (this.progressSlider_.setValueToStringFunction)
428 this.progressSlider_.setValueToStringFunction(valueToString);
430 if (this.media_.seekable)
431 this.restorePlayState();
435 * 'timeupdate' event handler.
438 MediaControls.prototype.onMediaProgress_ = function() {
439 if (!this.media_ || !this.media_.duration) {
440 this.displayProgress_(0, 1);
444 var current = this.media_.currentTime;
445 var duration = this.media_.duration;
447 if (this.progressSlider_.isDragging())
450 this.displayProgress_(current, duration);
452 if (current == duration) {
453 this.onMediaComplete();
455 this.onPlayStateChanged();
459 * Called when the media playback is complete.
461 MediaControls.prototype.onMediaComplete = function() {};
464 * Called when play/pause state is changed or on playback progress.
465 * This is the right moment to save the play state.
467 MediaControls.prototype.onPlayStateChanged = function() {};
470 * Updates the play button state.
471 * @param {boolean} playing If the video is playing.
474 MediaControls.prototype.updatePlayButtonState_ = function(playing) {
476 this.playButton_.setAttribute('state',
477 MediaControls.ButtonStateType.PLAYING);
478 } else if (!this.media_.ended) {
479 this.playButton_.setAttribute('state',
480 MediaControls.ButtonStateType.DEFAULT);
482 this.playButton_.setAttribute('state',
483 MediaControls.ButtonStateType.ENDED);
488 * Restore play state. Base implementation is empty.
490 MediaControls.prototype.restorePlayState = function() {};
493 * Encode current state into the page URL or the app state.
495 MediaControls.prototype.encodeState = function() {
496 if (!this.media_ || !this.media_.duration)
499 if (window.appState) {
500 window.appState.time = this.media_.currentTime;
507 * Decode current state from the page URL or the app state.
508 * @return {boolean} True if decode succeeded.
510 MediaControls.prototype.decodeState = function() {
511 if (!this.media_ || !window.appState || !('time' in window.appState))
513 // There is no page reload for apps v2, only app restart.
514 // Always restart in paused state.
515 this.media_.currentTime = window.appState.time;
521 * Remove current state from the page URL or the app state.
523 MediaControls.prototype.clearState = function() {
524 if (!window.appState)
527 if ('time' in window.appState)
528 delete window.appState.time;
534 * Create a customized slider control.
536 * @param {HTMLElement} container The containing div element.
537 * @param {number} value Initial value [0..1].
538 * @param {number} range Number of distinct slider positions to be supported.
539 * @param {function(number)} onChange Value change handler.
540 * @param {function(boolean)} onDrag Drag begin/end handler.
544 MediaControls.Slider = function(container, value, range, onChange, onDrag) {
545 this.container_ = container;
546 this.onChange_ = onChange;
547 this.onDrag_ = onDrag;
549 var document = this.container_.ownerDocument;
551 this.container_.classList.add('custom-slider');
553 this.input_ = document.createElement('input');
554 this.input_.type = 'range';
556 this.input_.max = range;
557 this.input_.value = value * range;
558 this.container_.appendChild(this.input_);
560 this.input_.addEventListener(
561 'change', this.onInputChange_.bind(this));
562 this.input_.addEventListener(
563 'mousedown', this.onInputDrag_.bind(this, true));
564 this.input_.addEventListener(
565 'mouseup', this.onInputDrag_.bind(this, false));
567 this.bar_ = document.createElement('div');
568 this.bar_.className = 'bar';
569 this.container_.appendChild(this.bar_);
571 this.filled_ = document.createElement('div');
572 this.filled_.className = 'filled';
573 this.bar_.appendChild(this.filled_);
575 var leftCap = document.createElement('div');
576 leftCap.className = 'cap left';
577 this.bar_.appendChild(leftCap);
579 var rightCap = document.createElement('div');
580 rightCap.className = 'cap right';
581 this.bar_.appendChild(rightCap);
584 this.setFilled_(value);
588 * @return {HTMLElement} The container element.
590 MediaControls.Slider.prototype.getContainer = function() {
591 return this.container_;
595 * @return {HTMLElement} The standard input element.
598 MediaControls.Slider.prototype.getInput_ = function() {
603 * @return {HTMLElement} The slider bar element.
605 MediaControls.Slider.prototype.getBar = function() {
610 * @return {number} [0..1] The current value.
612 MediaControls.Slider.prototype.getValue = function() {
617 * @param {number} value [0..1].
619 MediaControls.Slider.prototype.setValue = function(value) {
621 this.setValueToUI_(value);
625 * Fill the given proportion the slider bar (from the left).
627 * @param {number} proportion [0..1].
630 MediaControls.Slider.prototype.setFilled_ = function(proportion) {
631 this.filled_.style.width = proportion * 100 + '%';
635 * Get the value from the input element.
637 * @return {number} Value [0..1].
640 MediaControls.Slider.prototype.getValueFromUI_ = function() {
641 return this.input_.value / this.input_.max;
645 * Update the UI with the current value.
647 * @param {number} value [0..1].
650 MediaControls.Slider.prototype.setValueToUI_ = function(value) {
651 this.input_.value = value * this.input_.max;
652 this.setFilled_(value);
656 * Compute the proportion in which the given position divides the slider bar.
658 * @param {number} position in pixels.
659 * @return {number} [0..1] proportion.
661 MediaControls.Slider.prototype.getProportion = function(position) {
662 var rect = this.bar_.getBoundingClientRect();
663 return Math.max(0, Math.min(1, (position - rect.left) / rect.width));
667 * 'change' event handler.
670 MediaControls.Slider.prototype.onInputChange_ = function() {
671 this.value_ = this.getValueFromUI_();
672 this.setFilled_(this.value_);
673 this.onChange_(this.value_);
677 * @return {boolean} True if dragging is in progress.
679 MediaControls.Slider.prototype.isDragging = function() {
680 return this.isDragging_;
684 * Mousedown/mouseup handler.
685 * @param {boolean} on True if the mouse is down.
688 MediaControls.Slider.prototype.onInputDrag_ = function(on) {
689 this.isDragging_ = on;
694 * Create a customized slider with animated thumb movement.
696 * @param {HTMLElement} container The containing div element.
697 * @param {number} value Initial value [0..1].
698 * @param {number} range Number of distinct slider positions to be supported.
699 * @param {function(number)} onChange Value change handler.
700 * @param {function(boolean)} onDrag Drag begin/end handler.
701 * @param {function(number):string} formatFunction Value formatting function.
704 MediaControls.AnimatedSlider = function(
705 container, value, range, onChange, onDrag, formatFunction) {
706 MediaControls.Slider.apply(this, arguments);
709 MediaControls.AnimatedSlider.prototype = {
710 __proto__: MediaControls.Slider.prototype
714 * Number of animation steps.
716 MediaControls.AnimatedSlider.STEPS = 10;
719 * Animation duration.
721 MediaControls.AnimatedSlider.DURATION = 100;
724 * @param {number} value [0..1].
727 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
728 if (this.animationInterval_) {
729 clearInterval(this.animationInterval_);
731 var oldValue = this.getValueFromUI_();
733 this.animationInterval_ = setInterval(function() {
735 var currentValue = oldValue +
736 (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS);
737 MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue);
738 if (step == MediaControls.AnimatedSlider.STEPS) {
739 clearInterval(this.animationInterval_);
742 MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS);
746 * Create a customized slider with a precise time feedback.
748 * The time value is shown above the slider bar at the mouse position.
750 * @param {HTMLElement} container The containing div element.
751 * @param {number} value Initial value [0..1].
752 * @param {number} range Number of distinct slider positions to be supported.
753 * @param {function(number)} onChange Value change handler.
754 * @param {function(boolean)} onDrag Drag begin/end handler.
755 * @param {function(number):string} formatFunction Value formatting function.
758 MediaControls.PreciseSlider = function(
759 container, value, range, onChange, onDrag, formatFunction) {
760 MediaControls.Slider.apply(this, arguments);
762 var doc = this.container_.ownerDocument;
765 * @type {function(number):string}
768 this.valueToString_ = null;
770 this.seekMark_ = doc.createElement('div');
771 this.seekMark_.className = 'seek-mark';
772 this.getBar().appendChild(this.seekMark_);
774 this.seekLabel_ = doc.createElement('div');
775 this.seekLabel_.className = 'seek-label';
776 this.seekMark_.appendChild(this.seekLabel_);
778 this.getContainer().addEventListener(
779 'mousemove', this.onMouseMove_.bind(this));
780 this.getContainer().addEventListener(
781 'mouseout', this.onMouseOut_.bind(this));
784 MediaControls.PreciseSlider.prototype = {
785 __proto__: MediaControls.Slider.prototype
789 * Show the seek mark after a delay.
791 MediaControls.PreciseSlider.SHOW_DELAY = 200;
794 * Hide the seek mark for this long after changing the position with a click.
796 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
799 * Hide the seek mark for this long after changing the position with a drag.
801 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
804 * Default hide timeout (no hiding).
806 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
809 * @param {function(number):string} func Value formatting function.
811 MediaControls.PreciseSlider.prototype.setValueToStringFunction =
813 this.valueToString_ = func;
815 /* It is not completely accurate to assume that the max value corresponds
816 to the longest string, but generous CSS padding will compensate for that. */
817 var labelWidth = this.valueToString_(1).length / 2 + 1;
818 this.seekLabel_.style.width = labelWidth + 'em';
819 this.seekLabel_.style.marginLeft = -labelWidth / 2 + 'em';
823 * Show the time above the slider.
825 * @param {number} ratio [0..1] The proportion of the duration.
826 * @param {number} timeout Timeout in ms after which the label should be hidden.
827 * MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call.
830 MediaControls.PreciseSlider.prototype.showSeekMark_ =
831 function(ratio, timeout) {
832 // Do not update the seek mark for the first 500ms after the drag is finished.
833 if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now()))
836 this.seekMark_.style.left = ratio * 100 + '%';
838 if (ratio < this.getValue()) {
839 this.seekMark_.classList.remove('inverted');
841 this.seekMark_.classList.add('inverted');
843 this.seekLabel_.textContent = this.valueToString_(ratio);
845 this.seekMark_.classList.add('visible');
847 if (this.seekMarkTimer_) {
848 clearTimeout(this.seekMarkTimer_);
849 this.seekMarkTimer_ = null;
851 if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
852 this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
859 MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() {
860 this.seekMarkTimer_ = null;
861 this.seekMark_.classList.remove('visible');
865 * 'mouseout' event handler.
866 * @param {Event} e Event.
869 MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) {
870 this.latestSeekRatio_ = this.getProportion(e.clientX);
873 function showMark() {
874 if (!self.isDragging()) {
875 self.showSeekMark_(self.latestSeekRatio_,
876 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY);
880 if (this.seekMark_.classList.contains('visible')) {
882 } else if (!this.seekMarkTimer_) {
883 this.seekMarkTimer_ =
884 setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
889 * 'mouseout' event handler.
890 * @param {Event} e Event.
893 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
894 for (var element = e.relatedTarget; element; element = element.parentNode) {
895 if (element == this.getContainer())
898 if (this.seekMarkTimer_) {
899 clearTimeout(this.seekMarkTimer_);
900 this.seekMarkTimer_ = null;
902 this.hideSeekMark_();
906 * 'change' event handler.
909 MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
910 MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
911 if (this.isDragging()) {
913 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
918 * Mousedown/mouseup handler.
919 * @param {boolean} on True if the mouse is down.
922 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) {
923 MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
926 // Dragging started, align the seek mark with the thumb position.
928 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
930 // Just finished dragging.
931 // Show the label for the last time with a shorter timeout.
933 this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
934 this.latestMouseUpTime_ = Date.now();
939 * Create video controls.
941 * @param {HTMLElement} containerElement The container for the controls.
942 * @param {function} onMediaError Function to display an error message.
943 * @param {function(string):string} stringFunction Function providing localized
945 * @param {function=} opt_fullScreenToggle Function to toggle fullscreen mode.
946 * @param {HTMLElement=} opt_stateIconParent The parent for the icon that
947 * gives visual feedback when the playback state changes.
950 function VideoControls(containerElement, onMediaError, stringFunction,
951 opt_fullScreenToggle, opt_stateIconParent) {
952 MediaControls.call(this, containerElement, onMediaError);
953 this.stringFunction_ = stringFunction;
955 this.container_.classList.add('video-controls');
956 this.initPlayButton();
957 this.initTimeControls(true /* show seek mark */);
958 this.initVolumeControls();
960 if (opt_fullScreenToggle) {
961 this.fullscreenButton_ =
962 this.createButton('fullscreen', opt_fullScreenToggle);
965 if (opt_stateIconParent) {
966 this.stateIcon_ = this.createControl(
967 'playback-state-icon', opt_stateIconParent);
968 this.textBanner_ = this.createControl('text-banner', opt_stateIconParent);
971 // Disables all controls at first.
972 this.enableControls_('.media-control', false);
974 var videoControls = this;
975 chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
976 function() { videoControls.togglePlayStateWithFeedback(); });
980 * No resume if we are within this margin from the start or the end.
982 VideoControls.RESUME_MARGIN = 0.03;
985 * No resume for videos shorter than this.
987 VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min.
990 * When resuming rewind back this much.
992 VideoControls.RESUME_REWIND = 5; // seconds.
994 VideoControls.prototype = { __proto__: MediaControls.prototype };
997 * Shows icon feedback for the current state of the video player.
1000 VideoControls.prototype.showIconFeedback_ = function() {
1001 this.stateIcon_.removeAttribute('state');
1002 setTimeout(function() {
1003 this.stateIcon_.setAttribute('state', this.isPlaying() ? 'play' : 'pause');
1008 * Shows a text banner.
1010 * @param {string} identifier String identifier.
1013 VideoControls.prototype.showTextBanner_ = function(identifier) {
1014 this.textBanner_.removeAttribute('visible');
1015 this.textBanner_.textContent = this.stringFunction_(identifier);
1016 setTimeout(function() {
1017 this.textBanner_.setAttribute('visible', 'true');
1022 * Toggle play/pause state on a mouse click on the play/pause button. Can be
1023 * called externally.
1025 * @param {Event} event Mouse click event.
1027 VideoControls.prototype.onPlayButtonClicked = function(event) {
1028 if (event.ctrlKey) {
1029 this.toggleLoopedModeWithFeedback(true);
1030 if (!this.isPlaying())
1031 this.togglePlayState();
1033 this.togglePlayState();
1038 * Media completion handler.
1040 VideoControls.prototype.onMediaComplete = function() {
1041 this.onMediaPlay_(false); // Just update the UI.
1042 this.savePosition(); // This will effectively forget the position.
1046 * Toggles the looped mode with feedback.
1047 * @param {boolean} on Whether enabled or not.
1049 VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) {
1050 if (!this.getMedia().duration)
1052 this.toggleLoopedMode(on);
1054 // TODO(mtomasz): Simplify, crbug.com/254318.
1055 this.showTextBanner_('GALLERY_VIDEO_LOOPED_MODE');
1060 * Toggles the looped mode.
1061 * @param {boolean} on Whether enabled or not.
1063 VideoControls.prototype.toggleLoopedMode = function(on) {
1064 this.getMedia().loop = on;
1068 * Toggles play/pause state and flash an icon over the video.
1070 VideoControls.prototype.togglePlayStateWithFeedback = function() {
1071 if (!this.getMedia().duration)
1074 this.togglePlayState();
1075 this.showIconFeedback_();
1079 * Toggles play/pause state.
1081 VideoControls.prototype.togglePlayState = function() {
1082 if (this.isPlaying()) {
1083 // User gave the Pause command. Save the state and reset the loop mode.
1084 this.toggleLoopedMode(false);
1085 this.savePosition();
1087 MediaControls.prototype.togglePlayState.apply(this, arguments);
1091 * Saves the playback position to the persistent storage.
1092 * @param {boolean=} opt_sync True if the position must be saved synchronously
1093 * (required when closing app windows).
1095 VideoControls.prototype.savePosition = function(opt_sync) {
1097 !this.media_.duration ||
1098 this.media_.duration < VideoControls.RESUME_THRESHOLD) {
1102 var ratio = this.media_.currentTime / this.media_.duration;
1104 if (ratio < VideoControls.RESUME_MARGIN ||
1105 ratio > (1 - VideoControls.RESUME_MARGIN)) {
1106 // We are too close to the beginning or the end.
1107 // Remove the resume position so that next time we start from the beginning.
1110 position = Math.floor(
1111 Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND));
1115 // Packaged apps cannot save synchronously.
1116 // Pass the data to the background page.
1117 if (!window.saveOnExit)
1118 window.saveOnExit = [];
1119 window.saveOnExit.push({ key: this.media_.src, value: position });
1121 util.AppCache.update(this.media_.src, position);
1126 * Resumes the playback position saved in the persistent storage.
1128 VideoControls.prototype.restorePlayState = function() {
1129 if (this.media_ && this.media_.duration >= VideoControls.RESUME_THRESHOLD) {
1130 util.AppCache.getValue(this.media_.src, function(position) {
1132 this.media_.currentTime = position;
1138 * Updates style to best fit the size of the container.
1140 VideoControls.prototype.updateStyle = function() {
1141 // We assume that the video controls element fills the parent container.
1142 // This is easier than adding margins to this.container_.clientWidth.
1143 var width = this.container_.parentNode.clientWidth;
1145 // Set the margin to 5px for width >= 400, 0px for width < 160,
1146 // interpolate linearly in between.
1147 this.container_.style.margin =
1148 Math.ceil((Math.max(160, Math.min(width, 400)) - 160) / 48) + 'px';
1150 var hideBelow = function(selector, limit) {
1151 this.container_.querySelector(selector).style.display =
1152 width < limit ? 'none' : '-webkit-box';
1155 hideBelow('.time', 350);
1156 hideBelow('.volume', 275);
1157 hideBelow('.volume-controls', 210);
1158 hideBelow('.fullscreen', 150);
1162 * Creates audio controls.
1164 * @param {HTMLElement} container Parent container.
1165 * @param {function(boolean)} advanceTrack Parameter: true=forward.
1166 * @param {function} onError Error handler.
1169 function AudioControls(container, advanceTrack, onError) {
1170 MediaControls.call(this, container, onError);
1172 this.container_.classList.add('audio-controls');
1174 this.advanceTrack_ = advanceTrack;
1176 this.initPlayButton();
1177 this.initTimeControls(false /* no seek mark */);
1178 /* No volume controls */
1179 this.createButton('previous', this.onAdvanceClick_.bind(this, false));
1180 this.createButton('next', this.onAdvanceClick_.bind(this, true));
1182 // Disables all controls at first.
1183 this.enableControls_('.media-control', false);
1185 var audioControls = this;
1186 chrome.mediaPlayerPrivate.onNextTrack.addListener(
1187 function() { audioControls.onAdvanceClick_(true); });
1188 chrome.mediaPlayerPrivate.onPrevTrack.addListener(
1189 function() { audioControls.onAdvanceClick_(false); });
1190 chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
1191 function() { audioControls.togglePlayState(); });
1194 AudioControls.prototype = { __proto__: MediaControls.prototype };
1197 * Media completion handler. Advances to the next track.
1199 AudioControls.prototype.onMediaComplete = function() {
1200 this.advanceTrack_(true);
1204 * The track position after which "previous" button acts as "restart".
1206 AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds.
1209 * @param {boolean} forward True if advancing forward.
1212 AudioControls.prototype.onAdvanceClick_ = function(forward) {
1214 (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) {
1215 // We are far enough from the beginning of the current track.
1216 // Restart it instead of than skipping to the previous one.
1217 this.getMedia().currentTime = 0;
1219 this.advanceTrack_(forward);