robot-folding / app /src /components /Video.astro
pepijn223's picture
pepijn223 HF Staff
Fix video caption overlap with speed controls, merge Build on this section
3a3a16d unverified
---
interface Props {
src: string;
}
const { src } = Astro.props;
const id = `video-${Math.random().toString(36).slice(2, 9)}`;
---
<div class="video-player" data-video-player={id}>
<video id={id} src={src} controls muted preload="auto" playsinline style="width:100%; border-radius: 8px; display: block; background: #000;" />
<div class="speed-controls">
<span class="speed-label">Speed:</span>
<button class="speed-btn active" data-speed="1">1x</button>
<button class="speed-btn" data-speed="2">2x</button>
<button class="speed-btn" data-speed="4">4x</button>
<button class="speed-btn" data-speed="8">8x</button>
<button class="speed-btn" data-speed="16">16x</button>
</div>
</div>
<script>
document.querySelectorAll<HTMLElement>('[data-video-player]').forEach(player => {
const videoId = player.dataset.videoPlayer!;
const video = document.getElementById(videoId) as HTMLVideoElement;
if (!video) return;
let speed = 1;
let rafId: number | null = null;
let lastTime: number | null = null;
let seeking = false;
function stopSeekLoop() {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
lastTime = null;
seeking = false;
}
function startSeekLoop() {
stopSeekLoop();
if (speed <= 2) return;
seeking = true;
video.pause();
lastTime = performance.now();
function step(now: number) {
if (!seeking || lastTime === null) return;
const dt = (now - lastTime) / 1000;
lastTime = now;
video.currentTime = Math.min(video.currentTime + dt * speed, video.duration);
if (video.currentTime >= video.duration) {
stopSeekLoop();
return;
}
rafId = requestAnimationFrame(step);
}
rafId = requestAnimationFrame(step);
}
player.querySelectorAll<HTMLButtonElement>('.speed-btn').forEach(btn => {
btn.addEventListener('click', () => {
speed = parseFloat(btn.dataset.speed || '1');
player.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if (speed <= 2) {
stopSeekLoop();
video.playbackRate = speed;
if (video.paused && video.currentTime < video.duration) video.play();
} else {
video.playbackRate = 1;
startSeekLoop();
}
});
});
video.addEventListener('play', () => {
if (speed > 2) startSeekLoop();
});
video.addEventListener('pause', () => {
if (speed > 2 && !seeking) stopSeekLoop();
});
});
</script>
<style>
.video-player {
position: relative;
}
.speed-controls {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
margin-bottom: 4px;
}
.speed-label {
font-size: 0.8rem;
color: var(--text-color-secondary, #888);
margin-right: 2px;
}
.speed-btn {
font-size: 0.75rem;
padding: 3px 10px;
border-radius: 4px;
border: 1px solid var(--border-color, #ddd);
background: var(--surface-bg, #f5f5f5);
color: var(--text-color, #333);
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.speed-btn:hover {
border-color: var(--text-color-secondary, #888);
}
.speed-btn.active {
background: var(--text-color, #333);
color: var(--surface-bg, #fff);
border-color: var(--text-color, #333);
}
</style>