Spaces:
No application file
No application file
| /** | |
| * 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; | |