type PermissionState = 'granted' | 'denied' | 'prompt'; export type BackgroundModePlugin = { checkMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>; requestMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>; checkNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>; requestNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>; checkBatteryOptimizations: () => Promise<{ enabled: boolean }>; requestDisableBatteryOptimizations: () => Promise; disableWebViewOptimizations: () => Promise; enable: (options: { title: string; text: string; channelName: string; channelDescription: string; resume: boolean; silent: boolean; disableWebViewOptimization: boolean; }) => Promise; addListener: (eventName: string, listener: () => void) => Promise; }; export type CapacitorRuntime = { Plugins?: { BackgroundMode?: BackgroundModePlugin; }; }; export type CapacitorWindow = Window & { Capacitor?: CapacitorRuntime; }; export type PlaybackState = 'stopped' | 'playing' | 'error'; export const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_b'; export class AudioPlayer { private url: string; private onStateChange: (state: PlaybackState, status: string) => void; private element: HTMLAudioElement | null = null; private playbackState: PlaybackState = 'stopped'; constructor(url: string, onStateChange: (state: PlaybackState, status: string) => void) { this.url = url; this.onStateChange = onStateChange; this.handleEnded = this.handleEnded.bind(this); this.handleError = this.handleError.bind(this); } setElement(el: HTMLAudioElement): void { this.element = el; el.addEventListener('ended', this.handleEnded); el.addEventListener('error', this.handleError); } private handleEnded(): void { this.playbackState = 'stopped'; this.onStateChange('stopped', 'Stream ended.'); } private handleError(): void { this.playbackState = 'error'; this.onStateChange('error', 'Stream error.'); } async initBackgroundMode(backgroundMode: BackgroundModePlugin): Promise { try { const notificationPermission = await backgroundMode.checkNotificationsPermission(); if (notificationPermission.notifications !== 'granted') { await backgroundMode.requestNotificationsPermission(); } const microphonePermission = await backgroundMode.checkMicrophonePermission(); if (microphonePermission.microphone !== 'granted') { const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission(); if (requestedMicrophonePermission.microphone !== 'granted') { console.warn( 'BackgroundMode microphone permission denied; background playback may stop when screen is off.' ); } } const batteryOptimizations = await backgroundMode.checkBatteryOptimizations(); if (batteryOptimizations.enabled) { console.warn( 'Battery optimizations are enabled; requesting exemption for reliable background playback.' ); await backgroundMode.requestDisableBatteryOptimizations(); } await backgroundMode.disableWebViewOptimizations(); await backgroundMode.enable({ title: 'KRYZ-Go!', text: 'Background mode is active', channelName: 'kryz-go background', channelDescription: 'Keeps kryz-go running while the app is in background', resume: true, silent: false, disableWebViewOptimization: true, }); await backgroundMode.addListener('appInBackground', () => { console.log('kryz-go moved to background'); }); await backgroundMode.addListener('appInForeground', () => { console.log('kryz-go returned to foreground'); }); console.log('BackgroundMode plugin enabled.'); } catch (error) { console.error('Failed to initialize BackgroundMode plugin:', error); } } private async ensureBackgroundPermissions(backgroundMode: BackgroundModePlugin): Promise { const microphonePermission = await backgroundMode.checkMicrophonePermission(); if (microphonePermission.microphone === 'granted') { return true; } const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission(); if (requestedMicrophonePermission.microphone === 'granted') { return true; } this.onStateChange( this.playbackState, 'Enable microphone permission in Android settings for screen-off playback.' ); console.warn('Background playback blocked: microphone permission not granted.'); return false; } async play(backgroundMode?: BackgroundModePlugin): Promise { if (!this.element) { return; } try { if (backgroundMode) { const hasPermissions = await this.ensureBackgroundPermissions(backgroundMode); if (!hasPermissions) { return; } } this.element.src = this.url; await this.element.play(); this.playbackState = 'playing'; this.onStateChange('playing', 'Stream is playing.'); } catch (error) { this.playbackState = 'error'; this.onStateChange('error', 'Unable to start stream.'); console.error('Failed to start stream:', error); } } stop(): void { if (!this.element) { return; } this.element.pause(); this.element.removeAttribute('src'); this.element.load(); this.playbackState = 'stopped'; this.onStateChange('stopped', 'Stream is stopped.'); } }