diff --git a/README.md b/README.md index 4bc28db..a86d8c9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RadioCast +# RadioPlayer A lightweight, cross-platform radio player built with Tauri and Vanilla JavaScript. Features local playback and Google Cast integration. @@ -13,10 +13,10 @@ Before you begin, ensure you have the following installed on your machine: ## Installation 1. **Clone the repository**: - ```bash - git clone - cd RadioCast - ``` + ```bash + git clone + cd RadioPlayer + ``` 2. **Install dependencies**: ```bash @@ -106,3 +106,40 @@ To change the default window size, edit `src-tauri/tauri.conf.json`: ## License [Add License Information Here] + + +## Release v0.1 + +Initial public preview (v0.1) — a minimal, working RadioPlayer experience: + +- Custom CAF Receiver UI (HTML/CSS/JS) in `receiver/` with branded artwork and playback status. +- Plays LIVE stream: `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`). +- Desktop sidecar (`sidecar/index.js`) launches the Default Media Receiver and sends LOAD commands; launch flow now retries if the device reports `NOT_ALLOWED` by stopping existing sessions first. + +Included receiver files: + +- `receiver/index.html` +- `receiver/receiver.js` (CAF Receiver initialization + LOAD interceptor for LIVE metadata) +- `receiver/styles.css` +- `receiver/assets/logo.svg` + +Quick testing notes + +- The receiver must be served over HTTPS for Cast devices to load it. For quick local testing you can use `mkcert` + a static HTTPS server: + +```bash +# create local certs +mkcert -install +mkcert localhost + +# serve the receiver folder over HTTPS +npx http-server receiver -p 8443 -S -C localhost.pem -K localhost-key.pem +``` + +- Use the Default Media Receiver App ID while developing, or register a Custom Receiver App in the Cast Developer Console and point its URL to your hosted `index.html` for production. + +Sidecar / troubleshoot + +- If a Cast launch fails with `NOT_ALLOWED`, the sidecar will now attempt to stop any existing sessions on the device and retry the launch (best-effort). Check sidecar logs for `Launch NOT_ALLOWED` and subsequent retry attempts. +- Note: the sidecar uses `castv2-client` (not the official Google sender SDK). Group/stereo behavior may vary across device types — for full sender capabilities consider adding an official sender implementation. + diff --git a/app-icon.png b/app-icon.png new file mode 100644 index 0000000..8f1299a Binary files /dev/null and b/app-icon.png differ diff --git a/package-lock.json b/package-lock.json index a2da348..0d043f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "name": "radio-tauri", "version": "0.1.0", "devDependencies": { - "@tauri-apps/cli": "^2" + "@tauri-apps/cli": "^2", + "rcedit": "^1.1.2" } }, "node_modules/@tauri-apps/cli": { @@ -227,6 +228,13 @@ "engines": { "node": ">= 10" } + }, + "node_modules/rcedit": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.1.2.tgz", + "integrity": "sha512-z2ypB4gbINhI6wVe0JJMmdpmOpmNc4g90sE6/6JSuch5kYnjfz9CxvVPqqhShgR6GIkmtW3W2UlfiXhWljA0Fw==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 6ba1d6a..7b3d63d 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "type": "module", "scripts": { "dev": "tauri dev", - "build": "tauri build", - "tauri": "tauri" + "build": "node tools/copy-binaries.js && tauri build && node tools/post-build-rcedit.js", + "tauri": "node tools/copy-binaries.js && tauri" }, "devDependencies": { - "@tauri-apps/cli": "^2" + "@tauri-apps/cli": "^2", + "rcedit": "^1.1.2" } } diff --git a/receiver/assets/logo.svg b/receiver/assets/logo.svg new file mode 100644 index 0000000..d093b4e --- /dev/null +++ b/receiver/assets/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + Radio + + diff --git a/receiver/index.html b/receiver/index.html new file mode 100644 index 0000000..a49f4c5 --- /dev/null +++ b/receiver/index.html @@ -0,0 +1,27 @@ + + + + + + Radio Player + + + + + + + +
+

Radio Player

+

Ready

+ +
+ Radio Player +
+ +

Radio 1 – Live Stream

