Transitions UI to react.js #1
@@ -0,0 +1,172 @@
|
||||
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<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>;
|
||||
};
|
||||
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
+19
-171
@@ -1,40 +1,7 @@
|
||||
'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';
|
||||
import { AudioPlayer, CapacitorWindow, STREAM_URL } from './AudioPlayer';
|
||||
|
||||
export default function HomePage() {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
@@ -48,147 +15,28 @@ export default function HomePage() {
|
||||
return (window as CapacitorWindow).Capacitor?.Plugins?.BackgroundMode;
|
||||
}, []);
|
||||
|
||||
const player = useMemo(
|
||||
() =>
|
||||
new AudioPlayer(STREAM_URL, (state, msg) => {
|
||||
setIsPlaying(state === 'playing');
|
||||
setStatus(msg);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) {
|
||||
return;
|
||||
if (audioRef.current) {
|
||||
player.setElement(audioRef.current);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
}, [player]);
|
||||
|
||||
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.');
|
||||
};
|
||||
void player.initBackgroundMode(backgroundMode);
|
||||
}, [player, backgroundMode]);
|
||||
|
||||
return (
|
||||
<main className="page-shell">
|
||||
@@ -200,11 +48,11 @@ export default function HomePage() {
|
||||
</p>
|
||||
|
||||
<div className="controls">
|
||||
<button type="button" onClick={startStream} disabled={isPlaying}>
|
||||
Start Stream
|
||||
<button type="button" onClick={() => void player.play(backgroundMode)} disabled={isPlaying}>
|
||||
Play
|
||||
</button>
|
||||
<button type="button" onClick={stopStream} disabled={!isPlaying}>
|
||||
Stop Stream
|
||||
<button type="button" onClick={() => player.stop()} disabled={!isPlaying}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user