videofolxtv/client/src/components/vast-player.tsx
sebastjanartic 056cbdc881 Add monetization features for ad revenue generation and management
Integrate VAST ad player, ad settings UI, and monetization dashboard for ad network integration and revenue tracking.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/nN8xkSA
2025-08-08 18:11:29 +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 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="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">
📺 Oglas {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" />
Preskoči oglas
</Button>
) : (
<div className="bg-gray-800 text-white px-4 py-2 rounded-full text-sm">
Preskoči čez {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} ogledov</span>
<span></span>
<span>{new Date(video.createdAt).toLocaleDateString('sl-SI')}</span>
{currentAdNetwork && (
<>
<span></span>
<span className="text-yellow-500">
💰 Monetizirano z {AD_NETWORKS[currentAdNetwork as keyof typeof AD_NETWORKS].name}
</span>
</>
)}
</div>
</div>
</div>
</div>
);
}