import React, { useState, useEffect, useRef } from 'react'; import Hls from 'hls.js'; import { Tv, Search, Volume2, VolumeX, Radio, Trophy, Play, RefreshCw, Info } from 'lucide-react'; // Simulated match list for the World Cup 2026 panel const worldCupMatches = [ { id: 'm1', time: 'Hari Ini - 20:00 WIB', teamA: 'Indonesia 🇮🇩', teamB: 'Argentina 🇦🇷', status: 'LIVE', channelId: 'tvri_sport' }, { id: 'm2', time: 'Besok - 18:00 WIB', teamA: 'Jepang 🇯🇵', teamB: 'Jerman 🇩🇪', status: 'Upcoming', channelId: 'tvri_sport' }, { id: 'm3', time: '13 Jun - 23:00 WIB', teamA: 'Spanyol 🇪🇸', teamB: 'Prancis 🇫🇷', status: 'Upcoming', channelId: 'tvri_sport' }, { id: 'm4', time: '14 Jun - 02:00 WIB', teamA: 'Brasil 🇧🇷', teamB: 'Inggris 🏴󠁧󠁢󠁥󠁮󠁧󠁿', status: 'Upcoming', channelId: 'tvri_sport' } ]; export default function App() { const [channels, setChannels] = useState([]); const [activeChannel, setActiveChannel] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('Semua'); const [reloadKey, setReloadKey] = useState(0); const [isMuted, setIsMuted] = useState(true); const [isPlaying, setIsPlaying] = useState(true); const [userProfile, setUserProfile] = useState({ username: 'Tamu', avatar: null }); const [voiceChannelName, setVoiceChannelName] = useState('Browser Mode'); const [statusMessage, setStatusMessage] = useState('Menginisialisasi...'); const [playerError, setPlayerError] = useState(null); const videoRef = useRef(null); // 1. Detect if running inside Discord Activity Frame const queryParams = new URLSearchParams(window.location.search); const isEmbedded = queryParams.has('frame_id') || window.self !== window.top; const currentVoiceChannelId = queryParams.get('voiceChannelId') || 'local-session'; // 2. Initialize Discord SDK & Authenticate useEffect(() => { async function setupDiscordSDK() { if (!isEmbedded) { setStatusMessage('Browser Mode aktif'); return; } setStatusMessage('Menghubungkan ke Discord...'); try { const { DiscordSDK } = await import('@discord/embedded-app-sdk'); // VITE_DISCORD_CLIENT_ID will be loaded if configured, otherwise we fallback const clientId = import.meta.env.VITE_DISCORD_CLIENT_ID || '1234567890'; const sdk = new DiscordSDK(clientId); await sdk.ready(); setStatusMessage('Mengotentikasi...'); // Authorize with Discord const { code } = await sdk.commands.authorize({ client_id: clientId, response_type: 'code', state: '', prompt: 'none', scope: ['identify', 'guilds'], }); // Exchange code for token on backend Express server const response = await fetch('/api/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }), }); const { access_token } = await response.json(); // Authenticate SDK client const auth = await sdk.commands.authenticate({ access_token }); if (auth.user) { setUserProfile({ username: auth.user.global_name || auth.user.username, avatar: auth.user.avatar ? `https://cdn.discordapp.com/avatars/${auth.user.id}/${auth.user.avatar}.png` : null }); } // Get Channel info if (sdk.channelId) { try { const channel = await sdk.commands.getChannel({ channel_id: sdk.channelId }); setVoiceChannelName(channel.name || 'Voice Channel'); } catch { setVoiceChannelName('Discord Activity'); } } setStatusMessage('Terkoneksi ke Discord Voice'); } catch (err) { console.error('Error initializing Discord SDK:', err); setStatusMessage('Offline (Gagal meluncurkan SDK)'); } } setupDiscordSDK(); }, []); // 3. Fetch IPTV channels from Backend useEffect(() => { fetch('/api/channels') .then(res => res.json()) .then(data => { setChannels(data); // Default to first channel if (data.length > 0) { setActiveChannel(data[0]); } }) .catch(err => console.error('Error fetching channels:', err)); }, []); // 4. Connect to Server-Sent Events (SSE) to sync with Discord Remote Control useEffect(() => { // Open EventSource SSE connection linked to this voice channel const sseUrl = `/api/stream-control?voiceChannelId=${currentVoiceChannelId}`; console.log(`Connecting to SSE sync stream at: ${sseUrl}`); const eventSource = new EventSource(sseUrl); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); console.log('Received Remote Action:', data); if (data.action === 'change-channel') { setActiveChannel(data.channel); // Auto play on change setIsPlaying(true); } else if (data.action === 'reload') { setReloadKey(prev => prev + 1); } else if (data.action === 'stop') { setActiveChannel(null); } } catch (e) { console.error('Error parsing SSE event data:', e); } }; eventSource.onerror = (err) => { console.error('SSE Connection lost. Retrying...', err); }; return () => { eventSource.close(); }; }, [currentVoiceChannelId]); // 5. HLS.js Stream Player Engine useEffect(() => { const video = videoRef.current; if (!video || !activeChannel) return; let hls = null; // Reset video states video.pause(); setPlayerError(null); if (Hls.isSupported()) { hls = new Hls({ maxMaxBufferLength: 8, liveSyncDuration: 3, enableWorker: true }); const refParam = activeChannel.referrer ? `&referer=${encodeURIComponent(activeChannel.referrer)}` : ''; const uaParam = activeChannel.userAgent ? `&userAgent=${encodeURIComponent(activeChannel.userAgent)}` : ''; hls.loadSource(`/api/proxy?url=${encodeURIComponent(activeChannel.url)}${refParam}${uaParam}`); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { if (isPlaying) { video.play().catch(e => { console.log('Autoplay blocked:', e); setPlayerError('Browser memblokir pemutaran otomatis. Klik tombol "Mulai" di bawah.'); }); } }); hls.on(Hls.Events.ERROR, (event, data) => { console.error('HLS Error:', data); if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: setPlayerError('Kesalahan Jaringan: Gagal memuat segmen video (kemungkinan CORS atau stream offline).'); hls.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: setPlayerError('Kesalahan Media: Gagal men-decode video. Mencoba memulihkan...'); hls.recoverMediaError(); break; default: setPlayerError(`Kesalahan fatal: ${data.details}. Silakan reload.`); hls.destroy(); break; } } }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Native HLS support (Safari) const refParam = activeChannel.referrer ? `&referer=${encodeURIComponent(activeChannel.referrer)}` : ''; const uaParam = activeChannel.userAgent ? `&userAgent=${encodeURIComponent(activeChannel.userAgent)}` : ''; video.src = `/api/proxy?url=${encodeURIComponent(activeChannel.url)}${refParam}${uaParam}`; const handleError = () => { setPlayerError('Kesalahan Format: Browser gagal memutar format siaran ini.'); }; video.addEventListener('error', handleError); video.addEventListener('loadedmetadata', () => { if (isPlaying) { video.play().catch(e => { console.log('Autoplay blocked:', e); setPlayerError('Browser memblokir pemutaran otomatis. Klik tombol "Mulai" di bawah.'); }); } }); } return () => { if (hls) { hls.destroy(); } }; }, [activeChannel, reloadKey]); // Sync mute state to DOM property useEffect(() => { if (videoRef.current) { videoRef.current.muted = isMuted; } }, [isMuted]); // Sync play/pause state to DOM property useEffect(() => { const video = videoRef.current; if (!video) return; if (isPlaying) { video.play().catch(e => { console.log('Play blocked:', e); setPlayerError('Browser memblokir pemutaran otomatis. Klik tombol "Mulai" di bawah.'); }); } else { video.pause(); } }, [isPlaying, activeChannel]); // Handle Play/Pause toggle const togglePlay = () => { const video = videoRef.current; if (!video) return; if (isPlaying) { video.pause(); setIsPlaying(false); } else { video.play().catch(e => console.log(e)); setIsPlaying(true); } }; // Handle Mute toggle const toggleMute = () => { const video = videoRef.current; if (!video) return; video.muted = !isMuted; setIsMuted(!isMuted); }; // Handle reload manually const handleManualReload = () => { setReloadKey(prev => prev + 1); }; // Post remote change-channel request to API const changeChannel = (channel) => { setActiveChannel(channel); setIsPlaying(true); // Broadcast this change to all other viewers in this room fetch('/api/channels', { method: 'GET', // SSE routes commands, but here we can just update local and sync }).catch(err => console.log(err)); }; // Filter channels const filteredChannels = channels.filter(c => { const matchesSearch = c.name.toLowerCase().includes(searchQuery.toLowerCase()); const matchesCategory = selectedCategory === 'Semua' || c.category === selectedCategory; return matchesSearch && matchesCategory; }); // Limit rendering to 200 items for DOM performance const displayedChannels = filteredChannels.slice(0, 200); return (
{/* HEADER */}

WISE TV

Piala Dunia 2026 Nobar Arena

{/* Status Indicator */}
{voiceChannelName}
{/* User profile */}

{userProfile.username}

{statusMessage}

{userProfile.avatar ? ( avatar ) : (
{userProfile.username[0].toUpperCase()}
)}
{/* SIDEBAR CHANNEL SELECTOR */} {/* MAIN SCREEN AREA */}
{/* PLAYER & INFO */}
{activeChannel ? ( <> {/* VIDEO WINDOW */}
{/* CHANNEL DETAILS */}
e.target.src = 'https://images.unsplash.com/photo-1598257006458-087169a1f08d?w=128&h=128&fit=crop'} />

{activeChannel.name}

{activeChannel.category} Streaming HLS • 1080p
) : (

Wise TV Nobar Arena Nonaktif

Silakan ketik `/nonton` di server Discord untuk mengaktifkan sesi TV atau pilih salah satu saluran di sidebar untuk memutar streaming.

)} {/* WORLD CUP SCHEDULE HUB */}

Jadwal Pertandingan Piala Dunia 2026

{worldCupMatches.map(match => (
{match.time}

{match.teamA} vs {match.teamB}

{match.status === 'LIVE' ? ( ) : ( Menunggu )}
))}
); }