diff --git a/client/src/components/ad-settings.tsx b/client/src/components/ad-settings.tsx new file mode 100644 index 0000000..c5575c5 --- /dev/null +++ b/client/src/components/ad-settings.tsx @@ -0,0 +1,324 @@ +import { useState, useEffect } from "react"; +import { Settings, DollarSign, TrendingUp, Users, Play } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; + +interface AdNetworkConfig { + id: string; + name: string; + description: string; + status: 'active' | 'inactive' | 'pending'; + eCPM: number; + fillRate: number; + priority: number; + vastUrl: string; +} + +const AD_NETWORKS: AdNetworkConfig[] = [ + { + id: 'publift', + name: 'Publift', + description: 'Agregator več oglaševalskih omrežij (Teads, Primis, Magnite)', + status: 'active', + eCPM: 2.85, + fillRate: 92, + priority: 1, + vastUrl: 'https://publiftvast.example.com/vast' + }, + { + id: 'vdo', + name: 'Vdo.ai', + description: 'Napredno AI ciljanje z visokim eCPM', + status: 'active', + eCPM: 3.20, + fillRate: 87, + priority: 2, + vastUrl: 'https://vdo.ai/vast' + }, + { + id: 'primis', + name: 'Primis', + description: 'Video discovery platforma z visoko angažiranostjo', + status: 'pending', + eCPM: 2.95, + fillRate: 89, + priority: 3, + vastUrl: 'https://primis.tech/vast' + }, + { + id: 'adplayer', + name: 'AdPlayer.Pro', + description: 'Outstream rešitve s sticky in rewarded oglasi', + status: 'inactive', + eCPM: 2.65, + fillRate: 85, + priority: 4, + vastUrl: 'https://adplayer.pro/vast' + }, + { + id: 'aniview', + name: 'Aniview', + description: 'Instream, outstream in CTV/OTT format', + status: 'inactive', + eCPM: 3.10, + fillRate: 91, + priority: 5, + vastUrl: 'https://aniview.com/vast' + } +]; + +interface AdSettingsProps { + isOpen: boolean; + onClose: () => void; +} + +export default function AdSettings({ isOpen, onClose }: AdSettingsProps) { + const [networks, setNetworks] = useState(AD_NETWORKS); + const [globalAdEnabled, setGlobalAdEnabled] = useState(true); + const [totalRevenue, setTotalRevenue] = useState(1247.83); + const [totalImpressions, setTotalImpressions] = useState(45231); + const [averageeCPM, setAverageeCPM] = useState(2.89); + + const toggleNetwork = (networkId: string) => { + setNetworks(networks.map(network => + network.id === networkId + ? { ...network, status: network.status === 'active' ? 'inactive' : 'active' } + : network + )); + }; + + const updatePriority = (networkId: string, newPriority: number) => { + setNetworks(networks.map(network => + network.id === networkId ? { ...network, priority: newPriority } : network + )); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': return 'bg-green-500'; + case 'pending': return 'bg-yellow-500'; + case 'inactive': return 'bg-gray-500'; + default: return 'bg-gray-500'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'active': return 'Aktivno'; + case 'pending': return 'V čakanju'; + case 'inactive': return 'Neaktivno'; + default: return 'Neznano'; + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+
+
+ +

+ Oglaševalske nastavitve +

+
+ +
+
+ +
+ {/* Revenue Overview */} +
+ + + + Skupni prihodek + + + +
+ €{totalRevenue.toFixed(2)} +
+
+ +12.5% ta mesec +
+
+
+ + + + + Skupni prikazi + + + +
+ {totalImpressions.toLocaleString()} +
+
+ +8.2% ta mesec +
+
+
+ + + + + Povprečni eCPM + + + +
+ €{averageeCPM.toFixed(2)} +
+
+ +5.7% ta mesec +
+
+
+
+ + {/* Global Ad Toggle */} + + + + Globalne oglaševalske nastavitve + + + + Omogoči ali onemogoči vse oglase na go4.video platformi + + + + + {/* Ad Networks Configuration */} + + + Oglaševalska omrežja + + Konfiguriraj VAST omrežja v waterfall vrstnem redu za optimalno monetizacijo + + + +
+ {networks + .sort((a, b) => a.priority - b.priority) + .map((network) => ( +
+
+
+

{network.name}

+ + {getStatusText(network.status)} + + + Prioriteta {network.priority} + +
+

+ {network.description} +

+
+ + eCPM: €{network.eCPM.toFixed(2)} + + + Fill Rate: {network.fillRate}% + +
+
+ +
+ + + toggleNetwork(network.id)} + disabled={network.status === 'pending'} + /> +
+
+ ))} +
+
+
+ + {/* Integration Guide */} + + + Napredna monetizacija + + Navodila za optimizacijo prihodkov z go4.video + + + +
+
+
+
+

Publift integracija

+

+ Agregator več omrežij za povišanje prihodkov do 55% +

+
+
+
+
+
+

Header bidding

+

+ Priporočeno za 100.000+ mesečnih ogledov +

+
+
+
+
+
+

VAST waterfall optimizacija

+

+ Avtomatsko preklapljanje med omrežji za maksimalne CPM-je +

+
+
+
+ + + +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/search-header.tsx b/client/src/components/search-header.tsx index 015c8d4..cf7df83 100644 --- a/client/src/components/search-header.tsx +++ b/client/src/components/search-header.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Search, Play, Menu, Grid3X3, List } from "lucide-react"; +import { Search, Play, Menu, Grid3X3, List, DollarSign, Settings } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -8,12 +8,14 @@ interface SearchHeaderProps { onSearch: (query: string) => void; onViewChange: (view: "grid" | "list") => void; currentView: "grid" | "list"; + onAdSettingsOpen?: () => void; } export default function SearchHeader({ onSearch, onViewChange, - currentView + currentView, + onAdSettingsOpen }: SearchHeaderProps) { const [searchQuery, setSearchQuery] = useState(""); @@ -43,6 +45,14 @@ export default function SearchHeader({ Admin + Library diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx index f000e3e..12daad7 100644 --- a/client/src/components/ui/badge.tsx +++ b/client/src/components/ui/badge.tsx @@ -33,4 +33,4 @@ function Badge({ className, variant, ...props }: BadgeProps) { ) } -export { Badge, badgeVariants } +export { Badge, badgeVariants } \ No newline at end of file diff --git a/client/src/components/vast-player.tsx b/client/src/components/vast-player.tsx new file mode 100644 index 0000000..180391f --- /dev/null +++ b/client/src/components/vast-player.tsx @@ -0,0 +1,452 @@ +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(null); + const adVideoRef = useRef(null); + const hlsRef = useRef(null); + const adContainerRef = useRef(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.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(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 => { + 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 ( +
+
+ {/* Close button */} + + + {/* Video Container */} +
+ {/* Main Video */} +