Compare commits
3 Commits
83c9bcf12e
...
0541b0b776
| Author | SHA1 | Date | |
|---|---|---|---|
| 0541b0b776 | |||
| 6dd2025d3d | |||
| 7176cc8f4b |
@@ -76,10 +76,13 @@ rl.on('line', (line) => {
|
||||
|
||||
function play(ip, url, metadata) {
|
||||
if (activeClient) {
|
||||
try { activeClient.removeAllListeners(); } catch (e) { }
|
||||
try { activeClient.close(); } catch (e) { }
|
||||
}
|
||||
|
||||
activeClient = new Client();
|
||||
// Increase max listeners for this client instance to avoid Node warnings
|
||||
try { if (typeof activeClient.setMaxListeners === 'function') activeClient.setMaxListeners(50); } catch (e) {}
|
||||
activeClient._playMetadata = metadata || {};
|
||||
|
||||
activeClient.connect(ip, () => {
|
||||
@@ -109,7 +112,10 @@ function play(ip, url, metadata) {
|
||||
// Join can fail if the session is stale; stop it and retry launch.
|
||||
stopSessions(activeClient, [session], () => launchPlayer(url, activeClient._playMetadata, /*didStopFirst*/ true));
|
||||
} else {
|
||||
// Clean up previous player listeners before replacing
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
activePlayer = player;
|
||||
try { if (typeof activePlayer.setMaxListeners === 'function') activePlayer.setMaxListeners(50); } catch (e) {}
|
||||
loadMedia(url, activeClient._playMetadata);
|
||||
}
|
||||
});
|
||||
@@ -154,7 +160,9 @@ function launchPlayer(url, metadata, didStopFirst) {
|
||||
try { error(`Launch retry error full: ${JSON.stringify(retryErr)}`); } catch (e) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
activePlayer = retryPlayer;
|
||||
try { if (typeof activePlayer.setMaxListeners === 'function') activePlayer.setMaxListeners(50); } catch (e) {}
|
||||
loadMedia(url, metadata);
|
||||
});
|
||||
});
|
||||
@@ -166,7 +174,9 @@ function launchPlayer(url, metadata, didStopFirst) {
|
||||
try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
activePlayer = player;
|
||||
try { if (typeof activePlayer.setMaxListeners === 'function') activePlayer.setMaxListeners(50); } catch (e) {}
|
||||
loadMedia(url, metadata);
|
||||
});
|
||||
}
|
||||
@@ -175,18 +185,41 @@ function loadMedia(url, metadata) {
|
||||
if (!activePlayer) return;
|
||||
|
||||
const meta = metadata || {};
|
||||
// Build a richer metadata payload. Many receivers only honor specific
|
||||
// fields; we set both Music metadata and generic hints via `customData`.
|
||||
const media = {
|
||||
contentId: url,
|
||||
contentType: 'audio/mpeg',
|
||||
streamType: 'LIVE',
|
||||
metadata: {
|
||||
metadataType: 0,
|
||||
title: meta.title || 'RadioPlayer',
|
||||
subtitle: meta.artist || meta.station || undefined,
|
||||
images: meta.image ? [{ url: meta.image }] : undefined
|
||||
// Use MusicTrack metadata (common on audio receivers) but include
|
||||
// a subtitle field in case receivers surface it.
|
||||
metadataType: 3, // MusicTrackMediaMetadata
|
||||
title: meta.title || 'Radio Station',
|
||||
albumName: 'Radio Player',
|
||||
artist: meta.artist || meta.subtitle || meta.station || '',
|
||||
subtitle: meta.subtitle || '',
|
||||
images: (meta.image ? [
|
||||
{ url: meta.image },
|
||||
// also include a large hint for receivers that prefer big artwork
|
||||
{ url: meta.image, width: 1920, height: 1080 }
|
||||
] : [])
|
||||
},
|
||||
// Many receivers ignore `customData`, but some Styled receivers will
|
||||
// use it. Include background and theming hints here.
|
||||
customData: {
|
||||
appName: meta.appName || 'Radio Player',
|
||||
backgroundImage: meta.backgroundImage || meta.image || undefined,
|
||||
backgroundGradient: meta.bgGradient || '#6a0dad',
|
||||
themeHint: {
|
||||
primary: '#6a0dad',
|
||||
accent: '#b36cf3'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure we don't accumulate 'status' listeners across loads
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners('status'); } catch (e) {}
|
||||
activePlayer.load(media, { autoplay: true }, (err, status) => {
|
||||
if (err) return error(`Load error: ${err.message}`);
|
||||
log('Media loaded, playing...');
|
||||
@@ -200,9 +233,11 @@ function loadMedia(url, metadata) {
|
||||
function stop() {
|
||||
if (activePlayer) {
|
||||
try { activePlayer.stop(); } catch (e) { }
|
||||
try { if (typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
log('Stopped playback');
|
||||
}
|
||||
if (activeClient) {
|
||||
try { if (typeof activeClient.removeAllListeners === 'function') activeClient.removeAllListeners(); } catch (e) {}
|
||||
try { activeClient.close(); } catch (e) { }
|
||||
activeClient = null;
|
||||
activePlayer = null;
|
||||
|
||||
@@ -575,7 +575,6 @@ impl Pipeline {
|
||||
let spawn = |codec: &str| -> Result<std::process::Child, String> {
|
||||
command_hidden(&ffmpeg)
|
||||
.arg("-nostdin")
|
||||
.arg("-re")
|
||||
.arg("-hide_banner")
|
||||
.arg("-loglevel")
|
||||
.arg("warning")
|
||||
@@ -636,7 +635,7 @@ impl Pipeline {
|
||||
});
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(256);
|
||||
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(1024);
|
||||
*self.cast_tx.lock().unwrap() = Some(tx);
|
||||
|
||||
let writer_join = std::thread::spawn(move || {
|
||||
@@ -666,7 +665,7 @@ impl Pipeline {
|
||||
std::thread::spawn(move || {
|
||||
use std::io::Read;
|
||||
let mut ffmpeg_out = stdout;
|
||||
let mut buffer = vec![0u8; 8192];
|
||||
let mut buffer = vec![0u8; 16384];
|
||||
|
||||
loop {
|
||||
if reader_stop.load(Ordering::SeqCst) {
|
||||
@@ -725,7 +724,7 @@ impl Pipeline {
|
||||
|
||||
// Spawn handler for each client
|
||||
let stop_flag = Arc::clone(&server_stop_clone);
|
||||
let (client_tx, client_rx) = mpsc::sync_channel::<Vec<u8>>(256);
|
||||
let (client_tx, client_rx) = mpsc::sync_channel::<Vec<u8>>(1024);
|
||||
|
||||
// Subscribe this client
|
||||
clients_server.lock().unwrap().push(client_tx);
|
||||
@@ -743,11 +742,28 @@ impl Pipeline {
|
||||
|
||||
// Send HTTP response headers
|
||||
let mut writer = stream;
|
||||
let headers = b"HTTP/1.1 200 OK\r\nContent-Type: audio/mpeg\r\nConnection: close\r\nCache-Control: no-cache\r\n\r\n";
|
||||
let headers = b"HTTP/1.1 200 OK\r\nContent-Type: audio/mpeg\r\nConnection: close\r\nCache-Control: no-cache\r\nAccept-Ranges: none\r\nicy-br: 128\r\n\r\n";
|
||||
if writer.write_all(headers).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-buffer before streaming to prevent initial stuttering
|
||||
let mut prebuffer = Vec::with_capacity(65536);
|
||||
let prebuffer_start = std::time::Instant::now();
|
||||
while prebuffer.len() < 32768 && prebuffer_start.elapsed() < Duration::from_millis(500) {
|
||||
match client_rx.recv_timeout(Duration::from_millis(50)) {
|
||||
Ok(chunk) => prebuffer.extend_from_slice(&chunk),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Send prebuffered data
|
||||
if !prebuffer.is_empty() {
|
||||
if writer.write_all(&prebuffer).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Stream chunks to client
|
||||
loop {
|
||||
if stop_flag.load(Ordering::SeqCst) {
|
||||
|
||||
@@ -1268,7 +1268,11 @@ async function play() {
|
||||
url: castUrl,
|
||||
title: station.title || 'Radio',
|
||||
artist: station.slogan || undefined,
|
||||
image: station.logo || undefined
|
||||
image: station.logo || undefined,
|
||||
// Additional metadata hints for receivers
|
||||
subtitle: station.slogan || station.name,
|
||||
backgroundImage: station.background || station.logo || undefined,
|
||||
bgGradient: station.bgGradient || 'linear-gradient(135deg,#5b2d91,#b36cf3)'
|
||||
});
|
||||
isPlaying = true;
|
||||
// Sync volume
|
||||
|
||||
Reference in New Issue
Block a user