Build a Minimal FIP Radio Player with Open-Source ToolsFIP is a beloved French public radio station known for its eclectic, carefully curated mixes spanning jazz, rock, electronic, world music and more. If you want a lightweight, privacy-friendly way to stream FIP (or any internet radio station) — and you enjoy learning by building — this guide walks you through creating a minimal FIP radio player using open-source tools and standard web technologies. You’ll get a functional web player, simple controls, metadata display (track title/artist), and options to run it on a local machine, Raspberry Pi, or small VPS.
What you’ll build
- A single-page web application (HTML/CSS/JavaScript) that plays FIP streams
- Basic playback controls: play/pause, volume, and station selection
- Now-playing metadata fetched from the stream or station API where available
- Optional: a systemd service or Raspberry Pi kiosk mode setup to auto-start the player
Why this approach
- Uses widely supported web audio APIs — no native desktop app required
- Fully open-source stack: static files, no backend required unless you want metadata proxies
- Easy to adapt for other stations or features (recording, playlists, equalizer)
Prerequisites
- Basic familiarity with HTML, CSS, and JavaScript
- Node.js/npm installed (optional — only needed for local dev server or build tooling)
- A modern browser (Chrome, Firefox, Edge) or a minimal Linux device (Raspberry Pi OS) for deployment
FIP stream URLs and metadata
FIP provides multiple streams (bitrate/language variants). Stream URLs can change; use the official site or station directory to confirm. Example stream (may change):
- FIP main stream (example): https://stream.radiofrance.fr/fip/fip-midfi.mp3
Many radio stations embed metadata in the stream (ICY/SHOUTcast tags) or provide a now-playing API endpoint. For robust metadata you may need a small proxy to parse ICY headers, because browsers’ audio element does not expose ICY metadata directly.
Project structure
Use a simple structure:
fip-player/ ├─ index.html ├─ styles.css ├─ player.js ├─ icons/ └─ README.md
index.html (core UI)
Create a minimal, accessible UI:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Minimal FIP Radio Player</title> <link rel="stylesheet" href="styles.css" /> </head> <body> <main> <header> <h1>FIP Radio Player</h1> <p class="subtitle">Eclectic music from France</p> </header> <section id="player"> <div class="now-playing" aria-live="polite"> <div id="cover" class="cover"></div> <div class="meta"> <div id="title" class="title">—</div> <div id="artist" class="artist">—</div> </div> </div> <audio id="audio" preload="none" crossorigin="anonymous"></audio> <div class="controls"> <button id="playBtn" aria-label="Play">Play</button> <button id="stopBtn" aria-label="Stop">Stop</button> <label> Volume <input id="volume" type="range" min="0" max="1" step="0.01" value="1" /> </label> </div> <div class="stations"> <label for="stationSelect">Station:</label> <select id="stationSelect"> <option value="https://stream.radiofrance.fr/fip/fip-midfi.mp3">FIP (mid)</option> </select> </div> </section> <footer> <small>Built with open-source tools • For personal use</small> </footer> </main> <script src="player.js"></script> </body> </html>
styles.css (simple, responsive)
Keep styling minimal and mobile-friendly:
:root{ --bg:#0f1720; --card:#111827; --text:#e6eef6; --muted:#9aa6b2; --accent:#1fb6ff; font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif; } html,body{height:100%;margin:0;background:linear-gradient(180deg,var(--bg),#07101a);color:var(--text);} main{max-width:720px;margin:2rem auto;padding:1.5rem;background:rgba(255,255,255,0.02);border-radius:12px} h1{margin:0;font-size:1.4rem} .subtitle{color:var(--muted);margin-top:0.25rem} #player{margin-top:1rem} .now-playing{display:flex;gap:12px;align-items:center} .cover{width:84px;height:84px;background:#223; border-radius:6px} .meta{min-width:0} .title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .artist{color:var(--muted);font-size:0.9rem;margin-top:0.25rem} .controls{display:flex;gap:8px;align-items:center;margin-top:1rem} button{background:var(--accent);border:0;padding:8px 12px;border-radius:8px;color:#022;cursor:pointer} button[aria-pressed="true"]{opacity:0.85} input[type="range"]{width:160px} .stations{margin-top:1rem;color:var(--muted)} footer{margin-top:1.25rem;color:var(--muted);font-size:0.85rem}
player.js (playback and metadata)
This script handles UI interaction, audio playback, and optional metadata fetching. Browsers cannot read ICY metadata directly from
Client-only version (uses station-supplied metadata endpoint if available):
const audio = document.getElementById('audio'); const playBtn = document.getElementById('playBtn'); const stopBtn = document.getElementById('stopBtn'); const volume = document.getElementById('volume'); const stationSelect = document.getElementById('stationSelect'); const titleEl = document.getElementById('title'); const artistEl = document.getElementById('artist'); let currentUrl = stationSelect.value; audio.src = currentUrl; audio.crossOrigin = 'anonymous'; audio.preload = 'none'; playBtn.addEventListener('click', async () => { try { await audio.play(); playBtn.textContent = 'Pause'; playBtn.setAttribute('aria-pressed','true'); } catch (err) { console.error('Play failed', err); alert('Playback failed — check CORS or stream URL.'); } }); playBtn.addEventListener('click', () => { if (audio.paused) audio.play(); else audio.pause(); }); audio.addEventListener('pause', () => { playBtn.textContent = 'Play'; playBtn.setAttribute('aria-pressed','false'); }); audio.addEventListener('play', () => { playBtn.textContent = 'Pause'; playBtn.setAttribute('aria-pressed','true'); }); stopBtn.addEventListener('click', () => { audio.pause(); audio.currentTime = 0; }); volume.addEventListener('input', () => { audio.volume = parseFloat(volume.value); }); stationSelect.addEventListener('change', () => { currentUrl = stationSelect.value; audio.src = currentUrl; audio.play().catch(()=>{}); }); // Example metadata fetching (if station provides JSON endpoint) async function fetchMetadata(){ // Replace with a valid metadata URL for FIP if available const metaUrl = 'https://some.metadata.endpoint/fip/now_playing.json'; try{ const res = await fetch(metaUrl, {cache: 'no-store'}); if(!res.ok) throw new Error('No metadata'); const data = await res.json(); titleEl.textContent = data.title || '—'; artistEl.textContent = data.artist || '—'; }catch(e){ // fallback: clear or keep last known // console.debug('Metadata fetch failed', e); } } setInterval(fetchMetadata, 15000); fetchMetadata();
Note: The example metadata endpoint is a placeholder. If you want exact FIP now-playing metadata and it’s not publicly available via CORS-friendly JSON, see the server-side proxy option below.
Handling ICY metadata (server-side proxy)
Problem: Browsers’ audio element does not expose ICY metadata. Solution: a tiny proxy that requests the stream with ICY support, reads metadata intervals, and serves JSON to the client.
Example Node.js proxy using icecast-metadata (conceptual):
// server.js (conceptual) const http = require('http'); const fetch = require('node-fetch'); // or native fetch in Node 18+ const ICY = require('icy'); http.createServer((req,res)=>{ if(req.url.startsWith('/meta')){ // connect to stream and parse metadata once, then respond ICY.get('https://stream.radiofrance.fr/fip/fip-midfi.mp3', (icyRes) => { icyRes.on('metadata', (meta) => { const parsed = ICY.parse(meta); // parsed.StreamTitle etc res.setHeader('Content-Type','application/json'); res.end(JSON.stringify({title: parsed.StreamTitle})); icyRes.destroy(); }); }).on('error',(err)=>{ res.statusCode=502; res.end('error'); }); } }).listen(3000);
Run this on a small VPS or Raspberry Pi. Client JS fetches /meta to get current track.
CORS: Add appropriate Access-Control-Allow-Origin headers if serving to browsers.
Deployment suggestions
- Local testing: open index.html in browser or use a tiny static server (http-server, serve).
- Raspberry Pi kiosk: set Chromium to open the page in kiosk mode on boot (systemd service or autostart).
- VPS: host static files on Netlify, GitHub Pages, or any static host; run metadata proxy separately (small Node service behind CORS headers).
- Docker: package the proxy and static files into a small image for portability.
Optional improvements (small checklist)
- Add station presets, icons, and a favorites list stored in localStorage
- Implement reconnect/backoff logic for unstable streams
- Add basic equalizer using Web Audio API (BiquadFilter nodes)
- Save volume and last station in localStorage
- Add keyboard shortcuts and media session API for lock screen / hardware controls
Privacy and licensing notes
- Respect station terms of service for streaming and embedding.
- This player only pulls public streams; redistributing streams may have restrictions.
- Use open-source libraries with compatible licenses; attribute as required.
This guide gives a compact, practical path to a minimal, extensible FIP radio player built from open tools. If you want, I can: provide a ready-to-run GitHub repo, write the Node proxy with full error handling, or show a Raspberry Pi systemd unit for kiosk mode.
Leave a Reply