This commit updates all instances of the old domain name "go4.video" to the new domain name "video.folx.tv" across various frontend components, including modals, headers, cards, and page metadata. It also includes updates to sharing intent URLs for Twitter and WhatsApp to use the new domain. Additionally, CSS for Video.js loading animations has been added to `index.css`. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 946a0075-7e32-454b-b348-9e7f576d7f45 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/60d372ff-2c10-46c7-b01b-10c3435136b0/946a0075-7e32-454b-b348-9e7f576d7f45/jh6R7y2
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 video.folx.tv");
|
|
|
|
// 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="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50">
|
|
<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-rose-600 hover:bg-rose-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-rose-500 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-rose-500/30"
|
|
>
|
|
{(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-rose-500/30"
|
|
>
|
|
{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-rose-500/30"
|
|
>
|
|
<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-rose-500 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>
|
|
);
|
|
} |