- Implemented a new Player class in player.js to handle audio playback, including HLS support using hls.js. - Created a shared API module in api.js for making HTTP requests with proper error handling. - Added DOM utility functions in dom.js for creating and clearing elements. - Introduced WebSocket connection handling in ws.js for real-time updates. - Developed a comprehensive CSS stylesheet for styling the application, including a high-contrast theme.
84 lines
3.0 KiB
JavaScript
84 lines
3.0 KiB
JavaScript
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(() => { });
|
|
}
|
|
}
|
|
}
|