1
0

Major Log Refactor & Cleanup

This commit is contained in:
2025-11-17 09:09:49 -05:00
parent cb35e05754
commit 96cb36663e
7 changed files with 153 additions and 95 deletions

View File

@@ -67,4 +67,4 @@ COPY src/ ./src/
EXPOSE 3000 EXPOSE 3000
# Start both services using JSON array format # Start both services using JSON array format
CMD ["/bin/sh", "-c", "cd /app && node index.js & cd /streaming-app && yarn start"] CMD ["/bin/sh", "-c", "cd /app && node index.js > /dev/null 2>&1 & cd /streaming-app && yarn start"]

View File

@@ -1,7 +1,7 @@
const express = require('express'); const express = require('express');
const { streamHandler } = require('./src/streamHandler'); const { streamHandler } = require('./src/streamHandler');
const { geocodeCity } = require('./src/geocode'); const { geocodeCity } = require('./src/geocode');
const { getAllMusicFiles } = require('./src/musicPlaylist'); const { getAllMusicFiles, initializeSharedPlaylist } = require('./src/musicPlaylist');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@@ -79,9 +79,11 @@ function buildWeatherUrl(latitude, longitude, settings) {
// Basic stream endpoint (no music) // Basic stream endpoint (no music)
app.get('/stream', (req, res) => { app.get('/stream', (req, res) => {
const startTime = Date.now();
streamHandler(req, res, { streamHandler(req, res, {
useMusic: false, useMusic: false,
musicPath: MUSIC_PATH musicPath: MUSIC_PATH,
startTime
}); });
}); });
@@ -133,38 +135,38 @@ app.get('/weather', async (req, res) => {
let initialUrl = 'data:text/html,<html><body style="margin:0;padding:0;background:#000"></body></html>'; let initialUrl = 'data:text/html,<html><body style="margin:0;padding:0;background:#000"></body></html>';
if (city && city !== 'Toronto, ON, CAN') { if (city && city !== 'Toronto, ON, CAN') {
// Try quick geocode first // Start geocoding (only call once)
const geocodePromise = Promise.race([ const geocodePromise = geocodeCity(city);
geocodeCity(city),
// Try to use quick result if available within 1 second
const quickResult = Promise.race([
geocodePromise,
new Promise((_, reject) => setTimeout(() => reject(new Error('Geocoding timeout')), 1000)) new Promise((_, reject) => setTimeout(() => reject(new Error('Geocoding timeout')), 1000))
]).then(geoResult => { ]).catch(() => null); // Timeout = null, will use late result
console.log(`Geocoded: ${city} -> ${geoResult.displayName}`);
const finalUrl = buildWeatherUrl(geoResult.lat, geoResult.lon, weatherSettings); // Build URL from quick result if available
console.log(`URL: ${finalUrl}`); const urlPromise = quickResult.then(geoResult => {
return { url: finalUrl, lat: geoResult.lat, lon: geoResult.lon }; if (geoResult) {
}).catch(error => { // Got quick result
// Continue geocoding in background return buildWeatherUrl(geoResult.lat, geoResult.lon, weatherSettings);
return geocodeCity(city).then(geoResult => { }
const finalUrl = buildWeatherUrl(geoResult.lat, geoResult.lon, weatherSettings); return null; // Will use initial black screen
console.log(`Geocoding completed: ${geoResult.displayName} (${geoResult.lat}, ${geoResult.lon})`); });
console.log(`Final URL: ${finalUrl}`);
return { url: finalUrl, lat: geoResult.lat, lon: geoResult.lon, isLate: true }; // Late geocode promise (reuses the same geocode call)
}).catch(err => { lateGeocodePromise = geocodePromise.then(geoResult => {
console.warn(`Geocoding failed: ${err.message}`); return buildWeatherUrl(geoResult.lat, geoResult.lon, weatherSettings);
// Fallback to Toronto }).catch(err => {
const fallbackUrl = buildWeatherUrl(43.6532, -79.3832, weatherSettings); console.warn(`Geocoding failed for ${city}, using fallback`);
return { url: fallbackUrl, lat: 43.6532, lon: -79.3832, isLate: true }; // Fallback to Toronto
}); return buildWeatherUrl(43.6532, -79.3832, weatherSettings);
}); });
lateGeocodePromise = geocodePromise.then(result => result.url);
} else { } else {
// Toronto default // Toronto default
initialUrl = buildWeatherUrl(43.6532, -79.3832, weatherSettings); initialUrl = buildWeatherUrl(43.6532, -79.3832, weatherSettings);
console.log(`URL: ${initialUrl}`);
} }
console.log(`Stream starting: ${city}`); const startTime = Date.now();
// Update request query for stream handler // Update request query for stream handler
req.query.url = initialUrl; req.query.url = initialUrl;
@@ -177,7 +179,8 @@ app.get('/weather', async (req, res) => {
return streamHandler(req, res, { return streamHandler(req, res, {
useMusic: true, useMusic: true,
musicPath: MUSIC_PATH, musicPath: MUSIC_PATH,
lateGeocodePromise lateGeocodePromise,
startTime
}); });
}); });
@@ -189,18 +192,23 @@ app.get('/health', (req, res) => {
// Start server // Start server
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Webpage to HLS server running on port ${PORT}`); console.log(`Webpage to HLS server running on port ${PORT}`);
console.log(`WS4KP weather service on port ${WS4KP_PORT}`); if (process.env.WS4KP_EXTERNAL_PORT) {
console.log(`WS4KP weather service on port ${process.env.WS4KP_EXTERNAL_PORT}`);
}
console.log(`Usage: http://localhost:${PORT}/stream?url=http://example.com`); console.log(`Usage: http://localhost:${PORT}/stream?url=http://example.com`);
console.log(`Weather: http://localhost:${PORT}/weather?city=YourCity`); console.log(`Weather: http://localhost:${PORT}/weather?city=YourCity`);
// Pre-validate music files on startup to cache results // Pre-validate music files on startup to cache results
if (MUSIC_PATH) { if (MUSIC_PATH) {
console.log(`\n🎵 Pre-validating music library at ${MUSIC_PATH}...`); console.log(`\nInitializing music library at ${MUSIC_PATH}...`);
const validFiles = getAllMusicFiles(MUSIC_PATH); const validFiles = getAllMusicFiles(MUSIC_PATH);
if (validFiles.length > 0) { if (validFiles.length > 0) {
console.log(`Music library ready: ${validFiles.length} valid tracks cached\n`); console.log(`Music library ready: ${validFiles.length} tracks validated`);
// Initialize shared playlist at startup
initializeSharedPlaylist(MUSIC_PATH);
console.log('');
} else { } else {
console.log(`⚠️ No valid music files found in ${MUSIC_PATH}\n`); console.log(`Warning: No valid music files found in ${MUSIC_PATH}\n`);
} }
} }
}); });

