diff --git a/client/src/components/video-card.tsx b/client/src/components/video-card.tsx index f42e984..36745e5 100644 --- a/client/src/components/video-card.tsx +++ b/client/src/components/video-card.tsx @@ -1,7 +1,7 @@ import { Play } from "lucide-react"; import { type Video } from "@shared/schema"; -import HLSPreviewThumbnail from "./hls-preview-thumbnail"; import { useState, useRef, useEffect } from "react"; +import Hls from "hls.js"; interface VideoCardProps { video: Video; @@ -46,6 +46,10 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP const [isHovered, setIsHovered] = useState(false); const [showPreview, setShowPreview] = useState(false); const hoverTimeoutRef = useRef(); + const videoRef = useRef(null); + const hlsRef = useRef(null); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); // Delay preview start to avoid loading on quick mouse passes useEffect(() => { @@ -62,15 +66,59 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP clearTimeout(hoverTimeoutRef.current); } setShowPreview(false); + // Clean up HLS when not hovering + if (hlsRef.current) { + hlsRef.current.destroy(); + hlsRef.current = null; + } } return () => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); } + if (hlsRef.current) { + hlsRef.current.destroy(); + } }; }, [isHovered]); + // Initialize HLS when preview shows + useEffect(() => { + if (showPreview && videoRef.current && video.videoUrl.includes('.m3u8')) { + const videoElement = videoRef.current; + + if (Hls.isSupported()) { + const hls = new Hls({ + startLevel: 0, // Start with lowest quality + capLevelToPlayerSize: true, + maxBufferLength: 5, // Minimal buffering + }); + + hls.loadSource(video.videoUrl); + hls.attachMedia(videoElement); + hlsRef.current = hls; + + hls.on(Hls.Events.MANIFEST_PARSED, () => { + videoElement.play().catch(e => console.log('Preview autoplay failed:', e)); + }); + + hls.on(Hls.Events.MEDIA_ATTACHED, () => { + videoElement.addEventListener('loadedmetadata', () => { + setDuration(videoElement.duration); + }); + }); + } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { + // Safari native HLS support + videoElement.src = video.videoUrl; + videoElement.addEventListener('loadedmetadata', () => { + setDuration(videoElement.duration); + }); + videoElement.play().catch(e => console.log('Preview autoplay failed:', e)); + } + } + }, [showPreview, video.videoUrl]); + return (
+
+ onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)} + onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)} + onMouseMove={(e) => { + if (!videoRef.current || duration === 0) return; + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percentage = x / rect.width; + const newTime = Math.max(0, Math.min(duration, percentage * duration)); + videoRef.current.currentTime = newTime; + setCurrentTime(newTime); + }} + /> + + {/* Video scrubbing progress bar - only show during preview */} + {duration > 0 && ( +
+
+
+ )}
)} diff --git a/server/bunny.ts b/server/bunny.ts index f973ca8..7161539 100644 --- a/server/bunny.ts +++ b/server/bunny.ts @@ -107,7 +107,7 @@ export class BunnyService { thumbnailUrl, customThumbnailUrl: null, videoUrl: hlsUrl, // Signed HLS URL - videoUrlMp4: `https://${this.hostname}/${bunnyVideo.guid}/play_720p.mp4`, // Direct MP4 URL for preview + videoUrlMp4: hlsUrl, // Use signed HLS URL for preview as well videoUrlIframe: iframeUrl, // iframe fallback duration: Math.floor(bunnyVideo.length || 0), views: bunnyVideo.views || 0,