Add player functionality with HLS support and API integration
- 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.
This commit is contained in:
83
web/player.js
Normal file
83
web/player.js
Normal file
@@ -0,0 +1,83 @@
|
||||
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(() => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user