Skip to main content

Overview

La Urban Radio Player is a single-page web application built with vanilla JavaScript, featuring real-time audio streaming, synchronized lyrics, and advanced audio visualization. The application follows a modular architecture with clear separation of concerns.

Technology Stack

Frontend

  • Vanilla JavaScript (ES6+)
  • HTML5 Audio API
  • Web Audio API (for visualization)
  • CSS3 with animations

External Services

  • Azura Cast (API & Stream)
  • Kick.com (Live streaming)
  • CBox (Chat integration)

File Structure

source/
├── index.html                 # Main HTML structure
├── css/
│   └── index.css             # Styles and responsive design (~2000 lines)
├── js/
│   ├── logger.js             # Unified logging system (90 lines)
│   ├── cache-manager.js      # API response caching (73 lines)
│   ├── lyrics.js             # Synchronized lyrics manager (323 lines)
│   └── index.js              # Main application logic (~2500 lines)
├── img/
│   ├── logo.png              # La Urban logo
│   ├── default.jpg           # Default album cover
│   └── background.png        # Tiled background pattern
└── docs/
    ├── DESARROLLO-LOCAL.md   # Local development guide
    └── MOBILE-BUFFERING-ANALYSIS.md  # Mobile optimization analysis

Module Loading Order

The modules must load in the following order to ensure proper dependency resolution:
1

Logger System

File: js/logger.jsLoaded first to provide logging capabilities to all other modules.
<script src="js/logger.js"></script>
Exports: window.logger (global instance)
2

Cache Manager

File: js/cache-manager.jsDepends on: logger
<script src="js/cache-manager.js"></script>
Exports: window.CacheManager (class)
3

Lyrics Manager

File: js/lyrics.jsDepends on: logger
<script src="js/lyrics.js"></script>
Exports: window.LyricsManager (class)
4

Main Application

File: js/index.jsDepends on: logger, CacheManager, LyricsManager
<script src="js/index.js"></script>
Exports: None (self-executing IIFE)

Core Modules

1. Logger System (logger.js)

A unified logging system that automatically detects the environment and adjusts verbosity. Key Features:
  • Environment detection (development vs production)
  • Multiple log levels: dev, info, success, warn, error, critical
  • Production mode silences debug logs
  • Development mode shows all logs
API:
logger.dev('Debug message');      // Only in development
logger.info('Info message');      // Only in development
logger.success('Success message'); // Only in development
logger.warn('Warning message');    // Always shown
logger.error('Error message');     // Always shown
logger.critical('Critical error'); // Always shown with styling
Example:
class Logger {
    constructor() {
        this.isDev = this._isDevelopmentEnvironment();
        this._initializeLogger();
    }

    _isDevelopmentEnvironment() {
        const hostname = globalThis?.location?.hostname ?? '';
        return hostname === 'localhost' || 
               hostname.startsWith('192.168.') || 
               hostname.includes('.local') ||
               hostname.endsWith('.test');
    }

    dev(...args) {
        if (this.isDev) {
            this._originalConsole.debug('[DEV]', ...args);
        }
    }
}

globalThis.logger = new Logger();

2. Cache Manager (cache-manager.js)

Manages API response caching to reduce server load and improve performance. Key Features:
  • Caches API responses for 15 seconds
  • Detects significant changes (song changes, live state changes)
  • Prevents unnecessary API calls
  • Smart update detection
API:
const cache = new CacheManager();

// Check if update is needed
if (cache.needsUpdate()) {
    // Fetch new data
}

// Check if data changed
if (cache.hasChanged(newData)) {
    // Update UI
}

// Update cache
cache.update(newData);

// Get cached data
const data = cache.get();
Data Structure:
class CacheManager {
    laurbanData = null;           // Cached API response
    lastCheck = 0;                // Last check timestamp
    currentSongId = null;         // Current song ID
    currentLiveState = false;     // Live streaming state
    updateInterval = 15000;       // 15 seconds
}

3. Lyrics Manager (lyrics.js)

Manages synchronized lyrics display with adaptive delay for different devices. Key Features:
  • Adaptive stream delay (4.5s for iOS, 1.5s for desktop)
  • Virtual time tracking for live streams
  • LRC format support
  • Smooth fade animations
  • Automatic device detection
API:
const lyricsManager = new LyricsManager();

// Initialize with audio element
lyricsManager.init(audioElement);

// Load lyrics with offset and custom delay
lyricsManager.loadLyrics(lyricsArray, startOffset, customDelay);

// Load from LRC format
lyricsManager.loadFromLRC(lrcText);

// Control display
lyricsManager.show();
lyricsManager.hide();
lyricsManager.clear();
Lyrics Format:
[
    { time: 0.5, text: "First line of lyrics" },
    { time: 3.5, text: "Second line of lyrics" },
    { time: 6.5, text: "Third line of lyrics" }
]
Adaptive Delay Logic:
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const LYRICS_CONFIG = {
    // iPhone/iPad need more delay due to aggressive buffering
    STREAM_DELAY: isIOS ? 4.5 : 1.5,
    UPDATE_INTERVAL: 100
};

4. Main Application (index.js)

The core application logic wrapped in an IIFE (Immediately Invoked Function Expression). Key Components:

Configuration

