Files
eranos/src/hooks/usePlayerControls.ts
T
Chad Curtis a7cd13228b Add autoplay videos setting (default off)
Adds a new autoplayVideos config field and a toggle in Settings > Content >
Video Playback. When enabled, videos auto-play muted in feeds, collage
thumbnails, profile sidebar tiles, the Vines feed, and the VideoPlayer
component. The preference syncs across devices via encrypted settings.

usePlayerControls now listens for the volumechange event to keep the
volume UI in sync when the video is programmatically muted for autoplay.
2026-04-20 12:26:58 -05:00

124 lines
4.1 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> | undefined>(undefined);
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);
// Keep React state in sync with the media element's muted/volume.
// Covers the initial state, programmatic changes (e.g. autoplay muting),
// and any external modifications.
useEffect(() => {
const media = mediaRef.current;
if (!media) return;
const sync = () => {
setIsMuted(media.muted);
setVolume(media.muted ? 0 : media.volume);
};
sync(); // initial read
media.addEventListener('volumechange', sync);
return () => media.removeEventListener('volumechange', sync);
}, [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 };
}