Skip to main content

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;
    }
}

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.

Media Session Integration

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

FeatureChromeFirefoxSafariSafari iOSEdge
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.

Performance Considerations

  1. Lazy Loading: Audio source is set to preload="none" to prevent unnecessary bandwidth usage
  2. Debounced Updates: Metadata updates are throttled to every 5 seconds
  3. GPU Acceleration: Volume indicators use CSS transforms for smooth animations
  4. Memory Management: Event listeners are properly cleaned up to prevent leaks

Build docs developers (and LLMs) love