/** * Proxy HLS direct — SANS FFmpeg * - Récupère le M3U8 depuis la source active * - Réécrit les URLs des segments pour passer par notre serveur * - Proxy chaque segment à la demande * - Le switch de source est immédiat : chaque requête playlist lit la source courante */ const express = require('express'); const router = express.Router(); const axios = require('axios'); const scheduler = require('../core/scheduler'); const HEADERS = { 'User-Agent': 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36', 'Accept': '*/*', 'Connection': 'keep-alive' }; const CORS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' }; // Résoudre une URL relative par rapport à une base const resolveUrl = (url, base) => { if (!url) return ''; if (url.startsWith('http')) return url; try { return new URL(url, base).href; } catch { return url; } }; // Encoder une URL en base64url pour la passer en paramètre const encode = (url) => Buffer.from(url).toString('base64url'); const decode = (b64) => Buffer.from(b64, 'base64url').toString('utf8'); // ── GET /stream/live/playlist.m3u8 ────────────────────────────────── // Appelé à chaque refresh HLS.js → toujours lit la SOURCE ACTIVE du moment router.get('/live/playlist.m3u8', async (req, res) => { const sourceUrl = scheduler.getCurrentUrl(); if (!sourceUrl) { return res.set(CORS) .type('application/vnd.apple.mpegurl') .status(503) .send('#EXTM3U\n#EXT-X-VERSION:3\n# Aucune emission programmee'); } try { const response = await axios.get(sourceUrl, { timeout: 12000, headers: HEADERS, responseType: 'text' }); const m3u8 = response.data; const baseUrl = sourceUrl.substring(0, sourceUrl.lastIndexOf('/') + 1); // Réécrire le M3U8 : // - Toutes les URLs de segments → /stream/seg?u=BASE64URL // - Les sous-playlists aussi (multi-quality) const rewritten = m3u8.split('\n').map(line => { const t = line.trim(); if (!t || t.startsWith('#')) return line; const absolute = resolveUrl(t, baseUrl); return `/stream/seg?u=${encode(absolute)}`; }).join('\n'); res.set({ ...CORS, 'Content-Type': 'application/vnd.apple.mpegurl', 'Cache-Control': 'no-cache, no-store, must-revalidate' }).send(rewritten); } catch (err) { console.error('[proxy] M3U8 error:', err.message); res.set(CORS) .type('application/vnd.apple.mpegurl') .status(502) .send('#EXTM3U\n#EXT-X-VERSION:3\n# Source indisponible'); } }); // ── GET /stream/seg?u=BASE64URL ────────────────────────────────────── // Proxy transparent d'un segment TS ou d'une sous-playlist router.get('/seg', async (req, res) => { const { u } = req.query; if (!u) return res.status(400).end(); let segUrl; try { segUrl = decode(u); } catch { return res.status(400).end(); } // Vérification de sécurité : source active doit être configurée if (!scheduler.getCurrentUrl()) return res.status(403).end(); try { const response = await axios.get(segUrl, { timeout: 20000, responseType: 'arraybuffer', headers: HEADERS }); const contentType = response.headers['content-type'] || 'video/MP2T'; // Si c'est une sous-playlist (multi-qualité), la réécrire aussi if (contentType.includes('mpegurl') || segUrl.endsWith('.m3u8')) { const text = Buffer.from(response.data).toString('utf8'); const baseUrl = segUrl.substring(0, segUrl.lastIndexOf('/') + 1); const rewritten = text.split('\n').map(line => { const t = line.trim(); if (!t || t.startsWith('#')) return line; const absolute = resolveUrl(t, baseUrl); return `/stream/seg?u=${encode(absolute)}`; }).join('\n'); return res.set({ ...CORS, 'Content-Type': 'application/vnd.apple.mpegurl', 'Cache-Control': 'no-cache' }) .send(rewritten); } res.set({ ...CORS, 'Content-Type': contentType, 'Cache-Control': 'no-cache', 'Content-Length': response.data.byteLength }).send(Buffer.from(response.data)); } catch (err) { console.error('[proxy] Segment error:', err.message); res.status(502).end(); } }); // ── GET /stream/live/info ──────────────────────────────────────────── router.get('/live/info', (req, res) => { const info = scheduler.getCurrentInfo(); if (!info) return res.set(CORS).json({ active: false }); res.set(CORS).json({ active: true, name: info.name, streamUrl: '/stream/live/playlist.m3u8', isManual: scheduler.isManual() }); }); module.exports = router;