Files
eranos/src/hooks/usePlayerControls.ts
T
Derek Ross adc370a598 Fix live streams showing as active when ended, and fix muted icon state
Implement NIP-53 staleness heuristic (24h threshold) to mark abandoned
status=live streams as ended. Fetch all streams globally and filter
follows client-side (including p-tag participants for streaming services).
Add Live/Planned/Past pill tabs to the streams strip on /videos.

Also fix pre-existing bug where the volume icon showed unmuted while the
video was actually muted (autoplay requires muted), by syncing the
initial muted state from the media element in usePlayerControls.

Closes #88
2026-03-16 16:36:54 -04:00

116 lines
3.9 KiB
TypeScript

import { useRef, useState, useEffect, useCallback, RefObject } from 'react';
interface UsePlayerControlsOptions {
/** The media element (audio or video) to observe and pause when offscreen. */
mediaRef: RefObject<HTMLMediaElement | null>;
/** The container element to observe for IntersectionObserver and mouse events. */
containerRef: RefObject<HTMLElement | null>;
/** Whether the media is currently playing. */
isPlaying: boolean;
}
interface UsePlayerControlsReturn {
showControls: boolean;
revealControls: () => void;
scheduleHide: () => void;
isMuted: boolean;
volume: number;
toggleMute: (e: React.MouseEvent) => void;
handleVolumeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
/**
* Shared player control behaviour used by both VideoPlayer and AudioVisualizer:
* - Auto-hides controls 2.5 s after the last mouse movement while playing.
* - Pauses playback when the container scrolls out of view.
* - Manages volume state and mute toggling.
*/
export function usePlayerControls({
mediaRef,
containerRef,
isPlaying,
}: UsePlayerControlsOptions): UsePlayerControlsReturn {
const [showControls, setShowControls] = useState(true);
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const scheduleHide = useCallback(() => {
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
if (isPlaying) {
hideTimeoutRef.current = setTimeout(() => setShowControls(false), 2500);
}
}, [isPlaying]);
const revealControls = useCallback(() => {
setShowControls(true);
scheduleHide();
}, [scheduleHide]);
useEffect(() => {
if (isPlaying) {
scheduleHide();
} else {
setShowControls(true);
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
}
return () => { if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); };
}, [isPlaying, scheduleHide]);
// Pause when scrolled out of view
useEffect(() => {
const media = mediaRef.current;
const container = containerRef.current;
if (!media || !container) return;
const observer = new IntersectionObserver(
([entry]) => { if (!entry.isIntersecting && !media.paused) media.pause(); },
{ threshold: 0.25 },
);
observer.observe(container);
return () => observer.disconnect();
}, [mediaRef, containerRef]);
// ── Volume ─────────────────────────────────────────────────────────────
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(1);
const prevVolumeRef = useRef(1);
// Sync initial muted/volume state from the media element (e.g. when the
// <video> has a `muted` attribute for autoplay).
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
setIsMuted(media.muted);
setVolume(media.muted ? 0 : media.volume);
}, [mediaRef]);
const toggleMute = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const media = mediaRef.current;
if (!media) return;
if (media.muted || media.volume === 0) {
const restored = prevVolumeRef.current > 0 ? prevVolumeRef.current : 0.5;
media.muted = false;
media.volume = restored;
setIsMuted(false);
setVolume(restored);
} else {
prevVolumeRef.current = media.volume;
media.muted = true;
setIsMuted(true);
}
}, [mediaRef]);
const handleVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
const media = mediaRef.current;
if (!media) return;
const v = parseFloat(e.target.value);
media.volume = v;
media.muted = v === 0;
if (v > 0) prevVolumeRef.current = v;
setVolume(v);
setIsMuted(v === 0);
}, [mediaRef]);
return { showControls, revealControls, scheduleHide, isMuted, volume, toggleMute, handleVolumeChange };
}