Update z-index for modals to ensure they display correctly, adjust carousel scroll amount and behavior, and refine video card z-indexing. 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/QCN70f2
452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { type Video } from "@shared/schema";
|
|
import Hls from "hls.js";
|
|
import { X, Volume2, VolumeX, Play, Pause, Maximize, SkipForward } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
interface VASTPlayerProps {
|
|
video: Video;
|
|
onClose: () => void;
|
|
vastTagUrl?: string;
|
|
enableAds?: boolean;
|
|
}
|
|
|
|
// VAST Ad Network Configuration
|
|
const AD_NETWORKS = {
|
|
publift: {
|
|
name: "Publift",
|
|
vastUrl: "https://publiftvast.example.com/vast?zone=video&w=854&h=480",
|
|
priority: 1
|
|
},
|
|
vdo: {
|
|
name: "Vdo.ai",
|
|
vastUrl: "https://vdo.ai/vast?publisher_id=123&zone=preroll",
|
|
priority: 2
|
|
},
|
|
adplayer: {
|
|
name: "AdPlayer.Pro",
|
|
vastUrl: "https://adplayer.pro/vast?site_id=456&format=preroll",
|
|
priority: 3
|
|
},
|
|
aniview: {
|
|
name: "Aniview",
|
|
vastUrl: "https://aniview.com/vast?pub=789&placement=video",
|
|
priority: 4
|
|
}
|
|
};
|
|
|
|
// VAST Ad States
|
|
enum AdState {
|
|
LOADING = "loading",
|
|
PLAYING = "playing",
|
|
PAUSED = "paused",
|
|
COMPLETED = "completed",
|
|
SKIPPED = "skipped",
|
|
ERROR = "error"
|
|
}
|
|
|
|
export default function VASTPlayer({ video, onClose, vastTagUrl, enableAds = true }: VASTPlayerProps) {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const adVideoRef = useRef<HTMLVideoElement>(null);
|
|
const hlsRef = useRef<Hls | null>(null);
|
|
const adContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Video Player States
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [isMuted, setIsMuted] = useState(false);
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
const [duration, setDuration] = useState(0);
|
|
const [volume, setVolume] = useState(1);
|
|
|
|
// VAST Ad States
|
|
const [adState, setAdState] = useState<AdState>(AdState.LOADING);
|
|
const [isAdPlaying, setIsAdPlaying] = useState(false);
|
|
const [adDuration, setAdDuration] = useState(0);
|
|
const [adCurrentTime, setAdCurrentTime] = useState(0);
|
|
const [canSkipAd, setCanSkipAd] = useState(false);
|
|
const [skipCountdown, setSkipCountdown] = useState(5);
|
|
const [currentAdNetwork, setCurrentAdNetwork] = useState<string | null>(null);
|
|
|
|
// Initialize VAST Ad System
|
|
useEffect(() => {
|
|
if (enableAds) {
|
|
initializeVASTSystem();
|
|
} else {
|
|
initializeMainVideo();
|
|
}
|
|
}, [video, enableAds]);
|
|
|
|
const initializeVASTSystem = async () => {
|
|
console.log("🎯 Initializing VAST Ad System for go4.video");
|
|
|
|
// Waterfall approach - try ad networks in priority order
|
|
const networks = Object.entries(AD_NETWORKS).sort((a, b) => a[1].priority - b[1].priority);
|
|
|
|
for (const [networkId, network] of networks) {
|
|
try {
|
|
console.log(`📡 Attempting to load ads from ${network.name}`);
|
|
const success = await loadVASTAd(network.vastUrl, networkId);
|
|
if (success) {
|
|
setCurrentAdNetwork(networkId);
|
|
console.log(`✅ Successfully loaded ads from ${network.name}`);
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`⚠️ Failed to load ads from ${network.name}:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Fallback to main video if no ads load
|
|
if (!currentAdNetwork) {
|
|
console.log("📺 No ads available, playing main video");
|
|
initializeMainVideo();
|
|
}
|
|
};
|
|
|
|
const loadVASTAd = async (vastUrl: string, networkId: string): Promise<boolean> => {
|
|
return new Promise((resolve) => {
|
|
const adVideo = adVideoRef.current;
|
|
if (!adVideo) {
|
|
resolve(false);
|
|
return;
|
|
}
|
|
|
|
// Simulate VAST ad loading (in production, use real VAST parser)
|
|
setTimeout(() => {
|
|
// Mock ad video URL (replace with real VAST-parsed ad URL)
|
|
const mockAdUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
|
|
|
adVideo.src = mockAdUrl;
|
|
adVideo.muted = false;
|
|
adVideo.volume = volume;
|
|
|
|
adVideo.addEventListener('loadedmetadata', () => {
|
|
setAdDuration(adVideo.duration);
|
|
setAdState(AdState.PLAYING);
|
|
setIsAdPlaying(true);
|
|
startSkipCountdown();
|
|
resolve(true);
|
|
}, { once: true });
|
|
|
|
adVideo.addEventListener('error', () => {
|
|
console.error(`❌ Ad failed to load from ${networkId}`);
|
|
resolve(false);
|
|
}, { once: true });
|
|
|
|
adVideo.load();
|
|
}, 1000); // Simulate network delay
|
|
});
|
|
};
|
|
|
|
const startSkipCountdown = () => {
|
|
const countdown = setInterval(() => {
|
|
setSkipCountdown((prev) => {
|
|
if (prev <= 1) {
|
|
setCanSkipAd(true);
|
|
clearInterval(countdown);
|
|
return 0;
|
|
}
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
};
|
|
|
|
const skipAd = () => {
|
|
console.log("⏭️ User skipped ad");
|
|
setAdState(AdState.SKIPPED);
|
|
setIsAdPlaying(false);
|
|
initializeMainVideo();
|
|
|
|
// Track ad skip for analytics
|
|
if (currentAdNetwork) {
|
|
trackAdEvent('skip', currentAdNetwork);
|
|
}
|
|
};
|
|
|
|
const trackAdEvent = (event: string, network: string) => {
|
|
console.log(`📊 Ad Event: ${event} on ${network}`);
|
|
// In production, send to analytics service
|
|
};
|
|
|
|
const initializeMainVideo = () => {
|
|
console.log("🎬 Initializing main video playback");
|
|
const videoElement = videoRef.current;
|
|
if (!videoElement) return;
|
|
|
|
// Clean up previous HLS instance
|
|
if (hlsRef.current) {
|
|
hlsRef.current.destroy();
|
|
hlsRef.current = null;
|
|
}
|
|
|
|
const videoUrl = video.videoUrl;
|
|
|
|
if (videoUrl.includes('.m3u8')) {
|
|
if (Hls.isSupported()) {
|
|
const hls = new Hls({
|
|
debug: false,
|
|
enableWorker: true,
|
|
lowLatencyMode: false,
|
|
startLevel: -1,
|
|
capLevelToPlayerSize: true,
|
|
maxLoadingDelay: 4,
|
|
maxBufferLength: 30,
|
|
maxBufferSize: 60 * 1000 * 1000,
|
|
});
|
|
|
|
hls.loadSource(videoUrl);
|
|
hls.attachMedia(videoElement);
|
|
|
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
console.log("📺 HLS manifest loaded for main video");
|
|
videoElement.play();
|
|
setIsPlaying(true);
|
|
});
|
|
|
|
hlsRef.current = hls;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Ad video event handlers
|
|
useEffect(() => {
|
|
const adVideo = adVideoRef.current;
|
|
if (!adVideo) return;
|
|
|
|
const handleAdTimeUpdate = () => {
|
|
setAdCurrentTime(adVideo.currentTime);
|
|
};
|
|
|
|
const handleAdEnded = () => {
|
|
console.log("✅ Ad completed successfully");
|
|
setAdState(AdState.COMPLETED);
|
|
setIsAdPlaying(false);
|
|
initializeMainVideo();
|
|
|
|
if (currentAdNetwork) {
|
|
trackAdEvent('complete', currentAdNetwork);
|
|
}
|
|
};
|
|
|
|
adVideo.addEventListener('timeupdate', handleAdTimeUpdate);
|
|
adVideo.addEventListener('ended', handleAdEnded);
|
|
|
|
return () => {
|
|
adVideo.removeEventListener('timeupdate', handleAdTimeUpdate);
|
|
adVideo.removeEventListener('ended', handleAdEnded);
|
|
};
|
|
}, [currentAdNetwork]);
|
|
|
|
const toggleAdPlayPause = () => {
|
|
const adVideo = adVideoRef.current;
|
|
if (!adVideo) return;
|
|
|
|
if (isAdPlaying) {
|
|
adVideo.pause();
|
|
setIsAdPlaying(false);
|
|
setAdState(AdState.PAUSED);
|
|
} else {
|
|
adVideo.play();
|
|
setIsAdPlaying(true);
|
|
setAdState(AdState.PLAYING);
|
|
}
|
|
};
|
|
|
|
const toggleMute = () => {
|
|
const activeVideo = isAdPlaying ? adVideoRef.current : videoRef.current;
|
|
if (!activeVideo) return;
|
|
|
|
const newMutedState = !isMuted;
|
|
activeVideo.muted = newMutedState;
|
|
setIsMuted(newMutedState);
|
|
};
|
|
|
|
// Format time for display
|
|
const formatTime = (seconds: number): string => {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = Math.floor(seconds % 60);
|
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
return (
|
|
<div className="modal-overlay fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center" style={{ zIndex: 2147483647, position: 'fixed' }}>
|
|
<div className="relative w-full max-w-6xl mx-4">
|
|
{/* Close button */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onClose}
|
|
className="absolute -top-12 right-0 text-white hover:bg-white/20 z-10"
|
|
data-testid="button-close-video"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</Button>
|
|
|
|
{/* Video Container */}
|
|
<div className="relative bg-black rounded-lg overflow-hidden aspect-video">
|
|
{/* Main Video */}
|
|
<video
|
|
ref={videoRef}
|
|
className={`w-full h-full object-contain ${isAdPlaying ? 'hidden' : 'block'}`}
|
|
playsInline
|
|
onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
|
|
onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}
|
|
/>
|
|
|
|
{/* Ad Video */}
|
|
<video
|
|
ref={adVideoRef}
|
|
className={`w-full h-full object-contain ${isAdPlaying ? 'block' : 'hidden'}`}
|
|
playsInline
|
|
autoPlay
|
|
/>
|
|
|
|
{/* Ad Overlay */}
|
|
{isAdPlaying && (
|
|
<div className="absolute top-4 left-4 right-4">
|
|
{/* Ad Network Badge */}
|
|
<div className="flex justify-between items-center">
|
|
<div className="bg-yellow-600 text-white px-3 py-1 rounded-full text-sm font-medium">
|
|
📺 Ad {currentAdNetwork && `• ${AD_NETWORKS[currentAdNetwork as keyof typeof AD_NETWORKS].name}`}
|
|
</div>
|
|
|
|
{/* Skip Ad Button */}
|
|
{canSkipAd ? (
|
|
<Button
|
|
onClick={skipAd}
|
|
className="bg-gray-800 hover:bg-gray-700 text-white px-4 py-2 rounded-full text-sm"
|
|
data-testid="button-skip-ad"
|
|
>
|
|
<SkipForward className="w-4 h-4 mr-2" />
|
|
Skip Ad
|
|
</Button>
|
|
) : (
|
|
<div className="bg-gray-800 text-white px-4 py-2 rounded-full text-sm">
|
|
Skip in {skipCountdown}s
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Ad Progress */}
|
|
<div className="mt-2 bg-gray-800 rounded-full h-1">
|
|
<div
|
|
className="bg-yellow-600 h-full rounded-full transition-all duration-100"
|
|
style={{ width: `${adDuration ? (adCurrentTime / adDuration) * 100 : 0}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Video Controls */}
|
|
<div className="absolute bottom-4 left-4 right-4">
|
|
<div className="bg-black/70 backdrop-blur-sm rounded-lg p-4">
|
|
<div className="flex items-center space-x-4">
|
|
{/* Play/Pause */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={isAdPlaying ? toggleAdPlayPause : () => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
if (isPlaying) {
|
|
video.pause();
|
|
setIsPlaying(false);
|
|
} else {
|
|
video.play();
|
|
setIsPlaying(true);
|
|
}
|
|
}}
|
|
className="text-white hover:bg-white/20"
|
|
>
|
|
{(isAdPlaying ? isAdPlaying : isPlaying) ?
|
|
<Pause className="w-5 h-5" /> :
|
|
<Play className="w-5 h-5" />
|
|
}
|
|
</Button>
|
|
|
|
{/* Volume */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={toggleMute}
|
|
className="text-white hover:bg-white/20"
|
|
>
|
|
{isMuted ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
|
|
</Button>
|
|
|
|
{/* Time Display */}
|
|
<div className="text-white text-sm">
|
|
{isAdPlaying ?
|
|
`${formatTime(adCurrentTime)} / ${formatTime(adDuration)}` :
|
|
`${formatTime(currentTime)} / ${formatTime(duration)}`
|
|
}
|
|
</div>
|
|
|
|
<div className="flex-1" />
|
|
|
|
{/* Fullscreen */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const container = document.querySelector('.video-container');
|
|
if (container && container.requestFullscreen) {
|
|
container.requestFullscreen();
|
|
}
|
|
}}
|
|
className="text-white hover:bg-white/20"
|
|
>
|
|
<Maximize className="w-5 h-5" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
{!isAdPlaying && (
|
|
<div className="mt-3">
|
|
<div className="bg-gray-600 rounded-full h-1 cursor-pointer"
|
|
onClick={(e) => {
|
|
const video = videoRef.current;
|
|
if (!video || !duration) return;
|
|
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const percentage = x / rect.width;
|
|
const newTime = percentage * duration;
|
|
|
|
video.currentTime = newTime;
|
|
setCurrentTime(newTime);
|
|
}}
|
|
>
|
|
<div
|
|
className="bg-bunny-blue h-full rounded-full transition-all duration-100"
|
|
style={{ width: `${duration ? (currentTime / duration) * 100 : 0}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Video Info */}
|
|
<div className="mt-6 text-white">
|
|
<h2 className="text-2xl font-bold mb-2">{video.title}</h2>
|
|
<div className="flex items-center space-x-4 text-bunny-muted">
|
|
<span>{video.views} views</span>
|
|
<span>•</span>
|
|
<span>{new Date(video.createdAt).toLocaleDateString('sl-SI')}</span>
|
|
{currentAdNetwork && (
|
|
<>
|
|
<span>•</span>
|
|
<span className="text-yellow-500">
|
|
💰 Monetized with {AD_NETWORKS[currentAdNetwork as keyof typeof AD_NETWORKS].name}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |