Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update hidden_seek_button.js [Handle scrolling] #8225

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 137 additions & 39 deletions ui/hidden_seek_button.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,46 @@ shaka.ui.HiddenSeekButton = class extends shaka.ui.Element {
constructor(parent, controls) {
super(parent, controls);

/**
* Replaces the original 1-second double-tap detection
* with 500 ms for a more responsive single tap.
* @private {number}
*/
this.DOUBLE_TAP_WINDOW_MS_ = 500;

/**
* Minimum distance (px) the finger must move during touch
* to consider it a scroll rather than a tap.
* @private {number}
*/
this.SCROLL_THRESHOLD_ = 10;

/** @private {?number} */
this.lastTouchEventTimeSet_ = null;

/** @private {?boolean} */
/** @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.
*
* Keeps track of whether the user has moved enough
* to be considered scrolling.
* @private {boolean}
*/
this.hasMoved_ = false;

/**
* Touch-start coordinates for detecting scroll distance.
* @private {?number}
*/
this.touchStartX_ = null;

/** @private {?number} */
this.touchStartY_ = null;

/**
* Timer used to hide the seek button container. In the timer’s callback,
* if the seek value is still 0s, we interpret it as a single tap
* (play/pause). If not, we perform the seek.
* @private {shaka.util.Timer}
*/
this.hideSeekButtonContainerTimer_ = new shaka.util.Timer(() => {
Expand All @@ -48,30 +78,21 @@ shaka.ui.HiddenSeekButton = class extends shaka.ui.Element {
this.seekContainer = shaka.util.Dom.createHTMLElement('div');
this.parent.appendChild(this.seekContainer);

this.eventManager.listen(this.seekContainer, 'touchend', (event) => {
// Do nothing if the controls are not visible
if (!this.controls.isOpaque()) {
return;
}
// 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_();
}
});
// ---------------------------------------------------------------
// TOUCH EVENT LISTENERS for SCROLL vs. TAP DETECTION
// ---------------------------------------------------------------
this.eventManager.listen(this.seekContainer, 'touchstart',
(e) => this.onTouchStart_(e));
this.eventManager.listen(this.seekContainer, 'touchmove',
(e) => this.onTouchMove_(e));
this.eventManager.listen(this.seekContainer, 'touchend',
(e) => this.onTouchEnd_(e));

/** @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(
Expand All @@ -83,45 +104,122 @@ shaka.ui.HiddenSeekButton = class extends shaka.ui.Element {
}

/**
* Called when the user starts touching the screen.
* We record the initial touch coordinates for scroll detection.
* @param {!Event} event
* @private
*/
onTouchStart_(event) {
// Only proceed if controls are visible.
if (!this.controls.isOpaque()) {
return;
}

// If multiple touches, handle or ignore as needed. Here, we assume single-touch.
if (event.touches.length > 0) {
this.touchStartX_ = event.touches[0].clientX;
this.touchStartY_ = event.touches[0].clientY;
}
this.hasMoved_ = false;
}

/**
* Called when the user moves the finger on the screen.
* If the movement exceeds the scroll threshold, we mark this as scrolling.
* @param {!Event} event
* @private
*/
onTouchMove_(event) {
if (event.touches.length > 0 &&
this.touchStartX_ != null &&
this.touchStartY_ != null) {
const dx = event.touches[0].clientX - this.touchStartX_;
const dy = event.touches[0].clientY - this.touchStartY_;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > this.SCROLL_THRESHOLD_) {
this.hasMoved_ = true;
}
}
}

/**
* Called when the user lifts the finger from the screen.
* If we haven't moved beyond the threshold, treat it as a tap.
* @param {!Event} event
* @private
*/
onTouchEnd_(event) {
// Only proceed if controls are visible.
if (!this.controls.isOpaque()) {
return;
}

// If user scrolled, don't handle as a tap.
if (this.hasMoved_) {
return;
}

// If any settings menus are open, this tap closes them instead of toggling play/seek.
if (this.controls.anySettingsMenusAreOpen()) {
event.preventDefault();
this.controls.hideSettingsMenus();
return;
}

// Normal tap logic (single vs double tap).
if (this.controls.getConfig().tapSeekDistance > 0) {
event.preventDefault();
this.onSeekButtonClick_();
}
}

/**
* Determines whether this tap is a single tap (leading to play/pause)
* or a double tap (leading to a seek). We use a 500 ms window.
* @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_) {
// First tap: start our 500 ms "double-tap" timer.
this.triggeredTouchValid_ = true;
this.lastTouchEventTimeSet_ = Date.now();
this.hideSeekButtonContainerTimer_.tickAfter(1);
} else if (this.lastTouchEventTimeSet_+1000 > Date.now()) {
// stops hiding of seek button incase the timer is active
// because of previous touch event.

this.hideSeekButtonContainerTimer_.tickAfter(
this.DOUBLE_TAP_WINDOW_MS_ / 1000);
} else if (this.lastTouchEventTimeSet_ + this.DOUBLE_TAP_WINDOW_MS_ > Date.now()) {
// Second tap arrived in time — interpret as a double tap to seek.
this.hideSeekButtonContainerTimer_.stop();
this.lastTouchEventTimeSet_ = Date.now();
let position = 0;

let position = parseInt(this.seekValue_.textContent, 10);
if (this.isRewind) {
position =
parseInt(this.seekValue_.textContent, 10) - tapSeekDistance;
position -= tapSeekDistance;
} else {
position =
parseInt(this.seekValue_.textContent, 10) + tapSeekDistance;
position += tapSeekDistance;
}
this.seekValue_.textContent = position.toString() + 's';
this.seekContainer.style.opacity = '1';
this.hideSeekButtonContainerTimer_.tickAfter(1);

// Restart timer if user might tap again (triple tap).
this.hideSeekButtonContainerTimer_.tickAfter(
this.DOUBLE_TAP_WINDOW_MS_ / 1000);
}
}

/**
* If the seek value is zero, interpret it as a single tap (play/pause).
* Otherwise, apply the seek and reset.
* @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);
const seekSeconds = parseInt(this.seekValue_.textContent, 10);
if (seekSeconds !== 0) {
// Perform the seek.
this.video.currentTime = this.controls.getDisplayTime() + seekSeconds;
}
// Hide and reset.
this.seekContainer.style.opacity = '0';
this.triggeredTouchValid_ = false;
this.seekValue_.textContent = '0s';
Expand Down
Loading