+
+ + + + diff --git a/receiver/receiver.js b/receiver/receiver.js new file mode 100644 index 0000000..4e82e93 --- /dev/null +++ b/receiver/receiver.js @@ -0,0 +1,73 @@ +/* Receiver for "Radio Player" using CAF Receiver SDK */ +(function () { + const STREAM_URL = 'https://live.radio1.si/Radio1MB'; + + function $(id) { return document.getElementById(id); } + + document.addEventListener('DOMContentLoaded', () => { + const context = cast.framework.CastReceiverContext.getInstance(); + const playerManager = context.getPlayerManager(); + const statusEl = $('status'); + const stationEl = $('station'); + + // Intercept LOAD to enforce correct metadata for LIVE audio + playerManager.setMessageInterceptor( + cast.framework.messages.MessageType.LOAD, + (request) => { + if (!request || !request.media) return request; + + request.media.contentId = request.media.contentId || STREAM_URL; + request.media.contentType = 'audio/mpeg'; + request.media.streamType = cast.framework.messages.StreamType.LIVE; + + request.media.metadata = request.media.metadata || {}; + request.media.metadata.title = request.media.metadata.title || 'Radio 1'; + request.media.metadata.images = request.media.metadata.images || [{ url: 'assets/logo.svg' }]; + + return request; + } + ); + + // Update UI on player state changes + playerManager.addEventListener( + cast.framework.events.EventType.PLAYER_STATE_CHANGED, + () => { + const state = playerManager.getPlayerState(); + switch (state) { + case cast.framework.messages.PlayerState.PLAYING: + statusEl.textContent = 'Playing'; + break; + case cast.framework.messages.PlayerState.PAUSED: + statusEl.textContent = 'Paused'; + break; + case cast.framework.messages.PlayerState.IDLE: + statusEl.textContent = 'Stopped'; + break; + default: + statusEl.textContent = state; + } + } + ); + + // When a new media is loaded, reflect metadata (station name, artwork) + playerManager.addEventListener(cast.framework.events.EventType.LOAD, (event) => { + const media = event && event.data && event.data.media; + if (media && media.metadata) { + if (media.metadata.title) stationEl.textContent = media.metadata.title; + if (media.metadata.images && media.metadata.images[0] && media.metadata.images[0].url) { + const img = document.querySelector('#artwork img'); + img.src = media.metadata.images[0].url; + } + } + }); + + // Optional: reflect volume in title attribute + playerManager.addEventListener(cast.framework.events.EventType.VOLUME_CHANGED, (evt) => { + const level = evt && evt.data && typeof evt.data.level === 'number' ? evt.data.level : null; + if (level !== null) statusEl.title = `Volume: ${Math.round(level * 100)}%`; + }); + + // Start the cast receiver context + context.start({ statusText: 'Radio Player Ready' }); + }); +})(); diff --git a/receiver/styles.css b/receiver/styles.css new file mode 100644 index 0000000..baefcf4 --- /dev/null +++ b/receiver/styles.css @@ -0,0 +1,58 @@ +html, body { + margin: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, #7b7fd8, #b57cf2); + font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + color: white; +} + +#app { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 24px; + box-sizing: border-box; +} + +#artwork { + width: 240px; + height: 240px; + margin: 20px 0; + border-radius: 24px; + overflow: hidden; + background: rgba(0,0,0,0.1); + box-shadow: 0 8px 24px rgba(0,0,0,0.2); +} + +#artwork img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +#status { + font-size: 18px; + opacity: 0.95; + margin: 6px 0 0 0; +} + +#station { + font-size: 16px; + opacity: 0.85; + margin: 6px 0 0 0; +} + +h1 { + font-size: 20px; + margin: 0 0 6px 0; +} + +@media (max-width: 480px) { + #artwork { width: 160px; height: 160px; } + h1 { font-size: 18px; } +} diff --git a/sidecar/index.js b/sidecar/index.js new file mode 100644 index 0000000..e84a5f8 --- /dev/null +++ b/sidecar/index.js @@ -0,0 +1,215 @@ +const { Client, DefaultMediaReceiver } = require('castv2-client'); +const readline = require('readline'); + +const rl = readline.createInterface({ + input: process.stdin, + terminal: false +}); + +let activeClient = null; +let activePlayer = null; + +function isNotAllowedError(err) { + if (!err) return false; + const msg = (err.message || String(err)).toUpperCase(); + return msg.includes('NOT_ALLOWED') || msg.includes('NOT ALLOWED'); +} + +function stopSessions(client, sessions, cb) { + if (!client || !sessions || sessions.length === 0) return cb(); + + const remaining = sessions.slice(); + const stopNext = () => { + const session = remaining.shift(); + if (!session) return cb(); + + client.stop(session, (err) => { + if (err) { + log(`Stop session failed (${session.appId || 'unknown app'}): ${err.message || String(err)}`); + } else { + log(`Stopped session (${session.appId || 'unknown app'})`); + } + // Continue regardless; best-effort. + stopNext(); + }); + }; + + stopNext(); +} + +function log(msg) { + console.log(JSON.stringify({ type: 'log', message: msg })); +} + +function error(msg) { + console.error(JSON.stringify({ type: 'error', message: msg })); +} + +rl.on('line', (line) => { + try { + const data = JSON.parse(line); + const { command, args } = data; + + switch (command) { + case 'play': + play(args.ip, args.url); + break; + case 'stop': + stop(); + break; + case 'volume': + setVolume(args.level); + break; + default: + error(`Unknown command: ${command}`); + } + } catch (e) { + error(`Failed to parse line: ${e.message}`); + } +}); + +function play(ip, url) { + if (activeClient) { + try { activeClient.close(); } catch (e) { } + } + + activeClient = new Client(); + + activeClient.connect(ip, () => { + log(`Connected to ${ip}`); + + // First, check if DefaultMediaReceiver is already running + activeClient.getSessions((err, sessions) => { + if (err) return error(`GetSessions error: ${err.message}`); + + // Log sessions for debugging (appId/sessionId if available) + try { + const sessInfo = sessions.map(s => ({ appId: s.appId, sessionId: s.sessionId, displayName: s.displayName })); + log(`Sessions: ${JSON.stringify(sessInfo)}`); + } catch (e) { + log('Sessions: (unable to stringify)'); + } + + // DefaultMediaReceiver App ID is CC1AD845 + const session = sessions.find(s => s.appId === 'CC1AD845'); + + if (session) { + log('Session already running, joining...'); + activeClient.join(session, DefaultMediaReceiver, (err, player) => { + if (err) { + log('Join failed, attempting launch...'); + log(`Join error: ${err && err.message ? err.message : String(err)}`); + // Join can fail if the session is stale; stop it and retry launch. + stopSessions(activeClient, [session], () => launchPlayer(url, /*didStopFirst*/ true)); + } else { + activePlayer = player; + loadMedia(url); + } + }); + } else { + // If another app is running, stop it first to avoid NOT_ALLOWED. + if (sessions.length > 0) { + log('Non-media session detected, stopping before launch...'); + stopSessions(activeClient, sessions, () => launchPlayer(url, /*didStopFirst*/ true)); + } else { + launchPlayer(url, /*didStopFirst*/ false); + } + } + }); + }); + + activeClient.on('error', (err) => { + error(`Client error: ${err.message}`); + try { activeClient.close(); } catch (e) { } + activeClient = null; + activePlayer = null; + }); +} + +function launchPlayer(url, didStopFirst) { + if (!activeClient) return; + + activeClient.launch(DefaultMediaReceiver, (err, player) => { + if (err) { + const details = `Launch error: ${err && err.message ? err.message : String(err)}${err && err.code ? ` (code: ${err.code})` : ''}`; + // If launch fails with NOT_ALLOWED, the device may be busy with another app/session. + // Best-effort: stop existing sessions once, then retry launch. + if (!didStopFirst && isNotAllowedError(err)) { + log('Launch NOT_ALLOWED; attempting to stop existing sessions and retry...'); + activeClient.getSessions((sessErr, sessions) => { + if (sessErr) { + error(`${details} | GetSessions error: ${sessErr.message || String(sessErr)}`); + return; + } + stopSessions(activeClient, sessions, () => { + activeClient.launch(DefaultMediaReceiver, (retryErr, retryPlayer) => { + if (retryErr) { + const retryDetails = `Launch retry error: ${retryErr && retryErr.message ? retryErr.message : String(retryErr)}${retryErr && retryErr.code ? ` (code: ${retryErr.code})` : ''}`; + error(retryDetails); + try { error(`Launch retry error full: ${JSON.stringify(retryErr)}`); } catch (e) { /* ignore */ } + return; + } + activePlayer = retryPlayer; + loadMedia(url); + }); + }); + }); + return; + } + + error(details); + try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ } + return; + } + activePlayer = player; + loadMedia(url); + }); +} + +function loadMedia(url) { + if (!activePlayer) return; + + const media = { + contentId: url, + contentType: 'audio/mpeg', + streamType: 'LIVE', + metadata: { + metadataType: 0, + title: 'Radio 1' + } + }; + + activePlayer.load(media, { autoplay: true }, (err, status) => { + if (err) return error(`Load error: ${err.message}`); + log('Media loaded, playing...'); + }); + + activePlayer.on('status', (status) => { + // Optional: track status + }); +} + +function stop() { + if (activePlayer) { + try { activePlayer.stop(); } catch (e) { } + log('Stopped playback'); + } + if (activeClient) { + try { activeClient.close(); } catch (e) { } + activeClient = null; + activePlayer = null; + } +} + +function setVolume(level) { + if (activeClient && activePlayer) { + activeClient.setVolume({ level }, (err, status) => { + if (err) return error(`Volume error: ${err.message}`); + log(`Volume set to ${level}`); + }); + } else { + log('Volume command ignored: Player not initialized'); + } +} + +log('Sidecar initialized and waiting for commands'); diff --git a/sidecar/package-lock.json b/sidecar/package-lock.json new file mode 100644 index 0000000..5a08fc4 --- /dev/null +++ b/sidecar/package-lock.json @@ -0,0 +1,190 @@ +{ + "name": "radiocast-sidecar", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "radiocast-sidecar", + "version": "1.0.0", + "dependencies": { + "castv2-client": "^1.2.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/castv2": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/castv2/-/castv2-0.1.10.tgz", + "integrity": "sha512-3QWevHrjT22KdF08Y2a217IYCDQDP7vEJaY4n0lPBeC5UBYbMFMadDfVTsaQwq7wqsEgYUHElPGm3EO1ey+TNw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "protobufjs": "^6.8.8" + } + }, + "node_modules/castv2-client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/castv2-client/-/castv2-client-1.2.0.tgz", + "integrity": "sha512-2diOsC0vSSxa3QEOgoGBy9fZRHzNXatHz464Kje2OpwQ7GM5vulyrD0gLFOQ1P4rgLAFsYiSGQl4gK402nEEuA==", + "license": "MIT", + "dependencies": { + "castv2": "~0.1.4", + "debug": "^2.2.0" + } + }, + "node_modules/castv2/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/castv2/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + } + } +} diff --git a/sidecar/package.json b/sidecar/package.json new file mode 100644 index 0000000..cfa730f --- /dev/null +++ b/sidecar/package.json @@ -0,0 +1,17 @@ +{ + "name": "radiocast-sidecar", + "version": "1.0.0", + "main": "index.js", + "bin": "index.js", + "dependencies": { + "castv2-client": "^1.2.0" + }, + "pkg": { + "assets": [ + "node_modules/castv2/lib/*.proto" + ] + }, + "scripts": { + "build": "pkg . --targets node18-win-x64 --output ../src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe" + } +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index eea6a1a..b12c6b8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -846,6 +846,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -2441,6 +2450,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -2894,6 +2913,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", + "tauri-plugin-shell", "tokio", ] @@ -3531,12 +3551,44 @@ dependencies = [ "digest", ] +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3980,6 +4032,27 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-shell" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "tauri-runtime" version = "2.9.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5f631ce..24d6253 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,4 +25,5 @@ serde_json = "1" rust_cast = "0.19.0" mdns-sd = "0.17.1" tokio = { version = "1.48.0", features = ["full"] } +tauri-plugin-shell = "2.3.3" diff --git a/src-tauri/binaries/RadioPlayer-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/RadioPlayer-x86_64-pc-windows-msvc.exe new file mode 100644 index 0000000..52ad4c3 Binary files /dev/null and b/src-tauri/binaries/RadioPlayer-x86_64-pc-windows-msvc.exe differ diff --git a/src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe new file mode 100644 index 0000000..52ad4c3 Binary files /dev/null and b/src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe differ diff --git a/src-tauri/build_log.txt b/src-tauri/build_log.txt new file mode 100644 index 0000000..a6037de --- /dev/null +++ b/src-tauri/build_log.txt @@ -0,0 +1,15 @@ + Compiling radio-tauri v0.1.0 (D:\Sites\Work\RadioCast\src-tauri) +error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope + --> src\lib.rs:33:36 + | +33 | let returned_child = child.clone(); + | ^^^^^ method not found in `CommandChild` + +error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope + --> src\lib.rs:58:32 + | +58 | let returned_child = child.clone(); + | ^^^^^ method not found in `CommandChild` + +For more information about this error, try `rustc --explain E0599`. +error: could not compile `radio-tauri` (lib) due to 2 previous errors diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index d2a6719..c812b6c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,6 +6,7 @@ "permissions": [ "core:default", "core:window:allow-close", - "opener:default" + "opener:default", + "shell:default" ] } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 6be5e50..5f6e7b1 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index e81bece..1049215 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index a437dd5..48aa2d7 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..b0b9997 Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 0ca4f27..fabc29e 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index b81f820..0b0332f 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 624c7bf..1664f17 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index c021d2b..e87bbd7 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 6219700..ad73f2a 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index f9bc048..fa5e990 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index d5fbfb2..edb5417 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 63440d7..441ad06 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index f3f705a..03737c5 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 4556388..a9b5834 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..0cc0187 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..82ed043 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..c542ae4 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..48cfca5 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..55f97c3 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..49084e5 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..d27c9c5 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8fc1779 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3256390 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..19a216c Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..36c1cb4 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..28a5888 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..da38c5c Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a7569ea Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..557aa29 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 12a5bce..b73727e 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index b3636e4..7d77104 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e1cd261..1d1bc44 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..d0ae02f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..17a4834 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..17a4834 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..5173df7 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..8fef17e Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..b984f3f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..b984f3f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..cd6260f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..17a4834 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..eeefd96 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..eeefd96 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..ca12c4f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..8b486c5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..ca12c4f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..0623768 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..defa91f Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..889e51e Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..f80cc8e Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 54afe63..7850cb4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,21 +1,23 @@ use std::collections::HashMap; -use std::net::IpAddr; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use std::thread; use mdns_sd::{ServiceDaemon, ServiceEvent}; -use rust_cast::channels::media::{Media, StreamType}; -use rust_cast::channels::receiver::CastDeviceApp; -use rust_cast::CastDevice; -use tauri::State; +use serde_json::json; +use tauri::{AppHandle, Manager, State}; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; + +struct SidecarState { + child: Mutex>, +} struct AppState { known_devices: Mutex>, } #[tauri::command] -async fn list_cast_devices(state: State<'_, Arc>) -> Result, String> { +async fn list_cast_devices(state: State<'_, AppState>) -> Result, String> { let devices = state.known_devices.lock().unwrap(); let mut list: Vec = devices.keys().cloned().collect(); list.sort(); @@ -24,7 +26,9 @@ async fn list_cast_devices(state: State<'_, Arc>) -> Result>, + app: AppHandle, + state: State<'_, AppState>, + sidecar_state: State<'_, SidecarState>, device_name: String, url: String, ) -> Result<(), String> { @@ -36,181 +40,130 @@ async fn cast_play( .ok_or("Device not found")? }; - println!("Connecting to {} ({})", device_name, ip); + let mut lock = sidecar_state.child.lock().unwrap(); - // Run connection logic - let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?; - - // Connect to port 8009 - let device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009) - .map_err(|e| format!("Failed to connect: {:?}", e))?; - - device - .connection - .connect("receiver-0") - .map_err(|e| format!("Failed to connect receiver: {:?}", e))?; - - // Check if Default Media Receiver is already running - let app = CastDeviceApp::DefaultMediaReceiver; - let status = device - .receiver - .get_status() - .map_err(|e| format!("Failed to get status: {:?}", e))?; - - // Determine if we need to launch or if we can use existing - let application = status.applications.iter().find(|a| a.app_id == "CC1AD845"); // Default Media Receiver ID - - let (transport_id, session_id) = if let Some(app_instance) = application { - println!( - "App already running, joining session {}", - app_instance.session_id - ); - ( - app_instance.transport_id.clone(), - app_instance.session_id.clone(), - ) + // Get or spawn child + let child = if let Some(ref mut child) = *lock { + child } else { - println!("Launching app..."); - let app_instance = device - .receiver - .launch_app(&app) - .map_err(|e| format!("Failed to launch app: {:?}", e))?; - (app_instance.transport_id, app_instance.session_id) + println!("Spawning new sidecar..."); + let sidecar_command = app + .shell() + .sidecar("radiocast-sidecar") + .map_err(|e| e.to_string())?; + let (mut rx, child) = sidecar_command.spawn().map_err(|e| e.to_string())?; + + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => { + println!("Sidecar: {}", String::from_utf8_lossy(&line)) + } + CommandEvent::Stderr(line) => { + eprintln!("Sidecar Error: {}", String::from_utf8_lossy(&line)) + } + _ => {} + } + } + }); + + *lock = Some(child); + lock.as_mut().unwrap() }; - device - .connection - .connect(&transport_id) - .map_err(|e| format!("Failed to connect transport: {:?}", e))?; + let play_cmd = json!({ + "command": "play", + "args": { "ip": ip, "url": url } + }); - // Load Media - let media = Media { - content_id: url, - stream_type: StreamType::Live, // Live stream - content_type: "audio/mp3".to_string(), - metadata: None, - duration: None, - }; - - device - .media - .load(&transport_id, &session_id, &media) - .map_err(|e| format!("Failed to load media: {:?}", e))?; - - println!("Playing on {}", device_name); + child + .write(format!("{}\n", play_cmd.to_string()).as_bytes()) + .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] -async fn cast_stop(state: State<'_, Arc>, device_name: String) -> Result<(), String> { - let ip = { - let devices = state.known_devices.lock().unwrap(); - devices - .get(&device_name) - .cloned() - .ok_or("Device not found")? - }; - - let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?; - let device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009) - .map_err(|e| format!("Failed to connect: {:?}", e))?; - - device.connection.connect("receiver-0").unwrap(); - let status = device - .receiver - .get_status() - .map_err(|e| format!("{:?}", e))?; - - if let Some(app) = status.applications.first() { - let _transport_id = &app.transport_id; - // device.connection.connect(transport_id).unwrap(); - - device - .receiver - .stop_app(app.session_id.as_str()) - .map_err(|e| format!("{:?}", e))?; +async fn cast_stop( + _app: AppHandle, + sidecar_state: State<'_, SidecarState>, + _device_name: String, +) -> Result<(), String> { + let mut lock = sidecar_state.child.lock().unwrap(); + if let Some(ref mut child) = *lock { + let stop_cmd = json!({ "command": "stop", "args": {} }); + child + .write(format!("{}\n", stop_cmd.to_string()).as_bytes()) + .map_err(|e| e.to_string())?; } - Ok(()) } #[tauri::command] async fn cast_set_volume( - state: State<'_, Arc>, - device_name: String, + _app: AppHandle, + sidecar_state: State<'_, SidecarState>, + _device_name: String, volume: f32, ) -> Result<(), String> { - let ip = { - let devices = state.known_devices.lock().unwrap(); - devices - .get(&device_name) - .cloned() - .ok_or("Device not found")? - }; - - let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?; - let device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009) - .map_err(|e| format!("Failed to connect: {:?}", e))?; - - device.connection.connect("receiver-0").unwrap(); - - // Volume is on the receiver struct - let vol = rust_cast::channels::receiver::Volume { - level: Some(volume), - muted: None, - }; - - device - .receiver - .set_volume(vol) - .map_err(|e| format!("{:?}", e))?; - + let mut lock = sidecar_state.child.lock().unwrap(); + if let Some(ref mut child) = *lock { + let vol_cmd = json!({ "command": "volume", "args": { "level": volume } }); + child + .write(format!("{}\n", vol_cmd.to_string()).as_bytes()) + .map_err(|e| e.to_string())?; + } Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let app_state = Arc::new(AppState { - known_devices: Mutex::new(HashMap::new()), - }); + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_opener::init()) + .setup(|app| { + app.manage(AppState { + known_devices: Mutex::new(HashMap::new()), + }); + app.manage(SidecarState { + child: Mutex::new(None), + }); - let state_clone = app_state.clone(); + let handle = app.handle().clone(); + thread::spawn(move || { + let mdns = ServiceDaemon::new().expect("Failed to create daemon"); + let receiver = mdns + .browse("_googlecast._tcp.local.") + .expect("Failed to browse"); + while let Ok(event) = receiver.recv() { + match event { + ServiceEvent::ServiceResolved(info) => { + let name = info + .get_property_val_str("fn") + .or_else(|| Some(info.get_fullname())) + .unwrap() + .to_string(); + let addresses = info.get_addresses(); + let ip = addresses + .iter() + .find(|ip| ip.is_ipv4()) + .or_else(|| addresses.iter().next()); - // Start Discovery Thread - thread::spawn(move || { - let mdns = ServiceDaemon::new().expect("Failed to create daemon"); - // Google Cast service - let receiver = mdns - .browse("_googlecast._tcp.local.") - .expect("Failed to browse"); - - while let Ok(event) = receiver.recv() { - match event { - ServiceEvent::ServiceResolved(info) => { - // Try to get "fn" property for Friendly Name - let name = info - .get_property_val_str("fn") - .or_else(|| Some(info.get_fullname())) - .unwrap() - .to_string(); - - if let Some(ip) = info.get_addresses().iter().next() { - let ip_str = ip.to_string(); - let mut devices = state_clone.known_devices.lock().unwrap(); - if !devices.contains_key(&name) { - println!("Discovered Cast Device: {} at {}", name, ip_str); - devices.insert(name, ip_str); + if let Some(ip) = ip { + let state = handle.state::(); + let mut devices = state.known_devices.lock().unwrap(); + let ip_str = ip.to_string(); + if !devices.contains_key(&name) { + println!("Discovered Cast Device: {} at {}", name, ip_str); + devices.insert(name, ip_str); + } + } } + _ => {} } } - _ => {} - } - } - }); - - tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) - .manage(app_state) + }); + Ok(()) + }) .invoke_handler(tauri::generate_handler![ list_cast_devices, cast_play, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1af05a4..d62a861 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "RadioPlayer", "version": "0.1.0", - "identifier": "com.radio.player", + "identifier": "si.klevze.radioPlayer", "build": { "frontendDist": "../src" }, @@ -25,6 +25,9 @@ "bundle": { "active": true, "targets": "all", + "externalBin": [ + "binaries/RadioPlayer" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/assets/appIcon.png b/src/assets/appIcon.png new file mode 100644 index 0000000..d789348 Binary files /dev/null and b/src/assets/appIcon.png differ diff --git a/src/assets/favicon_io.zip b/src/assets/favicon_io.zip new file mode 100644 index 0000000..15adffb Binary files /dev/null and b/src/assets/favicon_io.zip differ diff --git a/src/assets/favicon_io/android-chrome-192x192.png b/src/assets/favicon_io/android-chrome-192x192.png new file mode 100644 index 0000000..4757781 Binary files /dev/null and b/src/assets/favicon_io/android-chrome-192x192.png differ diff --git a/src/assets/favicon_io/android-chrome-512x512.png b/src/assets/favicon_io/android-chrome-512x512.png new file mode 100644 index 0000000..8f1299a Binary files /dev/null and b/src/assets/favicon_io/android-chrome-512x512.png differ diff --git a/src/assets/favicon_io/app-icon.png b/src/assets/favicon_io/app-icon.png new file mode 100644 index 0000000..8f1299a Binary files /dev/null and b/src/assets/favicon_io/app-icon.png differ diff --git a/src/assets/favicon_io/apple-touch-icon.png b/src/assets/favicon_io/apple-touch-icon.png new file mode 100644 index 0000000..f29ebcf Binary files /dev/null and b/src/assets/favicon_io/apple-touch-icon.png differ diff --git a/src/assets/favicon_io/favicon-16x16.png b/src/assets/favicon_io/favicon-16x16.png new file mode 100644 index 0000000..4eb99da Binary files /dev/null and b/src/assets/favicon_io/favicon-16x16.png differ diff --git a/src/assets/favicon_io/favicon-32x32.png b/src/assets/favicon_io/favicon-32x32.png new file mode 100644 index 0000000..73c330b Binary files /dev/null and b/src/assets/favicon_io/favicon-32x32.png differ diff --git a/src/assets/favicon_io/icon.ico b/src/assets/favicon_io/icon.ico new file mode 100644 index 0000000..586e969 Binary files /dev/null and b/src/assets/favicon_io/icon.ico differ diff --git a/src/assets/favicon_io/site.webmanifest b/src/assets/favicon_io/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/src/assets/favicon_io/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/src/assets/tauri.svg b/src/assets/tauri.svg index 31b62c9..0c0e6aa 100644 --- a/src/assets/tauri.svg +++ b/src/assets/tauri.svg @@ -1,6 +1,4 @@ - - - - - + + + diff --git a/src/index.html b/src/index.html index f4741d1..3ea45e7 100644 --- a/src/index.html +++ b/src/index.html @@ -35,7 +35,6 @@ -
+ + + + + + + + + + + + + + + 1
@@ -112,7 +137,7 @@