Build a Minimal FIP Radio Player with Open-Source Tools

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):

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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *