From 3a19197cea7875c012c78111c407a39f3655d0fc Mon Sep 17 00:00:00 2001 From: idrimi Date: Sat, 27 Sep 2025 09:00:48 +0200 Subject: [PATCH 01/10] check for snapcast servers in local network, use byrds cap plugin fork --- package-lock.json | 179 +++++++++++++++++---- package.json | 1 + src/app/app-routing.module.ts | 4 + src/app/model/zero-conf.model.ts | 2 + src/app/services/zero-conf.service.spec.ts | 16 ++ src/app/services/zero-conf.service.ts | 90 +++++++++++ 6 files changed, 264 insertions(+), 28 deletions(-) create mode 100644 src/app/model/zero-conf.model.ts create mode 100644 src/app/services/zero-conf.service.spec.ts create mode 100644 src/app/services/zero-conf.service.ts diff --git a/package-lock.json b/package-lock.json index ab26344..7d0c41d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/lodash-es": "^4.17.12", "bootstrap": "^5.3.6", "bootstrap-icons": "^1.13.1", + "capacitor-zeroconf": "github:byrdsandbytes/capacitor-zeroconf", "immer": "^10.1.1", "ionicons": "^7.0.0", "lodash-es": "^4.17.21", @@ -8042,6 +8043,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", @@ -8052,6 +8068,38 @@ "multicast-dns": "^7.2.5" } }, + "node_modules/bonjour/node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "license": "MIT", + "peer": true + }, + "node_modules/bonjour/node_modules/dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bonjour/node_modules/multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -8205,6 +8253,13 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "license": "MIT", + "peer": true + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -8332,7 +8387,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -8350,7 +8404,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -8363,7 +8416,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -8432,6 +8484,15 @@ } ] }, + "node_modules/capacitor-zeroconf": { + "version": "4.0.0", + "resolved": "git+ssh://git@github.com/byrdsandbytes/capacitor-zeroconf.git#12ec8738cd5ceba84cf499f3c8e4510233a5a15f", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0", + "bonjour": "^3.5.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -9521,6 +9582,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -9581,7 +9663,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -9610,7 +9691,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -9787,6 +9867,13 @@ "node": ">=8" } }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "license": "MIT", + "peer": true + }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -9799,6 +9886,16 @@ "node": ">=6" } }, + "node_modules/dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -9903,7 +10000,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -10210,7 +10306,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -10219,7 +10314,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -10234,7 +10328,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -11307,7 +11400,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11336,7 +11428,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11375,7 +11466,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -11616,7 +11706,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -11857,7 +11946,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -11963,7 +12051,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -11990,7 +12077,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -12002,7 +12088,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -12017,7 +12102,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -12423,6 +12507,13 @@ "@stencil/core": "^4.0.3" } }, + "node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "license": "MIT", + "peer": true + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -12445,6 +12536,23 @@ "node": ">= 10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -12578,7 +12686,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -12802,7 +12909,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -14235,7 +14341,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -14929,6 +15034,13 @@ "multicast-dns": "cli.js" } }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", + "license": "MIT", + "peer": true + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -15509,11 +15621,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -16990,7 +17118,6 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -17607,7 +17734,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -17968,7 +18094,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -17985,7 +18110,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -19252,8 +19376,7 @@ "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, "node_modules/tinyglobby": { "version": "0.2.13", diff --git a/package.json b/package.json index bc6621f..8119c08 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/lodash-es": "^4.17.12", "bootstrap": "^5.3.6", "bootstrap-icons": "^1.13.1", + "capacitor-zeroconf": "github:byrdsandbytes/capacitor-zeroconf", "immer": "^10.1.1", "ionicons": "^7.0.0", "lodash-es": "^4.17.21", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 705ed98..cac6317 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -66,6 +66,10 @@ const routes: Routes = [ path: 'settings', loadChildren: () => import('./pages/settings/settings.module').then(m => m.SettingsPageModule) }, + { + path: 'zeroconf', + loadChildren: () => import('./pages/zeroconf/zeroconf.module').then( m => m.ZeroconfPageModule) + }, ]; diff --git a/src/app/model/zero-conf.model.ts b/src/app/model/zero-conf.model.ts new file mode 100644 index 0000000..5fc8c55 --- /dev/null +++ b/src/app/model/zero-conf.model.ts @@ -0,0 +1,2 @@ +export class ZeroConf { +} diff --git a/src/app/services/zero-conf.service.spec.ts b/src/app/services/zero-conf.service.spec.ts new file mode 100644 index 0000000..23c8255 --- /dev/null +++ b/src/app/services/zero-conf.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ZeroConfService } from './zero-conf.service'; + +describe('ZeroConfService', () => { + let service: ZeroConfService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ZeroConfService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/zero-conf.service.ts b/src/app/services/zero-conf.service.ts new file mode 100644 index 0000000..93ac0c6 --- /dev/null +++ b/src/app/services/zero-conf.service.ts @@ -0,0 +1,90 @@ +// src/app/services/zeroconf.service.ts + +import { Injectable, OnDestroy, NgZone } from '@angular/core'; +// @ts-ignore +import { ZeroConf, ZeroConfService, ZeroConfWatchResult } from 'capacitor-zeroconf'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { distinctUntilChanged, scan } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class ZeroconfService implements OnDestroy { + private readonly servicesSubject = new BehaviorSubject([]); + + // Expose the list of services as an observable + public readonly services$: Observable = this.servicesSubject.asObservable(); + + constructor(private ngZone: NgZone) { + // Listen for discovery events and update the services list + ZeroConf.addListener('discover', (result: any) => { + this.ngZone.run(() => { + console.log('[ZeroConf] Discover event:', result); + this.handleDiscoveryEvent(result); + }); + }); + } + + private handleDiscoveryEvent(result: ZeroConfWatchResult) { + const currentServices = this.servicesSubject.getValue(); + const service = result.service; + + switch (result.action) { + case 'added': + // The service has been discovered but not yet resolved. + // We don't add it to the public list yet. We can log it for debugging. + console.log('[ZeroConf] Service added (unresolved):', service.name); + break; + case 'resolved': + // The service is now resolved with an IP address and port. + // Now we can add or update it in our list. + const existingIndex = currentServices.findIndex(s => s.name === service.name && s.type === service.type); + if (existingIndex > -1) { + currentServices[existingIndex] = service; + this.servicesSubject.next([...currentServices]); + } else { + this.servicesSubject.next([...currentServices, service]); + } + break; + case 'removed': + // Remove the service from the list + const filteredServices = currentServices.filter(s => s.name !== service.name || s.type !== service.type); + this.servicesSubject.next(filteredServices); + break; + } + } + + // Start watching for a specific service type + async watch(type: string, domain = 'local.') { + // Clear previous results before starting a new watch + this.servicesSubject.next([]); + console.log(`[ZeroConf] Watching for type: ${type}`); + await ZeroConf.watch({ type, domain }); + } + + // Publish a new service + // async publish(service: { type: string; name: string; port: number; props?: { [key: string]: string; } }) { + // await ZeroConf.register(service); + // console.log('[ZeroConf] Service published:', service.name); + // } + + // // Unpublish a service + // async unpublish(service: { type: string; name: string }) { + // await ZeroConf.unregister(service); + // console.log('[ZeroConf] Service unpublished:', service.name); + // } + + // Stop all operations and clean up + async stop() { + await ZeroConf.stop(); + await ZeroConf.close(); + this.servicesSubject.next([]); // Clear the list + console.log('[ZeroConf] Stopped all operations.'); + } + + // Angular's OnDestroy lifecycle hook for cleanup + ngOnDestroy() { + this.stop(); + // ZeroConf.removeAllListeners(); + } +} \ No newline at end of file From 918e1f64842cf9052ba7cf4515a8b9edb43128c4 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sat, 27 Sep 2025 09:01:01 +0200 Subject: [PATCH 02/10] zeroconf page --- src/app/pages/menu/menu.page.html | 7 +- .../pages/zeroconf/zeroconf-routing.module.ts | 17 ++++ src/app/pages/zeroconf/zeroconf.module.ts | 20 +++++ src/app/pages/zeroconf/zeroconf.page.html | 90 +++++++++++++++++++ src/app/pages/zeroconf/zeroconf.page.scss | 0 src/app/pages/zeroconf/zeroconf.page.spec.ts | 17 ++++ src/app/pages/zeroconf/zeroconf.page.ts | 68 ++++++++++++++ 7 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/app/pages/zeroconf/zeroconf-routing.module.ts create mode 100644 src/app/pages/zeroconf/zeroconf.module.ts create mode 100644 src/app/pages/zeroconf/zeroconf.page.html create mode 100644 src/app/pages/zeroconf/zeroconf.page.scss create mode 100644 src/app/pages/zeroconf/zeroconf.page.spec.ts create mode 100644 src/app/pages/zeroconf/zeroconf.page.ts diff --git a/src/app/pages/menu/menu.page.html b/src/app/pages/menu/menu.page.html index e804425..c9072a9 100644 --- a/src/app/pages/menu/menu.page.html +++ b/src/app/pages/menu/menu.page.html @@ -25,10 +25,15 @@ About + + + + ZeroConf Scan + -

v.{{version}}

+

v.{{version}}

\ No newline at end of file diff --git a/src/app/pages/zeroconf/zeroconf-routing.module.ts b/src/app/pages/zeroconf/zeroconf-routing.module.ts new file mode 100644 index 0000000..79096d7 --- /dev/null +++ b/src/app/pages/zeroconf/zeroconf-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { ZeroconfPage } from './zeroconf.page'; + +const routes: Routes = [ + { + path: '', + component: ZeroconfPage + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ZeroconfPageRoutingModule {} diff --git a/src/app/pages/zeroconf/zeroconf.module.ts b/src/app/pages/zeroconf/zeroconf.module.ts new file mode 100644 index 0000000..ee67d9d --- /dev/null +++ b/src/app/pages/zeroconf/zeroconf.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { IonicModule } from '@ionic/angular'; + +import { ZeroconfPageRoutingModule } from './zeroconf-routing.module'; + +import { ZeroconfPage } from './zeroconf.page'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + ZeroconfPageRoutingModule + ], + declarations: [ZeroconfPage] +}) +export class ZeroconfPageModule {} diff --git a/src/app/pages/zeroconf/zeroconf.page.html b/src/app/pages/zeroconf/zeroconf.page.html new file mode 100644 index 0000000..71fb7b8 --- /dev/null +++ b/src/app/pages/zeroconf/zeroconf.page.html @@ -0,0 +1,90 @@ + + + + + + + + + + Network Scanner + + + + + + + + + + + + + + + +
+ + + Discovered Services ({{ services.length }}) + + + + Searching for services... + + + + +

{{ service.hostname }}

+

{{ service.name }}

+

{{ service.type }}

+

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

+
+
+
+ + + +

Scanning for devices...

+
+
+ + + +

No services found.

+
+
+
+ Scan for Services +
\ No newline at end of file diff --git a/src/app/pages/zeroconf/zeroconf.page.scss b/src/app/pages/zeroconf/zeroconf.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/zeroconf/zeroconf.page.spec.ts b/src/app/pages/zeroconf/zeroconf.page.spec.ts new file mode 100644 index 0000000..88b1de1 --- /dev/null +++ b/src/app/pages/zeroconf/zeroconf.page.spec.ts @@ -0,0 +1,17 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ZeroconfPage } from './zeroconf.page'; + +describe('ZeroconfPage', () => { + let component: ZeroconfPage; + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(ZeroconfPage); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/zeroconf/zeroconf.page.ts b/src/app/pages/zeroconf/zeroconf.page.ts new file mode 100644 index 0000000..f8e1d17 --- /dev/null +++ b/src/app/pages/zeroconf/zeroconf.page.ts @@ -0,0 +1,68 @@ +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'; + + +@Component({ + selector: 'app-zeroconf', + templateUrl: './zeroconf.page.html', + styleUrls: ['./zeroconf.page.scss'], + standalone: false, +}) +export class ZeroconfPage implements OnDestroy { + services$: Observable; + private readonly SERVICE_TYPE = '_snapcast._tcp.'; + isScanning = false; + + constructor(private zeroconf: ZeroconfService) { + this.services$ = this.zeroconf.services$; + } + + async ngOnInit() { + + } + + async scanForServices(): Promise { + this.isScanning = true; + try { + await this.zeroconf.watch(this.SERVICE_TYPE); + console.log(`Started scanning for services of type: ${this.SERVICE_TYPE}`); + } + catch (error) { + console.error('Error starting service scan:', error); + } + } + + async getHostname(): Promise { + try { + const result = await ZeroConf.getHostname(); + console.log('Hostname:', result.hostname); + } catch (error) { + console.error('Error getting hostname:', error); + } + } + + async stopScan(): Promise { + this.isScanning = false; + try { + await this.zeroconf.stop(); + console.log('Stopped scanning for services.'); + } catch (error) { + console.error('Error stopping service scan:', error); + } + } + + // Example of publishing a service + // this.zeroconf.publish({ + // type: '_my-app._tcp.', + // name: 'My Angular App', + // port: 8080 + // }); + + + // Clean up when the component is destroyed + ngOnDestroy() { + this.stopScan(); + } +} From 17e2dc55ee22d5eb1fb69b739034747e274bcb99 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sat, 27 Sep 2025 09:11:08 +0200 Subject: [PATCH 03/10] zeroconf plugin update --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 7d0c41d..e97c4cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8486,7 +8486,7 @@ }, "node_modules/capacitor-zeroconf": { "version": "4.0.0", - "resolved": "git+ssh://git@github.com/byrdsandbytes/capacitor-zeroconf.git#12ec8738cd5ceba84cf499f3c8e4510233a5a15f", + "resolved": "git+ssh://git@github.com/byrdsandbytes/capacitor-zeroconf.git#b2a61a75d7c6e5c0d125cb6440c3cc42341a4d62", "license": "MIT", "peerDependencies": { "@capacitor/core": ">=7.0.0", From ef7c0d11556d9e0b039328c00d13def51a5e628d Mon Sep 17 00:00:00 2001 From: idrimi Date: Sat, 4 Oct 2025 19:09:44 +0200 Subject: [PATCH 04/10] zeroconf and setup server poc --- package-lock.json | 6 +- src/app/app-routing.module.ts | 4 + src/app/pages/dashboard/dashboard.page.html | 6 +- .../setup-server-routing.module.ts | 17 +++ .../setup/setup-server/setup-server.module.ts | 20 ++++ .../setup/setup-server/setup-server.page.html | 105 +++++++++++++++++ .../setup/setup-server/setup-server.page.scss | 80 +++++++++++++ .../setup-server/setup-server.page.spec.ts | 17 +++ .../setup/setup-server/setup-server.page.ts | 110 ++++++++++++++++++ src/app/services/snapcast.service.ts | 5 +- src/theme/variables.scss | 25 ++-- 11 files changed, 380 insertions(+), 15 deletions(-) create mode 100644 src/app/pages/setup/setup-server/setup-server-routing.module.ts create mode 100644 src/app/pages/setup/setup-server/setup-server.module.ts create mode 100644 src/app/pages/setup/setup-server/setup-server.page.html create mode 100644 src/app/pages/setup/setup-server/setup-server.page.scss create mode 100644 src/app/pages/setup/setup-server/setup-server.page.spec.ts create mode 100644 src/app/pages/setup/setup-server/setup-server.page.ts diff --git a/package-lock.json b/package-lock.json index e97c4cb..61a35d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "@types/lodash-es": "^4.17.12", "bootstrap": "^5.3.6", "bootstrap-icons": "^1.13.1", - "capacitor-zeroconf": "github:byrdsandbytes/capacitor-zeroconf", + "capacitor-zeroconf": "github:byrdsandbytes/capacitor-zeroconf#v7.0.0", "immer": "^10.1.1", "ionicons": "^7.0.0", "lodash-es": "^4.17.21", @@ -8485,8 +8485,8 @@ ] }, "node_modules/capacitor-zeroconf": { - "version": "4.0.0", - "resolved": "git+ssh://git@github.com/byrdsandbytes/capacitor-zeroconf.git#b2a61a75d7c6e5c0d125cb6440c3cc42341a4d62", + "version": "7.0.0", + "resolved": "git+ssh://git@github.com/byrdsandbytes/capacitor-zeroconf.git#2ddf8c51d9bfc41f81915b1a6337d7fbffe23145", "license": "MIT", "peerDependencies": { "@capacitor/core": ">=7.0.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index cac6317..cf9bf06 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -70,6 +70,10 @@ const routes: Routes = [ path: 'zeroconf', loadChildren: () => import('./pages/zeroconf/zeroconf.module').then( m => m.ZeroconfPageModule) }, + { + path: 'setup-server', + loadChildren: () => import('./pages/setup/setup-server/setup-server.module').then( m => m.SetupServerPageModule) + }, ]; diff --git a/src/app/pages/dashboard/dashboard.page.html b/src/app/pages/dashboard/dashboard.page.html index a279ade..626f18b 100644 --- a/src/app/pages/dashboard/dashboard.page.html +++ b/src/app/pages/dashboard/dashboard.page.html @@ -13,7 +13,6 @@ {{name ? name+"s": 'Your'}} Home -
@@ -49,7 +48,7 @@ {{name ? name+"s": 'Your'}} Devices - + @@ -126,6 +125,9 @@ Enable Demo Mode + + Setup Server +
diff --git a/src/app/pages/setup/setup-server/setup-server-routing.module.ts b/src/app/pages/setup/setup-server/setup-server-routing.module.ts new file mode 100644 index 0000000..aec6e81 --- /dev/null +++ b/src/app/pages/setup/setup-server/setup-server-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { SetupServerPage } from './setup-server.page'; + +const routes: Routes = [ + { + path: '', + component: SetupServerPage + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class SetupServerPageRoutingModule {} diff --git a/src/app/pages/setup/setup-server/setup-server.module.ts b/src/app/pages/setup/setup-server/setup-server.module.ts new file mode 100644 index 0000000..24a2f45 --- /dev/null +++ b/src/app/pages/setup/setup-server/setup-server.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { IonicModule } from '@ionic/angular'; + +import { SetupServerPageRoutingModule } from './setup-server-routing.module'; + +import { SetupServerPage } from './setup-server.page'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + SetupServerPageRoutingModule + ], + declarations: [SetupServerPage] +}) +export class SetupServerPageModule {} diff --git a/src/app/pages/setup/setup-server/setup-server.page.html b/src/app/pages/setup/setup-server/setup-server.page.html new file mode 100644 index 0000000..688da17 --- /dev/null +++ b/src/app/pages/setup/setup-server/setup-server.page.html @@ -0,0 +1,105 @@ + + + + + + + + + + + + + Add Device + + + + Turn on your Beatnik device and ensure it is connected to the same network as this app. Click the scan button to + discover devices or enter the IP address manually. + + + No devices found. Try scanning again or enter the IP address manually. + + +
+ +
+ +
+
+
+
+
+
+
+
+

Scanning for devices...

+
+ + Discovered Devices ({{ services.length }}) + + + +

Beatnik / {{ service.name }} Server

+

Host: {{ service.hostname }}

+

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

+
+
+
+ + +
+
+

Device Found. Checking Server Connection...

+
+ + Discovered Devices ({{ services.length }}) + + + +

Beatnik / {{ service.name }} Server

+

Host: {{ service.hostname }}

+

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

+
+
+ + + +

Server Status: {{ serverStatus.server.server.snapserver.version }}

+

Clients: {{ serverStatus.server.groups[0] }}

+ +
+
+
+
+ +
+

Waiting for user to start scan.

+
+
+
+ +
+
+ + + +
+ + Scan for Devices + +
+
+ + Stop Scan + +
+
+ + Enter URL or IP Manually + + + +
+
+
\ No newline at end of file diff --git a/src/app/pages/setup/setup-server/setup-server.page.scss b/src/app/pages/setup/setup-server/setup-server.page.scss new file mode 100644 index 0000000..8f66845 --- /dev/null +++ b/src/app/pages/setup/setup-server/setup-server.page.scss @@ -0,0 +1,80 @@ +.custom-icon { + font-size: 80px; +} + +.icon-container { + margin-top: 16px; + margin-bottom: 16px; + width: 200px; + height: 200px; + border-radius: 50%; + background-color: var(--ion-color-dark); + display: flex; + align-items: center; + justify-content: center; +} + +.animation-container { + width: 100%; + height: 300px; + display: flex; + align-items: center; + justify-content: center; + & .active { + animation: pulse 3s infinite; + } +} + +.outer-ring { + width: 220px; + height: 220px; + border: 4px solid var(--ion-color-primary); + border-radius: 50%; + background-color: var(--ion-color-primary); + position: absolute; + display: flex; + align-items: center; + justify-content: center; + & .active { + animation: pulse-ring 3s infinite; + } + z-index: -1; +} + +.inner-ring { + width: 200px; + height: 200px; + background-color: var(--ion-color-primary); + border-radius: 50%; + & .active { + animation: pulse-ring 3s infinite; + } +} + +// pulsing animation for the icon +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.9; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +// opacity and scale animation for the outer ring +@keyframes pulse-ring { + 0% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(1.5); + opacity: 0; + } +} diff --git a/src/app/pages/setup/setup-server/setup-server.page.spec.ts b/src/app/pages/setup/setup-server/setup-server.page.spec.ts new file mode 100644 index 0000000..b1ebdb5 --- /dev/null +++ b/src/app/pages/setup/setup-server/setup-server.page.spec.ts @@ -0,0 +1,17 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SetupServerPage } from './setup-server.page'; + +describe('SetupServerPage', () => { + let component: SetupServerPage; + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(SetupServerPage); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/setup/setup-server/setup-server.page.ts b/src/app/pages/setup/setup-server/setup-server.page.ts new file mode 100644 index 0000000..a91e2ab --- /dev/null +++ b/src/app/pages/setup/setup-server/setup-server.page.ts @@ -0,0 +1,110 @@ +import { Component, OnInit } from '@angular/core'; +import { ZeroconfService } from 'src/app/services/zero-conf.service'; +import { ZeroConf, ZeroConfService as ZeroConfServiceModel } from 'capacitor-zeroconf'; +import { firstValueFrom, Observable } from 'rxjs'; +import { SnapcastService } from 'src/app/services/snapcast.service'; +import { ServerDetail, SnapCastServerStatusResponse } from 'src/app/model/snapcast.model'; + + +@Component({ + selector: 'app-setup-server', + templateUrl: './setup-server.page.html', + styleUrls: ['./setup-server.page.scss'], + standalone: false, +}) +export class SetupServerPage implements OnInit { + + services$: Observable; + private readonly SERVICE_TYPE = '_snapcast._tcp.'; + isScanning = false; + state: 'initial' | 'scanning' | 'manual' | 'selected' | 'deviceFound' = 'initial'; + snapcastServerStatus: Observable | null = null; + + constructor( + private zeroconf: ZeroconfService, + private snapcastService: SnapcastService + ) { + this.services$ = this.zeroconf.services$; + } + + async ngOnInit() { + this.services$.subscribe(services => { + if (services.length > 0) { + this.state = 'deviceFound'; + this.connectToSnapcast(services[0]); + + } + }); + } + + async scanForServices(): Promise { + this.isScanning = true; + this.state = 'scanning'; + try { + await this.zeroconf.watch(this.SERVICE_TYPE); + console.log(`Started scanning for services of type: ${this.SERVICE_TYPE}`); + } + catch (error) { + console.error('Error starting service scan:', error); + } + } + + async getHostname(): Promise { + try { + const result = await ZeroConf.getHostname(); + console.log('Hostname:', result.hostname); + } catch (error) { + console.error('Error getting hostname:', error); + } + } + + async stopScan(): Promise { + this.isScanning = false; + try { + await this.zeroconf.stop(); + console.log('Stopped scanning for services.'); + } catch (error) { + console.error('Error stopping service scan:', error); + } + } + + async openManualEntry(): Promise { + // Logic to open a modal or navigate to a page for manual IP entry + console.log('Manual IP entry not implemented yet.'); + } + + async selectService(service: ZeroConfServiceModel): Promise { + // Logic to handle the selected service, e.g., save its IP and port + console.log('Selected service:', service); + } + + async connectToSnapcast(service: ZeroConfServiceModel): Promise { + console.log('Connecting to Snapcast service:', service); + console.log('hostname:', service.hostname); + console.log('port:', service.port); + // remove any trailing dot from the hostname + if (service.hostname.endsWith('.')) { + service.hostname = service.hostname.slice(0, -1); + } + + try { + await this.snapcastService.connect(service.ipv4Addresses[0],undefined, true); + + console.log('Connected to service:', service); + // this.state = 'selected'; + } catch (error) { + console.error('Error connecting to service:', error); + } + + try { + this.snapcastServerStatus = this.snapcastService.state$ + console.log('Fetching server status...'); + const status = await firstValueFrom(this.snapcastServerStatus); + console.log('Server status:', status); + } catch (error) { + console.error('Error fetching server status:', error); + } + } + + +} diff --git a/src/app/services/snapcast.service.ts b/src/app/services/snapcast.service.ts index f1bac09..e6fc3ca 100644 --- a/src/app/services/snapcast.service.ts +++ b/src/app/services/snapcast.service.ts @@ -90,11 +90,12 @@ export class SnapcastService implements OnDestroy { // --- Core Connection and RPC Logic --- - async connect(host: string = this.DEFAULT_HOSTNAME, port: number = this.DEFAULT_PORT): Promise { + async connect(host: string = this.DEFAULT_HOSTNAME, port: number = this.DEFAULT_PORT, overrideUserPreference: boolean = false): Promise { + console.log(`SnapcastService: connect called with host=${host}, port=${port}, overrideUserPreference=${overrideUserPreference}`); // Load user preference for hostname if available const url = await Preferences.get({ key: UserPreference.SERVER_URL }); // overwrite host if user did set a custom URL - if (url.value) { + if (url.value && !overrideUserPreference) { this.USERPREFERENCE_HOSTNAME = url.value; host = this.USERPREFERENCE_HOSTNAME; } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index b4d1a40..74cc16f 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -2,16 +2,12 @@ // http://ionicframework.com/docs/theming/ :root { - --ion-font-family: 'IBM Plex Mono', - monospace; - --ion-font-family-base: 'IBM Plex Mono', - monospace; + --ion-font-family: "IBM Plex Mono", monospace; + --ion-font-family-base: "IBM Plex Mono", monospace; // background --ion-background-color: #f5f3e8; /* Light background color */ - - --ion-color-secondary: #3ea3dc; /* Blue */ --ion-color-secondary-rgb: 62, 163, 220; --ion-color-secondary-contrast: #ffffff; @@ -82,6 +78,19 @@ --ion-color-white-shade: #e0e0e0; --ion-color-white-tint: #ffffff; - - + // Buttons + ion-button { + --border-radius: 0; + } + + // Underline text for clear buttons + .button-clear { + text-decoration: underline; + } + + // toolbar in light color + ion-toolbar { + --background: var(--ion-color-light); + --color: var(--ion-color-dark); + } } From a907e6fe69358de2f8f5981ceb3d2408070d7f95 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 5 Oct 2025 10:32:39 +0200 Subject: [PATCH 05/10] camillaDSP Congig model --- src/app/model/camilla-dsp.model.ts | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/app/model/camilla-dsp.model.ts diff --git a/src/app/model/camilla-dsp.model.ts b/src/app/model/camilla-dsp.model.ts new file mode 100644 index 0000000..b06e1a8 --- /dev/null +++ b/src/app/model/camilla-dsp.model.ts @@ -0,0 +1,66 @@ + + + +export interface CamillaDspConfig { + title: string | null; + description: string | null; + devices: Devices; + mixers: any; + filters: { [key: string]: Filter }; + processors: any; + pipeline: Pipeline[]; +} + +export interface Devices { + samplerate: number; + chunksize: number; + queuelimit: number | null; + silence_threshold: number | null; + silence_timeout: number | null; + capture: Capture; + playback: Playback; + enable_rate_adjust: boolean | null; + target_level: number | null; + adjust_period: number | null; + resampler: any; + capture_samplerate: number | null; + stop_on_rate_change: boolean | null; + rate_measure_interval: number | null; + volume_ramp_time: number | null; +} + +export interface Capture { + type: string; + channels: number; + device: string; + format: string; +} + +export interface Playback { + type: string; + channels: number; + device: string; + format: string; +} + +export interface Filter { + type: string; + description: string | null; + parameters: BiquadParameters; +} + +export interface BiquadParameters { + type: 'Peaking' | 'Lowshelf' | 'Highshelf'; + freq: number; + q: number; + gain: number; +} + +export interface Pipeline { + type: string; + channel: number; + names: string[]; + description: string | null; + bypassed: boolean | null; +} + From 77465a4d49b5ab7984afca9e209c25f29eeca490 Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 5 Oct 2025 10:32:53 +0200 Subject: [PATCH 06/10] camilla dsp testing page --- .../camilla-dsp/camilla-dsp-routing.module.ts | 17 +++ .../pages/camilla-dsp/camilla-dsp.module.ts | 20 ++++ .../pages/camilla-dsp/camilla-dsp.page.html | 44 ++++++++ .../pages/camilla-dsp/camilla-dsp.page.scss | 0 .../camilla-dsp/camilla-dsp.page.spec.ts | 17 +++ src/app/pages/camilla-dsp/camilla-dsp.page.ts | 104 ++++++++++++++++++ 6 files changed, 202 insertions(+) create mode 100644 src/app/pages/camilla-dsp/camilla-dsp-routing.module.ts create mode 100644 src/app/pages/camilla-dsp/camilla-dsp.module.ts create mode 100644 src/app/pages/camilla-dsp/camilla-dsp.page.html create mode 100644 src/app/pages/camilla-dsp/camilla-dsp.page.scss create mode 100644 src/app/pages/camilla-dsp/camilla-dsp.page.spec.ts create mode 100644 src/app/pages/camilla-dsp/camilla-dsp.page.ts diff --git a/src/app/pages/camilla-dsp/camilla-dsp-routing.module.ts b/src/app/pages/camilla-dsp/camilla-dsp-routing.module.ts new file mode 100644 index 0000000..002be93 --- /dev/null +++ b/src/app/pages/camilla-dsp/camilla-dsp-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { CamillaDspPage } from './camilla-dsp.page'; + +const routes: Routes = [ + { + path: '', + component: CamillaDspPage + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class CamillaDspPageRoutingModule {} diff --git a/src/app/pages/camilla-dsp/camilla-dsp.module.ts b/src/app/pages/camilla-dsp/camilla-dsp.module.ts new file mode 100644 index 0000000..5876f7a --- /dev/null +++ b/src/app/pages/camilla-dsp/camilla-dsp.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { IonicModule } from '@ionic/angular'; + +import { CamillaDspPageRoutingModule } from './camilla-dsp-routing.module'; + +import { CamillaDspPage } from './camilla-dsp.page'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + CamillaDspPageRoutingModule + ], + declarations: [CamillaDspPage] +}) +export class CamillaDspPageModule {} diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.html b/src/app/pages/camilla-dsp/camilla-dsp.page.html new file mode 100644 index 0000000..a156f7d --- /dev/null +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.html @@ -0,0 +1,44 @@ + + + camilla-dsp + + + + + + + camilla-dsp + + + +
+

CamillaDSP Control

+

Connection Status: {{ connectionStatus }}

+ + + + + + +
{{ lastMessage | json }}
+
{{ parsedConfig | json }}
+
+
+

Parsed Configuration

+
+ {{ filter.key }}: + + Type: {{ filter.value.type }} + + + Gain: {{ filter.value.parameters.gain }} + + + Frequency: {{ filter.value.parameters.freq }} + + + {{ param.key }}: {{ param.value }} + + + +
\ No newline at end of file diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.scss b/src/app/pages/camilla-dsp/camilla-dsp.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.spec.ts b/src/app/pages/camilla-dsp/camilla-dsp.page.spec.ts new file mode 100644 index 0000000..a2148cd --- /dev/null +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.spec.ts @@ -0,0 +1,17 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CamillaDspPage } from './camilla-dsp.page'; + +describe('CamillaDspPage', () => { + let component: CamillaDspPage; + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(CamillaDspPage); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.ts b/src/app/pages/camilla-dsp/camilla-dsp.page.ts new file mode 100644 index 0000000..1322634 --- /dev/null +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.ts @@ -0,0 +1,104 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { CamillaDspConfig } from 'src/app/model/camilla-dsp.model'; +import { CamillaDspService, ConnectionStatus } from 'src/app/services/camilla-dsp.service'; + +@Component({ + selector: 'app-camilla-dsp', + templateUrl: './camilla-dsp.page.html', + styleUrls: ['./camilla-dsp.page.scss'], + standalone: false +}) +export class CamillaDspPage implements OnInit, OnDestroy { + + connectionStatus: ConnectionStatus = 'Disconnected'; + lastMessage: any = { message: 'No messages received yet.' }; + private subscriptions = new Subscription(); + private configJson: any = null; + parsedConfig: CamillaDspConfig | null = null; + // Your CamillaDSP URL + private readonly CAMILLA_URL = 'ws://beatnik-client-amp.local:1234'; + + constructor(private camillaService: CamillaDspService) {} + + ngOnInit() { + // Subscribe to connection status changes + this.subscriptions.add( + this.camillaService.connectionStatus$.subscribe(status => { + this.connectionStatus = status; + }) + ); + + // Subscribe to incoming messages + this.subscriptions.add( + this.camillaService.messages$.subscribe(message => { + console.log('Received message in component:', message); + this.lastMessage = message; + if (message.GetConfigJson) { + console.log('Config JSON received:', message.GetConfigJson); + this.configJson = message.GetConfigJson.value; + console.log('Unparsed Config JSON:', this.configJson); + try { + this.configJson = JSON.parse(this.configJson); + console.log('Parsed Config JSON:', this.configJson); + this.parsedConfig = this.configJson; + } catch (error) { + console.error('Error parsing Config JSON:', error); + } + } + }) + ); + } + + connect() { + this.camillaService.connect(this.CAMILLA_URL); + } + + disconnect() { + this.camillaService.disconnect(); + } + + getState() { + this.camillaService.sendCommand('GetState'); + } + + getConfigJson() { + this.camillaService.sendCommand('GetConfigJson'); + } + + getConfigYaml() { + this.camillaService.sendCommand('GetConfig'); + } + + // onParameterChange(filter.key, param.key, param.value) + updateFilterParameter(filterKey: string, paramKey: string, newValue: any) { + if (!this.parsedConfig) { + console.error('No configuration loaded.'); + return; + } + + const filter = this.parsedConfig.filters[filterKey]; + if (!filter) { + console.error(`Filter with key ${filterKey} not found.`); + return; + } + + // Update the parameter locally + (filter.parameters as any)[paramKey] = newValue; + + // format the conffigJson to send to CamillaDSP + console.log('Updated filter parameter:', filterKey, paramKey, newValue); + console.log('Updated configuration to send:', this.parsedConfig); + + + + // send the full configJson back to CamillaDSP + this.camillaService.sendCommand('SetConfigJson', JSON.stringify(this.parsedConfig)); + + } + ngOnDestroy() { + // Clean up subscriptions to prevent memory leaks + this.subscriptions.unsubscribe(); + this.camillaService.disconnect(); + } +} \ No newline at end of file From 1e394d4adfa13818ed17cd074cad684108dda15a Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 5 Oct 2025 10:33:05 +0200 Subject: [PATCH 07/10] camilla dsp link --- src/app/pages/menu/menu.page.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/pages/menu/menu.page.html b/src/app/pages/menu/menu.page.html index c9072a9..3e57549 100644 --- a/src/app/pages/menu/menu.page.html +++ b/src/app/pages/menu/menu.page.html @@ -30,6 +30,11 @@ ZeroConf Scan + + + Camilla DSP Setup + + From 81195f892d9d15705f7bd72a3f755c0bd89cccea Mon Sep 17 00:00:00 2001 From: idrimi Date: Sun, 5 Oct 2025 10:33:17 +0200 Subject: [PATCH 08/10] camilla dsp service --- src/app/app-routing.module.ts | 4 + src/app/services/camilla-dsp.service.spec.ts | 16 +++ src/app/services/camilla-dsp.service.ts | 109 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 src/app/services/camilla-dsp.service.spec.ts create mode 100644 src/app/services/camilla-dsp.service.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index cf9bf06..28da52f 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -74,6 +74,10 @@ const routes: Routes = [ path: 'setup-server', loadChildren: () => import('./pages/setup/setup-server/setup-server.module').then( m => m.SetupServerPageModule) }, + { + path: 'camilla-dsp', + loadChildren: () => import('./pages/camilla-dsp/camilla-dsp.module').then( m => m.CamillaDspPageModule) + }, ]; diff --git a/src/app/services/camilla-dsp.service.spec.ts b/src/app/services/camilla-dsp.service.spec.ts new file mode 100644 index 0000000..879b2d6 --- /dev/null +++ b/src/app/services/camilla-dsp.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CamillaDspService } from './camilla-dsp.service'; + +describe('CamillaDspService', () => { + let service: CamillaDspService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CamillaDspService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/camilla-dsp.service.ts b/src/app/services/camilla-dsp.service.ts new file mode 100644 index 0000000..cf03461 --- /dev/null +++ b/src/app/services/camilla-dsp.service.ts @@ -0,0 +1,109 @@ +import { Injectable } from '@angular/core'; +import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; +import { Subject, Observable, BehaviorSubject, timer, of } from 'rxjs'; +import { retryWhen, switchMap, tap, delayWhen } from 'rxjs/operators'; + +// Defines the possible connection states +export type ConnectionStatus = 'Connected' | 'Connecting' | 'Disconnected' | 'Error'; + +// A simple type for the JSON commands CamillaDSP expects +// Example: { "command": "GetState", "params": null } +interface CamillaDspCommand { + [command: string]: any; +} + +@Injectable({ + providedIn: 'root', +}) +export class CamillaDspService { + private socket$!: WebSocketSubject; + private messagesSubject = new Subject(); + private connectionStatusSubject = new BehaviorSubject('Disconnected'); + private readonly RECONNECT_INTERVAL_MS = 5000; + + // Public observables for components to subscribe to + public messages$: Observable = this.messagesSubject.asObservable(); + public connectionStatus$: Observable = this.connectionStatusSubject.asObservable(); + + constructor() {} + + /** + * Establishes a connection to the CamillaDSP WebSocket server. + * @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; + } + + this.connectionStatusSubject.next('Connecting'); + console.log(`Connecting to ${url}...`); + + this.socket$ = webSocket({ + url: url, + openObserver: { + next: () => { + console.log('WebSocket connection established successfully! 🎉'); + this.connectionStatusSubject.next('Connected'); + }, + }, + closeObserver: { + next: () => { + console.log('WebSocket connection closed.'); + this.connectionStatusSubject.next('Disconnected'); + }, + }, + }); + + 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)) + ) + ) + ) + .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'); + }, + }); + } + + /** + * Sends a command to the CamillaDSP server. + * @param command The command name (e.g., 'GetState', 'SetConfigJson'). + * @param params Optional parameters for the command. + */ + public sendCommand(command: string, params: any = null): void { + if (this.connectionStatusSubject.value !== 'Connected') { + console.warn('Cannot send command while not connected.'); + return; + } + + // CamillaDSP expects a JSON object where the key is the command name + const commandToSend: CamillaDspCommand = { [command]: params }; + + console.log('Sending command:', commandToSend); + this.socket$.next(commandToSend); + } + + /** + * Closes the WebSocket connection gracefully. + */ + public disconnect(): void { + if (this.socket$) { + this.socket$.complete(); // This will trigger the closeObserver + } + } +} From 5fa3d694d85152d9405a999dc63548bf430d9895 Mon Sep 17 00:00:00 2001 From: idrimi Date: Mon, 6 Oct 2025 09:40:48 +0200 Subject: [PATCH 09/10] get signal levels --- src/app/model/camilla-dsp.model.ts | 10 ++-- .../pages/camilla-dsp/camilla-dsp.page.html | 51 ++++++++++++++++--- .../pages/camilla-dsp/camilla-dsp.page.scss | 7 +++ src/app/pages/camilla-dsp/camilla-dsp.page.ts | 35 +++++++++++-- src/app/services/camilla-dsp.service.ts | 27 +++++++++- 5 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/app/model/camilla-dsp.model.ts b/src/app/model/camilla-dsp.model.ts index b06e1a8..056a3a8 100644 --- a/src/app/model/camilla-dsp.model.ts +++ b/src/app/model/camilla-dsp.model.ts @@ -1,6 +1,3 @@ - - - export interface CamillaDspConfig { title: string | null; description: string | null; @@ -64,3 +61,10 @@ export interface Pipeline { bypassed: boolean | null; } +export interface SignalLevels { + playback_rms: number[]; + playback_peak: number[]; + capture_rms: number[]; + capture_peak: number[]; +} + diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.html b/src/app/pages/camilla-dsp/camilla-dsp.page.html index a156f7d..4c357f2 100644 --- a/src/app/pages/camilla-dsp/camilla-dsp.page.html +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.html @@ -1,5 +1,8 @@ + + + camilla-dsp @@ -14,15 +17,43 @@

CamillaDSP Control

Connection Status: {{ connectionStatus }}

- - - - - + Connect + Disconnect + Get State + Get Config JSON + Get Config YAML + Get Update Interval + Get Capture Signal Levels + Set update Interval
{{ lastMessage | json }}
{{ parsedConfig | json }}
+
+

Signal Levels

+
+

Capture (Snapcast Signal)

+

Capture RMS 0{{ levels.capture_rms[0] }} dB

+
+

Capture RMS 1{{ levels.capture_rms[1] }} dB

+
+

Capture Peak 0{{ levels.capture_peak[0] }} dB

+
+

Capture Peak 1 {{ levels.capture_peak[1] }} dB

+
+ +

Playback

+ +

Playback RMS 0{{ levels.playback_rms[0] }} dB

+
+

Playback RMS 1{{ levels.playback_rms[1] }} dB

+
+

Playback Peak 0{{ levels.playback_peak[0] }} dB

+
+

Playback Peak 1 {{ levels.playback_peak[1] }} dB

+
+
+

Parsed Configuration

@@ -32,13 +63,17 @@

Parsed Configuration

Gain: {{ filter.value.parameters.gain }} + Frequency: {{ filter.value.parameters.freq }} - + + Q: {{ filter.value.parameters.q }} + + + - + -->
\ No newline at end of file diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.scss b/src/app/pages/camilla-dsp/camilla-dsp.page.scss index e69de29..8a46277 100644 --- a/src/app/pages/camilla-dsp/camilla-dsp.page.scss +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.scss @@ -0,0 +1,7 @@ +.bar { + height: 20px; + background-color: var(--ion-color-primary); + margin: 2px 0; +// transition: width 0.01s; + width: 20px; +} \ No newline at end of file diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.ts b/src/app/pages/camilla-dsp/camilla-dsp.page.ts index 1322634..baddb22 100644 --- a/src/app/pages/camilla-dsp/camilla-dsp.page.ts +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.ts @@ -1,6 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { set } from 'lodash-es'; import { Subscription } from 'rxjs'; -import { CamillaDspConfig } from 'src/app/model/camilla-dsp.model'; +import { CamillaDspConfig, SignalLevels } from 'src/app/model/camilla-dsp.model'; import { CamillaDspService, ConnectionStatus } from 'src/app/services/camilla-dsp.service'; @Component({ @@ -18,8 +19,11 @@ export class CamillaDspPage implements OnInit, OnDestroy { parsedConfig: CamillaDspConfig | null = null; // Your CamillaDSP URL private readonly CAMILLA_URL = 'ws://beatnik-client-amp.local:1234'; + levels: SignalLevels | null = null; - constructor(private camillaService: CamillaDspService) {} + private levelSubscription: Subscription | undefined; + + constructor(private camillaService: CamillaDspService) { } ngOnInit() { // Subscribe to connection status changes @@ -45,9 +49,18 @@ export class CamillaDspPage implements OnInit, OnDestroy { } catch (error) { console.error('Error parsing Config JSON:', error); } + } else if (message.GetSignalLevels) { + this.levels = message.GetSignalLevels.value; + console.log('Signal Levels received:', this.levels); } }) ); + + this.levelSubscription = this.camillaService.signalLevels$.subscribe(message => { + console.log('Received message in component:', message); + this.levels = message.SignalLevels.value; + console.log('Signal Levels received:', this.levels); + }); } connect() { @@ -95,7 +108,23 @@ export class CamillaDspPage implements OnInit, OnDestroy { // send the full configJson back to CamillaDSP this.camillaService.sendCommand('SetConfigJson', JSON.stringify(this.parsedConfig)); - } + } + + getUpdateInterval() { + this.camillaService.sendCommand('GetUpdateInterval'); + } + + getCaptureSignalLevels() { + const interval = 50; // e.g., 100 ms + setInterval(() => { + this.camillaService.sendCommand('GetSignalLevels'); + }, interval); + } + + setUpdateInterval(interval: number) { + this.camillaService.startLevelUpdates(interval); + } + ngOnDestroy() { // Clean up subscriptions to prevent memory leaks this.subscriptions.unsubscribe(); diff --git a/src/app/services/camilla-dsp.service.ts b/src/app/services/camilla-dsp.service.ts index cf03461..b6df1a7 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 { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { Subject, Observable, BehaviorSubject, timer, of } from 'rxjs'; -import { retryWhen, switchMap, tap, delayWhen } from 'rxjs/operators'; +import { retryWhen, switchMap, tap, delayWhen, filter } from 'rxjs/operators'; // Defines the possible connection states export type ConnectionStatus = 'Connected' | 'Connecting' | 'Disconnected' | 'Error'; @@ -24,8 +24,13 @@ export class CamillaDspService { // Public observables for components to subscribe to public messages$: Observable = this.messagesSubject.asObservable(); public connectionStatus$: Observable = this.connectionStatusSubject.asObservable(); + public signalLevels$: Observable; - constructor() {} + constructor() { + this.signalLevels$ = this.messages$.pipe( + filter(msg => msg && msg.SignalLevels) + ); + } /** * Establishes a connection to the CamillaDSP WebSocket server. @@ -98,11 +103,29 @@ export class CamillaDspService { this.socket$.next(commandToSend); } + /** + * Tells CamillaDSP to start sending SignalLevels messages periodically. + * @param intervalMs The update interval in milliseconds. + */ + public startLevelUpdates(intervalMs: number): void { + if (intervalMs > 0) { + this.sendCommand('SetUpdateInterval', intervalMs); + } + } + + /** + * Tells CamillaDSP to stop sending SignalLevels messages. + */ + public stopLevelUpdates(): void { + this.sendCommand('SetUpdateInterval', { value: 0 }); + } + /** * Closes the WebSocket connection gracefully. */ public disconnect(): void { if (this.socket$) { + this.stopLevelUpdates(); this.socket$.complete(); // This will trigger the closeObserver } } From 18980cdcb745c98145084a8bdf20102a2e3ec289 Mon Sep 17 00:00:00 2001 From: idrimi Date: Fri, 17 Oct 2025 07:45:51 +0200 Subject: [PATCH 10/10] mixer model and signals --- src/app/model/camilla-dsp.model.ts | 27 ++++++- .../pages/camilla-dsp/camilla-dsp.page.html | 73 ++++++++++++++----- src/app/pages/camilla-dsp/camilla-dsp.page.ts | 28 ++++++- 3 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/app/model/camilla-dsp.model.ts b/src/app/model/camilla-dsp.model.ts index 056a3a8..6c3a65e 100644 --- a/src/app/model/camilla-dsp.model.ts +++ b/src/app/model/camilla-dsp.model.ts @@ -2,7 +2,7 @@ export interface CamillaDspConfig { title: string | null; description: string | null; devices: Devices; - mixers: any; + mixers: { [key: string]: Mixer }; filters: { [key: string]: Filter }; processors: any; pipeline: Pipeline[]; @@ -68,3 +68,28 @@ export interface SignalLevels { capture_peak: number[]; } +export interface Mixer { + description: string | null; + channels: MixerChannels; + mapping: MixerMapping[]; +} + +export interface MixerChannels { + in: number; + out: number; +} + +export interface MixerMapping { + dest: number; + sources: MixerSource[]; + mute: boolean | null; +} + +export interface MixerSource { + channel: number; + gain: number; + inverted: boolean | null; + mute: boolean; + scale: string | null; +} + diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.html b/src/app/pages/camilla-dsp/camilla-dsp.page.html index 4c357f2..8ba6ba6 100644 --- a/src/app/pages/camilla-dsp/camilla-dsp.page.html +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.html @@ -16,23 +16,30 @@

CamillaDSP Control

+

Connection Status: {{ connectionStatus }}

- Connect + Connect Disconnect - Get State + Get State Get Config JSON - Get Config YAML - Get Update Interval - Get Capture Signal Levels - Set update Interval - + Get Config YAML + Get Update Interval + Get Capture Signal Levels + Set update Interval + Get Volume + + Volume: {{ currentVolume }} + +" +
{{ lastMessage | json }}
{{ parsedConfig | json }}
+

Signal Levels

-

Capture (Snapcast Signal)

+

Capture (Snapcast Signal)

Capture RMS 0{{ levels.capture_rms[0] }} dB

Capture RMS 1{{ levels.capture_rms[1] }} dB

@@ -41,7 +48,7 @@

Capture (Snapcast Signal)

Capture Peak 1 {{ levels.capture_peak[1] }} dB

- +

Playback

Playback RMS 0{{ levels.playback_rms[0] }} dB

@@ -56,24 +63,50 @@

Playback

Parsed Configuration

-
- {{ filter.key }}: - +
+ {{ filter.key }}: + Type: {{ filter.value.type }} - - + + Gain: {{ filter.value.parameters.gain }} - - + + - Frequency: {{ filter.value.parameters.freq }} + Frequency: {{ filter.value.parameters.freq }} - Q: {{ filter.value.parameters.q }} + Q: {{ filter.value.parameters.q }} - -
\ No newline at end of file +
+
+ {{ mixer.key }}: + + Channels: {{ mixer.value.channels }} + +
+

Destination {{ mapping.dest }} + (Left) + (Right) +

+
+

Source {{ source.channel }} - Gain: {{ source.gain }}

+ +

Inverted: {{ source.inverted}}

+ +

Muted: {{ source.mute}}

+ +
+ +
+
+ + +
\ No newline at end of file diff --git a/src/app/pages/camilla-dsp/camilla-dsp.page.ts b/src/app/pages/camilla-dsp/camilla-dsp.page.ts index baddb22..a30e264 100644 --- a/src/app/pages/camilla-dsp/camilla-dsp.page.ts +++ b/src/app/pages/camilla-dsp/camilla-dsp.page.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { set } from 'lodash-es'; import { Subscription } from 'rxjs'; -import { CamillaDspConfig, SignalLevels } from 'src/app/model/camilla-dsp.model'; +import { CamillaDspConfig, MixerSource, SignalLevels } from 'src/app/model/camilla-dsp.model'; import { CamillaDspService, ConnectionStatus } from 'src/app/services/camilla-dsp.service'; @Component({ @@ -18,8 +18,9 @@ export class CamillaDspPage implements OnInit, OnDestroy { private configJson: any = null; parsedConfig: CamillaDspConfig | null = null; // Your CamillaDSP URL - private readonly CAMILLA_URL = 'ws://beatnik-client-amp.local:1234'; + CAMILLA_URL = 'ws://beatnik-server.local:1234'; levels: SignalLevels | null = null; + currentVolume: number = 0; private levelSubscription: Subscription | undefined; @@ -110,6 +111,19 @@ export class CamillaDspPage implements OnInit, OnDestroy { } + updateMixerMapping(mixerSource:MixerSource, mixerKey: string) { + if (!this.parsedConfig) { + console.error('No configuration loaded.'); + return; + } + + + // Send the full configJson back to CamillaDSP + this.camillaService.sendCommand('SetConfigJson', JSON.stringify(this.parsedConfig)); + + + } + getUpdateInterval() { this.camillaService.sendCommand('GetUpdateInterval'); } @@ -125,6 +139,16 @@ export class CamillaDspPage implements OnInit, OnDestroy { this.camillaService.startLevelUpdates(interval); } + getVolume() { + this.camillaService.sendCommand('GetVolume'); + this.currentVolume = this.lastMessage.GetVolume?.value ?? this.currentVolume; + console.log('Current volume:', this.currentVolume); + } + + setVolume(volume: number) { + this.camillaService.sendCommand('SetVolume', volume); + } + ngOnDestroy() { // Clean up subscriptions to prevent memory leaks this.subscriptions.unsubscribe();