Ajout d'un lecteur HLS.

This commit is contained in:
Johnnybegood90
2026-03-10 10:10:27 +01:00
parent 71dc57aaf7
commit bf0e6fb685
+124 -1
View File
@@ -631,6 +631,7 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? "");
#navBtns { display: none !important; }
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js"></script>
<link id="themeStylesheet" rel="stylesheet" href="themes/default.css">
</head>
<body>
@@ -697,6 +698,17 @@ foreach ($theme_files as $file) {
<div class="live-dot">LIVE</div>
</div>
<!-- PLAYER PiP -->
<div class="pip" id="pip">
<div class="pip-header">
<div class="pip-channel" id="pipChannel">—</div>
<div class="pip-program" id="pipProgram"></div>
<button class="pip-close" onclick="closePip()" title="Fermer">✕</button>
</div>
<video id="pipVideo" controls playsinline></video>
<div class="pip-error" id="pipError">⚠ Stream indisponible</div>
</div>
<!-- GRID (desktop/tablet) -->
<div class="grid-wrapper">
<div class="channels-col">
@@ -734,6 +746,7 @@ function msToX(ms) { return (ms - GRID_START.getTime()) / 60000 * PX_PER_MIN; }
let channels = [];
let programs = {};
let m3uStreams = {}; // slug normalisé → url stream
let scrollListenerAdded = false; // sync scroll vertical chaînes
function pad(n) { return String(n).padStart(2,'0'); }
@@ -891,6 +904,7 @@ function renderChannels() {
const name = document.createElement('div');
name.className = 'channel-name'; name.textContent = ch.name; name.title = ch.name;
cell.appendChild(name);
cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id)));
list.appendChild(cell);
});
}
@@ -953,6 +967,9 @@ function renderPrograms() {
block.addEventListener('mouseenter', e=>showTooltip(e,p));
block.addEventListener('mousemove', e=>moveTooltip(e));
block.addEventListener('mouseleave', hideTooltip);
if (p.start <= now && p.stop > now) {
block.addEventListener('click', () => openPip(ch.name, p.title));
}
row.appendChild(block);
});
inner.appendChild(row);
@@ -1042,6 +1059,13 @@ function renderMobile() {
});
}
// ── HELPERS ──────────────────────────────────────────────────────────────────
function getNowPlaying(chId) {
const now = new Date();
const p = (programs[chId] || []).find(p => p.start <= now && p.stop > now);
return p ? p.title : null;
}
// ── TOOLTIP ───────────────────────────────────────────────────────────────────
function showTooltip(e, p) {
const ep = fmtEpisode(p.season, p.episode);
@@ -1060,6 +1084,105 @@ function moveTooltip(e) {
function hideTooltip() { document.getElementById('tooltip').classList.remove('visible'); }
// ── M3U PARSER ────────────────────────────────────────────────────────────────
function slugify(str) {
return str.toLowerCase()
.replace(/^\d+\s*/, '') // virer le numéro de chaîne en début
.replace(/[^a-z0-9]/g, ''); // garder que alphanum
}
async function loadM3U(url) {
if (!url) return;
try {
let text;
try {
const r = await fetch(url); text = await r.text();
} catch(e) {
const r = await fetch(CORS_PROXY + encodeURIComponent(url)); text = await r.text();
}
parseM3U(text);
} catch(e) { console.warn('M3U load failed:', e); }
}
function parseM3U(text) {
m3uStreams = {};
const lines = text.split('\n');
let currentName = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#EXTINF')) {
// Extraire le nom : dernière partie après la virgule
const comma = line.lastIndexOf(',');
currentName = comma !== -1 ? line.slice(comma + 1).trim() : null;
} else if (line && !line.startsWith('#') && currentName) {
m3uStreams[slugify(currentName)] = line;
currentName = null;
}
}
console.log(`M3U: ${Object.keys(m3uStreams).length} streams chargés`);
}
function getStreamUrl(channelName) {
const slug = slugify(channelName);
if (m3uStreams[slug]) return m3uStreams[slug];
// Fallback : chercher une clé qui contient le slug ou l'inverse
for (const [key, url] of Object.entries(m3uStreams)) {
if (key.includes(slug) || slug.includes(key)) return url;
}
return null;
}
// ── PLAYER PiP ────────────────────────────────────────────────────────────────
let hlsInstance = null;
function openPip(channelName, programTitle) {
const url = getStreamUrl(channelName);
const pip = document.getElementById('pip');
const video = document.getElementById('pipVideo');
const err = document.getElementById('pipError');
document.getElementById('pipChannel').textContent = channelName;
document.getElementById('pipProgram').textContent = programTitle || '';
err.style.display = 'none';
pip.classList.add('visible');
// Détruire l'instance HLS précédente
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
video.src = '';
if (!url) {
err.style.display = 'block';
err.textContent = '⚠ Aucun stream trouvé pour cette chaîne';
return;
}
if (Hls.isSupported()) {
hlsInstance = new Hls({ enableWorker: true });
hlsInstance.loadSource(url);
hlsInstance.attachMedia(video);
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
hlsInstance.on(Hls.Events.ERROR, (e, data) => {
if (data.fatal) { err.style.display = 'block'; err.textContent = '⚠ Erreur de lecture du stream'; }
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari natif HLS
video.src = url;
video.addEventListener('loadedmetadata', () => video.play().catch(() => {}));
} else {
err.style.display = 'block';
err.textContent = '⚠ HLS non supporté sur ce navigateur';
}
}
function closePip() {
const pip = document.getElementById('pip');
const video = document.getElementById('pipVideo');
pip.classList.remove('visible');
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
video.pause(); video.src = '';
}
// ── THÈMES ────────────────────────────────────────────────────────────────────
function setTheme(theme) {
document.getElementById('themeStylesheet').href = 'themes/' + theme + '.css';
@@ -1087,7 +1210,7 @@ window.addEventListener('resize', () => {
if (m !== lastMobile) { lastMobile=m; scrollListenerAdded=false; if (channels.length) renderAll(); }
});
window.addEventListener('load', () => loadEPG(DEFAULT_EPG_URL));
window.addEventListener('load', () => { loadEPG(DEFAULT_EPG_URL); loadM3U(DEFAULT_M3U_URL); });
</script>
</body>
</html>