const CONFIG = {
    STREAM_URL: 'https://azura.laurban.cl/listen/laurban/media',
    API_URL: 'https://azura.laurban.cl/api/nowplaying/laurban',
    KICK_API_URL: 'https://kick.com/api/v2/channels/laurban/livestream',
    KICK_EMBED_URL: 'https://player.kick.com/laurban?muted=true&autoplay=true',
    REQUEST_IFRAME_URL: 'https://azura.laurban.cl/public/laurban/embed-requests?theme=dark',
    CHAT_IFRAME_URL: 'https://www3.cbox.ws/box/?boxid=3539409&boxtag=Q2vpWH',
    WHATSAPP_URL: 'whatsapp://wa.me/+56949242000?text=...',
    DEFAULT_TITLE: 'La Urban · Emisora Online',
    DEFAULT_COVER: 'https://laurban.cl/img/default.jpg',
    UPDATE_INTERVAL: 5000,
    INITIAL_DELAY: 500,
    RETRY_DELAY: 2000,
    THEME_LIGHT_START: 6,
    THEME_LIGHT_END: 18
};

Application State

const state = {
    userPaused: false,
    isKickLive: false,
    showingKickVideo: false,
    iframeLoaded: false,
    iframe2Loaded: false,
    audioContext: null,
    analyser: null,
    audioSource: null,
    isVisualizerActive: false,
    retryCount: 0,
    maxRetries: 3,
    currentCoverUrl: '',
    lastSongId: null,
    isFirstPlay: true,
    volumeFadeInterval: null,
    volumeDisplayTimeout: null,
    currentSloganIndex: 0,
    sloganInterval: null,
    currentPlayMessageIndex: 0,
    playMessageInterval: null,
    lyricsManager: null,
    songStartTime: null,
    songElapsed: 0,
    songDuration: 0,
    hasStartedPlaying: false,
    wasInterrupted: false
};

Data Flow

Application Initialization

Audio Playback Flow

API Update Cycle

Audio Visualization

Web Audio API Integration

The application uses the Web Audio API for real-time frequency analysis:
function initializeAudioVisualizer() {
    // Create audio context
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    state.audioContext = new AudioContext();
    
    // Create analyzer
    state.analyser = state.audioContext.createAnalyser();
    state.analyser.fftSize = 2048;
    state.analyser.smoothingTimeConstant = 0.6;
    
    // Connect audio source
    state.audioSource = state.audioContext.createMediaElementSource(elements.audio);
    state.audioSource.connect(state.analyser);
    state.analyser.connect(state.audioContext.destination);
    
    // Start visualization
    startLogoVisualization();
    startBackgroundVisualization();
}

Frequency Analysis

The visualizer analyzes different frequency ranges:

Sub-Bass

20-60 HzKick drum detection Indices 0-5

Bass

60-250 Hz808s and bass lines Indices 6-20

Mids

250-2000 HzVocals and melodies Indices 21-160

High-Mids

2000-4000 HzVocal presence Indices 161-320

Highs

4000+ HzHi-hats and cymbals Indices 321+

Kick Detection

AdaptiveThreshold: 0.7 Min time: 150ms

Mobile Optimization

On mobile devices, the visualizer is automatically disabled to avoid CORS issues and improve performance:
if (isMobileDevice()) {
    logger.warn('Mobile device - Visualizer disabled');
    state.isVisualizerActive = true;
    
    // Use CSS animation instead
    if (elements.logo) {
        elements.logo.classList.add('active');
        elements.logo.style.animation = 'pulse 0.8s ease infinite';
    }
    return;
}

Event Handling

DOM Events

// Play/Pause
elements.customPlayBtn.addEventListener('click', togglePlayPause);

// Volume Control
elements.volumeSlider.addEventListener('input', handleVolumeChange);
elements.customMuteBtn.addEventListener('click', toggleMute);

// Mobile Volume Popup
elements.customMuteBtn.addEventListener('click', toggleVolumePopup);

// Visibility Change (tab switching)
document.addEventListener('visibilitychange', handleVisibilityChange);

Audio Events

// Audio ready
elements.audio.addEventListener('canplay', handleCanPlay);

// Playback started
elements.audio.addEventListener('playing', handlePlaying);

// Playback paused
elements.audio.addEventListener('pause', handlePause);

// Error handling
elements.audio.addEventListener('error', handleAudioError);

// Time updates (for lyrics sync)
elements.audio.addEventListener('timeupdate', updateLyrics);

Performance Optimizations

GPU Acceleration

All animations use translate3d for GPU acceleration:
elements.logo.style.transform = 
    `translate3d(0, ${-verticalMove}px, 0) scale(${scale})`;

Throttling and Debouncing

  • API Updates: 5-second intervals with caching
  • Lyrics Updates: 100ms intervals
  • Visualizer: 60 FPS via requestAnimationFrame

Lazy Loading

  • iframes loaded on-demand
  • Covers preloaded before transition
  • Audio source lazy-initialized

Error Handling

Browser Extension Errors

The application suppresses common browser extension errors:
console.error = function(...args) {
    const message = args.join(' ');
    if (message.includes('Extension context invalidated') ||
        message.includes('message port closed') ||
        message.includes('chrome.runtime')) {
        return; // Silence extension errors
    }
    originalError.apply(console, args);
};

Audio Playback Errors

elements.audio.addEventListener('error', (e) => {
    logger.error('Audio error:', e);
    if (state.retryCount < state.maxRetries) {
        state.retryCount++;
        setTimeout(retryPlayback, CONFIG.RETRY_DELAY);
    } else {
        showErrorMessage('Unable to load audio stream');
    }
});

State Management

State is managed through a central state object with reactive updates:
// Update state
state.userPaused = true;

// Trigger side effects
if (state.userPaused) {
    elements.audio.pause();
    updateCustomPlayButton();
}

Next Steps

Build docs developers (and LLMs) love