Files
RadioPlayer/sidecar/index.js
2026-01-14 18:42:16 +01:00

224 lines
7.8 KiB
JavaScript

const { Client, DefaultMediaReceiver } = require('castv2-client');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
terminal: false
});
let activeClient = null;
let activePlayer = null;
function isNotAllowedError(err) {
if (!err) return false;
const msg = (err.message || String(err)).toUpperCase();
return msg.includes('NOT_ALLOWED') || msg.includes('NOT ALLOWED');
}
function stopSessions(client, sessions, cb) {
if (!client || !sessions || sessions.length === 0) return cb();
const remaining = sessions.slice();
const stopNext = () => {
const session = remaining.shift();
if (!session) return cb();
try {
client.stop(session, (err) => {
if (err) {
log(`Stop session failed (${session.appId || 'unknown app'}): ${err.message || String(err)}`);
} else {
log(`Stopped session (${session.appId || 'unknown app'})`);
}
// Continue regardless; best-effort.
stopNext();
});
} catch (err) {
// Some devices/library versions may throw synchronously; just log and continue.
log(`Stop session threw (${session.appId || 'unknown app'}): ${err.message || String(err)}`);
stopNext();
}
};
stopNext();
}
function log(msg) {
console.log(JSON.stringify({ type: 'log', message: msg }));
}
function error(msg) {
console.error(JSON.stringify({ type: 'error', message: msg }));
}
rl.on('line', (line) => {
try {
const data = JSON.parse(line);
const { command, args } = data;
switch (command) {
case 'play':
play(args.ip, args.url, args.metadata);
break;
case 'stop':
stop();
break;
case 'volume':
setVolume(args.level);
break;
default:
error(`Unknown command: ${command}`);
}
} catch (e) {
error(`Failed to parse line: ${e.message}`);
}
});
function play(ip, url, metadata) {
if (activeClient) {
try { activeClient.close(); } catch (e) { }
}
activeClient = new Client();
activeClient._playMetadata = metadata || {};
activeClient.connect(ip, () => {
log(`Connected to ${ip}`);
// First, check if DefaultMediaReceiver is already running
activeClient.getSessions((err, sessions) => {
if (err) return error(`GetSessions error: ${err.message}`);
// Log sessions for debugging (appId/sessionId if available)
try {
const sessInfo = sessions.map(s => ({ appId: s.appId, sessionId: s.sessionId, displayName: s.displayName }));
log(`Sessions: ${JSON.stringify(sessInfo)}`);
} catch (e) {
log('Sessions: (unable to stringify)');
}
// DefaultMediaReceiver App ID is CC1AD845
const session = sessions.find(s => s.appId === 'CC1AD845');
if (session) {
log('Session already running, joining...');
activeClient.join(session, DefaultMediaReceiver, (err, player) => {
if (err) {
log('Join failed, attempting launch...');
log(`Join error: ${err && err.message ? err.message : String(err)}`);
// Join can fail if the session is stale; stop it and retry launch.
stopSessions(activeClient, [session], () => launchPlayer(url, activeClient._playMetadata, /*didStopFirst*/ true));
} else {
activePlayer = player;
loadMedia(url, activeClient._playMetadata);
}
});
} else {
// Backdrop or other non-media session present: skip stopping to avoid platform sender crash, just launch.
if (sessions.length > 0) {
log('Non-media session detected; skipping stop and launching DefaultMediaReceiver...');
}
launchPlayer(url, activeClient._playMetadata, /*didStopFirst*/ false);
}
});
});
activeClient.on('error', (err) => {
error(`Client error: ${err.message}`);
try { activeClient.close(); } catch (e) { }
activeClient = null;
activePlayer = null;
});
}
function launchPlayer(url, metadata, didStopFirst) {
if (!activeClient) return;
activeClient.launch(DefaultMediaReceiver, (err, player) => {
if (err) {
const details = `Launch error: ${err && err.message ? err.message : String(err)}${err && err.code ? ` (code: ${err.code})` : ''}`;
// If launch fails with NOT_ALLOWED, the device may be busy with another app/session.
// Best-effort: stop existing sessions once, then retry launch.
if (!didStopFirst && isNotAllowedError(err)) {
log('Launch NOT_ALLOWED; attempting to stop existing sessions and retry...');
activeClient.getSessions((sessErr, sessions) => {
if (sessErr) {
error(`${details} | GetSessions error: ${sessErr.message || String(sessErr)}`);
return;
}
stopSessions(activeClient, sessions, () => {
activeClient.launch(DefaultMediaReceiver, (retryErr, retryPlayer) => {
if (retryErr) {
const retryDetails = `Launch retry error: ${retryErr && retryErr.message ? retryErr.message : String(retryErr)}${retryErr && retryErr.code ? ` (code: ${retryErr.code})` : ''}`;
error(retryDetails);
try { error(`Launch retry error full: ${JSON.stringify(retryErr)}`); } catch (e) { /* ignore */ }
return;
}
activePlayer = retryPlayer;
loadMedia(url, metadata);
});
});
});
return;
}
error(details);
try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ }
return;
}
activePlayer = player;
loadMedia(url, metadata);
});
}
function loadMedia(url, metadata) {
if (!activePlayer) return;
const meta = metadata || {};
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
}
};
activePlayer.load(media, { autoplay: true }, (err, status) => {
if (err) return error(`Load error: ${err.message}`);
log('Media loaded, playing...');
});
activePlayer.on('status', (status) => {
// Optional: track status
});
}
function stop() {
if (activePlayer) {
try { activePlayer.stop(); } catch (e) { }
log('Stopped playback');
}
if (activeClient) {
try { activeClient.close(); } catch (e) { }
activeClient = null;
activePlayer = null;
}
}
function setVolume(level) {
if (activeClient && activePlayer) {
activeClient.setVolume({ level }, (err, status) => {
if (err) return error(`Volume error: ${err.message}`);
log(`Volume set to ${level}`);
});
} else {
log('Volume command ignored: Player not initialized');
}
}
log('Sidecar initialized and waiting for commands');