Refactor UI range elements

This factors out common functionality and structure from the volume
bar and seek bar, both of which involved specific CSS workarounds for
IE.  The seek bar had logic to keep the value from jumping around
during casting, which now the volume bar benefits from, as well.
Finally, the seek bar code was spread out throughout controls.js, and
now it has its own class.

Closes #1913

Change-Id: I299476ccbc27f28f7b225a6e6f8b0d21abe5baf2
This commit is contained in:
Joey Parrish
2019-07-10 10:18:09 -07:00
parent 22a69ed28d
commit fcbb72561f
6 changed files with 449 additions and 383 deletions
+2
View File
@@ -17,8 +17,10 @@
+../../ui/pip_button.js
+../../ui/play_pause_button.js
+../../ui/presentation_time.js
+../../ui/range_element.js
+../../ui/resolution_selection.js
+../../ui/rewind_button.js
+../../ui/seek_bar.js
+../../ui/spacer.js
+../../ui/text_displayer.js
+../../ui/text_selection.js
+52 -304
View File
@@ -19,12 +19,12 @@
goog.provide('shaka.ui.Controls');
goog.provide('shaka.ui.ControlsPanel');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.ui.Constants');
goog.require('shaka.ui.Enums');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.SeekBar');
goog.require('shaka.ui.Utils');
goog.require('shaka.util.Dom');
goog.require('shaka.util.EventManager');
@@ -79,19 +79,14 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
/** @private {!HTMLElement} */
this.videoContainer_ = videoContainer;
/** @private {shaka.ui.SeekBar} */
this.seekBar_ = null;
/** @private {boolean} */
this.isSeeking_ = false;
/**
* This timer is used to introduce a delay between the user scrubbing across
* the seek bar and the seek being sent to the player.
*
* @private {shaka.util.Timer}
*/
this.seekTimer_ = new shaka.util.Timer(() => {
goog.asserts.assert(this.seekBar_ != null, 'Seekbar should not be null!');
this.video_.currentTime = parseFloat(this.seekBar_.value);
});
/** @private {!Array.<!Element>} */
this.settingsMenus_ = [];
/**
* This timer is used to detect when the user has stopped moving the mouse
@@ -133,7 +128,10 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
* @private {shaka.util.Timer}
*/
this.timeAndSeekRangeTimer_ = new shaka.util.Timer(() => {
this.updateTimeAndSeekRange_();
// Suppress timer-based updates if the controls are hidden.
if (this.isOpaque_()) {
this.updateTimeAndSeekRange_();
}
});
/** @private {?number} */
@@ -170,7 +168,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.timeAndSeekRangeTimer_.tickEvery(/* seconds= */ 0.125);
}
/**
* @override
* @export
@@ -181,11 +178,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.eventManager_ = null;
}
if (this.seekTimer_) {
this.seekTimer_.stop();
this.seekTimer_ = null;
}
if (this.mouseStillTimer_) {
this.mouseStillTimer_.stop();
this.mouseStillTimer_ = null;
@@ -222,6 +214,10 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
await Promise.all(this.elements_.map((element) => element.destroy()));
this.elements_ = [];
if (this.seekBar_) {
await this.seekBar_.destroy();
}
}
@@ -318,7 +314,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
shaka.ui.ControlsPanel.elementNamesToFactories_.set(name, factory);
}
/**
* This allows the application to inhibit casting.
*
@@ -330,7 +325,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.onCastStatusChange_();
}
/**
* Used by the application to notify the controls that a load operation is
* complete. This allows the controls to recalculate play/paused state, which
@@ -343,7 +337,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.onPlayStateChange_();
}
/**
* @param {!shaka.extern.UIConfiguration} config
* @export
@@ -367,8 +360,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.addEventListeners_();
}
/**
/**
* Enable or disable the custom controls. Enabling disables native
* browser controls.
@@ -400,7 +391,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.onPlayStateChange_();
}
/**
* Enable or disable native browser controls. Enabling disables shaka
* controls.
@@ -421,7 +411,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* @export
* @return {shaka.cast.CastProxy}
@@ -430,7 +419,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.castProxy_;
}
/**
* @return {shaka.ui.Localization}
* @export
@@ -439,7 +427,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.localization_;
}
/**
* @return {!HTMLElement}
* @export
@@ -448,7 +435,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.videoContainer_;
}
/**
* @return {HTMLMediaElement}
* @export
@@ -457,7 +443,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.video_;
}
/**
* @return {HTMLMediaElement}
* @export
@@ -466,7 +451,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.localVideo_;
}
/**
* @return {shaka.Player}
* @export
@@ -475,7 +459,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.player_;
}
/**
* @return {shaka.Player}
* @export
@@ -484,7 +467,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.localPlayer_;
}
/**
* @return {!HTMLElement}
* @export
@@ -493,7 +475,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.controlsContainer_;
}
/**
* @return {!shaka.extern.UIConfiguration}
* @export
@@ -502,7 +483,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.config_;
}
/**
* @return {boolean}
* @export
@@ -511,7 +491,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.isSeeking_;
}
/**
* @return {boolean}
* @export
@@ -520,20 +499,14 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.castAllowed_;
}
/**
* @return {number}
* @export
*/
getDisplayTime() {
const displayTime = this.isSeeking_ ?
Number(this.seekBar_.value) :
Number(this.video_.currentTime);
return displayTime;
return this.seekBar_ ? this.seekBar_.getValue() : this.video_.currentTime;
}
/**
* @param {?number} time
* @export
@@ -542,7 +515,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.lastTouchEventTime_ = time;
}
/**
* Display controls even if css says overwise.
* Normally, controls opacity is controled by CSS, but there are
@@ -557,7 +529,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.overrideCssShowControls_ = true;
}
/**
* @return {boolean}
* @export
@@ -567,26 +538,15 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
(menu) => menu.classList.contains('shaka-displayed'));
}
/**
* @export
*/
/** @export */
hideSettingsMenus() {
this.hideSettingsMenusTimer_.tickNow();
}
/**
* @private
*/
/** @private */
updateLocalizedStrings_() {
const LocIds = shaka.ui.Locales.Ids;
if (this.seekBar_) {
this.seekBar_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
this.localization_.resolve(LocIds.SEEK));
}
// Localize state-dependant labels
const makePlayNotPause = this.video_.paused && !this.isSeeking_;
const playButtonAriaLabelId = makePlayNotPause ? LocIds.PLAY : LocIds.PAUSE;
@@ -594,18 +554,12 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.localization_.resolve(playButtonAriaLabelId));
}
/**
* @private
*/
/** @private */
createDOM_() {
// TODO: encapsulate/abstract range inputs and their containers
/** @private {HTMLElement} */
this.seekBarContainer_ = null;
/** @private {HTMLInputElement} */
this.seekBar_ = null;
if (this.seekBar_) {
this.seekBar_.destroy();
this.seekBar_ = null;
}
this.videoContainer_.classList.add('shaka-video-container');
this.video_.classList.add('shaka-video');
@@ -616,27 +570,20 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.addControlsButtonPanel_();
// Seek bar
if (this.config_.addSeekBar) {
this.addSeekBar_();
}
/** @private {!Array.<!Element>} */
this.settingsMenus_ = Array.from(
this.videoContainer_.getElementsByClassName('shaka-settings-menu'));
// Settings menus need to be positioned lower, if the seekbar is absent.
if (!this.seekBar_) {
if (this.config_.addSeekBar) {
this.seekBar_ = new shaka.ui.SeekBar(this.bottomControls_, this);
} else {
// Settings menus need to be positioned lower if the seekbar is absent.
for (const menu of this.settingsMenus_) {
menu.classList.add('shaka-low-position');
}
}
}
/**
* @private
*/
/** @private */
addControlsContainer_() {
/** @private {!HTMLElement} */
this.controlsContainer_ = shaka.util.Dom.createHTMLElement('div');
@@ -652,10 +599,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
});
}
/**
* @private
*/
/** @private */
addPlayButton_() {
/** @private {!HTMLElement} */
this.playButtonContainer_ = shaka.util.Dom.createHTMLElement('div');
@@ -669,10 +613,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.onPlayStateChange_();
}
/**
* @private
*/
/** @private */
addBufferingSpinner_() {
/** @private {!HTMLElement} */
this.spinnerContainer_ = shaka.util.Dom.createHTMLElement('div');
@@ -708,10 +649,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
svg.appendChild(spinnerCircle);
}
/**
* @private
*/
/** @private */
addControlsButtonPanel_() {
/** @private {!HTMLElement} */
this.bottomControls_ = shaka.util.Dom.createHTMLElement('div');
@@ -739,10 +677,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* @private
*/
/** @private */
addEventListeners_() {
this.eventManager_.listen(this.player_, 'buffering', () => {
this.onBufferingStateChange_();
@@ -768,28 +703,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.onPlayStateChange_();
});
if (this.seekBar_) {
this.eventManager_.listen(this.seekBar_, 'mousedown', () => {
this.onSeekStart_();
});
this.eventManager_.listen(this.seekBar_, 'touchstart', () => {
this.onSeekStart_();
}, {passive: true});
this.eventManager_.listen(this.seekBar_, 'input', () => {
this.onSeekInput_();
});
this.eventManager_.listen(this.seekBar_, 'touchend', () => {
this.onSeekEnd_();
});
this.eventManager_.listen(this.seekBar_, 'mouseup', () => {
this.onSeekEnd_();
});
}
// Elements that should not propagate clicks (controls panel, menus)
const noPropagationElements = this.videoContainer_.getElementsByClassName(
'shaka-no-propagation');
@@ -853,34 +766,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
(e) => this.updateLocalizedStrings_());
}
/**
* @private
*/
addSeekBar_() {
// This container is to support IE 11. See detailed notes in
// less/range_elements.less for a complete explanation.
// TODO: Factor this into a range-element component.
this.seekBarContainer_ = shaka.util.Dom.createHTMLElement('div');
this.seekBarContainer_.classList.add('shaka-seek-bar-container');
this.seekBar_ =
/** @type {!HTMLInputElement} */ (document.createElement('input'));
this.seekBar_.classList.add('shaka-seek-bar');
this.seekBar_.type = 'range';
// NOTE: step=any causes keyboard nav problems on IE 11.
this.seekBar_.setAttribute('step', 'any');
this.seekBar_.setAttribute('min', '0');
this.seekBar_.setAttribute('max', '1');
this.seekBar_.value = '0';
this.seekBar_.classList.add('shaka-no-propagation');
this.seekBar_.classList.add('shaka-show-controls-on-mouse-over');
this.seekBarContainer_.appendChild(this.seekBar_);
this.bottomControls_.appendChild(this.seekBarContainer_);
}
/**
* Hiding the cursor when the mouse stops moving seems to be the only
* decent UX in fullscreen mode. Since we can't use pure CSS for that,
@@ -917,8 +802,15 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
// Make sure we are not about to hide the settings menus and then force them
// open.
this.hideSettingsMenusTimer_.stop();
this.setControlsOpacity_(shaka.ui.Enums.Opacity.OPAQUE);
this.updateTimeAndSeekRange_();
if (!this.isOpaque_()) {
// Only update the time and seek range on mouse movement if it's the very
// first movement and we're about to show the controls. Otherwise, the
// seek bar will be updated much more rapidly during mouse movement. Do
// this right before making it visible.
this.updateTimeAndSeekRange_();
this.setControlsOpacity_(shaka.ui.Enums.Opacity.OPAQUE);
}
// Hide the cursor when the mouse stops moving.
// Only applies while the cursor is over the video container.
@@ -932,7 +824,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/** @private */
onMouseLeave_() {
// We sometimes get 'mouseout' events with touches. Since we can never
@@ -947,7 +838,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.mouseStillTimer_.tickNow();
}
/**
* This callback is for when we are pretty sure that the mouse has stopped
* moving (aka the mouse is still). This method should only be called via
@@ -970,7 +860,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* @param {!Event} event
* @private
@@ -993,10 +882,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* @private
*/
/** @private */
onContainerClick_() {
if (!this.enabled_) {
return;
@@ -1009,7 +895,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/** @private */
onPlayPauseClick_() {
if (!this.enabled_) {
@@ -1030,10 +915,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* @private
*/
/** @private */
onCastStatusChange_() {
const isCasting = this.castProxy_.isCasting();
this.dispatchEvent(new shaka.util.FakeEvent('caststatuschanged', {
@@ -1047,7 +929,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/** @private */
onPlayStateChange_() {
// On IE 11, a video may end without going into a paused state. To correct
@@ -1069,60 +950,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/** @private */
onSeekStart_() {
if (!this.enabled_) {
return;
}
this.isSeeking_ = true;
this.video_.pause();
}
/** @private */
onSeekInput_() {
if (!this.enabled_) {
return;
}
if (!this.video_.duration) {
// Can't seek yet. Ignore.
return;
}
// Update the UI right away.
this.updateTimeAndSeekRange_();
// We want to wait until the user has stopped moving the seek bar for a
// little bit to avoid the number of times we ask the player to seek.
//
// To do this, we will start a timer that will fire in a little bit, but if
// we see another seek bar change, we will cancel that timer and re-start
// it.
//
// Calling |start| on an already pending timer will cancel the old request
// and start the new one.
this.seekTimer_.tickAfter(/* seconds= */ 0.125);
}
/** @private */
onSeekEnd_() {
if (!this.enabled_) {
return;
}
// They just let go of the seek bar, so cancel the timer and manually
// call the event so that we can respond immediately.
this.seekTimer_.tickNow();
this.isSeeking_ = false;
this.video_.play();
}
/**
* Support controls with keyboard inputs.
* @param {!Event} event
@@ -1177,7 +1004,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* Called both as an event listener and directly by the controls to initialize
* the buffering state.
@@ -1192,7 +1018,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.spinnerContainer_, this.player_.isBuffering());
}
/**
* @return {boolean}
* @private
@@ -1211,7 +1036,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
return this.controlsContainer_.getAttribute('shown') != null;
}
/**
* Update the video's current time based on the keyboard operations.
* @param {number} currentTime
@@ -1220,107 +1044,35 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
*/
seek_(currentTime, event) {
this.video_.currentTime = currentTime;
this.updateTimeAndSeekRange_();
if (this.isOpaque_()) {
// Only update the time and seek range if it's visible.
this.updateTimeAndSeekRange_();
}
}
/**
* Called when the seek range or current time need to be updated.
* @private
*/
updateTimeAndSeekRange_() {
// Suppress updates if the controls are hidden.
if (!this.isOpaque_()) {
return;
}
const Constants = shaka.ui.Constants;
let displayTime = this.isSeeking_ ?
Number(this.seekBar_.value) :
Number(this.video_.currentTime);
const bufferedLength = this.video_.buffered.length;
const bufferedStart = bufferedLength ? this.video_.buffered.start(0) : 0;
const bufferedEnd =
bufferedLength ? this.video_.buffered.end(bufferedLength - 1) : 0;
const seekRange = this.player_.seekRange();
const seekRangeSize = seekRange.end - seekRange.start;
if (this.seekBar_) {
this.seekBar_.min = seekRange.start;
this.seekBar_.max = seekRange.end;
}
this.seekBar_.setValue(this.video_.currentTime);
this.seekBar_.update();
if (this.player_.isLive()) {
// The amount of time we are behind the live edge.
const behindLive = Math.floor(seekRange.end - displayTime);
displayTime = Math.max(0, behindLive);
if (!this.isSeeking_ && this.seekBar_) {
this.seekBar_.value = seekRange.end - displayTime;
}
} else {
if (!this.isSeeking_ && this.seekBar_) {
this.seekBar_.value = displayTime;
}
}
if (this.seekBar_) {
// Hide seekbar seek window is very small.
const seekRange = this.player_.seekRange();
const seekWindow = seekRange.end - seekRange.start;
if (this.player_.isLive() &&
seekWindow < Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR) {
shaka.ui.Utils.setDisplay(this.seekBarContainer_, false);
for (const menu of this.settingsMenus_) {
menu.classList.add('shaka-low-position');
}
} else {
shaka.ui.Utils.setDisplay(this.seekBarContainer_, true);
if (this.seekBar_.isShowing()) {
for (const menu of this.settingsMenus_) {
menu.classList.remove('shaka-low-position');
}
const gradient = ['to right'];
if (bufferedLength == 0) {
gradient.push('#000 0%');
} else {
const clampedBufferStart = Math.max(bufferedStart, seekRange.start);
const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end);
const bufferStartDistance =
clampedBufferStart - seekRange.start;
const bufferEndDistance =
clampedBufferEnd - seekRange.start;
const playheadDistance = displayTime - seekRange.start;
// NOTE: the fallback to zero eliminates NaN.
const bufferStartFraction =
(bufferStartDistance / seekRangeSize) || 0;
const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
const playheadFraction = (playheadDistance / seekRangeSize) || 0;
gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
(bufferStartFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
(bufferStartFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
(playheadFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
(playheadFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
(bufferEndFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
(bufferEndFraction * 100) + '%');
} else {
for (const menu of this.settingsMenus_) {
menu.classList.add('shaka-low-position');
}
this.seekBarContainer_.style.background =
'linear-gradient(' + gradient.join(',') + ')';
}
}
this.dispatchEvent(new shaka.util.FakeEvent('timeandseekrangeupdated'));
}
/**
* Add behaviors for keyboard navigation.
* 1. Add blue outline for focused elements.
@@ -1358,7 +1110,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* When the user is using keyboard to navigate inside the overflow settings
* menu (pressing Tab key to go forward, or pressing Shift + Tab keys to go
@@ -1411,7 +1162,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* For keyboard navigation, we use blue borders to highlight the active
* element. If we detect that a mouse is being used, remove the blue border
@@ -1423,7 +1173,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
this.eventManager_.listen(window, 'keydown', (e) => this.onKeyDown_(e));
}
/**
* @param {!shaka.ui.Enums.Opacity} opacity
* @private
@@ -1441,7 +1190,6 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
}
}
/**
* Create a localization instance already pre-loaded with all the locales that
* we support.
+4 -8
View File
@@ -157,9 +157,11 @@
}
}
.shaka-volume-bar-container {
.shaka-range-container {
.range-container();
}
.shaka-volume-bar-container {
width: 100px;
/* Hide volume slider on mobile-sized screens. */
@@ -169,16 +171,10 @@
}
.shaka-seek-bar-container {
.range-container();
/* TODO: Document why! */
width: 96.5%;
}
.shaka-seek-bar {
.range-element();
}
.shaka-volume-bar {
.shaka-range-element {
.range-element();
}
+142
View File
@@ -0,0 +1,142 @@
/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.ui.RangeElement');
goog.require('shaka.ui.Element');
goog.require('shaka.util.Dom');
/**
* A range element, built to work across browsers.
*
* In particular, getting styles to work right on IE requires a specific
* structure.
*
* This also handles the case where the range element is being manipulated and
* updated at the same time. This can happen when seeking during playback or
* when casting.
*
* @extends {shaka.ui.Element}
* @export
*/
shaka.ui.RangeElement = class extends shaka.ui.Element {
/**
* @param {!HTMLElement} parent
* @param {!shaka.ui.Controls} controls
* @param {!Array.<string>} containerClassNames
* @param {!Array.<string>} barClassNames
*/
constructor(parent, controls, containerClassNames, barClassNames) {
super(parent, controls);
/**
* This container is to support IE 11. See detailed notes in
* less/range_elements.less for a complete explanation.
* @protected {!HTMLElement}
*/
this.container = shaka.util.Dom.createHTMLElement('div');
this.container.classList.add('shaka-range-container');
this.container.classList.add(...containerClassNames);
/** @private {boolean} */
this.isChanging_ = false;
/** @protected {!HTMLInputElement} */
this.bar =
/** @type {!HTMLInputElement} */ (document.createElement('input'));
this.bar.classList.add('shaka-range-element');
this.bar.classList.add(...barClassNames);
this.bar.type = 'range';
// TODO(#2027): step=any causes keyboard nav problems on IE 11.
this.bar.step = 'any';
this.bar.min = '0';
this.bar.max = '1';
this.bar.value = '0';
this.container.appendChild(this.bar);
this.parent.appendChild(this.container);
this.eventManager.listen(this.bar, 'mousedown', () => {
this.isChanging_ = true;
this.onChangeStart();
});
this.eventManager.listen(this.bar, 'touchstart', () => {
this.isChanging_ = true;
this.onChangeStart();
}, {passive: true});
this.eventManager.listen(this.bar, 'input', () => {
this.onChange();
});
this.eventManager.listen(this.bar, 'touchend', () => {
this.isChanging_ = false;
this.onChangeEnd();
});
this.eventManager.listen(this.bar, 'mouseup', () => {
this.isChanging_ = false;
this.onChangeEnd();
});
}
/**
* @param {number} min
* @param {number} max
*/
setRange(min, max) {
this.bar.min = min;
this.bar.max = max;
}
/**
* Called when user interaction begins.
* To be overridden by subclasses.
*/
onChangeStart() {}
/**
* Called when a new value is set by user interaction.
* To be overridden by subclasses.
*/
onChange() {}
/**
* Called when user interaction ends.
* To be overridden by subclasses.
*/
onChangeEnd() {}
/** @return {number} */
getValue() {
return parseFloat(this.bar.value);
}
/** @param {number} value */
setValue(value) {
// The user interaction overrides any external values being pushed in.
if (this.isChanging_) {
return;
}
this.bar.value = value;
}
};
+206
View File
@@ -0,0 +1,206 @@
/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.ui.SeekBar');
goog.require('shaka.ui.Constants');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.RangeElement');
goog.require('shaka.ui.Utils');
goog.require('shaka.util.Timer');
/**
* @extends {shaka.ui.RangeElement}
* @final
* @export
*/
shaka.ui.SeekBar = class extends shaka.ui.RangeElement {
/**
* @param {!HTMLElement} parent
* @param {!shaka.ui.Controls} controls
*/
constructor(parent, controls) {
super(parent, controls,
['shaka-seek-bar-container'],
[
'shaka-seek-bar',
'shaka-no-propagation',
'shaka-show-controls-on-mouse-over',
]);
/**
* This timer is used to introduce a delay between the user scrubbing across
* the seek bar and the seek being sent to the player.
*
* @private {shaka.util.Timer}
*/
this.seekTimer_ = new shaka.util.Timer(() => {
this.video.currentTime = this.getValue();
});
this.eventManager.listen(this.localization,
shaka.ui.Localization.LOCALE_UPDATED,
() => this.updateAriaLabel_());
this.eventManager.listen(this.localization,
shaka.ui.Localization.LOCALE_CHANGED,
() => this.updateAriaLabel_());
// Initialize seek state and label.
this.setValue(this.video.currentTime);
this.update();
this.updateAriaLabel_();
}
/** @override */
async destroy() {
if (this.seekTimer_) {
this.seekTimer_.stop();
this.seekTimer_ = null;
}
await super.destroy();
}
/**
* Called by the base class when user interaction with the input element
* begins.
*
* @override
*/
onChangeStart() {
this.video.pause();
}
/**
* Update the video element's state to match the input element's state.
* Called by the base class when the input element changes.
*
* @override
*/
onChange() {
if (!this.video.duration) {
// Can't seek yet. Ignore.
return;
}
// Update the UI right away.
this.update();
// We want to wait until the user has stopped moving the seek bar for a
// little bit to reduce the number of times we ask the player to seek.
//
// To do this, we will start a timer that will fire in a little bit, but if
// we see another seek bar change, we will cancel that timer and re-start
// it.
//
// Calling |start| on an already pending timer will cancel the old request
// and start the new one.
this.seekTimer_.tickAfter(/* seconds= */ 0.125);
}
/**
* Called by the base class when user interaction with the input element
* ends.
*
* @override
*/
onChangeEnd() {
// They just let go of the seek bar, so cancel the timer and manually
// call the event so that we can respond immediately.
this.seekTimer_.tickNow();
this.video.play();
}
/** @return {boolean} */
isShowing() {
// It is showing by default, so it is hidden if shaka-hidden is in the list.
return !this.container.classList.contains('shaka-hidden');
}
/**
* Called by Controls on a timer to update the state of the seek bar.
* Also called internally when the user interacts with the input element.
*/
update() {
const Constants = shaka.ui.Constants;
const currentTime = this.getValue();
const bufferedLength = this.video.buffered.length;
const bufferedStart = bufferedLength ? this.video.buffered.start(0) : 0;
const bufferedEnd =
bufferedLength ? this.video.buffered.end(bufferedLength - 1) : 0;
const seekRange = this.player.seekRange();
const seekRangeSize = seekRange.end - seekRange.start;
this.setRange(seekRange.start, seekRange.end);
// Hide seekbar if the seek window is very small.
if (this.player.isLive() &&
seekRangeSize < Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR) {
shaka.ui.Utils.setDisplay(this.container, false);
} else {
shaka.ui.Utils.setDisplay(this.container, true);
const gradient = ['to right'];
if (bufferedLength == 0) {
gradient.push('#000 0%');
} else {
const clampedBufferStart = Math.max(bufferedStart, seekRange.start);
const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end);
const clampedCurrentTime = Math.min(
Math.max(currentTime, seekRange.start),
seekRange.end);
const bufferStartDistance = clampedBufferStart - seekRange.start;
const bufferEndDistance = clampedBufferEnd - seekRange.start;
const playheadDistance = clampedCurrentTime - seekRange.start;
// NOTE: the fallback to zero eliminates NaN.
const bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0;
const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
const playheadFraction = (playheadDistance / seekRangeSize) || 0;
gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
(bufferStartFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
(bufferStartFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_PLAYED_COLOR + ' ' +
(playheadFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
(playheadFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_BUFFERED_COLOR + ' ' +
(bufferEndFraction * 100) + '%');
gradient.push(Constants.SEEK_BAR_BASE_COLOR + ' ' +
(bufferEndFraction * 100) + '%');
}
this.container.style.background =
'linear-gradient(' + gradient.join(',') + ')';
}
}
/** @private */
updateAriaLabel_() {
this.bar.setAttribute(shaka.ui.Constants.ARIA_LABEL,
this.localization.resolve(shaka.ui.Locales.Ids.SEEK));
}
};
+43 -71
View File
@@ -18,95 +18,51 @@
goog.provide('shaka.ui.VolumeBar');
goog.require('shaka.ui.Element');
goog.require('shaka.ui.Constants');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.RangeElement');
/**
* @extends {shaka.ui.Element}
* @extends {shaka.ui.RangeElement}
* @final
* @export
*/
shaka.ui.VolumeBar = class extends shaka.ui.Element {
shaka.ui.VolumeBar = class extends shaka.ui.RangeElement {
/**
* @param {!HTMLElement} parent
* @param {!shaka.ui.Controls} controls
*/
constructor(parent, controls) {
super(parent, controls);
super(parent, controls,
['shaka-volume-bar-container'], ['shaka-volume-bar']);
// This container is to support IE 11. See detailed notes in
// less/range_elements.less for a complete explanation.
// TODO: Factor this into a range-element component.
/** @private {!HTMLElement} */
this.container_ = shaka.util.Dom.createHTMLElement('div');
this.container_.classList.add('shaka-volume-bar-container');
this.eventManager.listen(this.video,
'volumechange',
() => this.onVolumeStateChange_());
this.bar_ =
/** @type {!HTMLInputElement} */ (document.createElement('input'));
this.bar_.classList.add('shaka-volume-bar');
this.bar_.setAttribute('type', 'range');
// NOTE: step=any causes keyboard nav problems on IE 11.
this.bar_.setAttribute('step', 'any');
this.bar_.setAttribute('min', '0');
this.bar_.setAttribute('max', '1');
this.bar_.setAttribute('value', '0');
this.eventManager.listen(this.localization,
shaka.ui.Localization.LOCALE_UPDATED,
() => this.updateAriaLabel_());
this.container_.appendChild(this.bar_);
this.parent.appendChild(this.container_);
this.updateAriaLabel_();
this.eventManager.listen(this.localization,
shaka.ui.Localization.LOCALE_CHANGED,
() => this.updateAriaLabel_());
this.eventManager.listen(this.video, 'volumechange', () => {
this.onVolumeStateChange_();
});
this.eventManager.listen(this.bar_, 'input', () => {
this.onVolumeInput_();
});
this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
this.updateAriaLabel_();
});
this.eventManager.listen(
this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
this.updateAriaLabel_();
});
// Initialize volume display with a fake event.
// Initialize volume display and label.
this.onVolumeStateChange_();
this.updateAriaLabel_();
}
/**
* @private
* Update the video element's state to match the input element's state.
* Called by the base class when the input element changes.
*
* @override
*/
onVolumeStateChange_() {
if (this.video.muted) {
this.bar_.value = 0;
} else {
this.bar_.value = this.video.volume;
}
// TODO: Can we do this with LESS?
const gradient = ['to right'];
gradient.push(shaka.ui.Constants.VOLUME_BAR_VOLUME_LEVEL_COLOR +
(this.bar_.value * 100) + '%');
gradient.push(shaka.ui.Constants.VOLUME_BAR_BASE_COLOR +
(this.bar_.value * 100) + '%');
gradient.push(shaka.ui.Constants.VOLUME_BAR_BASE_COLOR + '100%');
this.container_.style.background =
'linear-gradient(' + gradient.join(',') + ')';
}
/**
* @private
*/
onVolumeInput_() {
this.video.volume = parseFloat(this.bar_.value);
onChange() {
this.video.volume = this.getValue();
if (this.video.volume == 0) {
this.video.muted = true;
} else {
@@ -114,12 +70,28 @@ shaka.ui.VolumeBar = class extends shaka.ui.Element {
}
}
/** @private */
onVolumeStateChange_() {
if (this.video.muted) {
this.setValue(0);
} else {
this.setValue(this.video.volume);
}
/**
* @private
*/
const gradient = ['to right'];
gradient.push(shaka.ui.Constants.VOLUME_BAR_VOLUME_LEVEL_COLOR +
(this.getValue() * 100) + '%');
gradient.push(shaka.ui.Constants.VOLUME_BAR_BASE_COLOR +
(this.getValue() * 100) + '%');
gradient.push(shaka.ui.Constants.VOLUME_BAR_BASE_COLOR + '100%');
this.container.style.background =
'linear-gradient(' + gradient.join(',') + ')';
}
/** @private */
updateAriaLabel_() {
this.bar_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
this.bar.setAttribute(shaka.ui.Constants.ARIA_LABEL,
this.localization.resolve(shaka.ui.Locales.Ids.VOLUME));
}
};