// Clarke Player Pro - Core Application class ClarkePlayerPro { constructor() { this.state = { playlists: [], currentPlaylist: null, channels: [], filteredChannels: [], favorites: JSON.parse(localStorage.getItem('clarke_favs')) || {}, currentChannel: null, hls: null, isPlaying: false, quality: 'Auto', searchQuery: '', filterCategory: 'all' }; this.elements = { video: document.getElementById('videoPlayer'), playlistUrl: document.getElementById('playlistUrl'), fileInput: document.getElementById('fileInput'), uploadArea: document.getElementById('uploadArea'), loadPlaylistBtn: document.getElementById('loadPlaylistBtn'), clearBtn: document.getElementById('clearBtn'), channelsContainer: document.getElementById('channelsContainer'), currentChannel: document.getElementById('currentChannel'), currentProgram: document.getElementById('currentProgram'), nowPlayingChannel: document.getElementById('nowPlayingChannel'), nowPlayingRegion: document.getElementById('nowPlayingRegion'), statusText: document.getElementById('statusText'), channelCount: document.getElementById('channelCount'), lastUpdate: document.getElementById('lastUpdate'), avgQuality: document.getElementById('avgQuality'), loadingOverlay: document.getElementById('loadingOverlay'), channelSearch: document.getElementById('channelSearch'), toggleFav: document.getElementById('toggleFav'), playPauseBtn: document.getElementById('playPauseBtn'), prevChannel: document.getElementById('prevChannel'), nextChannel: document.getElementById('nextChannel'), volumeSlider: document.getElementById('volumeSlider'), favoritesList: document.getElementById('favoritesList'), categoryFilter: document.getElementById('categoryFilter'), fullscreenBtn: document.getElementById('fullscreenBtn'), settingsBtn: document.getElementById('settingsBtn'), bitrateStat: document.getElementById('bitrateStat'), bufferStat: document.getElementById('bufferStat'), healthStat: document.getElementById('healthStat'), qualityBadge: document.getElementById('qualityBadge'), bufferProgress: document.getElementById('bufferProgress'), currentTime: document.getElementById('currentTime'), totalTime: document.getElementById('totalTime') }; this.init(); } init() { console.log('🚀 Clarke Player Pro v2.1 Initializing...'); this.setupEventListeners(); this.setupKeyboardControls(); this.loadDefaultPlaylist(); this.updateStats(); this.renderFavorites(); // Set initial volume this.elements.video.volume = this.elements.volumeSlider.value / 100; } setupEventListeners() { // Load playlist from URL this.elements.loadPlaylistBtn.addEventListener('click', () => { this.loadPlaylistFromUrl(); }); // Quick preset buttons document.querySelectorAll('.preset-btn').forEach(btn => { btn.addEventListener('click', (e) => { const url = e.currentTarget.dataset.url; this.elements.playlistUrl.value = url; this.loadPlaylistFromUrl(); }); }); // File upload this.elements.uploadArea.addEventListener('click', () => { this.elements.fileInput.click(); }); this.elements.fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { this.loadPlaylistFromFile(e.target.files[0]); } }); // Drag and drop for file upload this.elements.uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); e.currentTarget.style.borderColor = 'var(--primary)'; e.currentTarget.style.background = 'rgba(139, 92, 246, 0.1)'; }); this.elements.uploadArea.addEventListener('dragleave', (e) => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'rgba(255, 255, 255, 0.02)'; }); this.elements.uploadArea.addEventListener('drop', (e) => { e.preventDefault(); e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'rgba(255, 255, 255, 0.02)'; const file = e.dataTransfer.files[0]; if (file && (file.name.endsWith('.m3u') || file.name.endsWith('.m3u8'))) { this.loadPlaylistFromFile(file); } else { this.showNotification('Please drop a valid .m3u or .m3u8 file', 'error'); } }); // Clear button this.elements.clearBtn.addEventListener('click', () => { this.clearPlaylist(); }); // Search this.elements.channelSearch.addEventListener('input', (e) => { this.state.searchQuery = e.target.value.toLowerCase(); this.filterChannels(); }); // Category filter this.elements.categoryFilter.addEventListener('change', (e) => { this.state.filterCategory = e.target.value; this.filterChannels(); }); // Player controls this.elements.playPauseBtn.addEventListener('click', () => { this.togglePlayPause(); }); this.elements.prevChannel.addEventListener('click', () => { this.selectPreviousChannel(); }); this.elements.nextChannel.addEventListener('click', () => { this.selectNextChannel(); }); this.elements.toggleFav.addEventListener('click', () => { if (this.state.currentChannel) { this.toggleFavorite(this.state.currentChannel.id); } }); // Volume control this.elements.volumeSlider.addEventListener('input', (e) => { this.elements.video.volume = e.target.value / 100; }); // Fullscreen this.elements.fullscreenBtn.addEventListener('click', () => { this.toggleFullscreen(); }); // Video events this.elements.video.addEventListener('play', () => { this.state.isPlaying = true; this.elements.playPauseBtn.innerHTML = ''; this.updateStatus('Playing'); }); this.elements.video.addEventListener('pause', () => { this.state.isPlaying = false; this.elements.playPauseBtn.innerHTML = ''; this.updateStatus('Paused'); }); this.elements.video.addEventListener('waiting', () => { this.showLoading(true); this.updateStatus('Buffering...'); }); this.elements.video.addEventListener('playing', () => { this.showLoading(false); this.updateStatus('Playing'); }); this.elements.video.addEventListener('timeupdate', () => { this.updatePlaybackInfo(); }); this.elements.video.addEventListener('loadedmetadata', () => { this.updatePlaybackInfo(); }); // Auto-hide controls let controlsTimeout; this.elements.video.addEventListener('mousemove', () => { const overlay = document.querySelector('.video-overlay'); const controls = document.querySelector('.player-controls'); overlay.style.opacity = '1'; controls.style.opacity = '1'; clearTimeout(controlsTimeout); controlsTimeout = setTimeout(() => { if (!document.fullscreenElement) { overlay.style.opacity = '0'; controls.style.opacity = '0'; } }, 3000); }); } setupKeyboardControls() { document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT') return; switch (e.key.toLowerCase()) { case ' ': e.preventDefault(); this.togglePlayPause(); break; case 'arrowleft': e.preventDefault(); this.seek(-10); break; case 'arrowright': e.preventDefault(); this.seek(10); break; case 'arrowup': e.preventDefault(); this.selectPreviousChannel(); break; case 'arrowdown': e.preventDefault(); this.selectNextChannel(); break; case 'f': e.preventDefault(); this.toggleFullscreen(); break; case 'm': e.preventDefault(); this.toggleMute(); break; case 'l': e.preventDefault(); this.loadPlaylistFromUrl(); break; case 'escape': if (document.fullscreenElement) { document.exitFullscreen(); } break; } }); } async loadDefaultPlaylist() { const defaultUrl = this.elements.playlistUrl.value; if (defaultUrl) { await this.loadPlaylistFromUrl(); } } async loadPlaylistFromUrl() { const url = this.elements.playlistUrl.value.trim(); if (!url) { this.showNotification('Please enter a playlist URL', 'error'); return; } this.updateStatus('Loading playlist...'); this.showLoading(true); try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const text = await response.text(); await this.parsePlaylist(text, url); this.showNotification('Playlist loaded successfully', 'success'); this.elements.lastUpdate.textContent = new Date().toLocaleTimeString(); } catch (error) { console.error('Failed to load playlist:', error); this.showNotification('Failed to load playlist. Please check the URL.', 'error'); this.updateStatus('Load failed'); } finally { this.showLoading(false); } } async loadPlaylistFromFile(file) { this.updateStatus('Reading playlist file...'); this.showLoading(true); try { const text = await file.text(); await this.parsePlaylist(text, file.name); this.showNotification(`Loaded playlist: ${file.name}`, 'success'); this.elements.lastUpdate.textContent = new Date().toLocaleTimeString(); } catch (error) { console.error('Failed to load file:', error); this.showNotification('Failed to load playlist file', 'error'); } finally { this.showLoading(false); } } async parsePlaylist(content, source) { const channels = []; const lines = content.split('\n'); let currentChannel = {}; let channelNumber = 1; for (let line of lines) { line = line.trim(); if (line.startsWith('#EXTINF')) { const titleMatch = line.match(/,(.*)$/); const logoMatch = line.match(/tvg-logo="([^"]+)"/); const groupMatch = line.match(/group-title="([^"]+)"/); currentChannel = { id: `channel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, title: titleMatch ? this.cleanTitle(titleMatch[1]) : `Channel ${channelNumber}`, logo: logoMatch ? logoMatch[1] : '', group: groupMatch ? groupMatch[1] : 'General', number: channelNumber, source: source, url: '', isFavorite: false, category: this.detectCategory(titleMatch ? titleMatch[1] : '') }; channelNumber++; } else if (line.startsWith('http')) { currentChannel.url = line; channels.push({...currentChannel}); } } this.state.channels = channels; this.state.filteredChannels = [...channels]; this.updateChannelCount(); this.renderChannels(); if (channels.length > 0) { this.selectChannel(0); } } cleanTitle(title) { return title .replace(/^Pluto TV\s*/i, '') .replace(/^\[.*?\]\s*/, '') .replace(/\|.*$/, '') .trim(); } detectCategory(title) { const lowerTitle = title.toLowerCase(); if (lowerTitle.includes('news') || lowerTitle.includes('cnn') || lowerTitle.includes('bbc')) { return 'news'; } else if (lowerTitle.includes('sport') || lowerTitle.includes('espn') || lowerTitle.includes('football')) { return 'sports'; } else if (lowerTitle.includes('movie') || lowerTitle.includes('cinema') || lowerTitle.includes('film')) { return 'movies'; } else if (lowerTitle.includes('music') || lowerTitle.includes('mtv') || lowerTitle.includes('vibe')) { return 'music'; } else if (lowerTitle.includes('kids') || lowerTitle.includes('cartoon') || lowerTitle.includes('disney')) { return 'kids'; } else { return 'entertainment'; } } renderChannels() { const container = this.elements.channelsContainer; if (this.state.filteredChannels.length === 0) { container.innerHTML = `

