Add default icons to repo, wire in changing them when stream is live and album art is found
This commit is contained in:
@@ -170,6 +170,9 @@ PLAYER_STYLE="rich"
|
||||
PLAYER_AUTOPLAY=false
|
||||
# Expose track info to OS / lock-screen media controls (MediaSession API).
|
||||
MEDIASESSION=true
|
||||
# Swap the OpenGraph share image and the browser favicon to the current
|
||||
# album art as tracks change.
|
||||
DYNAMIC_META=true
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -165,9 +165,25 @@ server {
|
||||
index index.php;
|
||||
|
||||
# Proxy the audio stream from Icecast (keeps Icecast off the public net).
|
||||
# Icecast emits connection-specific headers (Upgrade/Connection/Keep-Alive/
|
||||
# Transfer-Encoding) that are ILLEGAL over HTTP/2 (RFC 9113 §8.2.2). iOS
|
||||
# Safari's strict HTTP/2 parser aborts the stream with "the network
|
||||
# connection was lost" when it sees them; desktop browsers ignore them.
|
||||
# Stripping them makes the stream play on iOS while keeping HTTP/2 site-wide.
|
||||
location = /stream {
|
||||
proxy_pass http://127.0.0.1:8000/your_mount;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Connection "";
|
||||
proxy_hide_header Upgrade;
|
||||
proxy_hide_header Connection;
|
||||
proxy_hide_header Keep-Alive;
|
||||
proxy_hide_header Proxy-Authenticate;
|
||||
proxy_hide_header Proxy-Authorization;
|
||||
proxy_hide_header TE;
|
||||
proxy_hide_header Trailers;
|
||||
proxy_hide_header Transfer-Encoding;
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
location / { try_files $uri $uri/ /index.php$is_args$args; }
|
||||
@@ -184,6 +200,63 @@ With the server-side `api/nowplaying.php` proxy you no longer need the old
|
||||
`status-json.xsl` / `peers.xsl` nginx location blocks — PHP fetches the backend
|
||||
directly via `NOWPLAYING_URL`.
|
||||
|
||||
To keep HTTP/2 for the main site but serve the stream over HTTP/1.1 for iOS
|
||||
(see "Playing on iOS" below), give the stream its own vhost:
|
||||
|
||||
```nginx
|
||||
# Dedicated stream host — HTTP/1.1 only (NO http2) so iOS Safari keeps the
|
||||
# connection open. Needs its own TLS cert, e.g. certbot -d stream.example.com.
|
||||
server {
|
||||
listen 443 ssl; # deliberately no "http2"
|
||||
server_name stream.example.com;
|
||||
ssl_certificate /etc/letsencrypt/live/stream.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/stream.example.com/privkey.pem;
|
||||
|
||||
location = /stream {
|
||||
proxy_pass http://127.0.0.1:8000/your_mount;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
chunked_transfer_encoding off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then set `STREAM_URL="https://stream.example.com/stream"` and reload PHP-FPM.
|
||||
Cross-origin audio playback needs no CORS headers, so nothing else changes.
|
||||
|
||||
### Playing on iOS
|
||||
|
||||
iOS Safari (CoreMedia) is far pickier about live audio than desktop browsers,
|
||||
and the same MP3 stream that plays on desktop can be silent on iPhone. To split
|
||||
page problems from stream problems, open the raw stream URL directly in mobile
|
||||
Safari (e.g. `https://radio.example.com/stream`): if Safari's built‑in player
|
||||
won't play it either, the issue is the stream/delivery, not this site.
|
||||
|
||||
Checklist when a plain Icecast MP3 stream is silent on iOS:
|
||||
|
||||
- **Strip Icecast's connection-specific headers when serving over HTTP/2.**
|
||||
This is the big one. Icecast sends `Upgrade`/`Connection`/`Keep-Alive`/
|
||||
`Transfer-Encoding` headers that are illegal in HTTP/2 (RFC 9113 §8.2.2); iOS
|
||||
Safari's strict parser aborts the stream with "the network connection was
|
||||
lost," while desktop browsers just ignore them. A finite file over HTTP/2 is
|
||||
fine (it carries none of those headers) — only the Icecast-proxied stream
|
||||
breaks, which is the classic "the file plays on iPhone but the live stream
|
||||
doesn't" symptom. Fix it with the `proxy_hide_header` lines in the `/stream`
|
||||
location above; this keeps HTTP/2 for the whole site. (Alternative: serve the
|
||||
stream over HTTP/1.1 instead — drop `http2` from the server block, or give the
|
||||
stream a dedicated HTTP/1.1-only hostname and point `STREAM_URL` at it, per
|
||||
the vhost above.)
|
||||
- **Codec** must be MP3 or AAC. iOS cannot decode Ogg Vorbis/Opus at all
|
||||
(desktop can) — check the mount's `server_type` in `status-json.xsl`.
|
||||
- **Don't buffer the stream** — the `/stream` location should keep
|
||||
`proxy_buffering off;` (see the nginx example above) so the first bytes reach
|
||||
iOS promptly; keep Icecast `<burst-on-connect>1</burst-on-connect>` too.
|
||||
|
||||
If a raw MP3 stream still resists iOS after all that, the guaranteed‑on‑iOS
|
||||
option is to also publish an HLS `.m3u8` of the mount via a small `ffmpeg`
|
||||
repackager and point iOS at that.
|
||||
|
||||
## Security notes
|
||||
|
||||
- `.env` is gitignored; keep your Genius token there, never in committed code.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,67 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-labelledby="title desc" width="600" height="600"><title id="title">Streaming radio placeholder artwork</title><desc id="desc">Animated fallback artwork showing a radio tower, pulsing broadcast rings, and moving equalizer bars for tracks without album art.</desc><defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#08131c"></stop>
|
||||
<stop offset="55%" stop-color="#0d2230"></stop>
|
||||
<stop offset="100%" stop-color="#12384d"></stop>
|
||||
<animate attributeName="x2" values="1;0.7;1" dur="10s" repeatCount="indefinite"></animate>
|
||||
<animate attributeName="y2" values="1;0.3;1" dur="10s" repeatCount="indefinite"></animate>
|
||||
</linearGradient>
|
||||
<linearGradient id="glow" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#67f7d4"></stop>
|
||||
<stop offset="100%" stop-color="#49b8ff"></stop>
|
||||
</linearGradient>
|
||||
<filter id="softGlow" x="-40%" y="-40%" width="180%" height="180%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur"></feGaussianBlur>
|
||||
<feMerge><feMergeNode in="blur"></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge>
|
||||
</filter>
|
||||
<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
|
||||
<path d="M 30 0 L 0 0 0 30" fill="none" stroke="#ffffff" stroke-opacity="0.06" stroke-width="1"></path>
|
||||
</pattern>
|
||||
<clipPath id="coverRadius"><rect x="40" y="40" width="520" height="520" rx="54" ry="54"></rect></clipPath>
|
||||
</defs><rect width="600" height="600" rx="56" fill="url(#bg)"></rect><rect width="600" height="600" rx="56" fill="url(#grid)" opacity="0.7"></rect><g clip-path="url(#coverRadius)">
|
||||
<circle cx="155" cy="120" r="120" fill="#67f7d4" opacity="0.08">
|
||||
<animate attributeName="cx" values="155;185;155" dur="13s" repeatCount="indefinite"></animate>
|
||||
<animate attributeName="cy" values="120;100;120" dur="13s" repeatCount="indefinite"></animate>
|
||||
</circle>
|
||||
<circle cx="488" cy="490" r="170" fill="#49b8ff" opacity="0.09">
|
||||
<animate attributeName="cx" values="488;462;488" dur="15s" repeatCount="indefinite"></animate>
|
||||
<animate attributeName="cy" values="490;515;490" dur="15s" repeatCount="indefinite"></animate>
|
||||
</circle>
|
||||
</g><g transform="translate(300 302)">
|
||||
<g fill="none" stroke="url(#glow)" stroke-width="6" stroke-linecap="round" opacity="0.95" filter="url(#softGlow)">
|
||||
<circle cx="0" cy="-182" r="20">
|
||||
<animate attributeName="r" values="16;88" dur="2.6s" repeatCount="indefinite"></animate>
|
||||
<animate attributeName="opacity" values="0.8;0" dur="2.6s" repeatCount="indefinite"></animate>
|
||||
</circle>
|
||||
<circle cx="0" cy="-182" r="20">
|
||||
<animate attributeName="r" values="16;88" dur="2.6s" begin="0.9s" repeatCount="indefinite"></animate>
|
||||
<animate attributeName="opacity" values="0.8;0" dur="2.6s" begin="0.9s" repeatCount="indefinite"></animate>
|
||||
</circle>
|
||||
<circle cx="0" cy="-182" r="20">
|
||||
<animate attributeName="r" values="16;88" dur="2.6s" begin="1.8s" repeatCount="indefinite"></animate>
|
||||
<animate attributeName="opacity" values="0.8;0" dur="2.6s" begin="1.8s" repeatCount="indefinite"></animate>
|
||||
</circle>
|
||||
</g>
|
||||
<circle cx="0" cy="-182" r="12" fill="url(#glow)" filter="url(#softGlow)">
|
||||
<animate attributeName="r" values="10;13;10" dur="1.8s" repeatCount="indefinite"></animate>
|
||||
<animate attributeName="opacity" values="0.85;1;0.85" dur="1.8s" repeatCount="indefinite"></animate>
|
||||
</circle>
|
||||
<g stroke="#d9f7ff" stroke-width="9" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M0 -168 L-54 -8"></path>
|
||||
<path d="M0 -168 L54 -8"></path>
|
||||
<path d="M-34 -74 L34 -74" opacity="0.85"></path>
|
||||
<path d="M-46 -40 L46 -40" opacity="0.7"></path>
|
||||
<path d="M0 -178 L0 -8" opacity="0.9"></path>
|
||||
</g>
|
||||
</g><g transform="translate(201 346)" fill="url(#glow)">
|
||||
<rect x="0" y="60" width="18" height="18" rx="9"><animate attributeName="y" values="60;12;54;30;60" dur="1.2s" repeatCount="indefinite"></animate><animate attributeName="height" values="18;66;24;48;18" dur="1.2s" repeatCount="indefinite"></animate></rect>
|
||||
<rect x="30" y="44" width="18" height="34" rx="9"><animate attributeName="y" values="44;22;50;18;44" dur="1.5s" repeatCount="indefinite"></animate><animate attributeName="height" values="34;56;28;60;34" dur="1.5s" repeatCount="indefinite"></animate></rect>
|
||||
<rect x="60" y="28" width="18" height="50" rx="9"><animate attributeName="y" values="28;52;16;44;28" dur="1.1s" repeatCount="indefinite"></animate><animate attributeName="height" values="50;26;62;34;50" dur="1.1s" repeatCount="indefinite"></animate></rect>
|
||||
<rect x="90" y="50" width="18" height="28" rx="9"><animate attributeName="y" values="50;20;58;36;50" dur="1.35s" repeatCount="indefinite"></animate><animate attributeName="height" values="28;58;20;42;28" dur="1.35s" repeatCount="indefinite"></animate></rect>
|
||||
<rect x="120" y="18" width="18" height="60" rx="9"><animate attributeName="y" values="18;48;8;34;18" dur="1.6s" repeatCount="indefinite"></animate><animate attributeName="height" values="60;30;70;44;60" dur="1.6s" repeatCount="indefinite"></animate></rect>
|
||||
<rect x="150" y="40" width="18" height="38" rx="9"><animate attributeName="y" values="40;14;46;24;40" dur="1.25s" repeatCount="indefinite"></animate><animate attributeName="height" values="38;64;32;54;38" dur="1.25s" repeatCount="indefinite"></animate></rect>
|
||||
<rect x="180" y="56" width="18" height="22" rx="9"><animate attributeName="y" values="56;30;60;42;56" dur="1.45s" repeatCount="indefinite"></animate><animate attributeName="height" values="22;48;18;36;22" dur="1.45s" repeatCount="indefinite"></animate></rect>
|
||||
</g><g font-family="Inter, Arial, sans-serif" text-anchor="middle">
|
||||
<text x="300" y="492" font-size="28" font-weight="700" letter-spacing="0.24em" fill="#eafcff">ON AIR</text>
|
||||
<text x="300" y="526" font-size="18" font-weight="500" letter-spacing="0.12em" fill="#99bbca">STREAMING RADIO</text>
|
||||
</g></svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Dou.Bet Radio",
|
||||
"short_name": "DD Radio",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/icons/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/assets/icons/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
+21
-1
@@ -215,13 +215,33 @@
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- artwork */
|
||||
// Point the OpenGraph image and the favicon at the current album art.
|
||||
function updateMeta(url) {
|
||||
if (!CFG.dynamicMeta || !url) return;
|
||||
if (el.ogImage) el.ogImage.setAttribute('content', url);
|
||||
// Update any existing favicon / touch-icon links...
|
||||
var icons = document.querySelectorAll('link[rel~="icon"], link[rel="apple-touch-icon"], link[rel="shortcut icon"]');
|
||||
for (var i = 0; i < icons.length; i++) {
|
||||
if (icons[i].id !== 'dyn-favicon') icons[i].setAttribute('href', url);
|
||||
}
|
||||
// ...and (re)create a dedicated icon link — a fresh <link> is the most
|
||||
// reliable way to make browsers actually repaint the favicon.
|
||||
var old = document.getElementById('dyn-favicon');
|
||||
if (old && old.parentNode) old.parentNode.removeChild(old);
|
||||
var link = document.createElement('link');
|
||||
link.id = 'dyn-favicon';
|
||||
link.rel = 'icon';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
function setArt(url, album) {
|
||||
var src = url || CFG.fallbackArt;
|
||||
if (el.art) {
|
||||
el.art.setAttribute('src', src);
|
||||
if (album) el.art.title = 'From "' + album + '"';
|
||||
}
|
||||
if (el.ogImage && url) el.ogImage.setAttribute('content', url);
|
||||
if (url) updateMeta(url);
|
||||
if (url) applyTint(url); else resetTint();
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +222,8 @@ if (!defined('RADIO_CONFIG_LOADED')) {
|
||||
'autoplay' => radio_env_bool('PLAYER_AUTOPLAY', false),
|
||||
// Show OS/lock-screen media controls via the MediaSession API.
|
||||
'mediasession' => radio_env_bool('MEDIASESSION', true),
|
||||
// Swap the OpenGraph image + favicon to the current album art.
|
||||
'dynamic_meta' => radio_env_bool('DYNAMIC_META', true),
|
||||
],
|
||||
|
||||
// ---- Listeners / extras ---------------------------------------
|
||||
@@ -291,6 +293,7 @@ function radio_client_config() {
|
||||
'autoplay' => cfg('player.autoplay'),
|
||||
'dynamicBg' => cfg('theme.dynamic_bg'),
|
||||
'mediaSession' => cfg('player.mediasession'),
|
||||
'dynamicMeta' => cfg('player.dynamic_meta'),
|
||||
'siteName' => cfg('site.name'),
|
||||
'tagline' => cfg('site.tagline'),
|
||||
'offlineStreamUrl' => cfg('offline.stream_url'),
|
||||
|
||||
Reference in New Issue
Block a user