diff --git a/index.php b/index.php
index fb97ee1..176a5cb 100644
--- a/index.php
+++ b/index.php
@@ -631,6 +631,86 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? "");
#navBtns { display: none !important; }
}
+
+ /* ═══════════════════════════════════════════
+ PLAYER PiP
+ ═══════════════════════════════════════════ */
+ .pip {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ width: 380px;
+ background: #000;
+ border: 1px solid var(--border-bright);
+ box-shadow: 0 8px 32px rgba(0,0,0,0.8);
+ z-index: 2000;
+ display: none;
+ flex-direction: column;
+ }
+ .pip.visible { display: flex; }
+ .pip-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 10px;
+ background: var(--surface);
+ border-bottom: 1px solid var(--border);
+ gap: 8px;
+ flex-shrink: 0;
+ }
+ .pip-channel {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--accent);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1;
+ }
+ .pip-program {
+ font-size: 11px;
+ color: var(--text-dim);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 2;
+ text-align: center;
+ }
+ .pip-close {
+ background: none;
+ border: none;
+ color: var(--text-dim);
+ font-size: 18px;
+ cursor: pointer;
+ padding: 0 4px;
+ line-height: 1;
+ flex-shrink: 0;
+ transition: color 0.15s;
+ }
+ .pip-close:hover { color: var(--accent-live); }
+ .pip video {
+ width: 100%;
+ aspect-ratio: 16/9;
+ display: block;
+ background: #000;
+ }
+ .pip-error {
+ padding: 20px;
+ text-align: center;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--accent-live);
+ letter-spacing: 0.06em;
+ }
+ .channel-cell { cursor: pointer; }
+ .channel-cell:hover { background: var(--surface2); }
+ .program-block.is-live { cursor: pointer; }
+ @media (max-width: 600px) {
+ .pip { width: calc(100vw - 16px); bottom: 8px; right: 8px; left: 8px; }
+ }
+
@@ -1125,12 +1205,21 @@ function parseM3U(text) {
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;
+ let url = null;
+ if (m3uStreams[slug]) {
+ url = m3uStreams[slug];
+ } else {
+ // Fallback : chercher une clé qui contient le slug ou l'inverse
+ for (const [key, u] of Object.entries(m3uStreams)) {
+ if (key.includes(slug) || slug.includes(key)) { url = u; break; }
+ }
}
- return null;
+ if (!url) return null;
+ // Passer par proxy.php pour éviter le mixed content HTTP/HTTPS
+ if (url.startsWith('http://')) {
+ return 'proxy.php?url=' + encodeURIComponent(url);
+ }
+ return url;
}
// ── PLAYER PiP ────────────────────────────────────────────────────────────────
diff --git a/proxy.php b/proxy.php
new file mode 100644
index 0000000..c36eed0
--- /dev/null
+++ b/proxy.php
@@ -0,0 +1,84 @@
+ [
+ 'method' => 'GET',
+ 'timeout' => 15,
+ 'header' => "User-Agent: GridTV-Proxy/1.0\r\nAccept: */*",
+ 'follow_location' => 1,
+]];
+$body = @file_get_contents($url, false, stream_context_create($opts));
+
+if ($body === false) {
+ http_response_code(502); die('Cannot reach upstream');
+}
+
+// Détecter si c'est une playlist M3U8
+$is_m3u8 = stripos($url, '.m3u8') !== false
+ || strpos($body, '#EXTM3U') === 0
+ || strpos($body, '#EXT-X-') !== false;
+
+header('Access-Control-Allow-Origin: *');
+header('Cache-Control: no-cache');
+
+if ($is_m3u8) {
+ header('Content-Type: application/vnd.apple.mpegurl');
+ // Réécrire chaque URL de segment/playlist pour passer par ce proxy
+ $proxy_base = (isset($_SERVER['HTTPS']) ? 'https' : 'http')
+ . '://' . $_SERVER['HTTP_HOST']
+ . strtok($_SERVER['REQUEST_URI'], '?')
+ . '?url=';
+
+ $lines = explode("\n", $body);
+ $out = [];
+ foreach ($lines as $line) {
+ $line = rtrim($line);
+ if ($line === '' || $line[0] === '#') {
+ // Réécrire les URIs dans les tags comme #EXT-X-MAP:URI="..."
+ $line = preg_replace_callback('/URI="([^"]+)"/', function($m) use ($base, $proxy_base) {
+ $seg_url = strpos($m[1], 'http') === 0 ? $m[1] : $base . $m[1];
+ return 'URI="' . $proxy_base . urlencode($seg_url) . '"';
+ }, $line);
+ $out[] = $line;
+ } else {
+ // Ligne d'URL : segment .ts ou sous-playlist .m3u8
+ $seg_url = strpos($line, 'http') === 0 ? $line : $base . $line;
+ $out[] = $proxy_base . urlencode($seg_url);
+ }
+ }
+ echo implode("\n", $out);
+} else {
+ // Segment binaire (.ts, .aac, etc.) — streamer direct
+ // Récupérer le Content-Type depuis les headers de réponse
+ $ct = 'video/MP2T';
+ foreach ($http_response_header ?? [] as $h) {
+ if (preg_match('/^Content-Type:\s*(.+)/i', $h, $m)) {
+ $ct = trim($m[1]); break;
+ }
+ }
+ header('Content-Type: ' . $ct);
+ echo $body;
+}