No Channels Found

Try a different search term or filter

`; return; } let html = ''; this.state.filteredChannels.forEach((channel, index) => { const isActive = this.state.currentChannel && this.state.currentChannel.id === channel.id; const isFavorite = this.state.favorites[channel.id]; html += `

${channel.title}

${channel.number} ${channel.category}
`; }); container.innerHTML = html; // Add event listeners container.querySelectorAll('.channel-card').forEach(card => { card.addEventListener('click', (e) => { if (!e.target.closest('.channel-fav')) { const index = parseInt(card.dataset.index); this.selectChannel(index); } }); }); container.querySelectorAll('.channel-fav').forEach(fav => { fav.addEventListener('click', (e) => { e.stopPropagation(); const channelId = fav.dataset.id; this.toggleFavorite(channelId); fav.classList.toggle('active'); }); }); } filterChannels() { let filtered = this.state.channels; // Apply search filter if (this.state.searchQuery) { filtered = filtered.filter(ch => ch.title.toLowerCase().includes(this.state.searchQuery) || ch.group.toLowerCase().includes(this.state.searchQuery) || ch.category.toLowerCase().includes(this.state.searchQuery) ); } // Apply category filter if (this.state.filterCategory !== 'all') { filtered = filtered.filter(ch => ch.category === this.state.filterCategory ); } this.state.filteredChannels = filtered; this.renderChannels(); this.updateChannelCount(); } selectChannel(index) { if (index < 0 || index >= this.state.filteredChannels.length) return; const channel = this.state.filteredChannels[index]; this.state.currentChannel = channel; // Update UI this.elements.currentChannel.textContent = channel.title; this.elements.currentProgram.textContent = `Now Playing • ${channel.group}`; this.elements.nowPlayingChannel.textContent = channel.title; this.elements.nowPlayingRegion.textContent = channel.group; // Update favorite button this.elements.toggleFav.classList.toggle('active', this.state.favorites[channel.id]); // Update active state in list document.querySelectorAll('.channel-card').forEach(card => { card.classList.remove('active'); }); const selectedCard = document.querySelector(`.channel-card[data-index="${index}"]`); if (selectedCard) { selectedCard.classList.add('active'); selectedCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } // Play the channel this.playChannel(channel.url); this.updateStatus(`Playing: ${channel.title}`); } selectNextChannel() { if (!this.state.currentChannel || this.state.filteredChannels.length === 0) return; const currentIndex = this.state.filteredChannels.findIndex( ch => ch.id === this.state.currentChannel.id ); if (currentIndex >= 0) { const nextIndex = (currentIndex + 1) % this.state.filteredChannels.length; this.selectChannel(nextIndex); } else if (this.state.filteredChannels.length > 0) { this.selectChannel(0); } } selectPreviousChannel() { if (!this.state.currentChannel || this.state.filteredChannels.length === 0) return; const currentIndex = this.state.filteredChannels.findIndex( ch => ch.id === this.state.currentChannel.id ); if (currentIndex >= 0) { const prevIndex = currentIndex - 1 >= 0 ? currentIndex - 1 : this.state.filteredChannels.length - 1; this.selectChannel(prevIndex); } else if (this.state.filteredChannels.length > 0) { this.selectChannel(0); } } playChannel(url) { this.showLoading(true); // Destroy previous HLS instance if (this.state.hls) { this.state.hls.destroy(); this.state.hls = null; } // Stop current video this.elements.video.pause(); this.elements.video.src = ''; if (Hls.isSupported()) { this.state.hls = new Hls({ enableWorker: true, lowLatencyMode: true, backBufferLength: 30, maxBufferLength: 60, debug: false, liveSyncDurationCount: 3, liveMaxLatencyDurationCount: 10 }); this.state.hls.loadSource(url); this.state.hls.attachMedia(this.elements.video); this.state.hls.on(Hls.Events.MANIFEST_PARSED, () => { this.elements.video.play().then(() => { this.showLoading(false); this.state.isPlaying = true; this.elements.playPauseBtn.innerHTML = ''; }).catch(error => { console.log('Autoplay prevented:', error); this.showLoading(false); this.updateStatus('Click play button to start'); this.elements.playPauseBtn.innerHTML = ''; }); }); this.state.hls.on(Hls.Events.ERROR, (event, data) => { console.error('HLS Error:', data); if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: this.updateStatus('Network error - retrying...'); this.state.hls.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: this.updateStatus('Media error - recovering...'); this.state.hls.recoverMediaError(); break; default: this.state.hls.destroy(); this.showNotification('Playback failed. Trying next channel...', 'error'); this.selectNextChannel(); break; } } }); this.state.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => { const level = this.state.hls.levels[data.level]; if (level) { const bitrate = Math.round(level.bitrate / 1000); const height = level.height || 'Auto'; this.state.quality = height === 'Auto' ? 'Auto' : `${height}p`; this.elements.qualityBadge.textContent = this.state.quality; this.elements.bitrateStat.textContent = `${bitrate} Mbps`; this.updateQualityStats(); } }); this.state.hls.on(Hls.Events.BUFFER_CREATED, () => { this.updateBufferStats(); }); } else if (this.elements.video.canPlayType('application/vnd.apple.mpegurl')) { // Safari native HLS support this.elements.video.src = url; this.elements.video.play().then(() => { this.showLoading(false); this.state.isPlaying = true; this.elements.playPauseBtn.innerHTML = ''; }).catch(error => { console.log('Native HLS autoplay prevented:', error); this.showLoading(false); this.updateStatus('Click play button to start'); this.elements.playPauseBtn.innerHTML = ''; }); } else { this.showLoading(false); this.showNotification('Your browser does not support HLS streaming', 'error'); } } togglePlayPause() { if (this.elements.video.paused) { this.elements.video.play(); this.elements.playPauseBtn.innerHTML = ''; } else { this.elements.video.pause(); this.elements.playPauseBtn.innerHTML = ''; } } toggleFavorite(channelId) { if (this.state.favorites[channelId]) { delete this.state.favorites[channelId]; this.showNotification('Removed from favorites', 'info'); } else { this.state.favorites[channelId] = true; this.showNotification('Added to favorites', 'success'); } localStorage.setItem('clarke_favs', JSON.stringify(this.state.favorites)); this.renderFavorites(); // Update favorite button if this is the current channel if (this.state.currentChannel && this.state.currentChannel.id === channelId) { this.elements.toggleFav.classList.toggle('active', this.state.favorites[channelId]); } } renderFavorites() { const container = this.elements.favoritesList; const favoriteChannels = this.state.channels.filter(ch => this.state.favorites[ch.id]); if (favoriteChannels.length === 0) { container.innerHTML = `

