diff --git a/sidecar/index.js b/sidecar/index.js new file mode 100644 index 0000000..55de7b3 --- /dev/null +++ b/sidecar/index.js @@ -0,0 +1,140 @@ +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 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}`); + + // 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...'); + launchPlayer(url); + } else { + activePlayer = player; + loadMedia(url); + } + }); + } else { + launchPlayer(url); + } + }); + }); + + activeClient.on('error', (err) => { + error(`Client error: ${err.message}`); + try { activeClient.close(); } catch (e) { } + activeClient = null; + activePlayer = null; + }); +} + +function launchPlayer(url) { + if (!activeClient) return; + + activeClient.launch(DefaultMediaReceiver, (err, player) => { + if (err) { + // If launch fails with NOT_ALLOWED, it sometimes means we MUST join or something else is occupying it + return error(`Launch error: ${err.message}`); + } + activePlayer = player; + loadMedia(url); + }); +} + +function loadMedia(url) { + if (!activePlayer) return; + + const media = { + contentId: url, + contentType: 'audio/mp3', + streamType: 'LIVE' + }; + + 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/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/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..21b2849 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -25,6 +25,9 @@ "bundle": { "active": true, "targets": "all", + "externalBin": [ + "binaries/radiocast-sidecar" + ], "icon": [ "icons/32x32.png", "icons/128x128.png",