new media player functionality
This commit is contained in:
1 parent
69f848c260
commit
d9b0964f07
20 files changed
+404
-5
No files matched your search
@@ -0,0 +1,131 @@
|
||||
import { Component, inject, effect, OnDestroy, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { StationConfigService } from '../services/station-config.service';
|
||||
|
||||
/** Shape of the Airtime live-info JSON response (subset of fields we use). */
|
||||
interface LiveInfoResponse {
|
||||
current: {
|
||||
name: string;
|
||||
metadata?: { track_title?: string; artist_name?: string };
|
||||
} | null;
|
||||
currentShow: Array<{ name: string }>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-media-player',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './media-player.component.html',
|
||||
styleUrl: './media-player.component.scss',
|
||||
})
|
||||
export class MediaPlayerComponent implements OnDestroy {
|
||||
private configService = inject(StationConfigService);
|
||||
|
||||
// Playback
|
||||
private audio: HTMLAudioElement | null = null;
|
||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
readonly isPlaying = signal(false);
|
||||
readonly showName = signal('');
|
||||
readonly trackInfo = signal('');
|
||||
readonly metadataError = signal(false);
|
||||
|
||||
readonly displayText = computed(() => {
|
||||
const track = this.trackInfo();
|
||||
if (track) return track;
|
||||
return this.metadataError() ? 'Stream Unavailable' : 'Live Stream';
|
||||
});
|
||||
|
||||
readonly hasStream = computed(() => {
|
||||
const cfg = this.configService.config();
|
||||
return !!cfg.stream_url && cfg.stream_url.trim().length > 0;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Start/stop metadata polling when playing state changes.
|
||||
effect(() => {
|
||||
if (this.isPlaying()) {
|
||||
this.startPolling();
|
||||
} else {
|
||||
this.stopPolling();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
play(): void {
|
||||
const cfg = this.configService.config();
|
||||
if (!cfg.stream_url) return;
|
||||
|
||||
if (!this.audio) {
|
||||
this.audio = new Audio();
|
||||
this.audio.preload = 'none';
|
||||
}
|
||||
this.audio.src = cfg.stream_url;
|
||||
this.audio.load();
|
||||
this.audio.play().catch(() => {
|
||||
this.isPlaying.set(false);
|
||||
});
|
||||
this.isPlaying.set(true);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.audio) {
|
||||
this.audio.pause();
|
||||
this.audio.src = '';
|
||||
this.audio.load();
|
||||
}
|
||||
this.isPlaying.set(false);
|
||||
}
|
||||
|
||||
private startPolling(): void {
|
||||
this.fetchMetadata();
|
||||
this.pollTimer = setInterval(() => this.fetchMetadata(), 15000);
|
||||
}
|
||||
|
||||
private stopPolling(): void {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchMetadata(): Promise<void> {
|
||||
const cfg = this.configService.config();
|
||||
if (!cfg.stream_metadata_url) {
|
||||
this.showName.set('');
|
||||
this.trackInfo.set('');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(cfg.stream_metadata_url);
|
||||
if (!resp.ok) throw new Error('Metadata fetch failed');
|
||||
const data: LiveInfoResponse = await resp.json();
|
||||
|
||||
const showName = data.currentShow?.[0]?.name ?? '';
|
||||
const current = data.current;
|
||||
const trackTitle = current?.metadata?.track_title ?? '';
|
||||
const artistName = current?.metadata?.artist_name ?? '';
|
||||
|
||||
let track = '';
|
||||
if (artistName && trackTitle) track = `${artistName} - ${trackTitle}`;
|
||||
else if (current?.name) track = current.name;
|
||||
else if (trackTitle) track = trackTitle;
|
||||
|
||||
this.showName.set(showName);
|
||||
this.trackInfo.set(track);
|
||||
this.metadataError.set(false);
|
||||
} catch {
|
||||
this.metadataError.set(true);
|
||||
this.showName.set('');
|
||||
this.trackInfo.set('');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stop();
|
||||
this.stopPolling();
|
||||
this.audio = null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user