Introduce a new `HeaderAd` component for displaying AdSense ads on multiple pages, including `FolxStadlPage`, `GeschichteLiedPage`, `GipfelstammtischPage`, `Impressum`, `LivePage`, `PrivacyPolicy`, `TermsOfService`, `VideoPage`, `admin`, `home`, and `not-found`. This component is implemented in `client/src/components/HeaderAd.tsx` and integrated into the respective page components. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 45a1dcfc-f8a2-475a-a6b9-96fbb841dc27 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/60d372ff-2c10-46c7-b01b-10c3435136b0/45a1dcfc-f8a2-475a-a6b9-96fbb841dc27/HUI2eAX
550 lines
22 KiB
TypeScript
550 lines
22 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
|
import { ChevronLeft, Maximize, Volume2, VolumeX, Radio, Menu, X, Search } from 'lucide-react';
|
|
import { Link } from 'wouter';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import VideoCard from '@/components/video-card';
|
|
import AdSenseAd from '@/components/adsense-ad';
|
|
import HeaderAd from '@/components/HeaderAd';
|
|
|
|
declare global {
|
|
interface Window {
|
|
Hls: any;
|
|
}
|
|
}
|
|
|
|
export default function LivePage() {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const hlsRef = useRef<any>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [volume, setVolume] = useState(1);
|
|
const [isMuted, setIsMuted] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
// HLS stream URL
|
|
const streamUrl = 'https://cdne.folxplay.tv/fxt/streams/ch-4/master.m3u8';
|
|
|
|
// Fetch suggested videos
|
|
const { data: videosData } = useQuery({
|
|
queryKey: ['/api/videos'],
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
});
|
|
|
|
const videos = (videosData && Array.isArray((videosData as any).videos)) ? (videosData as any).videos : [];
|
|
|
|
useEffect(() => {
|
|
// Set page meta tags
|
|
document.title = 'LIVE Stream | video.folx.tv';
|
|
|
|
const metaDescription = document.querySelector('meta[name="description"]');
|
|
if (metaDescription) {
|
|
metaDescription.setAttribute('content', 'Live stream on video.folx.tv - watch exclusive content in real time.');
|
|
}
|
|
|
|
const updateMetaTag = (property: string, content: string) => {
|
|
let meta = document.querySelector(`meta[property="${property}"]`);
|
|
if (!meta) {
|
|
meta = document.createElement('meta');
|
|
meta.setAttribute('property', property);
|
|
document.head.appendChild(meta);
|
|
}
|
|
meta.setAttribute('content', content);
|
|
};
|
|
|
|
updateMetaTag('og:title', 'LIVE Stream - video.folx.tv');
|
|
updateMetaTag('og:description', 'Live stream on video.folx.tv - watch exclusive content in real time.');
|
|
updateMetaTag('og:type', 'video.other');
|
|
}, []);
|
|
|
|
// Initialize player immediately on mount
|
|
useEffect(() => {
|
|
initializePlayer();
|
|
return () => {
|
|
if (hlsRef.current) {
|
|
hlsRef.current.destroy();
|
|
}
|
|
if (videoRef.current) {
|
|
videoRef.current.src = '';
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
|
|
const initializePlayer = async () => {
|
|
console.log('🔴 LivePage: Starting to initialize player...');
|
|
|
|
if (!videoRef.current) {
|
|
console.error('🚨 Video ref not available!');
|
|
return;
|
|
}
|
|
|
|
console.log('🔴 Video element found, continuing...');
|
|
|
|
try {
|
|
// Load HLS.js if not already loaded
|
|
console.log('🔴 Checking if HLS.js is loaded...');
|
|
if (!window.Hls) {
|
|
console.log('🔴 Loading HLS.js script...');
|
|
const script = document.createElement('script');
|
|
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.8';
|
|
script.async = true;
|
|
document.head.appendChild(script);
|
|
|
|
await new Promise((resolve, reject) => {
|
|
script.onload = () => {
|
|
console.log('✅ HLS.js script loaded successfully');
|
|
resolve(null);
|
|
};
|
|
script.onerror = (e) => {
|
|
console.error('❌ Failed to load HLS.js script:', e);
|
|
reject(e);
|
|
};
|
|
});
|
|
} else {
|
|
console.log('✅ HLS.js already available');
|
|
}
|
|
|
|
const video = videoRef.current;
|
|
console.log('🔴 Stream URL:', streamUrl);
|
|
|
|
if (window.Hls && window.Hls.isSupported()) {
|
|
console.log('🔴 HLS.js is supported, initializing...');
|
|
const hls = new window.Hls({
|
|
debug: true,
|
|
enableWorker: false,
|
|
lowLatencyMode: true,
|
|
backBufferLength: 90,
|
|
maxBufferLength: 30,
|
|
maxMaxBufferLength: 600,
|
|
maxBufferSize: 60 * 1000 * 1000,
|
|
maxBufferHole: 0.5
|
|
});
|
|
|
|
console.log('🔴 Loading source:', streamUrl);
|
|
hls.loadSource(streamUrl);
|
|
hls.attachMedia(video);
|
|
|
|
hls.on(window.Hls.Events.MEDIA_ATTACHED, () => {
|
|
console.log('✅ HLS media attached successfully');
|
|
});
|
|
|
|
hls.on(window.Hls.Events.MANIFEST_PARSED, (event: any, data: any) => {
|
|
console.log('✅ HLS manifest parsed successfully:', data);
|
|
console.log('Available levels:', data.levels);
|
|
setIsLoading(false);
|
|
|
|
// Try auto-play
|
|
video.play().then(() => {
|
|
console.log('✅ Auto-play started successfully');
|
|
}).catch((e) => {
|
|
console.log('⚠️ Auto-play blocked, user interaction required:', e);
|
|
setError('Kliknite play za zagon streama');
|
|
});
|
|
});
|
|
|
|
hls.on(window.Hls.Events.LEVEL_LOADED, (event: any, data: any) => {
|
|
console.log('📊 Level loaded:', data);
|
|
});
|
|
|
|
hls.on(window.Hls.Events.FRAG_LOADED, (event: any, data: any) => {
|
|
console.log('📦 Fragment loaded:', data.frag.url);
|
|
});
|
|
|
|
hls.on(window.Hls.Events.ERROR, (event: any, data: any) => {
|
|
console.error('🚨 HLS Error occurred:', {
|
|
type: data.type,
|
|
details: data.details,
|
|
fatal: data.fatal,
|
|
error: data.error,
|
|
event,
|
|
data
|
|
});
|
|
|
|
if (data.fatal) {
|
|
switch (data.type) {
|
|
case window.Hls.ErrorTypes.NETWORK_ERROR:
|
|
console.log('🔄 Network error, attempting recovery...');
|
|
setError('Napaka pri povezavi s streamom - poskušam znova...');
|
|
hls.startLoad();
|
|
break;
|
|
case window.Hls.ErrorTypes.MEDIA_ERROR:
|
|
console.log('🔄 Media error, attempting recovery...');
|
|
setError('Napaka pri predvajanju - poskušam znova...');
|
|
hls.recoverMediaError();
|
|
break;
|
|
default:
|
|
console.error('💥 Fatal error, cannot recover:', data);
|
|
setError(`Napaka pri streamingu: ${data.details}`);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
hlsRef.current = hls;
|
|
|
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
// Native HLS support (Safari)
|
|
console.log('🍎 Using native HLS support (Safari)');
|
|
video.src = streamUrl;
|
|
|
|
video.addEventListener('loadedmetadata', () => {
|
|
console.log('✅ Native HLS metadata loaded');
|
|
setIsLoading(false);
|
|
});
|
|
|
|
video.addEventListener('error', (e) => {
|
|
console.error('❌ Native video error:', e);
|
|
setError('Napaka pri nalaganju streama');
|
|
});
|
|
|
|
} else {
|
|
console.error('❌ HLS not supported in this browser');
|
|
setError('HLS ni podprt v tem brskalniku');
|
|
setIsLoading(false);
|
|
}
|
|
|
|
// Add comprehensive video event listeners
|
|
video.addEventListener('play', () => {
|
|
console.log('▶️ Video started playing');
|
|
setIsPlaying(true);
|
|
});
|
|
|
|
video.addEventListener('pause', () => {
|
|
console.log('⏸️ Video paused');
|
|
setIsPlaying(false);
|
|
});
|
|
|
|
video.addEventListener('volumechange', () => {
|
|
setVolume(video.volume);
|
|
setIsMuted(video.muted);
|
|
});
|
|
|
|
video.addEventListener('loadstart', () => {
|
|
console.log('🔄 Video load started');
|
|
});
|
|
|
|
video.addEventListener('loadeddata', () => {
|
|
console.log('📊 Video data loaded');
|
|
});
|
|
|
|
video.addEventListener('canplay', () => {
|
|
console.log('✅ Video can start playing');
|
|
});
|
|
|
|
video.addEventListener('error', (e) => {
|
|
console.error('❌ Video element error:', e);
|
|
setError('Napaka video elementa');
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('💥 Failed to initialize live stream player:', error);
|
|
setError('Napaka pri inicializaciji playerja');
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const togglePlayPause = () => {
|
|
if (videoRef.current) {
|
|
if (isPlaying) {
|
|
videoRef.current.pause();
|
|
} else {
|
|
videoRef.current.play().catch((e) => {
|
|
console.log('Play failed:', e);
|
|
setError('Napaka pri predvajanju');
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const toggleMute = () => {
|
|
if (videoRef.current) {
|
|
videoRef.current.muted = !isMuted;
|
|
}
|
|
};
|
|
|
|
const toggleFullscreen = () => {
|
|
if (videoRef.current) {
|
|
if (videoRef.current.requestFullscreen) {
|
|
videoRef.current.requestFullscreen();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
return (
|
|
<div className="min-h-screen bg-bunny-dark">
|
|
{/* Header */}
|
|
<div className="bg-bunny-darker border-b border-white/10">
|
|
<div className="container mx-auto px-4 py-4">
|
|
<div className="flex items-center justify-between">
|
|
{/* Left side - Logo */}
|
|
<div className="flex items-center space-x-6 flex-1">
|
|
<Link href="/" className="flex items-center space-x-3 hover:opacity-80 transition-opacity py-4">
|
|
<div className="w-10 h-10 bg-[#da234d] rounded-lg flex items-center justify-center shadow-lg">
|
|
<div className="w-0 h-0 border-l-[11px] border-l-white border-y-[8px] border-y-transparent ml-1"></div>
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-white tracking-wide">video.folx.tv</h1>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Right side - Navigation + Search */}
|
|
<div className="flex items-center gap-4">
|
|
{/* Desktop navigation */}
|
|
<div className="hidden md:flex items-center space-x-6">
|
|
<nav className="flex space-x-6">
|
|
<Link href="/" className="relative text-bunny-light hover:text-white transition-colors group">
|
|
Startseite
|
|
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
|
|
</Link>
|
|
<Link href="/folx-stadl" className="relative text-bunny-light hover:text-white transition-colors group">
|
|
FOLX STADL
|
|
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
|
|
</Link>
|
|
<Link href="/geschichte-lied" className="relative text-bunny-light hover:text-white transition-colors group">
|
|
DIE GESCHICHTE DES LIEDES
|
|
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
|
|
</Link>
|
|
<Link href="/gipfelstammtisch" className="relative text-bunny-light hover:text-white transition-colors group">
|
|
GIPFELSTAMMTISCH
|
|
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
|
|
</Link>
|
|
<Link href="/live" className="relative text-red-500 hover:text-red-400 transition-colors group font-bold flex items-center space-x-1">
|
|
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
|
|
<span>LIVE</span>
|
|
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-red-400 via-red-500 to-red-600 transition-all duration-300 group-hover:w-full"></span>
|
|
</Link>
|
|
</nav>
|
|
|
|
<div className="relative">
|
|
<Input
|
|
type="search"
|
|
placeholder="Suchen..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="bg-white border border-gray-300 rounded-lg px-4 py-1.5 pl-10 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-[#da234d] transition-colors w-56"
|
|
/>
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu button */}
|
|
<button
|
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
className="md:hidden p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
|
|
data-testid="button-mobile-menu-live"
|
|
>
|
|
{isMobileMenuOpen ? (
|
|
<X className="w-6 h-6 text-white" />
|
|
) : (
|
|
<Menu className="w-6 h-6 text-white" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu dropdown */}
|
|
{isMobileMenuOpen && (
|
|
<div className="md:hidden border-t border-white/20 bg-bunny-dark/95 backdrop-blur-md">
|
|
<div className="px-6 py-4">
|
|
{/* Navigation Section */}
|
|
<div className="mb-6">
|
|
<h3 className="text-white text-xs font-semibold uppercase tracking-wider mb-3 opacity-70">Navigation</h3>
|
|
<nav className="flex flex-col space-y-4">
|
|
<Link
|
|
href="/"
|
|
className="text-bunny-light hover:text-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
>
|
|
Startseite
|
|
</Link>
|
|
<Link
|
|
href="/folx-stadl"
|
|
className="text-bunny-light hover:text-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
>
|
|
FOLX STADL
|
|
</Link>
|
|
<Link
|
|
href="/geschichte-lied"
|
|
className="text-bunny-light hover:text-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
>
|
|
DIE GESCHICHTE DES LIEDES
|
|
</Link>
|
|
<Link
|
|
href="/gipfelstammtisch"
|
|
className="text-bunny-light hover:text-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
>
|
|
GIPFELSTAMMTISCH
|
|
</Link>
|
|
<Link
|
|
href="/live"
|
|
className="text-red-500 hover:text-red-400 transition-colors font-bold py-1 border-l-2 border-transparent hover:border-red-500 pl-3 flex items-center space-x-2"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
>
|
|
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
|
|
<span>LIVE</span>
|
|
</Link>
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Separator */}
|
|
<div className="border-t border-white/10 mb-4"></div>
|
|
|
|
{/* Search Section */}
|
|
<div>
|
|
<h3 className="text-white text-xs font-semibold uppercase tracking-wider mb-3 opacity-70">Suchen</h3>
|
|
<div className="relative">
|
|
<Input
|
|
type="search"
|
|
placeholder="Suchen..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="bg-white/10 border border-white/20 rounded-lg px-4 py-2.5 pl-10 text-sm text-white placeholder-white/60 focus:outline-none focus:border-[#da234d] focus:bg-white/15 transition-all w-full"
|
|
/>
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/60 w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Fixed Header Ad */}
|
|
<HeaderAd />
|
|
|
|
<div className="container mx-auto px-4 py-3">
|
|
<div className="flex flex-col lg:flex-row gap-4">
|
|
{/* Main Video Player */}
|
|
<div className="flex-1">
|
|
<div className="bg-black rounded-lg overflow-hidden shadow-2xl">
|
|
<div className="relative aspect-video">
|
|
{error && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/80 text-red-400 z-20">
|
|
<div className="text-center">
|
|
<Radio className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
<p className="text-lg font-semibold mb-2">Stream Error</p>
|
|
<p className="text-sm opacity-75">{error}</p>
|
|
<Button
|
|
onClick={() => {
|
|
setError(null);
|
|
setIsLoading(true);
|
|
window.location.reload();
|
|
}}
|
|
className="mt-4 bg-red-600 hover:bg-red-700"
|
|
data-testid="button-retry"
|
|
>
|
|
Poskusite znova
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<video
|
|
ref={videoRef}
|
|
className="w-full h-full bg-black"
|
|
playsInline
|
|
autoPlay
|
|
muted
|
|
controls
|
|
crossOrigin="anonymous"
|
|
/>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* AdSense Ad under video stream */}
|
|
<div className="mt-6">
|
|
<AdSenseAd
|
|
adSlot="3047267170"
|
|
width={728}
|
|
height={90}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar - Empfohlene Videos */}
|
|
<div className="w-full lg:w-96 space-y-4">
|
|
{/* Sidebar Ad */}
|
|
<div className="mb-6">
|
|
<AdSenseAd
|
|
adSlot="5629279662"
|
|
width={728}
|
|
height={90}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<h2 className="text-lg font-semibold text-bunny-light mb-4">Empfohlene Videos</h2>
|
|
|
|
<div className="space-y-2">
|
|
{videos.slice(0, 10).map((video: any) => (
|
|
<div
|
|
key={video.id}
|
|
onClick={() => {
|
|
const shortId = video.id.replace(/-/g, '').substring(0, 8);
|
|
window.location.href = `/video/${shortId}`;
|
|
}}
|
|
className="flex gap-3 p-2 bg-bunny-gray/30 hover:bg-bunny-gray/50 rounded-lg cursor-pointer transition-colors"
|
|
>
|
|
<div className="relative w-24 h-16 bg-gray-700 rounded overflow-hidden flex-shrink-0">
|
|
<img
|
|
src={video.thumbnailUrl}
|
|
alt={video.title}
|
|
className="w-full h-full object-cover"
|
|
onError={(e) => {
|
|
const target = e.target as HTMLImageElement;
|
|
target.style.display = 'none';
|
|
if (target.parentElement && !target.parentElement.querySelector('.thumbnail-placeholder')) {
|
|
target.parentElement.style.background = 'linear-gradient(135deg, #1f2937, #374151)';
|
|
const placeholder = document.createElement('div');
|
|
placeholder.className = 'thumbnail-placeholder absolute inset-0 flex items-center justify-center text-white text-xs';
|
|
placeholder.innerHTML = '<div>🎬</div>';
|
|
target.parentElement.appendChild(placeholder);
|
|
}
|
|
}}
|
|
/>
|
|
<div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1 py-0.5 rounded">
|
|
{video.duration ? Math.floor(video.duration / 60) + ':' + String(video.duration % 60).padStart(2, '0') : '0:00'}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-sm font-medium text-bunny-light mb-1 line-clamp-2"
|
|
style={{
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 2,
|
|
WebkitBoxOrient: 'vertical',
|
|
overflow: 'hidden'
|
|
}}>
|
|
{video.title}
|
|
</h3>
|
|
<div className="text-xs text-bunny-muted">
|
|
<div>{video.views ? (video.views >= 1000 ? Math.floor(video.views / 1000) + 'K' : video.views) : '0'} views • {video.createdAt ? new Date(video.createdAt).toLocaleDateString('de-DE') : 'Unknown date'}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Bottom Sidebar Ad */}
|
|
<div className="mt-6">
|
|
<AdSenseAd
|
|
adSlot="9876543210"
|
|
width={728}
|
|
height={90}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |