import Hls from 'hls.js'; import { api } from './shared/api.js'; export class Player { constructor({ onState }) { this.audio = new Audio(); this.audio.preload = 'none'; // Note: do NOT set crossOrigin — most Icecast/SHOUTcast servers don't send // CORS headers and the browser will then refuse to play the stream. this.hls = null; this.station = null; this.onState = onState || (() => { }); this.audio.addEventListener('playing', () => this.emit({ playing: true, loading: false, error: null })); this.audio.addEventListener('pause', () => this.emit({ playing: false, loading: false })); this.audio.addEventListener('waiting', () => this.emit({ loading: true })); this.audio.addEventListener('error', () => { const code = this.audio.error?.code; const map = { 1: 'aborted', 2: 'network', 3: 'decode', 4: 'src not supported' }; const reason = map[code] || `code ${code}`; console.warn('[player] audio error', reason, this.audio.currentSrc); this.emit({ playing: false, loading: false, error: `stream error: ${reason}` }); }); } emit(extra) { this.onState({ stationId: this.station?.id ?? null, stationName: this.station?.name ?? null, genres: this.station?.genres || [], volume: this.audio.volume, ...extra }); } setVolume(v) { this.audio.volume = Math.max(0, Math.min(1, v)); this.emit({}); } stop() { this.audio.pause(); this.audio.removeAttribute('src'); this.audio.load(); if (this.hls) { this.hls.destroy(); this.hls = null; } } togglePause() { if (!this.station) return; if (this.audio.paused) this.audio.play().catch(() => { }); else this.audio.pause(); } async play(station) { this.stop(); this.station = station; this.emit({ playing: false, loading: true }); let resolved; try { const r = await api.post(`/api/stations/${station.id}/resolve`); resolved = r.resolved; } catch (err) { this.emit({ playing: false, loading: false, error: err.message }); return; } const url = resolved.url; if (resolved.format === 'hls') { if (Hls.isSupported()) { this.hls = new Hls({ enableWorker: true }); this.hls.loadSource(url); this.hls.attachMedia(this.audio); this.hls.on(Hls.Events.MANIFEST_PARSED, () => this.audio.play().catch(() => { })); } else if (this.audio.canPlayType('application/vnd.apple.mpegurl')) { this.audio.src = url; this.audio.play().catch(() => { }); } else { this.emit({ playing: false, loading: false, error: 'HLS not supported' }); } } else { this.audio.src = url; this.audio.play().catch(() => { }); } } }