Add video scrubbing and sound preview for interactive browsing

Implement mouse-based video scrubbing and low-volume audio playback for previews in the video card component.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 2eb1084e-b728-4449-9231-f1665924c8d5
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2eb1084e-b728-4449-9231-f1665924c8d5/P3O2FU7
This commit is contained in:
sebastjanartic 2025-08-29 10:38:06 +00:00
parent f1331dafc6
commit 05355d44fe

View File

@ -51,31 +51,18 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<any>(null); const hlsRef = useRef<any>(null);
// Create subtle hover sound // Handle mouse scrubbing for video preview
const playHoverSound = () => { const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
try { if (!showPreview || !videoRef.current) return;
// Create a subtle audio context for hover sound
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const rect = e.currentTarget.getBoundingClientRect();
const oscillator = audioContext.createOscillator(); const x = e.clientX - rect.left;
const gainNode = audioContext.createGain(); const progress = Math.max(0, Math.min(1, x / rect.width));
oscillator.connect(gainNode); // Scrub video based on mouse position
gainNode.connect(audioContext.destination); if (videoRef.current.duration) {
const targetTime = progress * videoRef.current.duration;
// Subtle high-pitched tone videoRef.current.currentTime = targetTime;
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(1200, audioContext.currentTime + 0.1);
// Very quiet volume
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.02, audioContext.currentTime + 0.02);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.15);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.15);
} catch (error) {
// Silently fail if audio context not supported
console.debug('Audio context not supported');
} }
}; };
@ -131,6 +118,9 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
hlsRef.current.on(Hls.Events.MANIFEST_PARSED, () => { hlsRef.current.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('HLS manifest parsed, starting playback'); console.log('HLS manifest parsed, starting playback');
// Enable audio and play
videoElement.muted = false;
videoElement.volume = 0.3; // Low volume for preview
videoElement.play().catch(e => console.log('Autoplay failed:', e)); videoElement.play().catch(e => console.log('Autoplay failed:', e));
}); });
@ -140,6 +130,8 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
} else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS support // Safari native HLS support
videoElement.src = video.videoUrl; videoElement.src = video.videoUrl;
videoElement.muted = false;
videoElement.volume = 0.3;
videoElement.play().catch(e => console.log('Autoplay failed:', e)); videoElement.play().catch(e => console.log('Autoplay failed:', e));
} }
} }
@ -156,16 +148,14 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
<div <div
data-testid={`card-video-${video.id}`} data-testid={`card-video-${video.id}`}
className={`video-card transition-transform duration-200 hover:scale-[1.02] p-1 md:p-2 ${className}`} className={`video-card transition-transform duration-200 hover:scale-[1.02] p-1 md:p-2 ${className}`}
onMouseEnter={() => { onMouseEnter={() => setIsHovered(true)}
setIsHovered(true);
playHoverSound();
}}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
> >
{/* Video preview container */} {/* Video preview container */}
<div <div
className="relative gradient-card rounded-lg overflow-hidden mb-2 aspect-[9/16] md:aspect-[16/9] cursor-pointer group" className="relative gradient-card rounded-lg overflow-hidden mb-2 aspect-[9/16] md:aspect-[16/9] cursor-pointer group"
onClick={() => onClick?.(video)} onClick={() => onClick?.(video)}
onMouseMove={handleMouseMove}
> >
{/* Static thumbnail - always visible */} {/* Static thumbnail - always visible */}
<img <img
@ -205,10 +195,11 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
objectPosition: video.faceCenterPosition || 'center center', objectPosition: video.faceCenterPosition || 'center center',
objectFit: 'cover' objectFit: 'cover'
}} }}
muted muted={false} // Enable audio for preview
loop loop
playsInline playsInline
preload="none" preload="none"
volume={0.3}
/> />
</div> </div>
)} )}