diff --git a/receiver/assets/logo.svg b/receiver/assets/logo.svg
new file mode 100644
index 0000000..d093b4e
--- /dev/null
+++ b/receiver/assets/logo.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/receiver/index.html b/receiver/index.html
new file mode 100644
index 0000000..a49f4c5
--- /dev/null
+++ b/receiver/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Radio Player
+
+
+
+
+
+
+
+
+
Radio Player
+
Ready
+
+
+

+
+
+
Radio 1 – Live Stream
+
+
+
+
+
diff --git a/receiver/receiver.js b/receiver/receiver.js
new file mode 100644
index 0000000..4e82e93
--- /dev/null
+++ b/receiver/receiver.js
@@ -0,0 +1,73 @@
+/* Receiver for "Radio Player" using CAF Receiver SDK */
+(function () {
+ const STREAM_URL = 'https://live.radio1.si/Radio1MB';
+
+ function $(id) { return document.getElementById(id); }
+
+ document.addEventListener('DOMContentLoaded', () => {
+ const context = cast.framework.CastReceiverContext.getInstance();
+ const playerManager = context.getPlayerManager();
+ const statusEl = $('status');
+ const stationEl = $('station');
+
+ // Intercept LOAD to enforce correct metadata for LIVE audio
+ playerManager.setMessageInterceptor(
+ cast.framework.messages.MessageType.LOAD,
+ (request) => {
+ if (!request || !request.media) return request;
+
+ request.media.contentId = request.media.contentId || STREAM_URL;
+ request.media.contentType = 'audio/mpeg';
+ request.media.streamType = cast.framework.messages.StreamType.LIVE;
+
+ request.media.metadata = request.media.metadata || {};
+ request.media.metadata.title = request.media.metadata.title || 'Radio 1';
+ request.media.metadata.images = request.media.metadata.images || [{ url: 'assets/logo.svg' }];
+
+ return request;
+ }
+ );
+
+ // Update UI on player state changes
+ playerManager.addEventListener(
+ cast.framework.events.EventType.PLAYER_STATE_CHANGED,
+ () => {
+ const state = playerManager.getPlayerState();
+ switch (state) {
+ case cast.framework.messages.PlayerState.PLAYING:
+ statusEl.textContent = 'Playing';
+ break;
+ case cast.framework.messages.PlayerState.PAUSED:
+ statusEl.textContent = 'Paused';
+ break;
+ case cast.framework.messages.PlayerState.IDLE:
+ statusEl.textContent = 'Stopped';
+ break;
+ default:
+ statusEl.textContent = state;
+ }
+ }
+ );
+
+ // When a new media is loaded, reflect metadata (station name, artwork)
+ playerManager.addEventListener(cast.framework.events.EventType.LOAD, (event) => {
+ const media = event && event.data && event.data.media;
+ if (media && media.metadata) {
+ if (media.metadata.title) stationEl.textContent = media.metadata.title;
+ if (media.metadata.images && media.metadata.images[0] && media.metadata.images[0].url) {
+ const img = document.querySelector('#artwork img');
+ img.src = media.metadata.images[0].url;
+ }
+ }
+ });
+
+ // Optional: reflect volume in title attribute
+ playerManager.addEventListener(cast.framework.events.EventType.VOLUME_CHANGED, (evt) => {
+ const level = evt && evt.data && typeof evt.data.level === 'number' ? evt.data.level : null;
+ if (level !== null) statusEl.title = `Volume: ${Math.round(level * 100)}%`;
+ });
+
+ // Start the cast receiver context
+ context.start({ statusText: 'Radio Player Ready' });
+ });
+})();
diff --git a/receiver/styles.css b/receiver/styles.css
new file mode 100644
index 0000000..baefcf4
--- /dev/null
+++ b/receiver/styles.css
@@ -0,0 +1,58 @@
+html, body {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, #7b7fd8, #b57cf2);
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ color: white;
+}
+
+#app {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 24px;
+ box-sizing: border-box;
+}
+
+#artwork {
+ width: 240px;
+ height: 240px;
+ margin: 20px 0;
+ border-radius: 24px;
+ overflow: hidden;
+ background: rgba(0,0,0,0.1);
+ box-shadow: 0 8px 24px rgba(0,0,0,0.2);
+}
+
+#artwork img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+#status {
+ font-size: 18px;
+ opacity: 0.95;
+ margin: 6px 0 0 0;
+}
+
+#station {
+ font-size: 16px;
+ opacity: 0.85;
+ margin: 6px 0 0 0;
+}
+
+h1 {
+ font-size: 20px;
+ margin: 0 0 6px 0;
+}
+
+@media (max-width: 480px) {
+ #artwork { width: 160px; height: 160px; }
+ h1 { font-size: 18px; }
+}
diff --git a/sidecar/index.js b/sidecar/index.js
index ef431d3..e84a5f8 100644
--- a/sidecar/index.js
+++ b/sidecar/index.js
@@ -9,6 +9,34 @@ const rl = readline.createInterface({
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();
+
+ 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();
+ });
+ };
+
+ stopNext();
+}
+
function log(msg) {
console.log(JSON.stringify({ type: 'log', message: msg }));
}
@@ -71,14 +99,21 @@ function play(ip, url) {
if (err) {
log('Join failed, attempting launch...');
log(`Join error: ${err && err.message ? err.message : String(err)}`);
- launchPlayer(url);
+ // Join can fail if the session is stale; stop it and retry launch.
+ stopSessions(activeClient, [session], () => launchPlayer(url, /*didStopFirst*/ true));
} else {
activePlayer = player;
loadMedia(url);
}
});
} else {
- launchPlayer(url);
+ // If another app is running, stop it first to avoid NOT_ALLOWED.
+ if (sessions.length > 0) {
+ log('Non-media session detected, stopping before launch...');
+ stopSessions(activeClient, sessions, () => launchPlayer(url, /*didStopFirst*/ true));
+ } else {
+ launchPlayer(url, /*didStopFirst*/ false);
+ }
}
});
});
@@ -91,13 +126,37 @@ function play(ip, url) {
});
}
-function launchPlayer(url) {
+function launchPlayer(url, didStopFirst) {
if (!activeClient) return;
activeClient.launch(DefaultMediaReceiver, (err, player) => {
if (err) {
- // If launch fails with NOT_ALLOWED, it sometimes means we MUST join or something else is occupying it
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);
+ });
+ });
+ });
+ return;
+ }
+
error(details);
try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ }
return;
@@ -112,8 +171,12 @@ function loadMedia(url) {
const media = {
contentId: url,
- contentType: 'audio/mp3',
- streamType: 'LIVE'
+ contentType: 'audio/mpeg',
+ streamType: 'LIVE',
+ metadata: {
+ metadataType: 0,
+ title: 'Radio 1'
+ }
};
activePlayer.load(media, { autoplay: true }, (err, status) => {