small fixes
This commit is contained in:
15
receiver/assets/logo.svg
Normal file
15
receiver/assets/logo.svg
Normal 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
27
receiver/index.html
Normal 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
73
receiver/receiver.js
Normal 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
58
receiver/styles.css
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user