View File

@@ -29,6 +29,7 @@ async function buildFFmpegArgs({ fps, useMusic, musicPath }) {
); );
// Input 1: audio from concat playlist with smoother demuxing // Input 1: audio from concat playlist with smoother demuxing
// Note: playlist is pre-shuffled, so each stream gets unique music
ffmpegArgs.push( ffmpegArgs.push(
'-f', 'concat', '-f', 'concat',
'-safe', '0', '-safe', '0',

View File

@@ -33,12 +33,12 @@ function getCachedGeocode(cityQuery) {
const cached = JSON.parse(data); const cached = JSON.parse(data);
// Verify the query matches // Verify the query matches
if (cached.query && cached.query.toLowerCase().trim() === cityQuery.toLowerCase().trim()) { if (cached.query && cached.query.toLowerCase().trim() === cityQuery.toLowerCase().trim()) {
console.log(`Using cached geocode for: ${cityQuery}`); console.log(`Geocode: ${cityQuery} (cached)`);
return cached; return cached;
} }
} }
} catch (error) { } catch (error) {
console.warn(`Cache read error for ${cityQuery}:`, error.message); // Silent fail
} }
return null; return null;
} }
@@ -52,9 +52,8 @@ function saveCachedGeocode(cityQuery, data) {
try { try {
const cacheFile = path.join(CACHE_DIR, getCacheFileName(cityQuery)); const cacheFile = path.join(CACHE_DIR, getCacheFileName(cityQuery));
fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8'); fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8');
console.log(`Cached geocode for: ${cityQuery}`);
} catch (error) { } catch (error) {
console.warn(`Cache write error for ${cityQuery}:`, error.message); // Silent fail
} }
} }
@@ -96,6 +95,7 @@ async function geocodeCity(cityQuery) {
lon: parseFloat(results[0].lon), lon: parseFloat(results[0].lon),
displayName: results[0].display_name displayName: results[0].display_name
}; };
console.log(`Geocode: ${cityQuery} -> ${geocodeResult.displayName} (API)`);
// Save to cache // Save to cache
saveCachedGeocode(cityQuery, geocodeResult); saveCachedGeocode(cityQuery, geocodeResult);
resolve(geocodeResult); resolve(geocodeResult);

