216 lines
6.3 KiB
TypeScript
216 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
type PermissionState = 'granted' | 'denied' | 'prompt';
|
|
|
|
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<void>;
|
|
disableWebViewOptimizations: () => Promise<void>;
|
|
enable: (options: {
|
|
title: string;
|
|
text: string;
|
|
channelName: string;
|
|
channelDescription: string;
|
|
resume: boolean;
|
|
silent: boolean;
|
|
disableWebViewOptimization: boolean;
|
|
}) => Promise<void>;
|
|
addListener: (eventName: string, listener: () => void) => Promise<void>;
|
|
};
|
|
|
|
type CapacitorRuntime = {
|
|
Plugins?: {
|
|
BackgroundMode?: BackgroundModePlugin;
|
|
};
|
|
};
|
|
|
|
type CapacitorWindow = Window & {
|
|
Capacitor?: CapacitorRuntime;
|
|
};
|
|
|
|
const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_b';
|
|
|
|
export default function HomePage() {
|
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
const [status, setStatus] = useState('Stream is stopped.');
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
const backgroundMode = useMemo(() => {
|
|
if (typeof window === 'undefined') {
|
|
return undefined;
|
|
}
|
|
return (window as CapacitorWindow).Capacitor?.Plugins?.BackgroundMode;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const audio = audioRef.current;
|
|
if (!audio) {
|
|
return;
|
|
}
|
|
|
|
const handleEnded = () => {
|
|
setIsPlaying(false);
|
|
setStatus('Stream ended.');
|
|
};
|
|
|
|
const handleError = () => {
|
|
setIsPlaying(false);
|
|
setStatus('Stream error.');
|
|
};
|
|
|
|
audio.addEventListener('ended', handleEnded);
|
|
audio.addEventListener('error', handleError);
|
|
|
|
return () => {
|
|
audio.removeEventListener('ended', handleEnded);
|
|
audio.removeEventListener('error', handleError);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!backgroundMode) {
|
|
console.warn('BackgroundMode plugin is not available in this runtime.');
|
|
return;
|
|
}
|
|
|
|
const initializeBackgroundMode = async () => {
|
|
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);
|
|
}
|
|
};
|
|
|
|
void initializeBackgroundMode();
|
|
}, [backgroundMode]);
|
|
|
|
const ensureBackgroundPlaybackPermissions = async () => {
|
|
if (!backgroundMode) {
|
|
return true;
|
|
}
|
|
|
|
const microphonePermission = await backgroundMode.checkMicrophonePermission();
|
|
if (microphonePermission.microphone === 'granted') {
|
|
return true;
|
|
}
|
|
|
|
const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
|
|
if (requestedMicrophonePermission.microphone === 'granted') {
|
|
return true;
|
|
}
|
|
|
|
setStatus('Enable microphone permission in Android settings for screen-off playback.');
|
|
console.warn('Background playback blocked: microphone permission not granted.');
|
|
return false;
|
|
};
|
|
|
|
const startStream = async () => {
|
|
const audio = audioRef.current;
|
|
if (!audio) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const hasBackgroundPermissions = await ensureBackgroundPlaybackPermissions();
|
|
if (!hasBackgroundPermissions) {
|
|
return;
|
|
}
|
|
|
|
audio.src = STREAM_URL;
|
|
await audio.play();
|
|
setIsPlaying(true);
|
|
setStatus('Stream is playing.');
|
|
} catch (error) {
|
|
setIsPlaying(false);
|
|
setStatus('Unable to start stream.');
|
|
console.error('Failed to start stream:', error);
|
|
}
|
|
};
|
|
|
|
const stopStream = () => {
|
|
const audio = audioRef.current;
|
|
if (!audio) {
|
|
return;
|
|
}
|
|
|
|
audio.pause();
|
|
audio.removeAttribute('src');
|
|
audio.load();
|
|
|
|
setIsPlaying(false);
|
|
setStatus('Stream is stopped.');
|
|
};
|
|
|
|
return (
|
|
<main className="page-shell">
|
|
<section className="player-card" aria-label="KRYZ live stream player">
|
|
<p className="eyebrow">KRYZ Go!</p>
|
|
<h1>Live Radio</h1>
|
|
<p className="status" aria-live="polite">
|
|
{status}
|
|
</p>
|
|
|
|
<div className="controls">
|
|
<button type="button" onClick={startStream} disabled={isPlaying}>
|
|
Start Stream
|
|
</button>
|
|
<button type="button" onClick={stopStream} disabled={!isPlaying}>
|
|
Stop Stream
|
|
</button>
|
|
</div>
|
|
|
|
<audio ref={audioRef} preload="none" />
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|