From 2c176e350813bba049278cfd4802b3852ef3c0a3 Mon Sep 17 00:00:00 2001 From: idrimi Date: Fri, 27 Mar 2026 15:13:45 +0100 Subject: [PATCH 01/10] zerconf fixes --- .../client-name/client-name.component.html | 3 ++ .../client-name/client-name.component.scss | 0 .../client-name/client-name.component.spec.ts | 24 ++++++++++ src/app/client-name/client-name.component.ts | 18 ++++++++ src/app/client-name/client-name.module.ts | 18 ++++++++ .../client-info/client-info.component.html | 3 ++ .../client-info/client-info.component.ts | 19 ++++++-- .../player-toolbar.component.html | 4 +- .../player-toolbar/player-toolbar.module.ts | 4 +- .../snapcast-group-preview.module.ts | 4 +- .../client-details/client-details.page.html | 2 +- src/app/pages/settings/settings.page.ts | 45 ++++++++++++++++-- src/app/pages/zeroconf/zeroconf.page.html | 46 ++++++++----------- src/app/pages/zeroconf/zeroconf.page.ts | 43 ++++++++++++++++- 14 files changed, 192 insertions(+), 41 deletions(-) create mode 100644 src/app/client-name/client-name.component.html create mode 100644 src/app/client-name/client-name.component.scss create mode 100644 src/app/client-name/client-name.component.spec.ts create mode 100644 src/app/client-name/client-name.component.ts create mode 100644 src/app/client-name/client-name.module.ts diff --git a/src/app/client-name/client-name.component.html b/src/app/client-name/client-name.component.html new file mode 100644 index 0000000..b3996e8 --- /dev/null +++ b/src/app/client-name/client-name.component.html @@ -0,0 +1,3 @@ +

+ {{group.name || group.clients[0].host.name}} +

diff --git a/src/app/client-name/client-name.component.scss b/src/app/client-name/client-name.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/client-name/client-name.component.spec.ts b/src/app/client-name/client-name.component.spec.ts new file mode 100644 index 0000000..231c239 --- /dev/null +++ b/src/app/client-name/client-name.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { ClientNameComponent } from './client-name.component'; + +describe('ClientNameComponent', () => { + let component: ClientNameComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ ClientNameComponent ], + imports: [IonicModule.forRoot()] + }).compileComponents(); + + fixture = TestBed.createComponent(ClientNameComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/client-name/client-name.component.ts b/src/app/client-name/client-name.component.ts new file mode 100644 index 0000000..7f825bc --- /dev/null +++ b/src/app/client-name/client-name.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Group } from '../model/snapcast.model'; + +@Component({ + selector: 'app-client-name', + templateUrl: './client-name.component.html', + styleUrls: ['./client-name.component.scss'], + standalone: false +}) +export class ClientNameComponent implements OnInit { + + @Input() group: Group; + + constructor() { } + + ngOnInit() {} + +} diff --git a/src/app/client-name/client-name.module.ts b/src/app/client-name/client-name.module.ts new file mode 100644 index 0000000..70810ab --- /dev/null +++ b/src/app/client-name/client-name.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ClientNameComponent } from './client-name.component'; + + + +@NgModule({ + declarations: [ + ClientNameComponent + ], + imports: [ + CommonModule + ], + exports: [ + ClientNameComponent + ] +}) +export class ClientNameModule { } diff --git a/src/app/components/client-info/client-info.component.html b/src/app/components/client-info/client-info.component.html index 3265bb1..5e757cb 100644 --- a/src/app/components/client-info/client-info.component.html +++ b/src/app/components/client-info/client-info.component.html @@ -123,6 +123,9 @@

Snapcast Server Status

