Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bbcc7d537 | ||
|
|
2b0b043d3d | ||
|
|
dcae0b604b |
@@ -1,3 +1,48 @@
|
||||
# kryz-go
|
||||
|
||||
KRYZ Go! Mobile radio app
|
||||
KRYZ Go! mobile radio app built with Next.js + React and packaged with Capacitor.
|
||||
|
||||
## Stack
|
||||
|
||||
- Next.js (App Router, static export)
|
||||
- React
|
||||
- Capacitor (Android and iOS)
|
||||
- `@anuradev/capacitor-background-mode` for improved background playback reliability
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Develop Frontend
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Build Web Assets For Capacitor
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This exports static files into `out/`, which is the Capacitor `webDir`.
|
||||
|
||||
## Sync Native Projects
|
||||
|
||||
```bash
|
||||
npm run sync
|
||||
```
|
||||
|
||||
## Run Android
|
||||
|
||||
```bash
|
||||
npm run android
|
||||
```
|
||||
|
||||
## Open Android Studio
|
||||
|
||||
```bash
|
||||
npm run open:android
|
||||
```
|
||||
@@ -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_a';
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
:root {
|
||||
--bg-1: #f8f2e6;
|
||||
--bg-2: #dce9f4;
|
||||
--card: #fff9f0;
|
||||
--ink: #1f2633;
|
||||
--muted: #586173;
|
||||
--primary: #0a5f94;
|
||||
--primary-press: #084d79;
|
||||
--disabled: #9fa9b8;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--ink);
|
||||
font-family: 'Trebuchet MS', 'Avenir Next', 'Segoe UI', sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 16% 18%, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 44%),
|
||||
linear-gradient(140deg, var(--bg-1), var(--bg-2));
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.player-card {
|
||||
width: min(30rem, 100%);
|
||||
border-radius: 1.25rem;
|
||||
background: var(--card);
|
||||
box-shadow:
|
||||
0 1.5rem 3rem rgba(18, 50, 73, 0.17),
|
||||
inset 0 0.1rem 0.25rem rgba(255, 255, 255, 0.6);
|
||||
padding: 1.4rem;
|
||||
text-align: center;
|
||||
animation: card-enter 420ms ease-out;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
font-weight: 700;
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0.4rem 0 0.6rem;
|
||||
font-size: clamp(2rem, 8vw, 2.8rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin: 0 0 1.2rem;
|
||||
color: var(--muted);
|
||||
font-size: 1rem;
|
||||
min-height: 1.3rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: 0.7rem;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
padding: 0.78rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: transform 120ms ease, background 120ms ease;
|
||||
}
|
||||
|
||||
button.playpause {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
font-size: clamp(2rem, 25vw, 6em);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: var(--primary-press);
|
||||
}
|
||||
|
||||
button:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
background: var(--disabled);
|
||||
}
|
||||
|
||||
@keyframes card-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'kryz-go',
|
||||
description: 'KRYZ Go! mobile radio app',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; media-src 'self' https://kryz.out.airtime.pro"
|
||||
/>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AudioPlayer, CapacitorWindow, STREAM_URL } from './AudioPlayer';
|
||||
|
||||
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;
|
||||
}, []);
|
||||
|
||||
const player = useMemo(
|
||||
() =>
|
||||
new AudioPlayer(STREAM_URL, (state, msg) => {
|
||||
setIsPlaying(state === 'playing');
|
||||
setStatus(msg);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
player.setElement(audioRef.current);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!backgroundMode) {
|
||||
console.warn('BackgroundMode plugin is not available in this runtime.');
|
||||
return;
|
||||
}
|
||||
void player.initBackgroundMode(backgroundMode);
|
||||
}, [player, backgroundMode]);
|
||||
|
||||
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" className='playpause' onClick={() => void player.play(backgroundMode)} disabled={isPlaying}>
|
||||
▶️
|
||||
</button>
|
||||
<button type="button" className='playpause' onClick={() => player.stop()} disabled={!isPlaying}>
|
||||
⏹️
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<audio ref={audioRef} preload="none" />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { CapacitorConfig } from '@capacitor/cli';
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.kryzgo.app',
|
||||
appName: 'kryz-go',
|
||||
webDir: 'www',
|
||||
webDir: 'out',
|
||||
android: {
|
||||
adjustMarginsForEdgeToEdge: 'force',
|
||||
buildOptions: {
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'export',
|
||||
trailingSlash: true,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
+11
-4
@@ -3,19 +3,26 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Capacitor mobile app",
|
||||
"scripts": {
|
||||
"build": "echo 'Add your web build command here'",
|
||||
"sync": "cap sync",
|
||||
"android": "cap run android",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"sync": "npm run build && cap sync",
|
||||
"android": "npm run sync && cap run android",
|
||||
"open:android": "cap open android"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anuradev/capacitor-background-mode": "^7.2.1",
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0"
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"next": "^16.2.4",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#stream-status {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: #1456a7;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #9da8b3;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.controls {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; media-src 'self' https://kryz.out.airtime.pro"
|
||||
/>
|
||||
<title>kryz-go</title>
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>kryz-go</h1>
|
||||
<p id="stream-status" aria-live="polite">Stream is stopped.</p>
|
||||
<div class="controls">
|
||||
<button id="start-stream" type="button">Start Stream</button>
|
||||
<button id="stop-stream" type="button" disabled>Stop Stream</button>
|
||||
</div>
|
||||
<audio id="stream-player" preload="none"></audio>
|
||||
</div>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
// kryz-go app entry point
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('kryz-go loaded');
|
||||
initializeStreamPlayer();
|
||||
initializeBackgroundMode();
|
||||
});
|
||||
|
||||
function initializeStreamPlayer() {
|
||||
const streamUrl = 'https://kryz.out.airtime.pro/kryz_b';
|
||||
const audio = document.getElementById('stream-player');
|
||||
const startButton = document.getElementById('start-stream');
|
||||
const stopButton = document.getElementById('stop-stream');
|
||||
const status = document.getElementById('stream-status');
|
||||
|
||||
if (!audio || !startButton || !stopButton || !status) {
|
||||
console.error('Stream player controls are missing from the page.');
|
||||
return;
|
||||
}
|
||||
|
||||
const setStatus = (isPlaying, text) => {
|
||||
startButton.disabled = isPlaying;
|
||||
stopButton.disabled = !isPlaying;
|
||||
status.textContent = text;
|
||||
};
|
||||
|
||||
const ensureBackgroundPlaybackPermissions = async () => {
|
||||
const backgroundMode = window.Capacitor?.Plugins?.BackgroundMode;
|
||||
|
||||
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(false, 'Enable microphone permission in Android settings for screen-off playback.');
|
||||
console.warn('Background playback blocked: microphone permission not granted.');
|
||||
return false;
|
||||
};
|
||||
|
||||
const startStream = async () => {
|
||||
try {
|
||||
const hasBackgroundPermissions = await ensureBackgroundPlaybackPermissions();
|
||||
if (!hasBackgroundPermissions) {
|
||||
return;
|
||||
}
|
||||
|
||||
audio.src = streamUrl;
|
||||
await audio.play();
|
||||
setStatus(true, 'Stream is playing.');
|
||||
} catch (error) {
|
||||
setStatus(false, 'Unable to start stream.');
|
||||
console.error('Failed to start stream:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const stopStream = () => {
|
||||
audio.pause();
|
||||
audio.removeAttribute('src');
|
||||
audio.load();
|
||||
setStatus(false, 'Stream is stopped.');
|
||||
};
|
||||
|
||||
startButton.addEventListener('click', startStream);
|
||||
stopButton.addEventListener('click', stopStream);
|
||||
audio.addEventListener('ended', () => {
|
||||
setStatus(false, 'Stream ended.');
|
||||
});
|
||||
audio.addEventListener('error', () => {
|
||||
setStatus(false, 'Stream error.');
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeBackgroundMode() {
|
||||
const backgroundMode = window.Capacitor?.Plugins?.BackgroundMode;
|
||||
|
||||
if (!backgroundMode) {
|
||||
console.warn('BackgroundMode plugin is not available in this runtime.');
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user