mirror of
https://github.com/fishtank-dashboard/fishtank-dashboard.git
synced 2026-05-02 04:52:04 -04:00
minor bug fixes
This commit is contained in:
committed by
GitHub
parent
46e2c58ba8
commit
e398ad69c3
@@ -1330,11 +1330,11 @@
|
|||||||
function initCamerman() {
|
function initCamerman() {
|
||||||
const video = document.getElementById('cammanVideo');
|
const video = document.getElementById('cammanVideo');
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
||||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||||
hls.loadSource('http://localhost:3000/cam/cameraman-5/index.m3u8');
|
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
||||||
hls.on(Hls.Events.ERROR, (e, d) => {
|
hls.on(Hls.Events.ERROR, (e, d) => {
|
||||||
@@ -1349,7 +1349,7 @@
|
|||||||
});
|
});
|
||||||
hlsInstances['camman'] = hls;
|
hlsInstances['camman'] = hls;
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
video.src = 'http://localhost:3000/cam/cameraman-5/index.m3u8';
|
video.src = 'http://localhost:3000/cam/cameraman2-5/index.m3u8';
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1360,7 +1360,7 @@
|
|||||||
_origSetFeatured(i);
|
_origSetFeatured(i);
|
||||||
// If cameraman is now in featured, show director in camman panel
|
// If cameraman is now in featured, show director in camman panel
|
||||||
// If cameraman is what's being swapped back, restore its own stream
|
// If cameraman is what's being swapped back, restore its own stream
|
||||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
const cammanVideo = document.getElementById('cammanVideo');
|
const cammanVideo = document.getElementById('cammanVideo');
|
||||||
const cammanLabel = document.getElementById('cammanLabel');
|
const cammanLabel = document.getElementById('cammanLabel');
|
||||||
if (!cammanVideo) return;
|
if (!cammanVideo) return;
|
||||||
@@ -1381,7 +1381,7 @@
|
|||||||
// Restore camman panel to cameraman stream
|
// Restore camman panel to cameraman stream
|
||||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||||
hls.loadSource('http://localhost:3000/cam/cameraman-5/index.m3u8');
|
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||||
hls.attachMedia(cammanVideo);
|
hls.attachMedia(cammanVideo);
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||||
hlsInstances['camman'] = hls;
|
hlsInstances['camman'] = hls;
|
||||||
@@ -1419,11 +1419,11 @@
|
|||||||
function initCamerman() {
|
function initCamerman() {
|
||||||
const video = document.getElementById('cammanVideo');
|
const video = document.getElementById('cammanVideo');
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
||||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||||
hls.loadSource('http://localhost:3000/cam/cameraman-5/index.m3u8');
|
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
||||||
hls.on(Hls.Events.ERROR, (e, d) => {
|
hls.on(Hls.Events.ERROR, (e, d) => {
|
||||||
@@ -1438,7 +1438,7 @@
|
|||||||
});
|
});
|
||||||
hlsInstances['camman'] = hls;
|
hlsInstances['camman'] = hls;
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
video.src = 'http://localhost:3000/cam/cameraman-5/index.m3u8';
|
video.src = 'http://localhost:3000/cam/cameraman2-5/index.m3u8';
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1449,7 +1449,7 @@
|
|||||||
_origSetFeatured(i);
|
_origSetFeatured(i);
|
||||||
// If cameraman is now in featured, show director in camman panel
|
// If cameraman is now in featured, show director in camman panel
|
||||||
// If cameraman is what's being swapped back, restore its own stream
|
// If cameraman is what's being swapped back, restore its own stream
|
||||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
const cammanVideo = document.getElementById('cammanVideo');
|
const cammanVideo = document.getElementById('cammanVideo');
|
||||||
const cammanLabel = document.getElementById('cammanLabel');
|
const cammanLabel = document.getElementById('cammanLabel');
|
||||||
if (!cammanVideo) return;
|
if (!cammanVideo) return;
|
||||||
@@ -1470,7 +1470,7 @@
|
|||||||
// Restore camman panel to cameraman stream
|
// Restore camman panel to cameraman stream
|
||||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||||
hls.loadSource('http://localhost:3000/cam/cameraman-5/index.m3u8');
|
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||||
hls.attachMedia(cammanVideo);
|
hls.attachMedia(cammanVideo);
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||||
hlsInstances['camman'] = hls;
|
hlsInstances['camman'] = hls;
|
||||||
@@ -1556,7 +1556,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cameraman Panel -->
|
<!-- Cameraman Panel -->
|
||||||
<div class="panel stocks-hide" style="grid-area:camman;overflow:hidden;background:#000;padding:0;cursor:pointer;" onclick="setFeatured(CAMERAS.findIndex(([,s])=>s==='cameraman-5'))" title="Click to feature Cameraman">
|
<div class="panel stocks-hide" style="grid-area:camman;overflow:hidden;background:#000;padding:0;cursor:pointer;" onclick="setFeatured(CAMERAS.findIndex(([,s])=>s==='cameraman2-5'))" title="Click to feature Cameraman">
|
||||||
<div style="position:relative;width:100%;height:100%;">
|
<div style="position:relative;width:100%;height:100%;">
|
||||||
<video id="cammanVideo" muted playsinline autoplay style="width:100%;height:100%;object-fit:contain;display:block;"></video>
|
<video id="cammanVideo" muted playsinline autoplay style="width:100%;height:100%;object-fit:contain;display:block;"></video>
|
||||||
<div class="cam-label" id="cammanLabel" style="position:absolute;bottom:0;left:0;right:0;">CAMERAMAN</div>
|
<div class="cam-label" id="cammanLabel" style="position:absolute;bottom:0;left:0;right:0;">CAMERAMAN</div>
|
||||||
@@ -2148,7 +2148,7 @@
|
|||||||
["Hallway Down", "hwdn-5"],
|
["Hallway Down", "hwdn-5"],
|
||||||
["Hallway Up", "hwup-5"],
|
["Hallway Up", "hwup-5"],
|
||||||
["Jungle Room", "br4j-5"],
|
["Jungle Room", "br4j-5"],
|
||||||
["Cameraman", "cameraman-5"],
|
["Cameraman", "cameraman2-5"],
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROOM_NAMES = {
|
const ROOM_NAMES = {
|
||||||
@@ -2160,7 +2160,7 @@
|
|||||||
"dnrm-5": "Dining Room", "mrke-5": "Market", "hwdn-5": "Hallway Down",
|
"dnrm-5": "Dining Room", "mrke-5": "Market", "hwdn-5": "Hallway Down",
|
||||||
"hwup-5": "Hallway Up",
|
"hwup-5": "Hallway Up",
|
||||||
"br4j-5": "Jungle Room",
|
"br4j-5": "Jungle Room",
|
||||||
"cameraman-5": "Cameraman", "site": "Site-wide",
|
"cameraman2-5": "Cameraman", "site": "Site-wide",
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_IDX = 1; // Director Mode
|
const DEFAULT_IDX = 1; // Director Mode
|
||||||
@@ -2854,7 +2854,7 @@
|
|||||||
|
|
||||||
if (thumbMode) {
|
if (thumbMode) {
|
||||||
// Switch back to thumbnail mode — destroy all live thumb streams, refresh canvases
|
// Switch back to thumbnail mode — destroy all live thumb streams, refresh canvases
|
||||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
CAMERAS.forEach(([name, slug], i) => {
|
CAMERAS.forEach(([name, slug], i) => {
|
||||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||||
if (hlsInstances[i]) { hlsInstances[i].destroy(); delete hlsInstances[i]; }
|
if (hlsInstances[i]) { hlsInstances[i].destroy(); delete hlsInstances[i]; }
|
||||||
@@ -2875,7 +2875,7 @@
|
|||||||
} else {
|
} else {
|
||||||
// Switch to live mode — replace canvases with live video streams
|
// Switch to live mode — replace canvases with live video streams
|
||||||
if (window._thumbInterval) { clearInterval(window._thumbInterval); window._thumbInterval = null; }
|
if (window._thumbInterval) { clearInterval(window._thumbInterval); window._thumbInterval = null; }
|
||||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
CAMERAS.forEach(([name, slug], i) => {
|
CAMERAS.forEach(([name, slug], i) => {
|
||||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||||
const cell = document.getElementById('cam-' + i);
|
const cell = document.getElementById('cam-' + i);
|
||||||
@@ -2928,7 +2928,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAllThumbnails() {
|
async function refreshAllThumbnails() {
|
||||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
const tasks = CAMERAS.map(([name, slug], i) => {
|
const tasks = CAMERAS.map(([name, slug], i) => {
|
||||||
if (i === DEFAULT_IDX || i === cammanIdx) return null;
|
if (i === DEFAULT_IDX || i === cammanIdx) return null;
|
||||||
if (slug === CAMERAS[featuredIdx][1]) return null;
|
if (slug === CAMERAS[featuredIdx][1]) return null;
|
||||||
@@ -2995,7 +2995,7 @@
|
|||||||
featVideo.addEventListener('canplay', () => { const s = document.getElementById('featuredVolume'); if (s) featVideo.volume = parseFloat(s.value); }, { once: true });
|
featVideo.addEventListener('canplay', () => { const s = document.getElementById('featuredVolume'); if (s) featVideo.volume = parseFloat(s.value); }, { once: true });
|
||||||
|
|
||||||
// Build thumb grid — skip Director Mode and Cameraman
|
// Build thumb grid — skip Director Mode and Cameraman
|
||||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === "cameraman-5");
|
const cammanIdx = CAMERAS.findIndex(([,s]) => s === "cameraman2-5");
|
||||||
CAMERAS.forEach(([name, slug], i) => {
|
CAMERAS.forEach(([name, slug], i) => {
|
||||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||||
const cell = document.createElement('div');
|
const cell = document.createElement('div');
|
||||||
@@ -3122,11 +3122,11 @@
|
|||||||
function initCamerman() {
|
function initCamerman() {
|
||||||
const video = document.getElementById('cammanVideo');
|
const video = document.getElementById('cammanVideo');
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const idx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
||||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||||
hls.loadSource('http://localhost:3000/cam/cameraman-5/index.m3u8');
|
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
|
||||||
hls.on(Hls.Events.ERROR, (e, d) => {
|
hls.on(Hls.Events.ERROR, (e, d) => {
|
||||||
@@ -3141,7 +3141,7 @@
|
|||||||
});
|
});
|
||||||
hlsInstances['camman'] = hls;
|
hlsInstances['camman'] = hls;
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
video.src = 'http://localhost:3000/cam/cameraman-5/index.m3u8';
|
video.src = 'http://localhost:3000/cam/cameraman2-5/index.m3u8';
|
||||||
video.play().catch(() => {});
|
video.play().catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3152,7 +3152,7 @@
|
|||||||
_origSetFeatured(i);
|
_origSetFeatured(i);
|
||||||
// If cameraman is now in featured, show director in camman panel
|
// If cameraman is now in featured, show director in camman panel
|
||||||
// If cameraman is what's being swapped back, restore its own stream
|
// If cameraman is what's being swapped back, restore its own stream
|
||||||
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([,s]) => s === 'cameraman2-5');
|
||||||
const cammanVideo = document.getElementById('cammanVideo');
|
const cammanVideo = document.getElementById('cammanVideo');
|
||||||
const cammanLabel = document.getElementById('cammanLabel');
|
const cammanLabel = document.getElementById('cammanLabel');
|
||||||
if (!cammanVideo) return;
|
if (!cammanVideo) return;
|
||||||
@@ -3173,7 +3173,7 @@
|
|||||||
// Restore camman panel to cameraman stream
|
// Restore camman panel to cameraman stream
|
||||||
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
if (hlsInstances['camman']) hlsInstances['camman'].destroy();
|
||||||
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
const hls = new Hls({ lowLatencyMode: true, maxBufferLength: 8 });
|
||||||
hls.loadSource('http://localhost:3000/cam/cameraman-5/index.m3u8');
|
hls.loadSource('http://localhost:3000/cam/cameraman2-5/index.m3u8');
|
||||||
hls.attachMedia(cammanVideo);
|
hls.attachMedia(cammanVideo);
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
hls.on(Hls.Events.MANIFEST_PARSED, () => { cammanVideo.muted = true; cammanVideo.play().catch(() => {}); });
|
||||||
hlsInstances['camman'] = hls;
|
hlsInstances['camman'] = hls;
|
||||||
@@ -3400,7 +3400,7 @@
|
|||||||
|
|
||||||
function autoDiscoverCameras(presenceData) {
|
function autoDiscoverCameras(presenceData) {
|
||||||
// presence keys are camera slugs (plus "total")
|
// presence keys are camera slugs (plus "total")
|
||||||
const knownSpecial = new Set(['total', 'cameraman-5']);
|
const knownSpecial = new Set(['total', 'cameraman2-5']);
|
||||||
let added = false;
|
let added = false;
|
||||||
|
|
||||||
Object.keys(presenceData).forEach(slug => {
|
Object.keys(presenceData).forEach(slug => {
|
||||||
@@ -3411,7 +3411,7 @@
|
|||||||
console.log('[CAM] Auto-discovered new camera:', slug);
|
console.log('[CAM] Auto-discovered new camera:', slug);
|
||||||
KNOWN_CAMERA_SLUGS.add(slug);
|
KNOWN_CAMERA_SLUGS.add(slug);
|
||||||
const label = slugToLabel(slug);
|
const label = slugToLabel(slug);
|
||||||
const cammanIdx = CAMERAS.findIndex(([, s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([, s]) => s === 'cameraman2-5');
|
||||||
CAMERAS.splice(cammanIdx, 0, [label, slug]);
|
CAMERAS.splice(cammanIdx, 0, [label, slug]);
|
||||||
added = true;
|
added = true;
|
||||||
});
|
});
|
||||||
@@ -3420,7 +3420,7 @@
|
|||||||
if (added) {
|
if (added) {
|
||||||
// Add new cells to the grid without destroying existing streams
|
// Add new cells to the grid without destroying existing streams
|
||||||
const grid = document.getElementById('cameraGrid');
|
const grid = document.getElementById('cameraGrid');
|
||||||
const cammanIdx = CAMERAS.findIndex(([, s]) => s === 'cameraman-5');
|
const cammanIdx = CAMERAS.findIndex(([, s]) => s === 'cameraman2-5');
|
||||||
CAMERAS.forEach(([name, slug], i) => {
|
CAMERAS.forEach(([name, slug], i) => {
|
||||||
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
if (i === DEFAULT_IDX || i === cammanIdx) return;
|
||||||
if (document.getElementById('cam-' + i)) return; // already exists
|
if (document.getElementById('cam-' + i)) return; // already exists
|
||||||
@@ -3447,7 +3447,7 @@
|
|||||||
|
|
||||||
// ── Viewer counts ────────────────────────────────────────────
|
// ── Viewer counts ────────────────────────────────────────────
|
||||||
window.updateViewerCounts = function(data) {
|
window.updateViewerCounts = function(data) {
|
||||||
const cammanIdx = typeof CAMERAS !== 'undefined' ? CAMERAS.findIndex(([,s]) => s === 'cameraman-5') : -1;
|
const cammanIdx = typeof CAMERAS !== 'undefined' ? CAMERAS.findIndex(([,s]) => s === 'cameraman2-5') : -1;
|
||||||
if (typeof CAMERAS !== 'undefined') {
|
if (typeof CAMERAS !== 'undefined') {
|
||||||
CAMERAS.forEach(([name, slug], i) => {
|
CAMERAS.forEach(([name, slug], i) => {
|
||||||
if (i === (typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1)) return;
|
if (i === (typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1)) return;
|
||||||
@@ -3506,7 +3506,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update every camera cell in the grid
|
// Update every camera cell in the grid
|
||||||
const cammanIdx = typeof CAMERAS !== 'undefined' ? CAMERAS.findIndex(([,s]) => s === 'cameraman-5') : -1;
|
const cammanIdx = typeof CAMERAS !== 'undefined' ? CAMERAS.findIndex(([,s]) => s === 'cameraman2-5') : -1;
|
||||||
if (typeof CAMERAS !== 'undefined') {
|
if (typeof CAMERAS !== 'undefined') {
|
||||||
CAMERAS.forEach(([name, slug], i) => {
|
CAMERAS.forEach(([name, slug], i) => {
|
||||||
if (i === (typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1)) return;
|
if (i === (typeof DEFAULT_IDX !== 'undefined' ? DEFAULT_IDX : 1)) return;
|
||||||
|
|||||||
116
server.js
116
server.js
@@ -3,11 +3,13 @@ const https = require('https');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
const zlib = require('zlib');
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const API_TARGET = 'api.fishtank.live';
|
const API_TARGET = 'api.fishtank.live';
|
||||||
const CAM_TARGET = 'epyc.goran.jetzt';
|
const CAM_TARGET = 'epyc.goran.jetzt';
|
||||||
|
const CAM_PORT = 443;
|
||||||
const FT_WS_URL = 'wss://ws.fishtank.live/socket.io/?EIO=4&transport=websocket';
|
const FT_WS_URL = 'wss://ws.fishtank.live/socket.io/?EIO=4&transport=websocket';
|
||||||
|
|
||||||
// ── Local WS clients (dashboard connections) ────────────────
|
// ── Local WS clients (dashboard connections) ────────────────
|
||||||
@@ -304,6 +306,51 @@ function connectFishtankWS(token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── HTTP helpers ─────────────────────────────────────────────
|
// ── HTTP helpers ─────────────────────────────────────────────
|
||||||
|
// Per-slug cookie store — tkn cookie from port 444 responses
|
||||||
|
const camCookies = {};
|
||||||
|
|
||||||
|
function fetchCam(slug, subPath, callback) {
|
||||||
|
// Step 1: request from port 443 (follows redirect to 444 manually)
|
||||||
|
const path443 = `/hls/${slug}/${subPath}`;
|
||||||
|
const cookie = camCookies[slug] ? `tkn=${camCookies[slug]}` : '';
|
||||||
|
|
||||||
|
const doRequest = (port, reqPath, reqCookie) => {
|
||||||
|
const opts = {
|
||||||
|
hostname: CAM_TARGET, port, path: reqPath, method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'host': port === 443 ? CAM_TARGET : `${CAM_TARGET}:444`,
|
||||||
|
'user-agent': 'VLC/3.0.20 LibVLC/3.0.20',
|
||||||
|
'accept': '*/*',
|
||||||
|
...(reqCookie ? { 'cookie': reqCookie } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const req = https.request(opts, (res) => {
|
||||||
|
const chunks = [];
|
||||||
|
res.on('data', c => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
const body = Buffer.concat(chunks);
|
||||||
|
// Store cookie from Set-Cookie header
|
||||||
|
const setCookie = res.headers['set-cookie'];
|
||||||
|
if (setCookie) {
|
||||||
|
const match = (Array.isArray(setCookie) ? setCookie.join(';') : setCookie).match(/tkn=(\d+)/);
|
||||||
|
if (match) camCookies[slug] = match[1];
|
||||||
|
}
|
||||||
|
// Follow redirect to port 444
|
||||||
|
if (res.statusCode === 302 && res.headers.location) {
|
||||||
|
const loc = res.headers.location;
|
||||||
|
const m = loc.match(/epyc\.goran\.jetzt:444(\/.*)/);
|
||||||
|
if (m) { doRequest(444, m[1], camCookies[slug] ? `tkn=${camCookies[slug]}` : ''); return; }
|
||||||
|
}
|
||||||
|
callback(null, res, body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', err => callback(err));
|
||||||
|
req.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
doRequest(443, path443, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
function fetchRemote(hostname, targetPath, callback) {
|
function fetchRemote(hostname, targetPath, callback) {
|
||||||
const req = https.request({
|
const req = https.request({
|
||||||
hostname, port: 443, path: targetPath, method: 'GET',
|
hostname, port: 443, path: targetPath, method: 'GET',
|
||||||
@@ -324,14 +371,15 @@ function fetchRemote(hostname, targetPath, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rewriteM3u8(body, basePath) {
|
function rewriteM3u8(body, basePath) {
|
||||||
|
// basePath examples: "dirc-5/" or "dirc-5/0_1/"
|
||||||
|
// Never starts with slash — strip it just in case
|
||||||
|
const base = basePath.replace(/^\//, '');
|
||||||
return body.split('\n').map(line => {
|
return body.split('\n').map(line => {
|
||||||
const t = line.trim();
|
const t = line.trim();
|
||||||
if (!t || t.startsWith('#')) {
|
if (!t || t.startsWith('#')) return t;
|
||||||
return t.replace(/URI="([^"]+)"/g, (m, uri) =>
|
|
||||||
uri.startsWith('http') ? m : `URI="http://localhost:${PORT}/cam${basePath}${uri}"`);
|
|
||||||
}
|
|
||||||
if (t.startsWith('http')) return t;
|
if (t.startsWith('http')) return t;
|
||||||
return `http://localhost:${PORT}/cam${basePath}${t}`;
|
if (t.startsWith('/')) return `http://localhost:${PORT}/cam${t}`;
|
||||||
|
return `http://localhost:${PORT}/cam/${base}${t}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,18 +447,60 @@ const server = http.createServer((req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.pathname.startsWith('/cam/')) {
|
if (parsed.pathname.startsWith('/cam/')) {
|
||||||
const targetPath = parsed.pathname.replace('/cam', '') + (parsed.search || '');
|
// Extract slug from any cam path variant
|
||||||
if (!targetPath.includes('.m3u8')) {
|
const camPart = parsed.pathname.replace('/cam', '').replace('/hls', '').replace(/^\//, '');
|
||||||
fetchRemote(CAM_TARGET, targetPath, (err, remoteRes, body) => {
|
const parts = camPart.split('/');
|
||||||
if (err) { res.writeHead(502); res.end(err.message); return; }
|
const slug = parts[0];
|
||||||
res.writeHead(remoteRes.statusCode, { ...remoteRes.headers, 'access-control-allow-origin': '*' });
|
|
||||||
|
// Is this a media segment request? e.g. /cam/dirc-5/0_1/seg001.ts
|
||||||
|
const isSegment = parsed.pathname.match(/\.(ts|fmp4|mp4|m4s)(\?|$)/i);
|
||||||
|
const isSubPlaylist = parts.length > 2 && !isSegment; // e.g. dirc-5/0_1/index.m3u8
|
||||||
|
|
||||||
|
if (isSegment || isSubPlaylist) {
|
||||||
|
// Sub-playlist or segment — reconstruct path with cookie token
|
||||||
|
let subPath = parts.slice(1).join('/');
|
||||||
|
const tkn = camCookies[slug];
|
||||||
|
if (tkn) subPath += (subPath.includes('?') ? '&' : '?') + 'tkn=' + tkn;
|
||||||
|
const opts = {
|
||||||
|
hostname: CAM_TARGET, port: 444,
|
||||||
|
path: '/hls/' + slug + '/' + subPath, method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'host': CAM_TARGET + ':444',
|
||||||
|
'user-agent': 'VLC/3.0.20 LibVLC/3.0.20',
|
||||||
|
'accept': '*/*',
|
||||||
|
...(tkn ? { 'cookie': 'tkn=' + tkn } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const preq = https.request(opts, (pres) => {
|
||||||
|
const chunks = [];
|
||||||
|
pres.on('data', c => chunks.push(c));
|
||||||
|
pres.on('end', () => {
|
||||||
|
const body = Buffer.concat(chunks);
|
||||||
|
if (!isSegment && pres.statusCode === 200) {
|
||||||
|
const rawText = body.toString('utf8');
|
||||||
|
// Log first segment line to debug path format
|
||||||
|
const firstSeg = rawText.split('\n').find(l => l.trim() && !l.startsWith('#'));
|
||||||
|
console.log('[CAM] sub-playlist first line:', JSON.stringify(firstSeg));
|
||||||
|
const basePath = slug + '/' + parts.slice(1, -1).join('/') + '/';
|
||||||
|
const rewritten = rewriteM3u8(rawText, basePath);
|
||||||
|
res.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl', 'access-control-allow-origin': '*', 'cache-control': 'no-cache' });
|
||||||
|
res.end(rewritten);
|
||||||
|
} else {
|
||||||
|
res.writeHead(pres.statusCode, { ...pres.headers, 'access-control-allow-origin': '*' });
|
||||||
res.end(body);
|
res.end(body);
|
||||||
}); return;
|
|
||||||
}
|
}
|
||||||
fetchRemote(CAM_TARGET, targetPath, (err, remoteRes, body) => {
|
});
|
||||||
|
});
|
||||||
|
preq.on('error', err => { res.writeHead(502); res.end(err.message); });
|
||||||
|
preq.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master playlist — use fetchCam which handles redirect + cookie storage
|
||||||
|
fetchCam(slug, 'index.m3u8', (err, remoteRes, body) => {
|
||||||
if (err) { res.writeHead(502); res.end(err.message); return; }
|
if (err) { res.writeHead(502); res.end(err.message); return; }
|
||||||
if (remoteRes.statusCode !== 200) { res.writeHead(remoteRes.statusCode); res.end(body); return; }
|
if (remoteRes.statusCode !== 200) { res.writeHead(remoteRes.statusCode); res.end(body); return; }
|
||||||
const basePath = targetPath.substring(0, targetPath.lastIndexOf('/') + 1);
|
const basePath = slug + '/';
|
||||||
const rewritten = rewriteM3u8(body.toString('utf8'), basePath);
|
const rewritten = rewriteM3u8(body.toString('utf8'), basePath);
|
||||||
res.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl', 'access-control-allow-origin': '*', 'cache-control': 'no-cache' });
|
res.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl', 'access-control-allow-origin': '*', 'cache-control': 'no-cache' });
|
||||||
res.end(rewritten);
|
res.end(rewritten);
|
||||||
|
|||||||
Reference in New Issue
Block a user