Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>IPTV Player - Hugging Face</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: white; | |
| overflow-x: hidden; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| animation: fadeInDown 0.8s ease-out; | |
| } | |
| .header h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.5); | |
| background: linear-gradient(45deg, #fff, #e0e0e0); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .header p { | |
| font-size: 1.1rem; | |
| opacity: 0.9; | |
| } | |
| .player-container { | |
| display: grid; | |
| grid-template-columns: 1fr 320px; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .video-section { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 20px; | |
| padding: 25px; | |
| backdrop-filter: blur(15px); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| animation: fadeInLeft 0.8s ease-out; | |
| } | |
| .video-player { | |
| width: 100%; | |
| height: 400px; | |
| border-radius: 15px; | |
| background: #000; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| } | |
| .channel-info { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| } | |
| .current-channel { | |
| font-size: 1.3rem; | |
| font-weight: bold; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.3); | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .btn { | |
| padding: 10px 18px; | |
| border: none; | |
| border-radius: 8px; | |
| background: rgba(255,255,255,0.2); | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 14px; | |
| font-weight: 500; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .btn:hover { | |
| background: rgba(255,255,255,0.3); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
| } | |
| .btn:active { | |
| transform: translateY(0); | |
| } | |
| .volume-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| margin-top: 10px; | |
| } | |
| .volume-slider { | |
| width: 120px; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: rgba(255,255,255,0.3); | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| .volume-slider::-webkit-slider-thumb { | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: white; | |
| cursor: pointer; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.3); | |
| } | |
| .playlist-section { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 20px; | |
| padding: 25px; | |
| backdrop-filter: blur(15px); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| max-height: 600px; | |
| overflow-y: auto; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| animation: fadeInRight 0.8s ease-out; | |
| } | |
| .playlist-header { | |
| font-size: 1.4rem; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| font-weight: bold; | |
| text-shadow: 1px 1px 2px rgba(0,0,0,0.3); | |
| } | |
| .url-input-section { | |
| margin-bottom: 25px; | |
| } | |
| .url-input { | |
| width: 100%; | |
| padding: 12px 15px; | |
| border: none; | |
| border-radius: 8px; | |
| background: rgba(255,255,255,0.2); | |
| color: white; | |
| margin-bottom: 12px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .url-input:focus { | |
| outline: none; | |
| background: rgba(255,255,255,0.25); | |
| box-shadow: 0 0 15px rgba(255,255,255,0.2); | |
| } | |
| .url-input::placeholder { | |
| color: rgba(255,255,255,0.7); | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .channel-list { | |
| list-style: none; | |
| } | |
| .channel-item { | |
| padding: 15px; | |
| margin-bottom: 8px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background: rgba(255,255,255,0.1); | |
| border-left: 4px solid transparent; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .channel-item:hover { | |
| background: rgba(255,255,255,0.2); | |
| transform: translateX(8px); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| } | |
| .channel-item.active { | |
| background: rgba(255,255,255,0.3); | |
| border-left-color: #fff; | |
| box-shadow: 0 6px 20px rgba(0,0,0,0.2); | |
| } | |
| .channel-name { | |
| font-weight: bold; | |
| margin-bottom: 4px; | |
| font-size: 1rem; | |
| } | |
| .channel-url { | |
| font-size: 0.85rem; | |
| opacity: 0.8; | |
| word-break: break-all; | |
| line-height: 1.3; | |
| } | |
| .error-message { | |
| background: rgba(255,59,48,0.3); | |
| border: 1px solid rgba(255,59,48,0.5); | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin: 15px 0; | |
| display: none; | |
| backdrop-filter: blur(10px); | |
| animation: shake 0.5s ease-in-out; | |
| } | |
| .success-message { | |
| background: rgba(52,199,89,0.3); | |
| border: 1px solid rgba(52,199,89,0.5); | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin: 15px 0; | |
| display: none; | |
| backdrop-filter: blur(10px); | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 20px; | |
| font-style: italic; | |
| opacity: 0.8; | |
| } | |
| .loading::after { | |
| content: ''; | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 2px solid rgba(255,255,255,0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| margin-left: 10px; | |
| } | |
| .stats { | |
| display: flex; | |
| justify-content: space-around; | |
| margin-top: 20px; | |
| padding: 15px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 10px; | |
| backdrop-filter: blur(10px); | |
| } | |
| .stat-item { | |
| text-align: center; | |
| } | |
| .stat-number { | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| color: #fff; | |
| } | |
| .stat-label { | |
| font-size: 0.9rem; | |
| opacity: 0.8; | |
| } | |
| @media (max-width: 768px) { | |
| .player-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .header h1 { | |
| font-size: 2rem; | |
| } | |
| .video-player { | |
| height: 250px; | |
| } | |
| .channel-info { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .controls { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .button-group { | |
| justify-content: center; | |
| } | |
| } | |
| /* Custom scrollbar */ | |
| .playlist-section::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .playlist-section::-webkit-scrollbar-track { | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 4px; | |
| } | |
| .playlist-section::-webkit-scrollbar-thumb { | |
| background: rgba(255,255,255,0.3); | |
| border-radius: 4px; | |
| } | |
| .playlist-section::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255,255,255,0.5); | |
| } | |
| /* Animations */ | |
| @keyframes fadeInDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeInLeft { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| @keyframes fadeInRight { | |
| from { | |
| opacity: 0; | |
| transform: translateX(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-5px); } | |
| 75% { transform: translateX(5px); } | |
| } | |
| .hf-badge { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: rgba(255,255,255,0.2); | |
| padding: 8px 12px; | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>📺 IPTV Player</h1> | |
| <p>Stream your favorite channels with modern web technology</p> | |
| </div> | |
| <div class="player-container"> | |
| <div class="video-section"> | |
| <video class="video-player" controls id="videoPlayer" preload="metadata"> | |
| <source src="" type="application/x-mpegURL"> | |
| <source src="" type="video/mp4"> | |
| Your browser does not support the video tag. | |
| </video> | |
| <div class="channel-info"> | |
| <div class="current-channel" id="currentChannel">Select a channel to start</div> | |
| <div class="controls"> | |
| <button class="btn" id="playBtn">▶️ Play</button> | |
| <button class="btn" id="pauseBtn">⏸️ Pause</button> | |
| <button class="btn" id="fullscreenBtn">🔳 Fullscreen</button> | |
| </div> | |
| </div> | |
| <div class="volume-control"> | |
| <span>🔊</span> | |
| <input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="50"> | |
| <span id="volumeValue">50%</span> | |
| </div> | |
| <div class="error-message" id="errorMessage"></div> | |
| <div class="success-message" id="successMessage"></div> | |
| <div class="stats"> | |
| <div class="stat-item"> | |
| <div class="stat-number" id="channelCount">0</div> | |
| <div class="stat-label">Channels</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-number" id="currentTime">00:00</div> | |
| <div class="stat-label">Duration</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="playlist-section"> | |
| <div class="playlist-header">📺 Channel Playlist</div> | |
| <div class="url-input-section"> | |
| <input type="text" class="url-input" id="streamUrl" placeholder="Enter stream URL (m3u8, mp4, etc.)"> | |
| <input type="text" class="url-input" id="channelName" placeholder="Channel name (optional)"> | |
| <div class="button-group"> | |
| <button class="btn" id="addChannelBtn">➕ Add</button> | |
| <button class="btn" id="loadM3uFileBtn">📂 Load File</button> | |
| <button class="btn" id="loadM3uUrlBtn">🌐 Load URL</button> | |
| <button class="btn" id="clearChannelsBtn">🗑️ Clear All</button> | |
| </div> | |
| <input type="file" id="m3uFile" accept=".m3u,.m3u8,.txt" style="display: none;"> | |
| </div> | |
| <div class="loading" id="loadingMessage" style="display: none;">Loading channels</div> | |
| <ul class="channel-list" id="channelList"> | |
| <!-- Channels will be loaded here --> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="hf-badge"> | |
| 🤗 Hosted on Hugging Face | |
| </div> | |
| <script> | |
| class IPTVPlayer { | |
| constructor() { | |
| this.videoPlayer = document.getElementById('videoPlayer'); | |
| this.currentChannel = document.getElementById('currentChannel'); | |
| this.channelList = document.getElementById('channelList'); | |
| this.streamUrl = document.getElementById('streamUrl'); | |
| this.channelName = document.getElementById('channelName'); | |
| this.errorMessage = document.getElementById('errorMessage'); | |
| this.successMessage = document.getElementById('successMessage'); | |
| this.loadingMessage = document.getElementById('loadingMessage'); | |
| this.channelCount = document.getElementById('channelCount'); | |
| this.currentTime = document.getElementById('currentTime'); | |
| this.channels = []; | |
| this.currentIndex = -1; | |
| this.initializeEventListeners(); | |
| } | |
| async init() { | |
| this.channels = this.loadChannelsFromStorage(); | |
| if (this.channels.length === 0) { | |
| await this.loadChannelsFromUrl('https://iptv-org.github.io/iptv/index.m3u', { replace: true }); | |
| } | |
| this.renderChannelList(); | |
| this.updateStats(); | |
| } | |
| loadFallbackChannels() { | |
| this.channels = [ | |
| { name: "Demo: Tears of Steel", url: "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8" }, | |
| { name: "Demo: Sintel", url: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8" }, | |
| { name: "Demo: Big Buck Bunny", url: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" } | |
| ]; | |
| this.showSuccess('Loaded fallback demo channels.'); | |
| } | |
| initializeEventListeners() { | |
| document.getElementById('playBtn').addEventListener('click', () => this.play()); | |
| document.getElementById('pauseBtn').addEventListener('click', () => this.pause()); | |
| document.getElementById('fullscreenBtn').addEventListener('click', () => this.toggleFullscreen()); | |
| const volumeSlider = document.getElementById('volumeSlider'); | |
| volumeSlider.addEventListener('input', (e) => this.setVolume(e.target.value / 100)); | |
| document.getElementById('addChannelBtn').addEventListener('click', () => this.addChannel()); | |
| document.getElementById('clearChannelsBtn').addEventListener('click', () => this.clearAllChannels()); | |
| document.getElementById('loadM3uFileBtn').addEventListener('click', () => document.getElementById('m3uFile').click()); | |
| document.getElementById('m3uFile').addEventListener('change', (e) => this.handleM3UFile(e)); | |
| document.getElementById('loadM3uUrlBtn').addEventListener('click', () => { | |
| const url = prompt('Enter the URL of the M3U playlist:'); | |
| if (url && this.isValidUrl(url)) { | |
| this.loadChannelsFromUrl(url, { replace: false }); | |
| } else if (url) { | |
| this.showError('The URL you entered is not valid.'); | |
| } | |
| }); | |
| this.streamUrl.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.addChannel(); }); | |
| this.channelName.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.addChannel(); }); | |
| this.videoPlayer.addEventListener('error', () => this.handleVideoError()); | |
| this.videoPlayer.addEventListener('loadstart', () => this.showLoading()); | |
| this.videoPlayer.addEventListener('canplay', () => this.hideLoading()); | |
| this.videoPlayer.addEventListener('timeupdate', () => this.updateCurrentTime()); | |
| document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e)); | |
| } | |
| handleKeyboardShortcuts(e) { | |
| if (e.target.tagName === 'INPUT') return; | |
| e.preventDefault(); | |
| switch(e.key) { | |
| case ' ': this.videoPlayer.paused ? this.play() : this.pause(); break; | |
| case 'f': this.toggleFullscreen(); break; | |
| case 'ArrowUp': this.adjustVolume(0.1); break; | |
| case 'ArrowDown': this.adjustVolume(-0.1); break; | |
| } | |
| } | |
| setVolume(level) { | |
| this.videoPlayer.volume = level; | |
| document.getElementById('volumeSlider').value = level * 100; | |
| document.getElementById('volumeValue').textContent = Math.round(level * 100) + '%'; | |
| } | |
| adjustVolume(delta) { | |
| this.setVolume(Math.max(0, Math.min(1, this.videoPlayer.volume + delta))); | |
| } | |
| updateCurrentTime() { | |
| if (this.videoPlayer.duration && !isNaN(this.videoPlayer.duration) && isFinite(this.videoPlayer.duration)) { | |
| const formatTime = (time) => { | |
| const minutes = Math.floor(time / 60); | |
| const seconds = Math.floor(time % 60); | |
| return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
| }; | |
| this.currentTime.textContent = formatTime(this.videoPlayer.currentTime); | |
| } else { | |
| this.currentTime.textContent = 'Live'; | |
| } | |
| } | |
| play() { this.videoPlayer.play().catch(e => this.showError(`Playback error: ${e.message}`)); } | |
| pause() { this.videoPlayer.pause(); } | |
| toggleFullscreen() { | |
| if (document.fullscreenElement) { | |
| document.exitFullscreen(); | |
| } else { | |
| this.videoPlayer.requestFullscreen().catch(e => this.showError(`Fullscreen error: ${e.message}`)); | |
| } | |
| } | |
| addChannel() { | |
| const url = this.streamUrl.value.trim(); | |
| const name = this.channelName.value.trim() || `Channel ${this.channels.length + 1}`; | |
| if (!url || !this.isValidUrl(url)) { | |
| this.showError('Please enter a valid stream URL'); | |
| return; | |
| } | |
| if (this.channels.some(channel => channel.url === url)) { | |
| this.showError('This channel already exists in the playlist'); | |
| return; | |
| } | |
| this.channels.push({ name, url }); | |
| this.saveChannelsToStorage(); | |
| this.renderChannelList(); | |
| this.updateStats(); | |
| this.streamUrl.value = ''; | |
| this.channelName.value = ''; | |
| this.showSuccess(`Channel "${name}" added successfully!`); | |
| } | |
| clearAllChannels() { | |
| if (confirm('Are you sure you want to clear all channels? This will load the default playlist on next refresh.')) { | |
| this.channels = []; | |
| this.saveChannelsToStorage(); | |
| this.renderChannelList(); | |
| this.updateStats(); | |
| this.currentChannel.textContent = 'Select a channel to start'; | |
| this.videoPlayer.src = ''; | |
| this.showSuccess('All channels cleared.'); | |
| } | |
| } | |
| async loadChannelsFromUrl(url, options = { replace: false }) { | |
| this.showLoading(`Loading from ${new URL(url).hostname}...`); | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |
| const m3uContent = await response.text(); | |
| const newChannels = this.parseM3U(m3uContent); | |
| if (newChannels.length > 0) { | |
| if (options.replace) { | |
| this.channels = newChannels; | |
| } else { | |
| const uniqueNewChannels = newChannels.filter(nc => !this.channels.some(ec => ec.url === nc.url)); | |
| this.channels.push(...uniqueNewChannels); | |
| } | |
| this.saveChannelsToStorage(); | |
| this.showSuccess(`Successfully loaded ${newChannels.length} channels.`); | |
| } else { | |
| this.showError('No valid channels found at the URL.'); | |
| if (options.replace) this.loadFallbackChannels(); | |
| } | |
| } catch (error) { | |
| this.showError(`Failed to load playlist: ${error.message}`); | |
| if (options.replace) this.loadFallbackChannels(); | |
| } finally { | |
| this.hideLoading(); | |
| } | |
| } | |
| handleM3UFile(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| this.showLoading('Parsing M3U file...'); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| try { | |
| const newChannels = this.parseM3U(e.target.result); | |
| if (newChannels.length > 0) { | |
| const uniqueNewChannels = newChannels.filter(nc => !this.channels.some(ec => ec.url === nc.url)); | |
| this.channels.push(...uniqueNewChannels); | |
| this.saveChannelsToStorage(); | |
| this.renderChannelList(); | |
| this.updateStats(); | |
| this.showSuccess(`Added ${uniqueNewChannels.length} new channels from file.`); | |
| } else { | |
| this.showError('No valid channels found in the file.'); | |
| } | |
| } catch (error) { | |
| this.showError(`File parse error: ${error.message}`); | |
| } finally { | |
| this.hideLoading(); | |
| } | |
| }; | |
| reader.onerror = () => this.showError('Failed to read the file.'); | |
| reader.readAsText(file); | |
| event.target.value = ''; // Reset file input | |
| } | |
| parseM3U(content) { | |
| const lines = content.split('\n'); | |
| const parsedChannels = []; | |
| let currentChannel = {}; | |
| lines.forEach((line, index) => { | |
| line = line.trim(); | |
| if (line.startsWith('#EXTINF:')) { | |
| const nameMatch = line.match(/,(.+)$/); | |
| const tvgNameMatch = line.match(/tvg-name="([^"]+)"/); | |
| currentChannel.name = tvgNameMatch?.[1] || nameMatch?.[1] || `Channel ${index}`; | |
| } else if (line && !line.startsWith('#') && this.isValidUrl(line)) { | |
| currentChannel.url = line; | |
| parsedChannels.push({ ...currentChannel }); | |
| currentChannel = {}; | |
| } | |
| }); | |
| return parsedChannels; | |
| } | |
| renderChannelList() { | |
| this.channelList.innerHTML = ''; | |
| this.channels.forEach((channel, index) => { | |
| const li = document.createElement('li'); | |
| li.className = 'channel-item'; | |
| li.dataset.index = index; | |
| li.title = `${channel.name}\n${channel.url}`; | |
| li.innerHTML = ` | |
| <div class="channel-name">${this.escapeHtml(channel.name)}</div> | |
| <div class="channel-url">${this.escapeHtml(channel.url)}</div> | |
| `; | |
| li.addEventListener('click', () => this.selectChannel(index)); | |
| this.channelList.appendChild(li); | |
| }); | |
| } | |
| selectChannel(index) { | |
| if (index < 0 || index >= this.channels.length) return; | |
| this.currentIndex = index; | |
| const channel = this.channels[index]; | |
| document.querySelectorAll('.channel-item.active').forEach(item => item.classList.remove('active')); | |
| const selectedItem = document.querySelector(`[data-index="${index}"]`); | |
| if (selectedItem) { | |
| selectedItem.classList.add('active'); | |
| selectedItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| this.currentChannel.textContent = channel.name; | |
| this.videoPlayer.src = channel.url; | |
| this.videoPlayer.load(); | |
| this.play(); | |
| this.hideError(); | |
| } | |
| updateStats() { | |
| this.channelCount.textContent = this.channels.length; | |
| } | |
| handleVideoError() { | |
| const error = this.videoPlayer.error; | |
| let message = 'An unknown video error occurred.'; | |
| if (error) { | |
| switch (error.code) { | |
| case error.MEDIA_ERR_ABORTED: message = 'Video loading aborted.'; break; | |
| case error.MEDIA_ERR_NETWORK: message = 'Network error. Check connection or CORS policy.'; break; | |
| case error.MEDIA_ERR_DECODE: message = 'Video decoding error. Format may be unsupported.'; break; | |
| case error.MEDIA_ERR_SRC_NOT_SUPPORTED: message = 'Stream format not supported by this browser.'; break; | |
| } | |
| } | |
| this.showError(message); | |
| this.hideLoading(); | |
| } | |
| showError(message) { this.showMessage(this.errorMessage, message, 5000); } | |
| showSuccess(message) { this.showMessage(this.successMessage, message, 3000); } | |
| showMessage(element, message, duration) { | |
| element.textContent = message; | |
| element.style.display = 'block'; | |
| // Hide other message type | |
| (element === this.errorMessage ? this.successMessage : this.errorMessage).style.display = 'none'; | |
| setTimeout(() => { element.style.display = 'none'; }, duration); | |
| } | |
| showLoading(message = 'Loading...') { | |
| this.loadingMessage.textContent = message; | |
| this.loadingMessage.style.display = 'block'; | |
| } | |
| hideLoading() { this.loadingMessage.style.display = 'none'; } | |
| saveChannelsToStorage() { | |
| try { | |
| localStorage.setItem('iptv_player_channels', JSON.stringify(this.channels)); | |
| } catch (e) { | |
| console.error('Failed to save to local storage:', e); | |
| } | |
| } | |
| loadChannelsFromStorage() { | |
| try { | |
| const stored = localStorage.getItem('iptv_player_channels'); | |
| return stored ? JSON.parse(stored) : []; | |
| } catch (e) { | |
| console.error('Failed to load from local storage:', e); | |
| return []; | |
| } | |
| } | |
| isValidUrl(string) { try { new URL(string); return true; } catch (_) { return false; } } | |
| escapeHtml(str) { | |
| const p = document.createElement('p'); | |
| p.textContent = str; | |
| return p.innerHTML; | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const player = new IPTVPlayer(); | |
| player.init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |