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 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) => {
|
||||
|
||||
Reference in New Issue
Block a user