Modify video-modal.tsx to enable controls, autoplay and fullscreen for embedded videos. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aa92e7e2-ec62-4c92-b21b-02ef78a664c2 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/aa92e7e2-ec62-4c92-b21b-02ef78a664c2/cqLZGoI
237 lines
7.5 KiB
TypeScript
237 lines
7.5 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { X } from "lucide-react";
|
|
import { type Video } from "@shared/schema";
|
|
import { Button } from "@/components/ui/button";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
import Hls from "hls.js";
|
|
|
|
interface VideoModalProps {
|
|
video: Video | null;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function formatDuration(seconds: number): string {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = seconds % 60;
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function formatViews(views: number): string {
|
|
if (views >= 1000000) {
|
|
return `${(views / 1000000).toFixed(1)}M views`;
|
|
} else if (views >= 1000) {
|
|
return `${(views / 1000).toFixed(1)}K views`;
|
|
}
|
|
return `${views} views`;
|
|
}
|
|
|
|
function formatDate(date: Date | string): string {
|
|
const now = new Date();
|
|
const createdDate = typeof date === 'string' ? new Date(date) : date;
|
|
|
|
if (!createdDate || isNaN(createdDate.getTime())) {
|
|
return "Unknown";
|
|
}
|
|
|
|
const diffTime = Math.abs(now.getTime() - createdDate.getTime());
|
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffDays === 0) return "Today";
|
|
if (diffDays === 1) return "1 day ago";
|
|
if (diffDays < 7) return `${diffDays} days ago`;
|
|
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
|
|
return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
|
|
}
|
|
|
|
export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const hlsRef = useRef<Hls | null>(null);
|
|
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape" && isOpen) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document.addEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "hidden";
|
|
} else {
|
|
document.body.style.overflow = "";
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "";
|
|
};
|
|
}, [isOpen, onClose]);
|
|
|
|
// Initialize HLS when video is available
|
|
useEffect(() => {
|
|
if (isOpen && video && videoRef.current) {
|
|
const videoElement = videoRef.current;
|
|
|
|
// Clean up previous HLS instance
|
|
if (hlsRef.current) {
|
|
hlsRef.current.destroy();
|
|
hlsRef.current = null;
|
|
}
|
|
|
|
const videoUrl = video.videoUrl;
|
|
console.log('Loading video:', videoUrl);
|
|
|
|
// Check if the video URL is HLS (.m3u8)
|
|
if (videoUrl.includes('.m3u8')) {
|
|
if (Hls.isSupported()) {
|
|
// Use HLS.js for browsers that don't support HLS natively
|
|
const hls = new Hls({
|
|
debug: true,
|
|
enableWorker: false,
|
|
lowLatencyMode: true,
|
|
backBufferLength: 90
|
|
});
|
|
|
|
hls.loadSource(videoUrl);
|
|
hls.attachMedia(videoElement);
|
|
|
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
console.log('HLS manifest loaded successfully');
|
|
});
|
|
|
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
|
console.error('HLS error:', data);
|
|
if (data.fatal) {
|
|
switch (data.type) {
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
console.log('Network error, trying to recover...');
|
|
hls.startLoad();
|
|
break;
|
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
console.log('Media error, trying to recover...');
|
|
hls.recoverMediaError();
|
|
break;
|
|
default:
|
|
console.log('Fatal error, destroying HLS instance...');
|
|
hls.destroy();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
hlsRef.current = hls;
|
|
} else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
|
|
// For Safari that supports HLS natively
|
|
videoElement.src = videoUrl;
|
|
console.log('Using native HLS support');
|
|
} else {
|
|
console.error('HLS is not supported in this browser');
|
|
}
|
|
} else {
|
|
// For regular MP4 videos
|
|
videoElement.src = videoUrl;
|
|
console.log('Using native video support for MP4');
|
|
}
|
|
}
|
|
|
|
// Cleanup when modal closes
|
|
return () => {
|
|
if (hlsRef.current) {
|
|
hlsRef.current.destroy();
|
|
hlsRef.current = null;
|
|
}
|
|
};
|
|
}, [isOpen, video]);
|
|
|
|
const handleVideoPlay = async () => {
|
|
if (video) {
|
|
try {
|
|
await apiRequest("POST", `/api/videos/${video.id}/view`);
|
|
} catch (error) {
|
|
console.error("Failed to track video view:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
if (e.target === e.currentTarget) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (!isOpen || !video) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-black/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
|
onClick={handleBackdropClick}
|
|
data-testid="modal-video"
|
|
>
|
|
<div className="relative w-full max-w-6xl">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onClose}
|
|
className="absolute -top-12 right-0 text-white hover:text-bunny-blue transition-colors z-60"
|
|
data-testid="button-close-modal"
|
|
>
|
|
<X className="text-2xl" />
|
|
</Button>
|
|
|
|
<div className="relative bg-black rounded-lg overflow-hidden">
|
|
{video.videoUrl.includes('iframe.mediadelivery.net') ? (
|
|
<iframe
|
|
src={`${video.videoUrl}?controls=true&autoplay=false&loop=false&muted=false&preload=metadata`}
|
|
className="w-full h-auto max-h-[80vh] aspect-video"
|
|
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture; fullscreen"
|
|
allowFullScreen
|
|
onLoad={handleVideoPlay}
|
|
data-testid="video-iframe"
|
|
title={video.title}
|
|
/>
|
|
) : (
|
|
<video
|
|
ref={videoRef}
|
|
className="w-full h-auto max-h-[80vh]"
|
|
controls
|
|
preload="metadata"
|
|
onPlay={handleVideoPlay}
|
|
data-testid="video-player"
|
|
crossOrigin="anonymous"
|
|
>
|
|
<source src={video.videoUrl} type="video/mp4" />
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
)}
|
|
|
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-6">
|
|
<h3
|
|
className="text-xl font-semibold mb-2 text-white"
|
|
data-testid="text-modal-title"
|
|
>
|
|
{video.title}
|
|
</h3>
|
|
<div className="flex items-center space-x-4 text-sm text-gray-300">
|
|
<span data-testid="text-modal-views">
|
|
{formatViews(video.views)}
|
|
</span>
|
|
<span data-testid="text-modal-date">
|
|
{formatDate(video.createdAt)}
|
|
</span>
|
|
<span data-testid="text-modal-duration">
|
|
{formatDuration(video.duration)}
|
|
</span>
|
|
</div>
|
|
{video.description && (
|
|
<p className="mt-3 text-gray-300 text-sm" data-testid="text-modal-description">
|
|
{video.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|