diff --git a/CHANGELOG.md b/CHANGELOG.md index e86b9c2..c141be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [Unreleased] + +### Added +- Add stream album art to toolbar +- Add rebooting and restarting functions +- Add loading spinners and wifi check in setup +- Add formatting for system info + +### Changed +- Replace zeroconf plugin with mdns plugin + +### Fixed +- Fix zeroconf issues on Android +- Fix issues during setup + +## [0.5.8] - 2026-07-20 + +### Added +- Add stream pic fallback + +### Changed +- Format telemetry info +- Update README + ## [0.5.7] - 2026-07-08 ### Added diff --git a/package-lock.json b/package-lock.json index 84eebeb..e709179 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "beatnik", - "version": "0.6.0", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "beatnik", - "version": "0.6.0", + "version": "0.6.1", "dependencies": { "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", @@ -16,7 +16,7 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", - "@byrds/capacitor-mdns": "^0.2.1", + "@byrds/capacitor-mdns": "^7.0.0", "@capacitor-community/bluetooth-le": "^7.2.0", "@capacitor/android": "^7.2.0", "@capacitor/app": "7.0.2", @@ -2435,9 +2435,9 @@ } }, "node_modules/@byrds/capacitor-mdns": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@byrds/capacitor-mdns/-/capacitor-mdns-0.2.1.tgz", - "integrity": "sha512-OsRzzhE3/1HupN+X1YgBsVwAym/0U3xQWQ9q7zEv+A3UMllhUDrJf8t0QVKrkMm9DMg0TFRRWAVzOmGG4MTAPA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@byrds/capacitor-mdns/-/capacitor-mdns-7.0.0.tgz", + "integrity": "sha512-KeFIumA7zFz1Mw+QqF27Ip77XsgrnLTgkvmn26XT9mZ/Y3RAvZr4SUoc1YY7Dc5kFOOUEnhEn4amvdFVhJR8MQ==", "license": "MIT", "peerDependencies": { "@capacitor/core": ">=7.0.0" diff --git a/package.json b/package.json index e290770..4ede731 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "beatnik", - "version": "0.6.0", + "version": "0.6.1", "author": "byrds & bytes gmbh", "homepage": "https://beatnik.audio", "scripts": { @@ -27,7 +27,7 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", - "@byrds/capacitor-mdns": "^0.2.1", + "@byrds/capacitor-mdns": "^7.0.0", "@capacitor-community/bluetooth-le": "^7.2.0", "@capacitor/android": "^7.2.0", "@capacitor/app": "7.0.2", diff --git a/src/app/components/camilla-dsp/camilla-dsp.component.ts b/src/app/components/camilla-dsp/camilla-dsp.component.ts index da463b7..7adf60b 100644 --- a/src/app/components/camilla-dsp/camilla-dsp.component.ts +++ b/src/app/components/camilla-dsp/camilla-dsp.component.ts @@ -30,8 +30,6 @@ export class CamillaDspComponent implements OnInit, OnDestroy { camillaConfigMessage = ''; camillaConfigError = ''; - private levelSubscription: Subscription | undefined; - constructor( private camillaService: CamillaDspService, private beatnikHardwareService: BeatnikHardwareService @@ -73,20 +71,18 @@ export class CamillaDspComponent implements OnInit, OnDestroy { } else if (message.GetSignalLevels) { // this.levels = message.GetSignalLevels.value; // console.log('Signal Levels received:', this.levels); + } else if (message.GetVolume) { + this.currentVolume = message.GetVolume.value; } }) ); - this.levelSubscription = this.camillaService.signalLevels$.subscribe(levels => { - // Service handles normalization, so we get the raw levels object directly - this.levels = levels; - }); - - // timeout to allow UI to update - // setTimeout(() => { - // this.getCaptureSignalLevels(); - - // }, 800); + this.subscriptions.add( + this.camillaService.signalLevels$.subscribe(levels => { + // Service handles normalization, so we get the raw levels object directly + this.levels = levels; + }) + ); } private getHardwareHost(): string | null { @@ -253,9 +249,8 @@ export class CamillaDspComponent implements OnInit, OnDestroy { } getVolume() { + // Response is applied reactively via the messages$ subscription in ngOnInit this.camillaService.sendCommand('GetVolume'); - this.currentVolume = this.lastMessage.GetVolume?.value ?? this.currentVolume; - console.log('Current volume:', this.currentVolume); } setVolume(volume: number) { @@ -263,16 +258,9 @@ export class CamillaDspComponent implements OnInit, OnDestroy { } ngOnDestroy() { - // Clean up subscriptions to prevent memory leaks - this.subscriptions.unsubscribe(); - // Tell server to stop sending updates - this.camillaService.stopLevelUpdates(); + // Disconnect before unsubscribing so no late message/status update fires after teardown this.camillaService.disconnect(); - } - - ionViewWillLeave() { - console.log('CamillaDspComponent: Leaving page, cleaning up resources if needed'); - this.ngOnDestroy(); + this.subscriptions.unsubscribe(); } updateProcessorParameter(processorKey: string, paramKey: string, newValue: any) { diff --git a/src/app/services/camilla-dsp.service.ts b/src/app/services/camilla-dsp.service.ts index 715dcb2..983d770 100644 --- a/src/app/services/camilla-dsp.service.ts +++ b/src/app/services/camilla-dsp.service.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; -import { Subject, Observable, BehaviorSubject, timer, of } from 'rxjs'; -import { retryWhen, switchMap, tap, delayWhen, filter, map } from 'rxjs/operators'; +import { Subject, Observable, BehaviorSubject, timer, Subscription } from 'rxjs'; +import { retry, filter, map, takeUntil } from 'rxjs/operators'; // Defines the possible connection states export type ConnectionStatus = 'Connected' | 'Connecting' | 'Disconnected' | 'Error'; @@ -15,8 +15,11 @@ interface CamillaDspCommand { @Injectable({ providedIn: 'root', }) -export class CamillaDspService { - private socket$!: WebSocketSubject; +export class CamillaDspService implements OnDestroy { + private socket$: WebSocketSubject | null = null; + private pipelineSubscription: Subscription | null = null; + // Emits to cancel any in-flight reconnect attempt (e.g. on manual disconnect) + private readonly stopReconnect$ = new Subject(); private messagesSubject = new Subject(); private connectionStatusSubject = new BehaviorSubject('Disconnected'); private readonly RECONNECT_INTERVAL_MS = 5000; @@ -36,13 +39,15 @@ export class CamillaDspService { /** * Establishes a connection to the CamillaDSP WebSocket server. + * Safe to call multiple times - ignored while already connected/connecting/retrying, + * to avoid stacking up duplicate sockets and subscriptions. * @param url The full WebSocket URL (e.g., 'ws://beatnik-client-amp.local:1234') */ public connect(url: string): void { - // if (this.socket$ && !this.socket$.closed) { - // console.log('Already connected.'); - // return; - // } + if (this.connectionStatusSubject.value !== 'Disconnected') { + console.log('CamillaDSP already connected/connecting; ignoring duplicate connect() call.'); + return; + } this.connectionStatusSubject.next('Connecting'); console.log(`Connecting to ${url}...`); @@ -63,24 +68,21 @@ export class CamillaDspService { }, }); - this.socket$ + this.pipelineSubscription = this.socket$ .pipe( - // The retryWhen operator handles reconnection logic - retryWhen(errors => - errors.pipe( - tap(err => { - console.error(`Connection error: ${err}. Retrying in ${this.RECONNECT_INTERVAL_MS / 1000}s...`); - this.connectionStatusSubject.next('Error'); - }), - // Wait for the specified interval before trying to reconnect - delayWhen(() => timer(this.RECONNECT_INTERVAL_MS)) - ) - ) + // Retries indefinitely with a fixed delay; takeUntil below lets disconnect() cancel it + retry({ + delay: err => { + console.error(`Connection error: ${err}. Retrying in ${this.RECONNECT_INTERVAL_MS / 1000}s...`); + this.connectionStatusSubject.next('Error'); + return timer(this.RECONNECT_INTERVAL_MS); + }, + }), + takeUntil(this.stopReconnect$) ) .subscribe({ next: msg => this.messagesSubject.next(msg), // Forward messages to our subject error: err => { - // This block is less likely to be hit due to retryWhen, but good for unrecoverable errors console.error('WebSocket unrecoverable error:', err); this.connectionStatusSubject.next('Error'); }, @@ -93,7 +95,7 @@ export class CamillaDspService { * @param params Optional parameters for the command. */ public sendCommand(command: string, params: any = null): void { - if (this.connectionStatusSubject.value !== 'Connected') { + if (this.connectionStatusSubject.value !== 'Connected' || !this.socket$) { console.warn('Cannot send command while not connected.'); return; } @@ -136,12 +138,24 @@ export class CamillaDspService { } /** - * Closes the WebSocket connection gracefully. + * Closes the WebSocket connection gracefully and cancels any pending reconnect attempt. */ public disconnect(): void { - if (this.socket$) { - this.stopLevelUpdates(); + this.stopLevelUpdates(); + this.stopReconnect$.next(); // cancel any in-flight retry delay so it doesn't reconnect afterwards + this.pipelineSubscription?.unsubscribe(); + this.pipelineSubscription = null; + if (this.socket$ && !this.socket$.closed) { this.socket$.complete(); // This will trigger the closeObserver } + this.socket$ = null; + this.connectionStatusSubject.next('Disconnected'); + } + + ngOnDestroy(): void { + this.disconnect(); + this.messagesSubject.complete(); + this.connectionStatusSubject.complete(); + this.stopReconnect$.complete(); } }