Enable Snapcast Server + + Reboot + - + \ No newline at end of file diff --git a/src/app/pages/settings/settings.page.ts b/src/app/pages/settings/settings.page.ts index 607f6a3..58926ce 100644 --- a/src/app/pages/settings/settings.page.ts +++ b/src/app/pages/settings/settings.page.ts @@ -2,6 +2,9 @@ import { Component, OnInit } from '@angular/core'; import { Preferences } from '@capacitor/preferences'; import { UserPreference } from '../../enum/user-preference.enum'; import { SnapcastService } from 'src/app/services/snapcast.service'; +import { AlertController, LoadingController, ToastController } from '@ionic/angular'; +import { result } from 'lodash-es'; +import { firstValueFrom, timeout } from 'rxjs'; @Component({ selector: 'app-settings', @@ -14,10 +17,13 @@ export class SettingsPage implements OnInit { userName?: string serverUrl?: string - + constructor( - private snapcastService: SnapcastService + private snapcastService: SnapcastService, + private loadingController: LoadingController, + private toastController: ToastController, + private alertController: AlertController ) { } ngOnInit() { @@ -52,9 +58,40 @@ export class SettingsPage implements OnInit { }); } - connectToServer() { + async connectToServer() { + // disocnnect first if already connected and timeout 2 seconds + timeout(2000); // Logic to connect to the server using the serverUrl console.log('Connecting to server at:', this.serverUrl); - this.snapcastService.connect(this.serverUrl || ''); + + // show loading indicator + const loading = await this.loadingController.create({ + message: 'Connecting to server...', + }); + await loading.present(); + const result = await this.snapcastService.connect(this.serverUrl!); + // get serverstaus to verify connection + try { + const status = await firstValueFrom(this.snapcastService.getServerStatus()); + console.log('Successfully connected to server'); + const toast = await this.toastController.create({ + message: 'Successfully connected to server', + duration: 2000, + color: 'success' + }); + await toast.present(); + } catch (error) { + console.error('Failed to connect to server:', error); + const alert = await this.alertController.create({ + header: 'Connection Failed', + message: 'Failed to connect to server. Please check the URL and try again.', + buttons: ['OK'] + }); + await alert.present(); + } finally { + loading.dismiss(); + } } } + + diff --git a/src/app/pages/zeroconf/zeroconf.page.html b/src/app/pages/zeroconf/zeroconf.page.html index 9f9b73e..9894278 100644 --- a/src/app/pages/zeroconf/zeroconf.page.html +++ b/src/app/pages/zeroconf/zeroconf.page.html @@ -33,27 +33,7 @@ - -
@@ -64,23 +44,27 @@

{{ device.name }}

Searching for services... - +

{{ service.hostname }}

{{ service.name }}

{{ service.type }}

{{ service.ipv4Addresses[0] }}:{{ service.port }}

- +

Beatnik Device - MAC: {{ service.txtRecord['mac'] }}

-

Beatnik Pi: {{ service.txtRecord['model'] }}

-

Beatnik Version: {{ service.txtRecord['version'] }}

-

RAM: {{ service.txtRecord['ram'] }}

-
+

Beatnik Pi: {{ service.txtRecord['model'] }}

+

Beatnik Version: {{ service.txtRecord['version'] }}

+

RAM: {{ service.txtRecord['ram'] }}

+
- View Device + View + Device + + + Set as Server
@@ -96,7 +80,13 @@

{{ service.hostname }}

No services found.

+
Scan for Services - + + +

Note: ZeroConf Scan does not work in Web Browser. iOS and Android only.

+
+
+
\ No newline at end of file diff --git a/src/app/pages/zeroconf/zeroconf.page.ts b/src/app/pages/zeroconf/zeroconf.page.ts index edd6bd7..bdf48cb 100644 --- a/src/app/pages/zeroconf/zeroconf.page.ts +++ b/src/app/pages/zeroconf/zeroconf.page.ts @@ -2,6 +2,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { ZeroConf, ZeroConfService as ZeroConfServiceModel } from 'capacitor-zeroconf'; import { ZeroconfService } from '../../services/zero-conf.service'; +import { Preferences } from '@capacitor/preferences'; +import { UserPreference } from '../../enum/user-preference.enum'; +import { AlertController } from '@ionic/angular'; @Component({ @@ -16,7 +19,10 @@ export class ZeroconfPage implements OnDestroy { readonly SERVICE_BEATNIK = '_beatnik._tcp.'; isScanning = false; - constructor(private zeroconf: ZeroconfService) { + + constructor(private zeroconf: ZeroconfService, + private alertController: AlertController + ) { this.services$ = this.zeroconf.services$; } @@ -27,7 +33,7 @@ export class ZeroconfPage implements OnDestroy { async scanForServices(): Promise { this.isScanning = true; try { - + await this.zeroconf.watch(this.SERVICE_SNAPCAST); console.log(`Started scanning for services of type: ${this.SERVICE_SNAPCAST}`); await this.zeroconf.watch(this.SERVICE_BEATNIK); @@ -57,6 +63,37 @@ export class ZeroconfPage implements OnDestroy { } } + async setAsServer(service: ZeroConfServiceModel): Promise { + const alert = await this.alertController.create({ + header: 'Set Snapcast Server', + message: `Do you want to set ${service.name} as the Snapcast server?`, + buttons: [ + { + text: 'Cancel', + role: 'cancel' + }, + { + text: 'Set as Server', + handler: async () => { + await this.saveServerUrl(service); + } + } + ] + }); + + await alert.present(); + } + + private async saveServerUrl(service: ZeroConfServiceModel): Promise { + const url = service.ipv4Addresses[0]; + await Preferences.set({ + key: UserPreference.SERVER_URL, + value: url, + }); + console.log('Server URL set to:', url); + + } + // Example of publishing a service // this.zeroconf.publish({ // type: '_my-app._tcp.', @@ -69,4 +106,6 @@ export class ZeroconfPage implements OnDestroy { ngOnDestroy() { this.stopScan(); } + + } From d9ce8178ae1cded555095e4e5fa9bdfebb0b7b46 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 29 Mar 2026 15:45:34 +0200 Subject: [PATCH 02/10] coverart data handling and placeholder image #33 --- src/app/services/cover-data.service.spec.ts | 16 ++++++++ src/app/services/cover-data.service.ts | 39 +++++++++++++++++++ .../placeholder/cover_art_placeholder.svg | 15 +++++++ 3 files changed, 70 insertions(+) create mode 100644 src/app/services/cover-data.service.spec.ts create mode 100644 src/app/services/cover-data.service.ts create mode 100644 src/assets/placeholder/cover_art_placeholder.svg diff --git a/src/app/services/cover-data.service.spec.ts b/src/app/services/cover-data.service.spec.ts new file mode 100644 index 0000000..3ef9c87 --- /dev/null +++ b/src/app/services/cover-data.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CoverDataService } from './cover-data.service'; + +describe('CoverDataService', () => { + let service: CoverDataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CoverDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/cover-data.service.ts b/src/app/services/cover-data.service.ts new file mode 100644 index 0000000..2978379 --- /dev/null +++ b/src/app/services/cover-data.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class CoverDataService { + + constructor() { } + + convertCoverDataBase64(coverData: string, extension: string): string { + if (!coverData) { + return ''; + } + // If it's already a data URL, return as-is + if (coverData.startsWith('data:')) { + return coverData; + } + + const ext = (extension || '').toLowerCase().replace('.', ''); + const mimeType = + ext === 'svg' ? 'image/svg+xml' : + ext === 'jpg' ? 'image/jpeg' : + `image/${ext || 'png'}`; + + const trimmed = coverData.trim(); + + // Some APIs return raw SVG markup instead of base64 + if (ext === 'svg' && trimmed.startsWith(' + + + + + + + + + + \ No newline at end of file From c7c210a3cd2ba0dde1f29b0af3f9da4c27707cf2 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 29 Mar 2026 15:45:58 +0200 Subject: [PATCH 03/10] typo --- src/app/model/cover-data.model.ts | 2 ++ src/app/pages/devices/device-details/device-details.page.html | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/app/model/cover-data.model.ts diff --git a/src/app/model/cover-data.model.ts b/src/app/model/cover-data.model.ts new file mode 100644 index 0000000..bdea65a --- /dev/null +++ b/src/app/model/cover-data.model.ts @@ -0,0 +1,2 @@ +export class CoverData { +} diff --git a/src/app/pages/devices/device-details/device-details.page.html b/src/app/pages/devices/device-details/device-details.page.html index fdf7b5c..dbd9bfc 100644 --- a/src/app/pages/devices/device-details/device-details.page.html +++ b/src/app/pages/devices/device-details/device-details.page.html @@ -4,7 +4,8 @@ - Device Details> + Device Details + From d53b394680bb33d33fccc36a0cba438dcccc6811 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 29 Mar 2026 15:46:14 +0200 Subject: [PATCH 04/10] connection button handling --- src/app/pages/settings/settings.page.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/pages/settings/settings.page.ts b/src/app/pages/settings/settings.page.ts index 58926ce..96247fa 100644 --- a/src/app/pages/settings/settings.page.ts +++ b/src/app/pages/settings/settings.page.ts @@ -60,7 +60,6 @@ export class SettingsPage implements OnInit { async connectToServer() { // disocnnect first if already connected and timeout 2 seconds - timeout(2000); // Logic to connect to the server using the serverUrl console.log('Connecting to server at:', this.serverUrl); @@ -70,10 +69,12 @@ export class SettingsPage implements OnInit { }); await loading.present(); const result = await this.snapcastService.connect(this.serverUrl!); + timeout(2000); + console.log('Connection result:', result); // get serverstaus to verify connection - try { - const status = await firstValueFrom(this.snapcastService.getServerStatus()); - console.log('Successfully connected to server'); + try { + const status = await firstValueFrom(this.snapcastService.getServerStatus()); + console.log('Successfully connected to server', status); const toast = await this.toastController.create({ message: 'Successfully connected to server', duration: 2000, From 9a2473acd94d363d27ee7e8b33e4d8114093f5e2 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 29 Mar 2026 15:46:37 +0200 Subject: [PATCH 05/10] add camilla dsp config logic --- src/app/services/beatnik-hardware.service.ts | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/app/services/beatnik-hardware.service.ts b/src/app/services/beatnik-hardware.service.ts index 22be0fd..876532e 100644 --- a/src/app/services/beatnik-hardware.service.ts +++ b/src/app/services/beatnik-hardware.service.ts @@ -19,6 +19,8 @@ export interface HardwareStatus { currentConfig: HatProfile | null; detectedHardware: HatProfile | null; isMatch: boolean; + eepromReadDisabled: boolean; + camillaConfigFile: string | null; } export interface ApplyResponse { @@ -27,6 +29,23 @@ export interface ApplyResponse { rebootRequired: boolean; } +export interface CamillaConfigListResponse { + configs: string[]; +} + +export interface CamillaDefaultConfigResponse { + fileName: string | null; +} + +export interface SetCamillaDefaultConfigRequest { + fileName: string; +} + +export interface SetCamillaDefaultConfigResponse { + status: string; + fileName: string; +} + @Injectable({ providedIn: 'root' }) @@ -52,6 +71,30 @@ export class BeatnikHardwareService { return this.http.get(`${this.getApiUrl(host)}/status`); } + /** + * Get all available CamillaDSP config files + */ + getCamillaConfigs(host: string): Observable { + return this.http.get(`${this.getApiUrl(host)}/camilla/configs`); + } + + /** + * Get the active default CamillaDSP config file + */ + getDefaultCamillaConfig(host: string): Observable { + return this.http.get(`${this.getApiUrl(host)}/camilla/configs/default`); + } + + /** + * Set the default CamillaDSP config file + */ + setDefaultCamillaConfig(fileName: string, host: string): Observable { + return this.http.put( + `${this.getApiUrl(host)}/camilla/configs/default`, + { fileName } satisfies SetCamillaDefaultConfigRequest + ); + } + /** * Apply a new hardware configuration * This will update config.txt and camilladsp.yml From dcdba2dbdba9eed9f53e89dd380be5ede68dc841 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 29 Mar 2026 15:47:00 +0200 Subject: [PATCH 06/10] user cover data service --- .../player-toolbar.component.html | 83 ++++++++++--------- .../player-toolbar.component.ts | 19 +++-- .../snapcast-group-preview.component.scss | 8 +- .../snapcast-group-preview.component.ts | 20 +++-- 4 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/app/components/player-toolbar/player-toolbar.component.html b/src/app/components/player-toolbar/player-toolbar.component.html index a326d77..20f0c43 100644 --- a/src/app/components/player-toolbar/player-toolbar.component.html +++ b/src/app/components/player-toolbar/player-toolbar.component.html @@ -1,18 +1,20 @@ - - - - - - cover image - - -

{{stream.id}}

-

{{ stream.properties.metadata.title }}

-

{{ stream.properties.metadata.artist }}

- -
- -
- - - - - -

- -

-

Clients: {{ group.clients.length }}

- - - - - - - -
-
-
+ + + + +

+ +

+

Clients: {{ group.clients.length }}

+ + + + + + + +
+
+
+
-
-
- -
+
+ +
-
+
\ No newline at end of file diff --git a/src/app/components/player-toolbar/player-toolbar.component.ts b/src/app/components/player-toolbar/player-toolbar.component.ts index dc0945a..f858e02 100644 --- a/src/app/components/player-toolbar/player-toolbar.component.ts +++ b/src/app/components/player-toolbar/player-toolbar.component.ts @@ -4,6 +4,7 @@ import { Observable, Subscription, tap, firstValueFrom } from 'rxjs'; // firstVa import { Group, Stream, ServerDetail, Client, SnapCastServerStatusResponse } from 'src/app/model/snapcast.model'; // Client importiert für Typisierung import { SnapcastService } from 'src/app/services/snapcast.service'; import { Haptics, ImpactStyle, NotificationType } from '@capacitor/haptics'; +import { CoverDataService } from 'src/app/services/cover-data.service'; @Component({ @@ -23,10 +24,11 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy { private knobMoveStart = false; private knobMoveEnd = false; - + constructor( - public snapcastService: SnapcastService + public snapcastService: SnapcastService, + private coverDateService: CoverDataService ) { @@ -131,11 +133,7 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy { convertCoverDataBase64(coverData: string, extension: string): string { - if (!coverData) { - return ''; - } - // Convert base64 data to a data URL - return `data:image/${extension};base64,${coverData}`; + return this.coverDateService.convertCoverDataBase64(coverData, extension); } knobMoveStartEvent(event: any): void { @@ -151,7 +149,12 @@ export class PlayerToolbarComponent implements OnInit, OnChanges, OnDestroy { // Optionally, you can add haptic feedback here } - + onCoverImageError(event: Event): void { + this.coverDateService.onCoverImageError(event); + } + + + } \ No newline at end of file diff --git a/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss b/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss index 8dcb314..e24a760 100644 --- a/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss +++ b/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss @@ -8,8 +8,8 @@ // background-position: top right; // background-repeat: no-repeat; width: 90%; - border-radius: 8px; - box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 0px; + // box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1); position: absolute; top: 0; right: 5%; @@ -42,4 +42,8 @@ // add filter // filter: brightness(1.4); } +} + +ion-card{ + box-shadow: none; } \ No newline at end of file diff --git a/src/app/components/snapcast-group-preview/snapcast-group-preview.component.ts b/src/app/components/snapcast-group-preview/snapcast-group-preview.component.ts index 96c2bc0..813c651 100644 --- a/src/app/components/snapcast-group-preview/snapcast-group-preview.component.ts +++ b/src/app/components/snapcast-group-preview/snapcast-group-preview.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Group, Stream } from 'src/app/model/snapcast.model'; import { Speaker } from 'src/app/model/speaker.model'; +import { CoverDataService } from 'src/app/services/cover-data.service'; @Component({ selector: 'app-snapcast-group-preview', @@ -9,7 +10,7 @@ import { Speaker } from 'src/app/model/speaker.model'; styleUrls: ['./snapcast-group-preview.component.scss'], standalone: false }) -export class SnapcastGroupPreviewComponent implements OnInit, OnChanges { +export class SnapcastGroupPreviewComponent implements OnInit, OnChanges { @Input() group?: Group; @Input() streams?: Stream[] | null; @@ -19,7 +20,8 @@ export class SnapcastGroupPreviewComponent implements OnInit, OnChanges { activeSpeaker?: Speaker; constructor( - private router: Router + private router: Router, + private coverDateService: CoverDataService ) { } ngOnInit() { @@ -64,14 +66,16 @@ export class SnapcastGroupPreviewComponent implements OnInit, OnChanges { } } - convertCoverDataBase64(coverData: string, extension: string): string { - if (!coverData) { - return ''; - } - // Convert base64 data to a data URL - return `data:image/${extension};base64,${coverData}`; + convertCoverDataBase64(coverData: string, extension: string): string { + return this.coverDateService.convertCoverDataBase64(coverData, extension); + } + + onCoverImageError(event: Event): void { + this.coverDateService.onCoverImageError(event); } + + getActiveSpeaker(): Speaker | undefined { if (!this.group || !this.speakerData) { return undefined; From f491f4f3c182801ad54cc4a7b73855dc8c3affb5 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 29 Mar 2026 15:47:14 +0200 Subject: [PATCH 07/10] handle camilla dsp configs --- .../camilla-dsp/camilla-dsp.component.html | 100 ++++++++++++---- .../camilla-dsp/camilla-dsp.component.ts | 109 +++++++++++++++++- 2 files changed, 187 insertions(+), 22 deletions(-) diff --git a/src/app/components/camilla-dsp/camilla-dsp.component.html b/src/app/components/camilla-dsp/camilla-dsp.component.html index 293018a..69f32b3 100644 --- a/src/app/components/camilla-dsp/camilla-dsp.component.html +++ b/src/app/components/camilla-dsp/camilla-dsp.component.html @@ -1,6 +1,8 @@
+ +
@@ -163,7 +165,7 @@

Playback

*ngFor="let filter of parsedConfig.filters | keyvalue"> - {{ filter.key }} + {{ filter.key }} Type: {{ filter.value.type }} @@ -231,27 +233,83 @@

Playback

-
-

CamillaDSP Control

- -

- Connection Status: {{ connectionStatus }} -

- + + + + CamillaDSP Connection + + +
+ WebSocket Connection + +

+ Connection Status: {{ connectionStatus }} +

+ + +
+
+
+ + + + Advanced CamillaDSP Config + -
+
+ Default Config +

Manage the default config file used on this device. Only use this if you know what you are doing.

+ + Current Default Config + {{ currentCamillaConfigFile || 'No default config selected' }} + + + + Available Config Files + + + {{ config }} + + + - \ No newline at end of file + +

Loading CamillaDSP configs...

+
+ + +

{{ camillaConfigError }}

+
+ + +

{{ camillaConfigMessage }}

+
+ + + Refresh Config List + + + + Set Default Config + +
+ + \ No newline at end of file diff --git a/src/app/components/camilla-dsp/camilla-dsp.component.ts b/src/app/components/camilla-dsp/camilla-dsp.component.ts index 1ddccb4..e2311c5 100644 --- a/src/app/components/camilla-dsp/camilla-dsp.component.ts +++ b/src/app/components/camilla-dsp/camilla-dsp.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { CamillaDspConfig, MixerSource, SignalLevels } from 'src/app/model/camilla-dsp.model'; +import { BeatnikHardwareService } from 'src/app/services/beatnik-hardware.service'; import { CamillaDspService, ConnectionStatus } from 'src/app/services/camilla-dsp.service'; @Component({ @@ -21,12 +22,23 @@ export class CamillaDspComponent implements OnInit, OnDestroy { levels: SignalLevels | null = null; currentVolume: number = 0; + availableCamillaConfigs: string[] = []; + selectedCamillaConfig: string | null = null; + currentCamillaConfigFile: string | null = null; + isLoadingCamillaConfigs = false; + isSavingCamillaConfig = false; + camillaConfigMessage = ''; + camillaConfigError = ''; private levelSubscription: Subscription | undefined; - constructor(private camillaService: CamillaDspService) { } + constructor( + private camillaService: CamillaDspService, + private beatnikHardwareService: BeatnikHardwareService + ) { } async ngOnInit() { + this.loadCamillaConfigState(); this.connect(); // Subscribe to connection status changes this.subscriptions.add( @@ -77,6 +89,99 @@ export class CamillaDspComponent implements OnInit, OnDestroy { // }, 800); } + private getHardwareHost(): string | null { + if (!this.url) { + return null; + } + + try { + return new URL(this.url).hostname; + } catch (error) { + console.error('Failed to parse CamillaDSP URL for hardware API access:', error); + return null; + } + } + + loadCamillaConfigState() { + const host = this.getHardwareHost(); + if (!host) { + this.camillaConfigError = 'Unable to determine device host for CamillaDSP config API.'; + return; + } + + this.isLoadingCamillaConfigs = true; + this.camillaConfigError = ''; + this.camillaConfigMessage = ''; + + this.subscriptions.add( + this.beatnikHardwareService.getCamillaConfigs(host).subscribe({ + next: (response) => { + this.availableCamillaConfigs = response.configs ?? []; + this.syncSelectedCamillaConfig(); + this.isLoadingCamillaConfigs = false; + }, + error: (error) => { + console.error('Failed to load available CamillaDSP configs:', error); + this.camillaConfigError = 'Failed to load available CamillaDSP configs.'; + this.isLoadingCamillaConfigs = false; + } + }) + ); + + this.subscriptions.add( + this.beatnikHardwareService.getDefaultCamillaConfig(host).subscribe({ + next: (response) => { + this.currentCamillaConfigFile = response.fileName; + this.syncSelectedCamillaConfig(); + }, + error: (error) => { + console.error('Failed to load default CamillaDSP config:', error); + this.camillaConfigError = 'Failed to load current default CamillaDSP config.'; + } + }) + ); + } + + private syncSelectedCamillaConfig() { + if (this.currentCamillaConfigFile && this.availableCamillaConfigs.includes(this.currentCamillaConfigFile)) { + this.selectedCamillaConfig = this.currentCamillaConfigFile; + return; + } + + if (!this.selectedCamillaConfig && this.availableCamillaConfigs.length > 0) { + this.selectedCamillaConfig = this.availableCamillaConfigs[0]; + } + } + + saveDefaultCamillaConfig() { + const host = this.getHardwareHost(); + + if (!host || !this.selectedCamillaConfig) { + this.camillaConfigError = 'Please choose a CamillaDSP config before saving.'; + return; + } + + this.isSavingCamillaConfig = true; + this.camillaConfigError = ''; + this.camillaConfigMessage = ''; + + this.subscriptions.add( + this.beatnikHardwareService.setDefaultCamillaConfig(this.selectedCamillaConfig, host).subscribe({ + next: (response) => { + this.currentCamillaConfigFile = response.fileName; + this.selectedCamillaConfig = response.fileName; + this.camillaConfigMessage = `Default config set to ${response.fileName}.`; + this.isSavingCamillaConfig = false; + }, + error: (error) => { + console.error('Failed to update default CamillaDSP config:', error); + this.camillaConfigError = error?.error?.error ?? 'Failed to update default CamillaDSP config.'; + this.isSavingCamillaConfig = false; + } + }) + ); + } + connect() { @@ -169,4 +274,6 @@ export class CamillaDspComponent implements OnInit, OnDestroy { console.log('CamillaDspComponent: Leaving page, cleaning up resources if needed'); this.ngOnDestroy(); } + + } From dd4a62e723ec8a1d7be7412924371bf0ee2f54d7 Mon Sep 17 00:00:00 2001 From: idrimi Date: Tue, 31 Mar 2026 14:38:33 +0200 Subject: [PATCH 08/10] read out camilla dsp and cover art fixes --- .../choose-speakers.component.html | 1 + .../choose-speakers.component.ts | 14 +- .../choose-speakers/choose-speakers.module.ts | 22 ++ .../client-info/client-info.component.html | 281 +++++++++--------- .../client-info/client-info.component.ts | 27 ++ .../client-info/client-info.module.ts | 4 +- .../player-toolbar.component.html | 2 +- .../snapcast-group-preview.component.scss | 3 +- .../snapcast-stream-preview.component.ts | 15 +- .../client-details/client-details.page.ts | 1 - .../device-details/device-details.page.ts | 19 +- src/app/pages/streams/streams.page.ts | 8 +- 12 files changed, 231 insertions(+), 166 deletions(-) create mode 100644 src/app/components/choose-speakers/choose-speakers.module.ts diff --git a/src/app/components/choose-speakers/choose-speakers.component.html b/src/app/components/choose-speakers/choose-speakers.component.html index 8c522fb..1515bc3 100644 --- a/src/app/components/choose-speakers/choose-speakers.component.html +++ b/src/app/components/choose-speakers/choose-speakers.component.html @@ -22,6 +22,7 @@

{{ speaker.manufacturer }} {{ speaker.model }}

+
diff --git a/src/app/components/choose-speakers/choose-speakers.component.ts b/src/app/components/choose-speakers/choose-speakers.component.ts index 97b94af..24e1496 100644 --- a/src/app/components/choose-speakers/choose-speakers.component.ts +++ b/src/app/components/choose-speakers/choose-speakers.component.ts @@ -1,8 +1,6 @@ -import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { Component, Input, OnInit } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { IonicModule, ModalController } from '@ionic/angular'; +import { ModalController } from '@ionic/angular'; import { first, firstValueFrom } from 'rxjs'; import { Speaker } from 'src/app/model/speaker.model'; import { SnapcastService } from 'src/app/services/snapcast.service'; @@ -10,14 +8,8 @@ import { SnapcastService } from 'src/app/services/snapcast.service'; @Component({ selector: 'app-choose-speakers', templateUrl: './choose-speakers.component.html', - styleUrls: ['./choose-speakers.component.scss'], - // import ionic module here if needed - imports: [ - IonicModule, - FormsModule, - CommonModule - ], - standalone: true + styleUrls: ['./choose-speakers.component.scss'], + standalone: false }) export class ChooseSpeakersComponent implements OnInit { diff --git a/src/app/components/choose-speakers/choose-speakers.module.ts b/src/app/components/choose-speakers/choose-speakers.module.ts new file mode 100644 index 0000000..51c843d --- /dev/null +++ b/src/app/components/choose-speakers/choose-speakers.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; +import { FormsModule } from '@angular/forms'; +import { ChooseSpeakersComponent } from './choose-speakers.component'; + + + +@NgModule({ + declarations: [ + ChooseSpeakersComponent + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + ], + exports: [ + ChooseSpeakersComponent + ] +}) +export class ChooseSpeakersModule { } diff --git a/src/app/components/client-info/client-info.component.html b/src/app/components/client-info/client-info.component.html index 5e757cb..c1c7c39 100644 --- a/src/app/components/client-info/client-info.component.html +++ b/src/app/components/client-info/client-info.component.html @@ -1,150 +1,159 @@ - - - - Camilla DSP - + + + + Camilla DSP + - - - Soundcard - - - - - Info - + + + Soundcard + - - Settings - - - - - - - - - -

Snap Client Information

-

Id: {{client.id}}

-

{{client.lastSeen.sec * 1000 | date: 'long'}}

-
-

Host

-

Name: {{client.host.name}}

-

OS: {{client.host.os}}

-

Architecture: {{client.host.arch}}

-

IP Address: {{cleanIpAddress(client.host.ip)}}

-

MAC Address: {{client.host.mac}}

-
-

Configuration

-

Name: {{ client.config.name}}

-

Client ID: {{ client.id }}

-

Volume: {{ client.config.volume.percent || 'N/A' }}%

-

Muted: {{ client.config.volume.muted}}

-

Latency: {{ client.config.latency }} ms

-

Connected: {{ client.connected }}

-

Last Seen: {{ client.lastSeen.sec * 1000 | date : 'long' }}

-

-
-
- - - - -

{{ client.id }}

-

Raw json

-
-
- -
{{ client| json }}
-
-
-
- + + Info + + + Settings + +
+ - - + + - - - - + + +

Snap Client Information

+

Id: {{client.id}}

+

{{client.lastSeen.sec * 1000 | date: 'long'}}

+
+

Host

+

Name: {{client.host.name}}

+

OS: {{client.host.os}}

+

Architecture: {{client.host.arch}}

+

IP Address: {{cleanIpAddress(client.host.ip)}}

+

MAC Address: {{client.host.mac}}

+
+

Configuration

+

Name: {{ client.config.name}}

+

Client ID: {{ client.id }}

+

Volume: {{ client.config.volume.percent || 'N/A' }}%

+

Muted: {{ client.config.volume.muted}}

+

Latency: {{ client.config.latency }} ms

+

Connected: {{ client.connected }}

+

Last Seen: {{ client.lastSeen.sec * 1000 | date : 'long' }}

+

+
+
+ + + + +

{{ client.id }}

+

Raw json

+
+
+ +
{{ client| json }}
+
+
+
+ - -
-

Config Status

- -

Name: {{ hardwareStatus.currentConfig.name}}

-

Id: {{ hardwareStatus.currentConfig.id}}

-

isMatched: {{ hardwareStatus.isMatch}}

-
- - - Choose Soundcard - - -

No current configuration available.

-
- - -
- -
- - Refresh Soundcard Info - -
- - -
-

Settings

- -
-

Snapcast Server Status

-

{{ snapcastServerStatus.message}}

-
{{ snapcastServerStatus | json }}
-
- - Refresh Snapcast Server Status - - - - Disable Snapcast Server - - - - Enable Snapcast Server - - - Reboot - - - -
-
- - - +
+
+ - + + + +

Soundcard Information

+
- +

Snapcast Server Status

+ +

Status: Active

+

"Active" Means this device is currently configured to be your Beatnik / snapcast server.

+
+ +

Status: Inactive

+

"Inactive" Means this device is configured to be a Beatnik / snapcast client. Clients will automatically try to connect to the server in your local Network.

+
+

{{ snapcastServerStatus.message}}

+ +
+ + + Refresh Snapcast Server Status + + + + Disable Snapcast Server + + + + Enable Snapcast Server + + + Reboot + + + + Choose Speakers + + + +
+ + + + + + + + - - - - + --> \ No newline at end of file diff --git a/src/app/components/client-info/client-info.component.ts b/src/app/components/client-info/client-info.component.ts index e79ddcb..a29b171 100644 --- a/src/app/components/client-info/client-info.component.ts +++ b/src/app/components/client-info/client-info.component.ts @@ -10,6 +10,7 @@ import { CamillaDspService } from 'src/app/services/camilla-dsp.service'; import { SnapcastService } from 'src/app/services/snapcast.service'; import { SoundcardPickerComponent } from '../soundcard-picker/soundcard-picker.component'; import { AlertController, ModalController } from '@ionic/angular'; +import { ChooseSpeakersComponent } from '../choose-speakers/choose-speakers.component'; @Component({ selector: 'app-client-info', @@ -43,9 +44,12 @@ export class ClientInfoComponent implements OnInit { this.serverState = this.snapcastService.state$; this.camillaDspUrl = await this.getCamillaDspUrl(); this.getHardwareInfo(); + this.refreshSnapcastStatus(); } + + async getCamillaDspUrl(): Promise { if (!this.client) { console.error('Client Info Component: No client available to get Camilla DSP URL'); @@ -263,4 +267,27 @@ export class ClientInfoComponent implements OnInit { } }); } + + + async chooseSpeakers() { + console.log('Client Info Component: Opening speaker selection for client:', this.client?.id); + const modal = await this.modalController.create({ + component: ChooseSpeakersComponent, + id: 'choose-speakers-modal', + componentProps: { + clientId: this.client?.id + } + }); + await modal.present(); + + const { data } = await modal.onDidDismiss(); + if (data && data.selectedId) { + console.log('Client Info Component: Speaker selected:', data.selectedId); + } else { + console.log('Client Info Component: Speaker selection cancelled or no selection made'); + } + } } + + + diff --git a/src/app/components/client-info/client-info.module.ts b/src/app/components/client-info/client-info.module.ts index 569be67..cfca638 100644 --- a/src/app/components/client-info/client-info.module.ts +++ b/src/app/components/client-info/client-info.module.ts @@ -5,6 +5,7 @@ import { ClientInfoComponent } from './client-info.component'; import { FormsModule } from '@angular/forms'; import { CamillaDspModule } from '../camilla-dsp/camilla-dsp.module'; import { SoundcardPickerModule } from '../soundcard-picker/soundcard-picker.module'; +import { ChooseSpeakersModule } from '../choose-speakers/choose-speakers.module'; @@ -17,7 +18,8 @@ import { SoundcardPickerModule } from '../soundcard-picker/soundcard-picker.modu IonicModule, FormsModule, CamillaDspModule, - SoundcardPickerModule + SoundcardPickerModule, + ChooseSpeakersModule ], exports: [ ClientInfoComponent diff --git a/src/app/components/player-toolbar/player-toolbar.component.html b/src/app/components/player-toolbar/player-toolbar.component.html index 20f0c43..6187e86 100644 --- a/src/app/components/player-toolbar/player-toolbar.component.html +++ b/src/app/components/player-toolbar/player-toolbar.component.html @@ -29,7 +29,7 @@

{{ stream.properties.metadata.title }}

--> - +

diff --git a/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss b/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss index e24a760..5711fcc 100644 --- a/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss +++ b/src/app/components/snapcast-group-preview/snapcast-group-preview.component.scss @@ -9,12 +9,13 @@ // background-repeat: no-repeat; width: 90%; border-radius: 0px; - // box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1); + box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1); position: absolute; top: 0; right: 5%; z-index: 1; opacity: 0.7; + background-color: var(--ion-color-dark); } .img-container{ diff --git a/src/app/components/snapcast-stream-preview/snapcast-stream-preview.component.ts b/src/app/components/snapcast-stream-preview/snapcast-stream-preview.component.ts index 0707429..2e3cee0 100644 --- a/src/app/components/snapcast-stream-preview/snapcast-stream-preview.component.ts +++ b/src/app/components/snapcast-stream-preview/snapcast-stream-preview.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Stream } from 'src/app/model/snapcast.model'; +import { CoverDataService } from 'src/app/services/cover-data.service'; @Component({ selector: 'app-snapcast-stream-preview', @@ -10,17 +11,19 @@ import { Stream } from 'src/app/model/snapcast.model'; export class SnapcastStreamPreviewComponent implements OnInit { @Input() stream?: Stream; - constructor() { } + constructor( + private coverDateService: CoverDataService + ) { } ngOnInit() {} convertCoverDataBase64(coverData: string, extension: string): string { - if (!coverData) { - return ''; - } - // Convert base64 data to a data URL - return `data:image/${extension};base64,${coverData}`; + return this.coverDateService.convertCoverDataBase64(coverData, extension); + } + + onCoverImageError(event: Event): void { + this.coverDateService.onCoverImageError(event); } } diff --git a/src/app/pages/clients/client-details/client-details.page.ts b/src/app/pages/clients/client-details/client-details.page.ts index b4de758..cee385f 100644 --- a/src/app/pages/clients/client-details/client-details.page.ts +++ b/src/app/pages/clients/client-details/client-details.page.ts @@ -3,7 +3,6 @@ import { ActivatedRoute } from '@angular/router'; import { Preferences } from '@capacitor/preferences'; import { AlertController, ModalController } from '@ionic/angular'; import { firstValueFrom, Observable } from 'rxjs'; -import { ChooseSpeakersComponent } from 'src/app/components/choose-speakers/choose-speakers.component'; import { SoundcardPickerComponent } from 'src/app/components/soundcard-picker/soundcard-picker.component'; import { SUPPORTED_HATS } from 'src/app/constant/hat.constant'; import { UserPreference } from 'src/app/enum/user-preference.enum'; diff --git a/src/app/pages/devices/device-details/device-details.page.ts b/src/app/pages/devices/device-details/device-details.page.ts index 375d623..f848f82 100644 --- a/src/app/pages/devices/device-details/device-details.page.ts +++ b/src/app/pages/devices/device-details/device-details.page.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { Client, Group, SnapCastServerStatusResponse, Stream } from 'src/app/model/snapcast.model'; +import { CoverDataService } from 'src/app/services/cover-data.service'; import { SnapcastService } from 'src/app/services/snapcast.service'; @Component({ @@ -13,7 +14,7 @@ import { SnapcastService } from 'src/app/services/snapcast.service'; export class DeviceDetailsPage implements OnInit { id?: string; - group:Group; + group: Group; serverState?: Observable; selectedClient: Client; @@ -21,7 +22,8 @@ export class DeviceDetailsPage implements OnInit { constructor( private avtivateRoute: ActivatedRoute, - private snapcastService: SnapcastService + private snapcastService: SnapcastService, + private coverDateService: CoverDataService ) { } async ngOnInit() { @@ -96,12 +98,13 @@ export class DeviceDetailsPage implements OnInit { }); } - convertCoverDataBase64(coverData: string, extension: string): string { - if (!coverData) { - return ''; - } - // Convert base64 data to a data URL - return `data:image/${extension};base64,${coverData}`; + convertCoverDataBase64(coverData: string, extension: string): string { + return this.coverDateService.convertCoverDataBase64(coverData, extension); + + } + + onCoverImageError(event: Event): void { + this.coverDateService.onCoverImageError(event); } diff --git a/src/app/pages/streams/streams.page.ts b/src/app/pages/streams/streams.page.ts index 32f50d2..4cd7a96 100644 --- a/src/app/pages/streams/streams.page.ts +++ b/src/app/pages/streams/streams.page.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { SnapcastWebsocketNotification } from 'src/app/model/snapcast-websocket-notification.model'; import { SnapCastServerStatusResponse } from 'src/app/model/snapcast.model'; +import { CoverDataService } from 'src/app/services/cover-data.service'; import { SnapcastService } from 'src/app/services/snapcast.service'; @Component({ @@ -15,6 +16,7 @@ export class StreamsPage implements OnInit { constructor( private readonly snapcastService: SnapcastService, + private readonly coverDateService: CoverDataService ) { } ngOnInit() { @@ -22,7 +24,11 @@ export class StreamsPage implements OnInit { } convertBase64ToImage(base64String: string, format: string): string { - return `data:image/${format};base64,${base64String}`; + return this.coverDateService.convertCoverDataBase64(base64String, format); + } + + onCoverImageError(event: Event): void { + this.coverDateService.onCoverImageError(event); } } From 21e577b670c4d13b7358701526ca66930f427294 Mon Sep 17 00:00:00 2001 From: idrimi Date: Tue, 31 Mar 2026 14:38:56 +0200 Subject: [PATCH 09/10] 0.5.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56fd09c..421d2bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "beatnik", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "beatnik", - "version": "0.5.0", + "version": "0.5.1", "dependencies": { "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", diff --git a/package.json b/package.json index 6eb841a..d037b72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "beatnik", - "version": "0.5.0", + "version": "0.5.1", "author": "byrds & bytes gmbh", "homepage": "https://beatnik.audio", "scripts": { From 15689bcf5f0241ea4126fbf4448581e28e534fe5 Mon Sep 17 00:00:00 2001 From: idrimi Date: Fri, 3 Apr 2026 08:09:12 +0200 Subject: [PATCH 10/10] purge state and wait to reconnect propperly --- src/app/pages/settings/settings.page.ts | 41 ++++++++++++++++++++----- src/app/services/snapcast.service.ts | 20 ++++++++++-- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/app/pages/settings/settings.page.ts b/src/app/pages/settings/settings.page.ts index 96247fa..c49d241 100644 --- a/src/app/pages/settings/settings.page.ts +++ b/src/app/pages/settings/settings.page.ts @@ -59,22 +59,47 @@ export class SettingsPage implements OnInit { } async connectToServer() { - // disocnnect first if already connected and timeout 2 seconds // Logic to connect to the server using the serverUrl console.log('Connecting to server at:', this.serverUrl); // show loading indicator const loading = await this.loadingController.create({ - message: 'Connecting to server...', + message: 'Establishing connection to server...', }); await loading.present(); - const result = await this.snapcastService.connect(this.serverUrl!); - timeout(2000); - console.log('Connection result:', result); - // get serverstaus to verify connection + try { - const status = await firstValueFrom(this.snapcastService.getServerStatus()); - console.log('Successfully connected to server', status); + // Disconnect first to clean up any existing or stalled connections + if (this.snapcastService.disconnect) { + this.snapcastService.disconnect(); + // Brief delay to ensure cleanup + await new Promise(resolve => setTimeout(resolve, 5500)); + } + + try { + const connectionResult = await this.snapcastService.connect(this.serverUrl!); + console.log('Connection result:', connectionResult); + loading.message = 'Connected to server, verifying status...'; + await new Promise(resolve => setTimeout(resolve, 5500)); + + } catch (error) { + console.error('Error during connection attempt:', error); + throw error; // Rethrow to be caught by outer catch + } + + try { + loading.message = 'Retrieving server status...'; + const status = await firstValueFrom(this.snapcastService.getServerStatus().pipe(timeout(5000))); + console.log('Successfully connected to server', status); + } catch (error) { + console.error('Failed to get server status after connection:', error); + throw new Error('Connected to server but failed to retrieve status. Please check the server and try again.'); + } + + // get server status to verify connection + // const status = await firstValueFrom(this.snapcastService.getServerStatus()); + // console.log('Successfully connected to server', status); + const toast = await this.toastController.create({ message: 'Successfully connected to server', duration: 2000, diff --git a/src/app/services/snapcast.service.ts b/src/app/services/snapcast.service.ts index 103a124..18a3dc3 100644 --- a/src/app/services/snapcast.service.ts +++ b/src/app/services/snapcast.service.ts @@ -105,9 +105,10 @@ class JsonRpcSocket { constructor(private reconnectIntervalMs: number = 3000) {} connect(url: string, onOpen?: () => void): void { + console.info(`[JsonRpcSocket] Attempting to connect to ${url}...`); if (this.ws$ && !this.ws$.closed) return; - // Clean up previous attempts + // // Clean up previous attempts this.disconnect(false); const config: WebSocketSubjectConfig = { @@ -139,12 +140,19 @@ class JsonRpcSocket { this.socketSubscription = this.ws$.pipe( catchError(err => { console.error('[JsonRpcSocket] Stream Error', err); - // The closeObserver will trigger reconnection logic + this.connected$.next(false); + this.ws$ = undefined; + this.scheduleReconnect(url, onOpen); return EMPTY; }) ).subscribe({ next: (msg) => this.handleMessage(msg), - error: (err) => console.error('[JsonRpcSocket] Fatal Error', err) + error: (err) => { + console.error('[JsonRpcSocket] Fatal Error', err); + this.connected$.next(false); + this.ws$ = undefined; + this.scheduleReconnect(url, onOpen); + } }); } @@ -362,6 +370,11 @@ export class SnapcastService implements OnDestroy { async connect(host = environment.snapcastServerUrl, port = 1780, overrideUserPreference = false): Promise { let finalHost = host; + console.log('SnapcastService: Initiating connection...', { host, port, overrideUserPreference }); + + // purge any existing connections to ensure clean state + this.disconnect(); + await new Promise(resolve => setTimeout(resolve, 500)); // Brief delay to ensure cleanup // Check User Preferences if (!overrideUserPreference) { @@ -372,6 +385,7 @@ export class SnapcastService implements OnDestroy { const wsUrl = `ws://${finalHost}:${port}/jsonrpc`; this.socket.connect(wsUrl, () => { + console.log('SnapcastService: Successfully connected to', wsUrl); // On Connect success: this.refreshState(); });