videofolxtv/client/src/components/vast-player.tsx
sebastjanartic b5f0b50c4a Update platform branding and sharing links to reflect the new domain name
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
2025-09-04 13:34:31 +00:00

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>
);
}