Custom video player
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Mobile-specific player features:
|
||||
* - Double-tap left/right to skip ±10s
|
||||
* - Object-fit toggle button for widescreen fill
|
||||
*/
|
||||
|
||||
export function isMobile() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
export function initMobileWidescreen(playerWrapper, video) {
|
||||
if (!isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controls = playerWrapper.querySelector('.hstream-player__controls');
|
||||
if (!controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'hstream-player__button hstream-player__mobile-fill-btn';
|
||||
btn.type = 'button';
|
||||
btn.setAttribute('aria-label', 'Toggle screen fill');
|
||||
btn.innerHTML = '<i class="fa-solid fa-arrows-left-right-to-line"></i>';
|
||||
btn.title = 'Fill Screen';
|
||||
|
||||
const fullscreenBtn = controls.querySelector('[data-action="fullscreen"]');
|
||||
if (fullscreenBtn) {
|
||||
fullscreenBtn.insertAdjacentElement('beforebegin', btn);
|
||||
} else {
|
||||
controls.appendChild(btn);
|
||||
}
|
||||
|
||||
let fillEnabled = true;
|
||||
video.style.objectFit = 'cover';
|
||||
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (fillEnabled) {
|
||||
video.style.objectFit = 'contain';
|
||||
fillEnabled = false;
|
||||
btn.classList.remove('hstream-player__button--active');
|
||||
} else {
|
||||
video.style.objectFit = 'cover';
|
||||
fillEnabled = true;
|
||||
btn.classList.add('hstream-player__button--active');
|
||||
}
|
||||
});
|
||||
|
||||
btn.classList.add('hstream-player__button--active');
|
||||
}
|
||||
|
||||
export function initMobileDoubleTap(playerWrapper, video, player) {
|
||||
if (!isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skipOverlay = playerWrapper.querySelector('.hstream-player__skip-overlay');
|
||||
if (!skipOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
class MultiClickCounter {
|
||||
constructor() {
|
||||
this.timers = [];
|
||||
this.count = 0;
|
||||
this.reseted = 0;
|
||||
this.lastSide = null;
|
||||
}
|
||||
|
||||
clicked() {
|
||||
this.count += 1;
|
||||
const xcount = this.count;
|
||||
this.timers.push(setTimeout(() => this.reset(xcount), 500));
|
||||
return this.count;
|
||||
}
|
||||
|
||||
resetCount(n) {
|
||||
this.reseted = this.count;
|
||||
this.count = n;
|
||||
this.timers.forEach(t => clearTimeout(t));
|
||||
this.timers = [];
|
||||
}
|
||||
|
||||
reset(xcount) {
|
||||
if (this.count > xcount) return;
|
||||
this.count = 0;
|
||||
this.lastSide = null;
|
||||
this.reseted = 0;
|
||||
skipOverlay.classList.remove('hstream-player__skip-overlay--visible');
|
||||
this.timers = [];
|
||||
}
|
||||
}
|
||||
|
||||
const counter = new MultiClickCounter();
|
||||
|
||||
const handleTap = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const count = counter.clicked();
|
||||
if (count < 2) return;
|
||||
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
|
||||
const perc = (x / rect.width) * 100;
|
||||
|
||||
let shouldReset = true;
|
||||
const lastSide = counter.lastSide;
|
||||
|
||||
if (lastSide === null) {
|
||||
shouldReset = false;
|
||||
}
|
||||
|
||||
if (perc < 40) {
|
||||
if (player.currentTime === 0) return;
|
||||
counter.lastSide = 'L';
|
||||
if (shouldReset && lastSide !== 'L') {
|
||||
counter.resetCount(1);
|
||||
return;
|
||||
}
|
||||
const skipSeconds = (count - 1) * 10;
|
||||
player.currentTime = Math.max(0, player.currentTime - skipSeconds);
|
||||
skipOverlay.innerHTML = '<i class="fa-solid fa-backward"></i>' + skipSeconds + 's';
|
||||
skipOverlay.classList.add('hstream-player__skip-overlay--visible');
|
||||
setTimeout(() => skipOverlay.classList.remove('hstream-player__skip-overlay--visible'), 800);
|
||||
} else if (perc > 60) {
|
||||
if (player.currentTime >= player.duration) return;
|
||||
counter.lastSide = 'R';
|
||||
if (shouldReset && lastSide !== 'R') {
|
||||
counter.resetCount(1);
|
||||
return;
|
||||
}
|
||||
const skipSeconds = (count - 1) * 10;
|
||||
player.currentTime = Math.min(player.duration, player.currentTime + skipSeconds);
|
||||
skipOverlay.innerHTML = '<i class="fa-solid fa-forward"></i>' + skipSeconds + 's';
|
||||
skipOverlay.classList.add('hstream-player__skip-overlay--visible');
|
||||
setTimeout(() => skipOverlay.classList.remove('hstream-player__skip-overlay--visible'), 800);
|
||||
} else {
|
||||
player.togglePlay();
|
||||
counter.lastSide = 'C';
|
||||
}
|
||||
};
|
||||
|
||||
playerWrapper.addEventListener('click', handleTap);
|
||||
|
||||
video.addEventListener('dblclick', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user