No favorites yet

`; return; } let html = ''; favoriteChannels.forEach(channel => { html += `
${channel.title}
`; }); container.innerHTML = html; // Add click listeners container.querySelectorAll('.favorite-item').forEach(item => { item.addEventListener('click', () => { const channelId = item.dataset.id; const channelIndex = this.state.filteredChannels.findIndex(ch => ch.id === channelId); if (channelIndex >= 0) { this.selectChannel(channelIndex); } }); }); } toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(err => { console.log(`Fullscreen error: ${err.message}`); }); } else { document.exitFullscreen(); } } toggleMute() { this.elements.video.muted = !this.elements.video.muted; this.updateStatus(this.elements.video.muted ? 'Muted' : 'Unmuted'); } seek(seconds) { this.elements.video.currentTime += seconds; } updateStatus(text) { this.elements.statusText.textContent = text; } updateChannelCount() { const count = this.state.filteredChannels.length; const total = this.state.channels.length; this.elements.channelCount.textContent = `${count}`; // Update average quality estimation if (total > 0) { const hdCount = this.state.channels.filter(ch => ch.title.toLowerCase().includes('hd') || ch.title.toLowerCase().includes('1080') || ch.title.toLowerCase().includes('4k') ).length; const hdPercentage = Math.round((hdCount / total) * 100); this.elements.avgQuality.textContent = `${hdPercentage}% HD`; } } updatePlaybackInfo() { const current = this.elements.video.currentTime; const duration = this.elements.video.duration || 0; // Format time const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; this.elements.currentTime.textContent = formatTime(current); this.elements.totalTime.textContent = formatTime(duration); // Update buffer progress if (this.elements.video.buffered.length > 0) { const bufferedEnd = this.elements.video.buffered.end(this.elements.video.buffered.length - 1); const bufferPercentage = duration > 0 ? (bufferedEnd / duration) * 100 : 0; this.elements.bufferProgress.style.width = `${bufferPercentage}%`; } } updateStats() { // Update health based on buffering if (this.state.hls) { const bufferLength = this.state.hls.media.buffered.length; const health = bufferLength > 0 ? 100 : 80; this.elements.healthStat.textContent = `${health}%`; } // Update buffer time if (this.elements.video.buffered.length > 0) { const bufferedEnd = this.elements.video.buffered.end(this.elements.video.buffered.length - 1); const bufferTime = Math.round(bufferedEnd - this.elements.video.currentTime); this.elements.bufferStat.textContent = `${bufferTime}s`; } // Update every second setTimeout(() => this.updateStats(), 1000); } updateBufferStats() { if (this.state.hls) { const bufferInfo = this.state.hls.media.buffered; if (bufferInfo.length > 0) { const bufferTime = bufferInfo.end(bufferInfo.length - 1) - this.elements.video.currentTime; this.elements.bufferStat.textContent = `${Math.round(bufferTime)}s`; } } } updateQualityStats() { // Update quality distribution if (this.state.channels.length > 0) { const hdChannels = this.state.channels.filter(ch => ch.title.toLowerCase().includes('hd') || ch.title.toLowerCase().includes('1080') || ch.title.toLowerCase().includes('4k') ).length; const hdPercentage = Math.round((hdChannels / this.state.channels.length) * 100); this.elements.avgQuality.textContent = `${hdPercentage}% HD`; } } clearPlaylist() { this.state.channels = []; this.state.filteredChannels = []; this.state.currentChannel = null; if (this.state.hls) { this.state.hls.destroy(); this.state.hls = null; } this.elements.video.pause(); this.elements.video.src = ''; this.renderChannels(); this.updateChannelCount(); this.updateStatus('Playlist cleared'); this.elements.currentChannel.textContent = 'Welcome to Clarke Player Pro'; this.elements.currentProgram.textContent = 'Load a playlist to start streaming'; this.elements.nowPlayingChannel.textContent = '--'; this.elements.nowPlayingRegion.textContent = '--'; this.showNotification('Playlist cleared', 'info'); } showLoading(show) { this.elements.loadingOverlay.style.display = show ? 'flex' : 'none'; } showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.innerHTML = ` ${message} `; // Add styles for notification notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${type === 'success' ? 'var(--accent)' : type === 'error' ? 'var(--danger)' : 'var(--info)'}; color: white; padding: 1rem 1.5rem; border-radius: 12px; display: flex; align-items: center; gap: 0.75rem; z-index: 10000; animation: slideIn 0.3s ease; box-shadow: var(--shadow-lg); max-width: 300px; `; document.body.appendChild(notification); // Remove after 3 seconds setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, 3000); // Add animation keyframes const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; if (!document.querySelector('#notification-styles')) { style.id = 'notification-styles'; document.head.appendChild(style); } } } // Initialize Clarke Player Pro let player; document.addEventListener('DOMContentLoaded', () => { player = new ClarkePlayerPro(); });