View File

@@ -6,6 +6,11 @@ const { execSync } = require('child_process');
let validationCache = null; let validationCache = null;
let cacheForPath = null; let cacheForPath = null;
// Global shared playlist cache
let sharedPlaylistFile = null;
let sharedPlaylistTrackCount = 0;
let sharedPlaylistPath = null;
/** /**
* Validate an audio file using ffprobe * Validate an audio file using ffprobe
* @param {string} filePath - Path to audio file * @param {string} filePath - Path to audio file
@@ -23,7 +28,6 @@ function validateAudioFile(filePath) {
// Check if audio stream exists and has valid properties // Check if audio stream exists and has valid properties
if (!data.streams || data.streams.length === 0) { if (!data.streams || data.streams.length === 0) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: No audio stream found`);
return false; return false;
} }
@@ -31,27 +35,23 @@ function validateAudioFile(filePath) {
// Validate codec // Validate codec
if (!stream.codec_name) { if (!stream.codec_name) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Unknown codec`);
return false; return false;
} }
// Validate sample rate (should be reasonable) // Validate sample rate (should be reasonable)
const sampleRate = parseInt(stream.sample_rate); const sampleRate = parseInt(stream.sample_rate);
if (!sampleRate || sampleRate < 8000 || sampleRate > 192000) { if (!sampleRate || sampleRate < 8000 || sampleRate > 192000) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Invalid sample rate (${sampleRate})`);
return false; return false;
} }
// Validate channels // Validate channels
const channels = parseInt(stream.channels); const channels = parseInt(stream.channels);
if (!channels || channels < 1 || channels > 8) { if (!channels || channels < 1 || channels > 8) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Invalid channel count (${channels})`);
return false; return false;
} }
return true; return true;
} catch (error) { } catch (error) {
console.warn(`⚠️ Skipping ${path.basename(filePath)}: Validation failed (${error.message})`);
return false; return false;
} }
} }
@@ -91,28 +91,24 @@ function getAllMusicFiles(musicPath) {
// Return cached results if available for this path // Return cached results if available for this path
if (validationCache && cacheForPath === musicPath) { if (validationCache && cacheForPath === musicPath) {
console.log(`✅ Using cached validation results: ${validationCache.length} files`);
return validationCache; return validationCache;
} }
const files = fs.readdirSync(musicPath).filter(f => f.endsWith('.ogg')); const files = fs.readdirSync(musicPath).filter(f => f.endsWith('.ogg'));
const filePaths = files.map(f => path.join(musicPath, f)); const filePaths = files.map(f => path.join(musicPath, f));
console.log(`🎵 Validating ${filePaths.length} audio files (first run, this will be cached)...`); console.log(`Validating ${filePaths.length} audio files...`);
// Validate each file // Validate each file
const validFiles = filePaths.filter(validateAudioFile); const validFiles = filePaths.filter(validateAudioFile);
console.log(`${validFiles.length}/${filePaths.length} audio files passed validation`);
if (validFiles.length < filePaths.length) { if (validFiles.length < filePaths.length) {
console.log(`⚠️ ${filePaths.length - validFiles.length} files were skipped due to issues`); console.log(`Warning: ${filePaths.length - validFiles.length} files skipped due to validation errors`);
} }
// Cache the results // Cache the results
validationCache = validFiles; validationCache = validFiles;
cacheForPath = musicPath; cacheForPath = musicPath;
console.log(`💾 Validation results cached for future requests`);
return validFiles; return validFiles;
} catch (error) { } catch (error) {
@@ -135,43 +131,95 @@ function shuffleArray(array) {
} }
/** /**
* Create a playlist file for FFmpeg concat demuxer with optimized format * Initialize shared playlist at startup
* @param {string} musicPath - Path to music directory * @param {string} musicPath - Path to music directory
* @returns {{playlistFile: string, trackCount: number}|null} Playlist info or null
*/ */
function createPlaylist(musicPath) { function initializeSharedPlaylist(musicPath) {
const allMusicFiles = getAllMusicFiles(musicPath); const allMusicFiles = getAllMusicFiles(musicPath);
console.log(`Found ${allMusicFiles.length} validated music files`);
if (allMusicFiles.length === 0) { if (allMusicFiles.length === 0) {
return null; return;
} }
// Create a temporary concat playlist file // Create a persistent shared playlist file
const playlistFile = path.join('/tmp', `playlist-${Date.now()}.txt`); const playlistFile = path.join('/tmp', `shared-playlist-${Date.now()}.txt`);
// Build playlist content - repeat enough times for ~24 hours of playback // Build playlist content - repeat enough times for ~24 hours of playback
// Assuming avg 3 min per track, repeat enough to cover a full day const repetitions = Math.max(20, Math.ceil(480 / allMusicFiles.length));
const repetitions = Math.max(20, Math.ceil(480 / allMusicFiles.length)); // At least 480 tracks (~24hrs)
const playlistLines = []; const playlistLines = [];
// Use FFmpeg concat demuxer format with improved options for smooth transitions
playlistLines.push('ffconcat version 1.0'); playlistLines.push('ffconcat version 1.0');
for (let i = 0; i < repetitions; i++) { for (let i = 0; i < repetitions; i++) {
// Re-shuffle each repetition for more variety
const shuffled = shuffleArray([...allMusicFiles]); const shuffled = shuffleArray([...allMusicFiles]);
shuffled.forEach(f => { shuffled.forEach(f => {
// Escape single quotes in file paths for concat format
const escapedPath = f.replace(/'/g, "'\\''"); const escapedPath = f.replace(/'/g, "'\\''");
playlistLines.push(`file '${escapedPath}'`); playlistLines.push(`file '${escapedPath}'`);
// Add duration metadata hint for smoother transitions (helps FFmpeg prebuffer)
playlistLines.push('# Auto-generated entry'); playlistLines.push('# Auto-generated entry');
}); });
} }
fs.writeFileSync(playlistFile, playlistLines.join('\n')); fs.writeFileSync(playlistFile, playlistLines.join('\n'));
console.log(`✅ Created seamless playlist: ${allMusicFiles.length} tracks × ${repetitions} repetitions = ${allMusicFiles.length * repetitions} total tracks`); const totalTracks = allMusicFiles.length * repetitions;
// Cache the shared playlist
sharedPlaylistFile = playlistFile;
sharedPlaylistTrackCount = totalTracks;
sharedPlaylistPath = musicPath;
console.log(`Shared playlist created: ${totalTracks} tracks`);
}
/**
* Create a rotated playlist starting at a random position
* @param {string} musicPath - Path to music directory
* @returns {{playlistFile: string, trackCount: number}|null} Playlist info or null
*/
function createPlaylist(musicPath) {
// Check if we have a shared playlist
if (!sharedPlaylistFile || !fs.existsSync(sharedPlaylistFile)) {
console.warn('Warning: Shared playlist not found, initializing...');
initializeSharedPlaylist(musicPath);
if (!sharedPlaylistFile) {
return null;
}
}
const allMusicFiles = getAllMusicFiles(musicPath);
if (allMusicFiles.length === 0) {
return null;
}
// Create a rotated copy of the playlist with random start
const playlistFile = path.join('/tmp', `playlist-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.txt`);
const randomStart = Math.floor(Math.random() * allMusicFiles.length);
const repetitions = Math.max(20, Math.ceil(480 / allMusicFiles.length));
const playlistLines = [];
playlistLines.push('ffconcat version 1.0');
// First repetition with rotation
const firstShuffle = shuffleArray([...allMusicFiles]);
for (let i = 0; i < firstShuffle.length; i++) {
const idx = (randomStart + i) % firstShuffle.length;
const escapedPath = firstShuffle[idx].replace(/'/g, "'\\''");
playlistLines.push(`file '${escapedPath}'`);
playlistLines.push('# Auto-generated entry');
}
// Remaining repetitions
for (let i = 1; i < repetitions; i++) {
const shuffled = shuffleArray([...allMusicFiles]);
shuffled.forEach(f => {
const escapedPath = f.replace(/'/g, "'\\''");
playlistLines.push(`file '${escapedPath}'`);
playlistLines.push('# Auto-generated entry');
});
}
fs.writeFileSync(playlistFile, playlistLines.join('\n'));
console.log(`Stream playlist created, starting at position ${randomStart}/${allMusicFiles.length}`);
return { return {
playlistFile, playlistFile,
@@ -183,5 +231,6 @@ module.exports = {
getRandomMusicFile, getRandomMusicFile,
getAllMusicFiles, getAllMusicFiles,
shuffleArray, shuffleArray,
createPlaylist createPlaylist,
initializeSharedPlaylist
}; };

View File

@@ -36,12 +36,10 @@ async function waitForPageFullyLoaded(page, url) {
try { try {
// Wait for DOM content and stylesheet to load // Wait for DOM content and stylesheet to load
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
console.log('Page DOM loaded, waiting for stylesheet...');
// Wait a brief moment for stylesheet to apply // Wait a brief moment for stylesheet to apply
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
console.log('Page stylesheet loaded, switching to live frames');
return true; return true;
} catch (err) { } catch (err) {
console.error('Page load error:', err.message); console.error('Page load error:', err.message);
@@ -65,7 +63,7 @@ async function hideLogo(page) {
}); });
}); });
} catch (err) { } catch (err) {
console.error('Logo hide error:', err); // Silent
} }
} }

