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:
parent
f1331dafc6
commit
05355d44fe
@ -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 oscillator = audioContext.createOscillator();
|
|
||||||
const gainNode = audioContext.createGain();
|
|
||||||
|
|
||||||
oscillator.connect(gainNode);
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
gainNode.connect(audioContext.destination);
|
const x = e.clientX - rect.left;
|
||||||
|
const progress = Math.max(0, Math.min(1, x / rect.width));
|
||||||
|
|
||||||
// Subtle high-pitched tone
|
// Scrub video based on mouse position
|
||||||
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
|
if (videoRef.current.duration) {
|
||||||
oscillator.frequency.exponentialRampToValueAtTime(1200, audioContext.currentTime + 0.1);
|
const targetTime = progress * videoRef.current.duration;
|
||||||
|
videoRef.current.currentTime = targetTime;
|
||||||
// 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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user