nextav/src/components/inline-video-player.tsx

265 lines
8.9 KiB
TypeScript

"use client";
import { useState, useRef, useEffect } from 'react';
import { X, Play, Pause, Maximize, Minimize, Volume2, VolumeX } from 'lucide-react';
interface InlineVideoPlayerProps {
video: {
id: number;
title: string;
path: string;
size: number;
thumbnail: string;
};
isOpen: boolean;
onClose: () => void;
scrollPosition?: number;
}
export default function InlineVideoPlayer({ video, isOpen, onClose, scrollPosition }: InlineVideoPlayerProps) {
const [isPlaying, setIsPlaying] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(1);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const [showControls, setShowControls] = useState(true);
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
} else {
setIsVisible(false);
}
}, [isOpen]);
useEffect(() => {
if (isOpen && videoRef.current) {
videoRef.current.src = `/api/stream/${video.id}`;
videoRef.current.load();
// Auto-play when video is loaded
videoRef.current.addEventListener('loadeddata', () => {
if (videoRef.current) {
videoRef.current.play().then(() => {
setIsPlaying(true);
}).catch((error) => {
console.log('Auto-play prevented by browser:', error);
// Auto-play might be blocked by browser, that's okay
});
}
});
}
}, [isOpen, video.id]);
const handlePlayPause = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const handleMute = () => {
if (videoRef.current) {
videoRef.current.muted = !isMuted;
setIsMuted(!isMuted);
}
};
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (videoRef.current) {
const newVolume = parseFloat(e.target.value);
videoRef.current.volume = newVolume;
setVolume(newVolume);
setIsMuted(newVolume === 0);
}
};
const handleTimeUpdate = () => {
if (videoRef.current) {
setCurrentTime(videoRef.current.currentTime);
}
};
const handleLoadedMetadata = () => {
if (videoRef.current) {
setDuration(videoRef.current.duration);
}
};
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (videoRef.current) {
const rect = e.currentTarget.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const newTime = (clickX / rect.width) * duration;
videoRef.current.currentTime = newTime;
setCurrentTime(newTime);
}
};
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
if (e.key === ' ') {
e.preventDefault();
handlePlayPause();
}
};
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
// Prevent body scroll when player is open
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Restore body scroll when player is closed
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className={`fixed inset-0 z-50 bg-background transition-opacity duration-300 flex flex-col ${isVisible ? 'opacity-100' : 'opacity-0'}`}>
{/* Header */}
<div className="flex-shrink-0 bg-background border-b border-border">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={onClose}
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
>
<X className="h-5 w-5" />
<span>Back</span>
</button>
</div>
<h1 className="text-xl font-semibold text-foreground truncate max-w-md">
{video.title}
</h1>
</div>
</div>
</div>
{/* Video Player */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="aspect-video bg-black rounded-lg overflow-hidden relative group">
<video
ref={videoRef}
className="w-full h-full object-contain"
onTimeUpdate={handleTimeUpdate}
onLoadedMetadata={handleLoadedMetadata}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onMouseMove={() => setShowControls(true)}
onMouseLeave={() => setShowControls(false)}
controls={false}
>
<source src={`/api/stream/${video.id}`} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Video Overlay Controls */}
<div className={`absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`}>
{/* Center Play Button */}
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={handlePlayPause}
className="w-20 h-20 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-all duration-200"
>
{isPlaying ? (
<Pause className="h-8 w-8 text-white" />
) : (
<Play className="h-8 w-8 text-white ml-1" />
)}
</button>
</div>
{/* Bottom Controls */}
<div className="absolute bottom-0 left-0 right-0 p-4">
{/* Progress Bar */}
<div
className="relative h-2 bg-white/20 rounded-full cursor-pointer mb-4"
onClick={handleProgressClick}
>
<div
className="absolute h-full bg-white rounded-full"
style={{ width: `${(currentTime / duration) * 100 || 0}%` }}
/>
</div>
{/* Control Buttons */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={handlePlayPause}
className="p-2 hover:bg-white/20 rounded-full transition-colors"
>
{isPlaying ? <Pause className="h-5 w-5 text-white" /> : <Play className="h-5 w-5 text-white" />}
</button>
<div className="flex items-center gap-2">
<button
onClick={handleMute}
className="p-2 hover:bg-white/20 rounded-full transition-colors"
>
{isMuted ? <VolumeX className="h-5 w-5 text-white" /> : <Volume2 className="h-5 w-5 text-white" />}
</button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={handleVolumeChange}
className="w-24 h-1 bg-white/20 rounded-full appearance-none cursor-pointer"
/>
</div>
<span className="text-sm text-white">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div>
<span className="text-sm text-white">
{Math.round(video.size / 1024 / 1024)} MB
</span>
</div>
</div>
</div>
</div>
{/* Video info below player */}
<div className="mt-6 space-y-3 pb-8">
<h2 className="text-2xl font-bold text-foreground break-words leading-tight">{video.title}</h2>
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
<p className="text-muted-foreground font-mono text-sm break-words leading-relaxed">
{video.path}
</p>
<p className="text-muted-foreground text-sm">
File size: {Math.round(video.size / 1024 / 1024)} MB
</p>
</div>
</div>
</div>
</div>
</div>
);
}