diff --git a/build/types/ui b/build/types/ui index c8bca18a49..547498ad49 100644 --- a/build/types/ui +++ b/build/types/ui @@ -16,6 +16,9 @@ +../../ui/element.js +../../ui/fast_forward_button.js +../../ui/fullscreen_button.js ++../../ui/hidden_fast_forward_button.js ++../../ui/hidden_rewind_button.js ++../../ui/hidden_seek_button.js +../../ui/language_utils.js +../../ui/localization.js +../../ui/loop_button.js diff --git a/ui/controls.js b/ui/controls.js index 14806f7f59..2b3700a37d 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -16,6 +16,8 @@ goog.require('shaka.ui.AdCounter'); goog.require('shaka.ui.AdPosition'); goog.require('shaka.ui.BigPlayButton'); goog.require('shaka.ui.ContextMenu'); +goog.require('shaka.ui.HiddenFastForwardButton'); +goog.require('shaka.ui.HiddenRewindButton'); goog.require('shaka.ui.Locales'); goog.require('shaka.ui.Localization'); goog.require('shaka.ui.SeekBar'); @@ -847,6 +849,11 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { this.addBufferingSpinner_(); } + if (this.config_.seekOnTaps) { + this.addFastForwardButtonOnControlsContainer_(); + this.addRewindButtonOnControlsContainer_(); + } + this.addDaiAdContainer_(); this.addControlsButtonPanel_(); @@ -970,6 +977,42 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { svg.appendChild(spinnerCircle); } + /** + * Add fast-forward button on Controls container for moving video some + * seconds ahead when the video is tapped more than once, video seeks ahead + * some seconds for every extra tap. + * @private + */ + addFastForwardButtonOnControlsContainer_() { + const hiddenFastForwardContainer = shaka.util.Dom.createHTMLElement('div'); + hiddenFastForwardContainer.classList.add( + 'shaka-hidden-fast-forward-container'); + this.controlsContainer_.appendChild(hiddenFastForwardContainer); + + /** @private {shaka.ui.HiddenFastForwardButton} */ + this.hiddenFastForwardButton_ = + new shaka.ui.HiddenFastForwardButton(hiddenFastForwardContainer, this); + this.elements_.push(this.hiddenFastForwardButton_); + } + + /** + * Add Rewind button on Controls container for moving video some seconds + * behind when the video is tapped more than once, video seeks behind some + * seconds for every extra tap. + * @private + */ + addRewindButtonOnControlsContainer_() { + const hiddenRewindContainer = shaka.util.Dom.createHTMLElement('div'); + hiddenRewindContainer.classList.add( + 'shaka-hidden-rewind-container'); + this.controlsContainer_.appendChild(hiddenRewindContainer); + + /** @private {shaka.ui.HiddenRewindButton} */ + this.hiddenRewindButton_ = + new shaka.ui.HiddenRewindButton(hiddenRewindContainer, this); + this.elements_.push(this.hiddenRewindButton_); + } + /** @private */ addControlsButtonPanel_() { /** @private {!HTMLElement} */ diff --git a/ui/externs/ui.js b/ui/externs/ui.js index 2eb59feaca..1d6b0c01b5 100644 --- a/ui/externs/ui.js +++ b/ui/externs/ui.js @@ -98,7 +98,9 @@ shaka.extern.UIVolumeBarColors; * keyboardLargeSeekDistance: number, * fullScreenElement: HTMLElement, * preferDocumentPictureInPicture: boolean, - * showAudioChannelCountVariants: boolean + * showAudioChannelCountVariants: boolean, + * seekOnTaps: boolean, + * tapSeekDistance: number * }} * * @property {!Array.} controlPanelElements @@ -215,6 +217,15 @@ shaka.extern.UIVolumeBarColors; * displayed or if, on the contrary, only the language should be displayed * regardless of the channel count. * Defaults to true. + * @property {boolean} seekOnTaps + * Indicates whether or not a fast-forward and rewind tap button that seeks + * video some seconds. + * Defaults to true. + * @property {number} tapSeekDistance + * The time interval, in seconds, to seek when the user presses the left or + * right part of the video. If less than or equal to 0, + * no seeking will occur. + * Defaults to 10 seconds. * @exportDoc */ shaka.extern.UIConfiguration; diff --git a/ui/hidden_fast_forward_button.js b/ui/hidden_fast_forward_button.js new file mode 100644 index 0000000000..27e216d03d --- /dev/null +++ b/ui/hidden_fast_forward_button.js @@ -0,0 +1,32 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.ui.HiddenFastForwardButton'); + +goog.require('shaka.ui.Enums'); +goog.require('shaka.ui.HiddenSeekButton'); + +goog.requireType('shaka.ui.Controls'); + +/** + * @extends {shaka.ui.HiddenSeekButton} + * @final + * @export + */ +shaka.ui.HiddenFastForwardButton = class extends shaka.ui.HiddenSeekButton { + /** + * @param {!HTMLElement} parent + * @param {!shaka.ui.Controls} controls + */ + constructor(parent, controls) { + super(parent, controls); + + this.seekContainer.classList.add('shaka-fast-foward-container'); + this.seekIcon.textContent = + shaka.ui.Enums.MaterialDesignIcons.FAST_FORWARD; + this.isRewind = false; + } +}; diff --git a/ui/hidden_rewind_button.js b/ui/hidden_rewind_button.js new file mode 100644 index 0000000000..257a119dd7 --- /dev/null +++ b/ui/hidden_rewind_button.js @@ -0,0 +1,32 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.ui.HiddenRewindButton'); + +goog.require('shaka.ui.Enums'); +goog.require('shaka.ui.HiddenSeekButton'); + +goog.requireType('shaka.ui.Controls'); + +/** + * @extends {shaka.ui.HiddenSeekButton} + * @final + * @export + */ +shaka.ui.HiddenRewindButton = class extends shaka.ui.HiddenSeekButton { + /** + * @param {!HTMLElement} parent + * @param {!shaka.ui.Controls} controls + */ + constructor(parent, controls) { + super(parent, controls); + + this.seekContainer.classList.add('shaka-rewind-container'); + this.seekIcon.textContent = + shaka.ui.Enums.MaterialDesignIcons.REWIND; + this.isRewind = true; + } +}; diff --git a/ui/hidden_seek_button.js b/ui/hidden_seek_button.js new file mode 100644 index 0000000000..5ea5138936 --- /dev/null +++ b/ui/hidden_seek_button.js @@ -0,0 +1,121 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.ui.HiddenSeekButton'); + +goog.require('shaka.ui.Element'); +goog.require('shaka.util.Timer'); +goog.require('shaka.util.Dom'); + +goog.requireType('shaka.ui.Controls'); + +/** + * @extends {shaka.ui.Element} + * @export + */ +shaka.ui.HiddenSeekButton = class extends shaka.ui.Element { + /** + * @param {!HTMLElement} parent + * @param {!shaka.ui.Controls} controls + */ + constructor(parent, controls) { + super(parent, controls); + + /** @private {?number} */ + this.lastTouchEventTimeSet_ = null; + + /** @private {?boolean} */ + this.triggeredTouchValid_ = false; + + /** + * This timer will be used to hide seek button on video Container. + * When the timer ticks it will force button to be invisible. + * + * @private {shaka.util.Timer} + */ + this.hideSeekButtonContainerTimer_ = new shaka.util.Timer(() => { + this.hideSeekButtonContainer_(); + }); + + /** @protected {!HTMLElement} */ + this.seekContainer = shaka.util.Dom.createHTMLElement('div'); + this.parent.appendChild(this.seekContainer); + + this.eventManager.listen(this.seekContainer, 'touchstart', (event) => { + // In case any settings menu are open this assigns the first touch + // to close the menu. + if (this.controls.anySettingsMenusAreOpen()) { + // prevent the default changes that browser triggers + event.preventDefault(); + this.controls.hideSettingsMenus(); + } else if (this.controls.getConfig().tapSeekDistance > 0) { + // prevent the default changes that browser triggers + event.preventDefault(); + this.onSeekButtonClick_(); + } + }); + + /** @private {!HTMLElement} */ + this.seekValue_ = shaka.util.Dom.createHTMLElement('span'); + this.seekValue_.textContent = '0s'; + this.seekContainer.appendChild(this.seekValue_); + + + /** @protected {!HTMLElement} */ + this.seekIcon = shaka.util.Dom.createHTMLElement('span'); + this.seekIcon.classList.add( + 'shaka-forward-rewind-container-icon'); + this.seekContainer.appendChild(this.seekIcon); + + /** @protected {boolean} */ + this.isRewind = false; + } + + /** + * @private + */ + onSeekButtonClick_() { + const tapSeekDistance = this.controls.getConfig().tapSeekDistance; + // This stores the time for first touch and makes touch valid for + // next 1s so incase the touch event is triggered again within 1s + // this if condition fails and the video seeking happens. + if (!this.triggeredTouchValid_) { + this.triggeredTouchValid_ = true; + this.lastTouchEventTimeSet_ = Date.now(); + this.hideSeekButtonContainerTimer_.tickAfter(1); + } else if (this.lastTouchEventTimeSet_+1000 > Date.now()) { + // stops hidding of seek button incase the timmer is active + // because of previous touch event. + this.hideSeekButtonContainerTimer_.stop(); + this.lastTouchEventTimeSet_ = Date.now(); + let position = 0; + if (this.isRewind) { + position = + parseInt(this.seekValue_.textContent, 10) - tapSeekDistance; + } else { + position = + parseInt(this.seekValue_.textContent, 10) + tapSeekDistance; + } + this.seekValue_.textContent = position.toString() + 's'; + this.seekContainer.style.opacity = '1'; + this.hideSeekButtonContainerTimer_.tickAfter(1); + } + } + + /** + * @private + */ + hideSeekButtonContainer_() { + // Prevent adding seek value if its a single tap. + if (parseInt(this.seekValue_.textContent, 10) != 0) { + this.video.currentTime = this.controls.getDisplayTime() + parseInt( + this.seekValue_.textContent, 10); + } + this.seekContainer.style.opacity = '0'; + this.triggeredTouchValid_ = false; + this.seekValue_.textContent = '0s'; + } +}; diff --git a/ui/less/buttons.less b/ui/less/buttons.less index fe9682df9e..a415db9a2d 100644 --- a/ui/less/buttons.less +++ b/ui/less/buttons.less @@ -106,3 +106,53 @@ outline: none; } } + +.shaka-fast-foward-container, +.shaka-rewind-container { + height: 100%; + width: 100%; + .shrinkable(); + .absolute-position(); + + /* Keep all the elements inside button div in center and in row */ + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + /* To be properly positioned, this should have no margin. */ + margin: 0; + + /* No border. */ + border: none; + + /* Setting text color to white */ + color: rgb(255 255 255); + + /* Setting background color to black so white text can be seen clearly */ + background-color: rgb(0 0 0 / 50%); + + cursor: default; + font-size: 20px; + + /* Hidding the container by setting opacity */ + opacity: 0; + + /* Make the text inside this button unselectable */ + .unselectable(); +} + +.shaka-fast-foward-container { + border-radius: 40% 0 0 40%; +} + +.shaka-rewind-container { + border-radius: 0 40% 40% 0; +} + +/* This class is instead of material-icon-round + * because the font-size of 24 doesn't look good */ +.shaka-forward-rewind-container-icon { + font-family: "Material Icons Round"; + font-size: 34px; +} diff --git a/ui/less/containers.less b/ui/less/containers.less index 253189f88a..a246b66a69 100644 --- a/ui/less/containers.less +++ b/ui/less/containers.less @@ -365,3 +365,21 @@ on a black background. */ filter: drop-shadow(0 0 2px rgba(255 255 255 / 50%)); } + +.shaka-hidden-fast-forward-container, +.shaka-hidden-rewind-container { + height: 100%; + width: 40%; + .shrinkable(); +} + +.shaka-hidden-fast-forward-container { + /* Keep the fast forward button to the right of this container. */ + .absolute-position(); + left: 60%; +} + +.shaka-hidden-rewind-container { + /* keep the rewind button to the left */ + .absolute-position(); +} diff --git a/ui/ui.js b/ui/ui.js index 166f43da13..222f2d7672 100644 --- a/ui/ui.js +++ b/ui/ui.js @@ -249,6 +249,8 @@ shaka.ui.Overlay = class { fullScreenElement: this.videoContainer_, preferDocumentPictureInPicture: true, showAudioChannelCountVariants: true, + seekOnTaps: true, + tapSeekDistance: 10, }; // eslint-disable-next-line no-restricted-syntax