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 */}
{/* User profile */}
{userProfile.username}
{statusMessage}
{userProfile.avatar ? (

) : (
{userProfile.username[0].toUpperCase()}
)}
{/* SIDEBAR CHANNEL SELECTOR */}
{/* MAIN SCREEN AREA */}
{/* PLAYER & INFO */}
{activeChannel ? (
<>
{/* VIDEO WINDOW */}
{/* Visual Error Overlay */}
{playerError && (
Info Pemutar Video
{playerError}
)}
{/* Control Overlay */}
LIVE
{/* 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
)}
))}
);
}