Spaces:
Running
Running
| // 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 = '<i class="fas fa-pause"></i>'; | |
| this.updateStatus('Playing'); | |
| }); | |
| this.elements.video.addEventListener('pause', () => { | |
| this.state.isPlaying = false; | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| 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 = ` | |
| <div class="welcome-state"> | |
| <i class="fas fa-broadcast-tower"></i> | |
| <h3>No Channels Found</h3> | |
| <p>Try a different search term or filter</p> | |
| </div> | |
| `; | |
| 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 += ` | |
| <div class="channel-card ${isActive ? 'active' : ''}" | |
| data-index="${index}" | |
| data-id="${channel.id}"> | |
| <div class="channel-logo"> | |
| ${channel.logo ? | |
| `<img src="${channel.logo}" alt="${channel.title}" | |
| onerror="this.style.display='none'; this.parentElement.innerHTML='<i class=\"fas fa-tv\"></i>'">` : | |
| `<i class="fas fa-tv"></i>`} | |
| </div> | |
| <div class="channel-info"> | |
| <h4>${channel.title}</h4> | |
| <div class="channel-meta"> | |
| <span class="channel-number">${channel.number}</span> | |
| <span class="channel-category">${channel.category}</span> | |
| </div> | |
| </div> | |
| <div class="channel-fav ${isFavorite ? 'active' : ''}" | |
| data-id="${channel.id}"> | |
| <i class="fas fa-star"></i> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| 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 = '<i class="fas fa-pause"></i>'; | |
| }).catch(error => { | |
| console.log('Autoplay prevented:', error); | |
| this.showLoading(false); | |
| this.updateStatus('Click play button to start'); | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| }); | |
| }); | |
| 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 = '<i class="fas fa-pause"></i>'; | |
| }).catch(error => { | |
| console.log('Native HLS autoplay prevented:', error); | |
| this.showLoading(false); | |
| this.updateStatus('Click play button to start'); | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| }); | |
| } 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 = '<i class="fas fa-pause"></i>'; | |
| } else { | |
| this.elements.video.pause(); | |
| this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| } | |
| } | |
| 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 = ` | |
| <div class="empty-favorites"> | |
| <i class="fas fa-star"></i> | |
| <p>No favorites yet</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| let html = ''; | |
| favoriteChannels.forEach(channel => { | |
| html += ` | |
| <div class="favorite-item" data-id="${channel.id}"> | |
| <i class="fas fa-star"></i> | |
| <span>${channel.title}</span> | |
| </div> | |
| `; | |
| }); | |
| 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 = ` | |
| <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i> | |
| <span>${message}</span> | |
| `; | |
| // 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(); | |
| }); |