small fixes

This commit is contained in:
2025-12-31 08:51:00 +01:00
parent 2d459e46d3
commit bd387cce69
5 changed files with 242 additions and 6 deletions

15
receiver/assets/logo.svg Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#7b7fd8"/>
<stop offset="1" stop-color="#b57cf2"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" rx="24" fill="url(#g)" />
<g fill="white" transform="translate(32,32)">
<circle cx="48" cy="48" r="28" fill="rgba(255,255,255,0.15)" />
<path d="M24 48c6-10 16-16 24-16v8c-6 0-14 4-18 12s-2 12 0 12 6-2 10-6c4-4 10-6 14-6v8c-6 0-14 4-18 12s-2 12 0 12" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity="0.95" />
<text x="96" y="98" font-family="sans-serif" font-size="18" fill="white" opacity="0.95">Radio</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 815 B

27
receiver/index.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Radio Player</title>
<!-- Google Cast Receiver SDK -->
<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app">
<h1>Radio Player</h1>
<p id="status">Ready</p>
<div id="artwork">
<img src="assets/logo.svg" alt="Radio Player" />
</div>
<p id="station">Radio 1 Live Stream</p>
</div>
<script src="receiver.js"></script>
</body>
</html>

73
receiver/receiver.js Normal file
View File

@@ -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' });
});
})();

58
receiver/styles.css Normal file
View File

@@ -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; }
}

View File

@@ -9,6 +9,34 @@ const rl = readline.createInterface({
let activeClient = null; let activeClient = null;
let activePlayer = 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) { function log(msg) {
console.log(JSON.stringify({ type: 'log', message: msg })); console.log(JSON.stringify({ type: 'log', message: msg }));
} }
@@ -71,14 +99,21 @@ function play(ip, url) {
if (err) { if (err) {
log('Join failed, attempting launch...'); log('Join failed, attempting launch...');
log(`Join error: ${err && err.message ? err.message : String(err)}`); 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 { } else {
activePlayer = player; activePlayer = player;
loadMedia(url); loadMedia(url);
} }
}); });
} else { } 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; if (!activeClient) return;
activeClient.launch(DefaultMediaReceiver, (err, player) => { activeClient.launch(DefaultMediaReceiver, (err, player) => {
if (err) { 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})` : ''}`; 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); error(details);
try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ } try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ }
return; return;
@@ -112,8 +171,12 @@ function loadMedia(url) {
const media = { const media = {
contentId: url, contentId: url,
contentType: 'audio/mp3', contentType: 'audio/mpeg',
streamType: 'LIVE' streamType: 'LIVE',
metadata: {
metadataType: 0,
title: 'Radio 1'
}
}; };
activePlayer.load(media, { autoplay: true }, (err, status) => { activePlayer.load(media, { autoplay: true }, (err, status) => {