Overview
The La Urban Radio Player features a robust HTML5 audio streaming system optimized for both desktop and mobile devices. It handles live radio streaming with automatic retry logic, fade-in volume control, and platform-specific optimizations.
Architecture
Core Components
The audio player is built on several key components defined in js/index.js:
// Audio element configuration
const audio = document . getElementById ( 'audio' );
audio . src = CONFIG . STREAM_URL ; // 'https://azura.laurban.cl/listen/laurban/media'
audio . preload = 'none' ;
audio . crossorigin = 'anonymous' ;
State Management
The player maintains comprehensive state tracking:
const state = {
userPaused: false , // User intentionally paused
isFirstPlay: true , // First time playing
hasStartedPlaying: false , // User has pressed play
wasInterrupted: false , // System interruption (calls, Siri)
retryCount: 0 , // Failed play attempts
maxRetries: 3 , // Maximum retry attempts
volumeFadeInterval: null // Volume fade control
};
Playback Features
Smart Play with Fade-In
The first playback includes a smooth volume fade-in to prevent audio shock:
async function playAudio () {
if ( ! elements . audio . src ) {
elements . audio . src = CONFIG . STREAM_URL ;
}
// Set Media Session before playing to avoid spinner
if ( 'mediaSession' in navigator ) {
navigator . mediaSession . playbackState = 'playing' ;
}
await elements . audio . play ();
// First play: fade-in from 0 to 1 over 2.5 seconds
if ( state . isFirstPlay ) {
elements . audio . volume = 0 ;
fadeInVolume ( 1.0 , 2500 );
state . isFirstPlay = false ;
}
}
iOS devices require user interaction to play audio. The player handles this with an overlay: // Overlay shown on mobile until user interaction
elements . playButton . addEventListener ( 'click' , async () => {
await playAudio ();
elements . overlay . classList . add ( 'hidden' );
});
Fade-In Volume Control
Smooth volume transitions prevent jarring audio starts:
function fadeInVolume ( targetVolume , duration = 2500 ) {
const startVolume = elements . audio . volume ;
const startTime = Date . now ();
const isMobile = isMobileDevice ();
// Mobile: slower updates for better performance (80ms vs 50ms)
const updateInterval = isMobile ? 80 : 50 ;
state . volumeFadeInterval = setInterval (() => {
const elapsed = Date . now () - startTime ;
const progress = Math . min ( elapsed / duration , 1 );
const currentVolume = startVolume + (( targetVolume - startVolume ) * progress );
elements . audio . volume = currentVolume ;
if ( progress >= 1 ) {
clearInterval ( state . volumeFadeInterval );
}
}, updateInterval );
}
Mobile Optimization : On mobile devices, the fade-in interval is set to 80ms instead of 50ms to reduce CPU usage while maintaining smooth playback.
Mobile Optimization
iOS-Specific Handling
iOS Safari has aggressive buffering that requires special handling:
The player detects iOS devices and adjusts buffering expectations: const isIOS = /iPhone | iPad | iPod/ i . test ( navigator . userAgent );
// Longer initial delay for iOS
const INITIAL_DELAY = isIOS ? 1000 : 500 ;
This prevents premature retry attempts while Safari buffers the stream.
Touch Controls
Mobile devices use optimized touch controls with visual feedback:
// Volume popup for mobile (easier than slider)
elements . customMuteBtn . addEventListener ( 'click' , ( event ) => {
if ( isMobileDevice ()) {
// Show volume popup
elements . volumePopup . classList . toggle ( 'show' );
event . stopPropagation ();
} else {
// Desktop: simple mute toggle
elements . audio . muted = ! elements . audio . muted ;
}
});
Viewport Configuration
The HTML includes mobile-optimized viewport settings:
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" >
< meta name = "theme-color" content = "#000000" >
Error Handling & Retry Logic
Automatic Retry System
The player automatically retries failed playback attempts:
elements . audio . addEventListener ( 'error' , async ( e ) => {
if ( state . userPaused ) return ;
logger . error ( 'Audio error:' , e );
if ( state . retryCount < state . maxRetries ) {
state . retryCount ++ ;
logger . warn ( `Retry attempt ${ state . retryCount } / ${ state . maxRetries } ` );
setTimeout (() => {
playAudio ();
}, CONFIG . RETRY_DELAY ); // 2000ms
} else {
logger . critical ( 'Max retries reached. Playback failed.' );
}
});
Stall Detection
Monitors for stream stalls and automatically recovers:
elements . audio . addEventListener ( 'stalled' , () => {
logger . warn ( 'Stream stalled, attempting to recover...' );
if ( ! state . userPaused ) {
elements . audio . load ();
playAudio ();
}
});
Network Sensitivity : The player is sensitive to network conditions. Poor connectivity may trigger multiple retry attempts before stabilizing.
System Controls
The player integrates with browser and OS media controls:
function updateMediaSession ( data ) {
if ( ! ( 'mediaSession' in navigator )) return ;
navigator . mediaSession . metadata = new MediaMetadata ({
title: `La Urban: ${ data ?. now_playing ?. song ?. title || 'Música' } ` ,
artist: data ?. now_playing ?. song ?. artist || 'Artista desconocido' ,
album: data ?. now_playing ?. song ?. album || '' ,
artwork: [{
src: data ?. now_playing ?. song ?. art || CONFIG . DEFAULT_COVER ,
sizes: '512x512' ,
type: 'image/jpeg'
}]
});
navigator . mediaSession . playbackState = elements . audio . paused ? 'paused' : 'playing' ;
// Action handlers
navigator . mediaSession . setActionHandler ( 'play' , playAudio );
navigator . mediaSession . setActionHandler ( 'pause' , pauseAudio );
navigator . mediaSession . setActionHandler ( 'stop' , pauseAudio );
}
This enables:
Lock screen controls (mobile)
Media keys (desktop)
Notification center controls
Bluetooth device integration
Volume Control
Visual Volume Indicator
A temporary volume percentage overlay appears on the cover when volume changes:
function showVolumePercentage ( volumePercent ) {
const volumeIndicator = document . getElementById ( 'volumeIndicator' );
volumeIndicator . textContent = ` ${ Math . round ( volumePercent ) } %` ;
volumeIndicator . classList . add ( 'show' );
// Hide after 2 seconds
setTimeout (() => {
volumeIndicator . classList . remove ( 'show' );
}, 2000 );
}
Volume Slider
Desktop users get a visual slider with real-time feedback:
elements . volumeSlider . addEventListener ( 'input' , ( e ) => {
const volume = e . target . value / 100 ;
elements . audio . volume = volume ;
elements . audio . muted = false ;
updateMuteButton ();
updateVolumeSlider ();
showVolumePercentage ( e . target . value );
});
Configuration
Stream Configuration
const CONFIG = {
STREAM_URL: 'https://azura.laurban.cl/listen/laurban/media' ,
API_URL: 'https://azura.laurban.cl/api/nowplaying/laurban' ,
UPDATE_INTERVAL: 5000 , // Update metadata every 5 seconds
INITIAL_DELAY: 500 , // Delay before first playback attempt
RETRY_DELAY: 2000 , // Delay between retry attempts
DEFAULT_COVER: 'https://laurban.cl/img/default.jpg'
};
Code Examples
Basic Playback Control
// Play audio
async function playAudio () {
try {
await elements . audio . play ();
state . userPaused = false ;
state . retryCount = 0 ;
updateCustomPlayButton ();
} catch ( error ) {
logger . error ( 'Play failed:' , error );
}
}
// Pause audio
function pauseAudio () {
elements . audio . pause ();
state . userPaused = true ;
updateCustomPlayButton ();
}
// Update UI
function updateCustomPlayButton () {
const icon = elements . customPlayBtn . querySelector ( 'i' );
icon . className = elements . audio . paused ? 'fas fa-play' : 'fas fa-pause' ;
}
Device Detection
function isMobileDevice () {
return /Android | webOS | iPhone | iPad | iPod | BlackBerry | IEMobile | Opera Mini/ i . test ( navigator . userAgent ) ||
( navigator . maxTouchPoints && navigator . maxTouchPoints > 2 );
}
Browser Compatibility
Feature Chrome Firefox Safari Safari iOS Edge Basic Playback ✅ ✅ ✅ ✅ ✅ Media Session ✅ ✅ ✅ ✅ ✅ Volume Fade ✅ ✅ ✅ ⚠️ ✅ Auto-Retry ✅ ✅ ✅ ✅ ✅
Safari iOS Volume : iOS devices maintain system volume control. Volume changes through the web interface may have limited effect.
Lazy Loading : Audio source is set to preload="none" to prevent unnecessary bandwidth usage
Debounced Updates : Metadata updates are throttled to every 5 seconds
GPU Acceleration : Volume indicators use CSS transforms for smooth animations
Memory Management : Event listeners are properly cleaned up to prevent leaks