View File

@@ -12,8 +12,9 @@ const { setupPage, waitForPageFullyLoaded, hideLogo } = require('./pageLoader');
* @param {boolean} options.useMusic - Whether to include music * @param {boolean} options.useMusic - Whether to include music
* @param {string} options.musicPath - Path to music directory * @param {string} options.musicPath - Path to music directory
* @param {Promise<string>} options.lateGeocodePromise - Optional promise for late URL update * @param {Promise<string>} options.lateGeocodePromise - Optional promise for late URL update
* @param {number} options.startTime - Request start timestamp
*/ */
async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocodePromise = null }) { async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocodePromise = null, startTime = Date.now() }) {
const { url, width = 1920, height = 1080, fps = 30, hideLogo: hideLogoFlag = 'false', refreshInterval = 90 } = req.query; const { url, width = 1920, height = 1080, fps = 30, hideLogo: hideLogoFlag = 'false', refreshInterval = 90 } = req.query;
if (!url) { if (!url) {
@@ -75,14 +76,13 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
browser = browserInstance; browser = browserInstance;
playlistFile = ffmpegConfig.playlistFile; playlistFile = ffmpegConfig.playlistFile;
console.log('Starting stream with black frames...'); // Create stream identifier from browser process PID
const streamId = `[${browser.process()?.pid || 'STREAM'}]`;
// Start FFmpeg immediately // Start FFmpeg immediately
ffmpegProcess = spawn('ffmpeg', ['-loglevel', 'error', '-hide_banner', ...ffmpegConfig.args], { ffmpegProcess = spawn('ffmpeg', ['-loglevel', 'error', '-hide_banner', ...ffmpegConfig.args], {
stdio: ['pipe', 'pipe', 'pipe'] stdio: ['pipe', 'pipe', 'pipe']
}); });
console.log('FFmpeg started with args:', ffmpegConfig.args.slice(0, 20).join(' ') + '...');
// Pipe FFmpeg output to response // Pipe FFmpeg output to response
ffmpegProcess.stdout.pipe(res); ffmpegProcess.stdout.pipe(res);
@@ -100,8 +100,7 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
message.includes('Invalid data found when processing input') || message.includes('Invalid data found when processing input') ||
message.includes('corrupt decoded frame') || message.includes('corrupt decoded frame') ||
message.includes('decoding for stream')) { message.includes('decoding for stream')) {
// These indicate problematic audio files that were hopefully filtered out // Silent - these indicate problematic audio files
console.warn(`⚠️ FFmpeg audio warning: ${message.trim()}`);
return; return;
} }
@@ -119,20 +118,20 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
}); });
ffmpegProcess.on('error', (error) => { ffmpegProcess.on('error', (error) => {
console.error('FFmpeg error:', error); console.error(`${streamId} FFmpeg error:`, error);
if (typeof cleanup === 'function') cleanup(); if (typeof cleanup === 'function') cleanup();
}); });
ffmpegProcess.on('close', (code) => { ffmpegProcess.on('close', (code) => {
if (code && code !== 0 && !isCleaningUp) { if (code && code !== 0 && !isCleaningUp) {
console.error(`FFmpeg exited with code ${code}`); console.error(`${streamId} FFmpeg exited with code ${code}`);
if (typeof cleanup === 'function') cleanup(); if (typeof cleanup === 'function') cleanup();
} }
}); });
ffmpegProcess.stdin.on('error', (error) => { ffmpegProcess.stdin.on('error', (error) => {
if (error.code !== 'EPIPE') { if (error.code !== 'EPIPE') {
console.error('FFmpeg stdin error:', error); console.error(`${streamId} FFmpeg stdin error:`, error);
} }
}); });
@@ -148,6 +147,8 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
waitForPageFullyLoaded(page, url) waitForPageFullyLoaded(page, url)
.then(async (loaded) => { .then(async (loaded) => {
if (loaded) { if (loaded) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`${streamId} Stream ready (${elapsed}s)`);
sendBlackFrames = false; sendBlackFrames = false;
if (hideLogoFlag === 'true') { if (hideLogoFlag === 'true') {
await hideLogo(page); await hideLogo(page);
@@ -155,7 +156,7 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
} }
}) })
.catch(err => { .catch(err => {
console.error('Page load promise error:', err.message); console.error(`${streamId} Page load error:`, err.message);
sendBlackFrames = false; sendBlackFrames = false;
}); });
} }
@@ -165,12 +166,12 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
lateGeocodePromise.then(async (updatedUrl) => { lateGeocodePromise.then(async (updatedUrl) => {
if (!isCleaningUp && page && !page.isClosed() && updatedUrl && updatedUrl !== url) { if (!isCleaningUp && page && !page.isClosed() && updatedUrl && updatedUrl !== url) {
try { try {
console.log('Updating to correct location...');
sendBlackFrames = true; sendBlackFrames = true;
await waitForPageFullyLoaded(page, updatedUrl); await waitForPageFullyLoaded(page, updatedUrl);
console.log('Correct location fully loaded, switching to live frames'); const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`${streamId} Stream ready (${elapsed}s)`);
waitingForCorrectUrl = false; waitingForCorrectUrl = false;
sendBlackFrames = false; sendBlackFrames = false;
@@ -178,31 +179,33 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
await hideLogo(page); await hideLogo(page);
} }
} catch (err) { } catch (err) {
console.error('Location update error:', err.message); console.error(`${streamId} Location update error:`, err.message);
waitingForCorrectUrl = false; waitingForCorrectUrl = false;
sendBlackFrames = false; sendBlackFrames = false;
} }
} else if (!updatedUrl || updatedUrl === url) { } else if (!updatedUrl || updatedUrl === url) {
console.log('Using initial URL, waiting for page load to complete...');
// URL is the same, so load it now // URL is the same, so load it now
try { try {
await waitForPageFullyLoaded(page, url); await waitForPageFullyLoaded(page, url);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`${streamId} Stream ready (${elapsed}s)`);
waitingForCorrectUrl = false; waitingForCorrectUrl = false;
sendBlackFrames = false; sendBlackFrames = false;
if (hideLogoFlag === 'true') { if (hideLogoFlag === 'true') {
await hideLogo(page); await hideLogo(page);
} }
} catch (err) { } catch (err) {
console.error('Initial page load error:', err.message); console.error(`${streamId} Page load error:`, err.message);
waitingForCorrectUrl = false; waitingForCorrectUrl = false;
sendBlackFrames = false; sendBlackFrames = false;
} }
} }
}).catch(() => { }).catch(() => {
console.warn('Geocoding failed, waiting for fallback location to load');
// Load the fallback URL // Load the fallback URL
waitForPageFullyLoaded(page, url) waitForPageFullyLoaded(page, url)
.then(async () => { .then(async () => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
console.log(`${streamId} Stream ready (${elapsed}s)`);
waitingForCorrectUrl = false; waitingForCorrectUrl = false;
sendBlackFrames = false; sendBlackFrames = false;
if (hideLogoFlag === 'true') { if (hideLogoFlag === 'true') {
@@ -221,13 +224,12 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
const pageRefreshInterval = setInterval(async () => { const pageRefreshInterval = setInterval(async () => {
if (!isCleaningUp && page && !page.isClosed()) { if (!isCleaningUp && page && !page.isClosed()) {
try { try {
console.log('Refreshing page for stability...');
await page.reload({ waitUntil: 'domcontentloaded', timeout: 10000 }); await page.reload({ waitUntil: 'domcontentloaded', timeout: 10000 });
if (hideLogoFlag === 'true') { if (hideLogoFlag === 'true') {
await hideLogo(page); await hideLogo(page);
} }
} catch (err) { } catch (err) {
console.error('Page refresh error:', err.message); // Silent
} }
} }
}, refreshIntervalMs); }, refreshIntervalMs);
@@ -259,7 +261,7 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
const start = Date.now(); const start = Date.now();
try { try {
if (page.isClosed()) { if (page.isClosed()) {
console.error('Page was closed unexpectedly'); console.error(`${streamId} Page closed unexpectedly`);
break; break;
} }
@@ -310,10 +312,10 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
} catch (error) { } catch (error) {
if (!isCleaningUp) { if (!isCleaningUp) {
consecutiveErrors++; consecutiveErrors++;
console.error(`Capture error (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}):`, error.message || error); console.error(`${streamId} Capture error (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}):`, error.message || error);
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
console.error('Too many consecutive errors, stopping stream'); console.error(`${streamId} Too many consecutive errors, stopping stream`);
try { await cleanup(); } catch (e) {} try { await cleanup(); } catch (e) {}
break; break;
} }
@@ -336,7 +338,6 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
cleanup = async () => { cleanup = async () => {
if (isCleaningUp) return; if (isCleaningUp) return;
isCleaningUp = true; isCleaningUp = true;
console.log('Cleaning up stream...');
try { captureLoopActive = false; } catch (e) {} try { captureLoopActive = false; } catch (e) {}
try { clearInterval(pageRefreshInterval); } catch (e) {} try { clearInterval(pageRefreshInterval); } catch (e) {}
@@ -356,6 +357,7 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
ffmpegProcess = null; ffmpegProcess = null;
} }
// Clean up per-stream playlist file (rotated copy, not the shared one)
if (playlistFile) { if (playlistFile) {
try { try {
fs.unlinkSync(playlistFile); fs.unlinkSync(playlistFile);
@@ -379,7 +381,7 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
req.on('close', () => { req.on('close', () => {
if (!disconnectLogged) { if (!disconnectLogged) {
console.log('Client disconnected'); console.log(`${streamId} Client disconnected`);
disconnectLogged = true; disconnectLogged = true;
} }
cleanup(); cleanup();
@@ -388,11 +390,11 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
req.on('error', (error) => { req.on('error', (error) => {
if (error.code === 'ECONNRESET' || error.code === 'EPIPE') { if (error.code === 'ECONNRESET' || error.code === 'EPIPE') {
if (!disconnectLogged) { if (!disconnectLogged) {
console.log('Client disconnected'); console.log(`${streamId} Client disconnected`);
disconnectLogged = true; disconnectLogged = true;
} }
} else { } else {
console.error('Request error:', error); console.error(`${streamId} Request error:`, error);
} }
cleanup(); cleanup();
}); });
@@ -400,11 +402,11 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
res.on('error', (error) => { res.on('error', (error) => {
if (error.code === 'ECONNRESET' || error.code === 'EPIPE') { if (error.code === 'ECONNRESET' || error.code === 'EPIPE') {
if (!disconnectLogged) { if (!disconnectLogged) {
console.log('Client disconnected'); console.log(`${streamId} Client disconnected`);
disconnectLogged = true; disconnectLogged = true;
} }
} else { } else {
console.error('Response error:', error); console.error(`${streamId} Response error:`, error);
} }
cleanup(); cleanup();
}); });
@@ -416,14 +418,14 @@ async function streamHandler(req, res, { useMusic = false, musicPath, lateGeocod
return; return;
} }
if (res.writableEnded || res.socket?.destroyed) { if (res.writableEnded || res.socket?.destroyed) {
console.log('Connection lost, cleaning up'); console.log(`${streamId} Connection lost`);
clearInterval(keepaliveInterval); clearInterval(keepaliveInterval);
cleanup(); cleanup();
} }
}, 10000); }, 10000);
} catch (error) { } catch (error) {
console.error('Error:', error); console.error(`${streamId || '[STREAM]'} Error:`, error);
if (ffmpegProcess && !ffmpegProcess.killed) ffmpegProcess.kill(); if (ffmpegProcess && !ffmpegProcess.killed) ffmpegProcess.kill();
if (browser) await browser.close(); if (browser) await browser.close();
if (!res.headersSent) { if (!res.headersSent) {