iptv-player / index.html
DARSAHANA's picture
Update index.html
81d66c4 verified
<!DOCTYPE html>
<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>