new media player functionality

This commit is contained in:
kfj001 committed 2026-06-25 06:09:28 +00:00
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;
}
}