diff --git a/cast-receiver/README.md b/cast-receiver/README.md new file mode 100644 index 0000000..07d3ea0 --- /dev/null +++ b/cast-receiver/README.md @@ -0,0 +1,18 @@ +# Radio Player - Custom Cast Receiver + +This folder contains a minimal Google Cast Web Receiver that displays a purple gradient background, station artwork, title and subtitle. It accepts `customData` hints sent from the sender (your app) for `backgroundImage`, `backgroundGradient` and `appName`. + +Hosting requirements +- The receiver must be served over HTTPS and be publicly accessible. +- Recommended: host under GitHub Pages (`gh-pages` branch or `/docs` folder) or any static host (Netlify, Vercel, S3 + CloudFront). + +Registering with Google Cast Console +1. Go to the Cast SDK Developer Console and create a new Application. +2. Choose "Custom Receiver" and provide the public HTTPS URL to `index.html` (e.g. `https://example.com/cast-receiver/index.html`). +3. Note the generated Application ID. + +Sender changes +- After obtaining the Application ID, update your sender (sidecar) to launch that app ID instead of the DefaultMediaReceiver. The sidecar already supports passing `metadata.appId` when launching. + +Testing locally +- You can serve this folder locally during development, but Chromecast devices require public HTTPS endpoints to use a registered app. diff --git a/cast-receiver/index.html b/cast-receiver/index.html new file mode 100644 index 0000000..8960ca5 --- /dev/null +++ b/cast-receiver/index.html @@ -0,0 +1,23 @@ + + + + + + Radio Player Receiver + + + +
+
+
Artwork
+
+
Radio Player
+

Radio Player

+

+
+
+ + + + + diff --git a/cast-receiver/receiver.js b/cast-receiver/receiver.js new file mode 100644 index 0000000..e7c7a14 --- /dev/null +++ b/cast-receiver/receiver.js @@ -0,0 +1,50 @@ +// Minimal CAF receiver that applies customData theming and shows media metadata. +const context = cast.framework.CastReceiverContext.getInstance(); +const playerManager = context.getPlayerManager(); + +function applyBranding(customData, metadata) { + try { + const bgEl = document.getElementById('bg'); + const art = document.getElementById('art'); + const title = document.getElementById('title'); + const subtitle = document.getElementById('subtitle'); + const appName = document.getElementById('appName'); + + if (customData) { + if (customData.backgroundImage) { + bgEl.style.backgroundImage = `url(${customData.backgroundImage})`; + bgEl.style.backgroundSize = 'cover'; + bgEl.style.backgroundPosition = 'center'; + } else if (customData.backgroundGradient) { + bgEl.style.background = customData.backgroundGradient; + } + if (customData.appName) appName.textContent = customData.appName; + } + + if (metadata) { + if (metadata.title) title.textContent = metadata.title; + const sub = metadata.subtitle || metadata.artist || ''; + subtitle.textContent = sub; + if (metadata.images && metadata.images.length) { + art.src = metadata.images[0].url || ''; + } + } + } catch (e) { + // swallow UI errors + console.warn('Branding apply failed', e); + } +} + +playerManager.setMessageInterceptor(cast.framework.messages.MessageType.LOAD, (request) => { + const media = request.media || {}; + const customData = media.customData || {}; + applyBranding(customData, media.metadata || {}); + return request; +}); + +playerManager.addEventListener(cast.framework.events.EventType.MEDIA_STATUS, () => { + const media = playerManager.getMediaInformation(); + if (media) applyBranding(media.customData || {}, media.metadata || {}); +}); + +context.start(); diff --git a/cast-receiver/styles.css b/cast-receiver/styles.css new file mode 100644 index 0000000..e9a2351 --- /dev/null +++ b/cast-receiver/styles.css @@ -0,0 +1,11 @@ +:root{--primary:#6a0dad;--accent:#b36cf3} +html,body{height:100%;margin:0;font-family:Inter,system-ui,Arial,Helvetica,sans-serif} +body{background:linear-gradient(135deg,var(--primary),var(--accent));color:#fff} +.bg{position:fixed;inset:0;background-size:cover;background-position:center;filter:blur(10px) saturate(120%);opacity:0.9} +.app{position:relative;z-index:2;display:flex;align-items:center;gap:24px;padding:48px} +.artwork{width:320px;height:320px;flex:0 0 320px;background:rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:center;border-radius:8px;overflow:hidden} +.artwork img{width:100%;height:100%;object-fit:cover} +.meta{display:flex;flex-direction:column} +.app-name{font-weight:600;opacity:0.9} +h1{margin:6px 0 0 0;font-size:28px} +h2{margin:6px 0 0 0;font-size:18px;opacity:0.9} diff --git a/sidecar/index.js b/sidecar/index.js index 003418f..56aefc8 100644 --- a/sidecar/index.js +++ b/sidecar/index.js @@ -140,7 +140,8 @@ function play(ip, url, metadata) { function launchPlayer(url, metadata, didStopFirst) { if (!activeClient) return; - activeClient.launch(DefaultMediaReceiver, (err, player) => { + const launchApp = (metadata && metadata.appId) ? metadata.appId : DefaultMediaReceiver; + activeClient.launch(launchApp, (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. diff --git a/src/build-info.json b/src/build-info.json deleted file mode 100644 index ed88ed9..0000000 --- a/src/build-info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": "0.1.1", - "debug": false, - "builtAt": "2026-01-13T12:34:31.351Z" -}