Major Log Refactor & Cleanup
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
72
index.js
72
index.js
@@ -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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user