fixed cast

This commit is contained in:
2025-12-30 18:38:25 +01:00
parent 30ebf5bc5a
commit b2f1b48d06
10 changed files with 551 additions and 158 deletions

140
sidecar/index.js Normal file
View File

@@ -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');

190
sidecar/package-lock.json generated Normal file
View File

@@ -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"
}
}
}

17
sidecar/package.json Normal file
View File

@@ -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"
}
}

73
src-tauri/Cargo.lock generated
View File

@@ -846,6 +846,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" 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]] [[package]]
name = "endi" name = "endi"
version = "1.1.1" version = "1.1.1"
@@ -2441,6 +2450,16 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@@ -2894,6 +2913,7 @@ dependencies = [
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-shell",
"tokio", "tokio",
] ]
@@ -3531,12 +3551,44 @@ dependencies = [
"digest", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.8" version = "1.4.8"
@@ -3980,6 +4032,27 @@ dependencies = [
"zbus", "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]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.9.2" version = "2.9.2"

View File

@@ -25,4 +25,5 @@ serde_json = "1"
rust_cast = "0.19.0" rust_cast = "0.19.0"
mdns-sd = "0.17.1" mdns-sd = "0.17.1"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
tauri-plugin-shell = "2.3.3"

15
src-tauri/build_log.txt Normal file
View File

@@ -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

View File

@@ -6,6 +6,7 @@
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:allow-close", "core:window:allow-close",
"opener:default" "opener:default",
"shell:default"
] ]
} }

View File

@@ -1,21 +1,23 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr; use std::sync::Mutex;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use mdns_sd::{ServiceDaemon, ServiceEvent}; use mdns_sd::{ServiceDaemon, ServiceEvent};
use rust_cast::channels::media::{Media, StreamType}; use serde_json::json;
use rust_cast::channels::receiver::CastDeviceApp; use tauri::{AppHandle, Manager, State};
use rust_cast::CastDevice; use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri::State; use tauri_plugin_shell::ShellExt;
struct SidecarState {
child: Mutex<Option<CommandChild>>,
}
struct AppState { struct AppState {
known_devices: Mutex<HashMap<String, String>>, known_devices: Mutex<HashMap<String, String>>,
} }
#[tauri::command] #[tauri::command]
async fn list_cast_devices(state: State<'_, Arc<AppState>>) -> Result<Vec<String>, String> { async fn list_cast_devices(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let devices = state.known_devices.lock().unwrap(); let devices = state.known_devices.lock().unwrap();
let mut list: Vec<String> = devices.keys().cloned().collect(); let mut list: Vec<String> = devices.keys().cloned().collect();
list.sort(); list.sort();
@@ -24,7 +26,9 @@ async fn list_cast_devices(state: State<'_, Arc<AppState>>) -> Result<Vec<String
#[tauri::command] #[tauri::command]
async fn cast_play( async fn cast_play(
state: State<'_, Arc<AppState>>, app: AppHandle,
state: State<'_, AppState>,
sidecar_state: State<'_, SidecarState>,
device_name: String, device_name: String,
url: String, url: String,
) -> Result<(), String> { ) -> Result<(), String> {
@@ -36,181 +40,130 @@ async fn cast_play(
.ok_or("Device not found")? .ok_or("Device not found")?
}; };
println!("Connecting to {} ({})", device_name, ip); let mut lock = sidecar_state.child.lock().unwrap();
// Run connection logic // Get or spawn child
let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?; let child = if let Some(ref mut child) = *lock {
child
// 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(),
)
} else { } else {
println!("Launching app..."); println!("Spawning new sidecar...");
let app_instance = device let sidecar_command = app
.receiver .shell()
.launch_app(&app) .sidecar("radiocast-sidecar")
.map_err(|e| format!("Failed to launch app: {:?}", e))?; .map_err(|e| e.to_string())?;
(app_instance.transport_id, app_instance.session_id) 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 let play_cmd = json!({
.connection "command": "play",
.connect(&transport_id) "args": { "ip": ip, "url": url }
.map_err(|e| format!("Failed to connect transport: {:?}", e))?; });
// Load Media child
let media = Media { .write(format!("{}\n", play_cmd.to_string()).as_bytes())
content_id: url, .map_err(|e| e.to_string())?;
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);
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
async fn cast_stop(state: State<'_, Arc<AppState>>, device_name: String) -> Result<(), String> { async fn cast_stop(
let ip = { _app: AppHandle,
let devices = state.known_devices.lock().unwrap(); sidecar_state: State<'_, SidecarState>,
devices _device_name: String,
.get(&device_name) ) -> Result<(), String> {
.cloned() let mut lock = sidecar_state.child.lock().unwrap();
.ok_or("Device not found")? if let Some(ref mut child) = *lock {
}; let stop_cmd = json!({ "command": "stop", "args": {} });
child
let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?; .write(format!("{}\n", stop_cmd.to_string()).as_bytes())
let device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009) .map_err(|e| e.to_string())?;
.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))?;
} }
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
async fn cast_set_volume( async fn cast_set_volume(
state: State<'_, Arc<AppState>>, _app: AppHandle,
device_name: String, sidecar_state: State<'_, SidecarState>,
_device_name: String,
volume: f32, volume: f32,
) -> Result<(), String> { ) -> Result<(), String> {
let ip = { let mut lock = sidecar_state.child.lock().unwrap();
let devices = state.known_devices.lock().unwrap(); if let Some(ref mut child) = *lock {
devices let vol_cmd = json!({ "command": "volume", "args": { "level": volume } });
.get(&device_name) child
.cloned() .write(format!("{}\n", vol_cmd.to_string()).as_bytes())
.ok_or("Device not found")? .map_err(|e| e.to_string())?;
}; }
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))?;
Ok(()) Ok(())
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let app_state = Arc::new(AppState { tauri::Builder::default()
known_devices: Mutex::new(HashMap::new()), .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 if let Some(ip) = ip {
thread::spawn(move || { let state = handle.state::<AppState>();
let mdns = ServiceDaemon::new().expect("Failed to create daemon"); let mut devices = state.known_devices.lock().unwrap();
// Google Cast service let ip_str = ip.to_string();
let receiver = mdns if !devices.contains_key(&name) {
.browse("_googlecast._tcp.local.") println!("Discovered Cast Device: {} at {}", name, ip_str);
.expect("Failed to browse"); devices.insert(name, ip_str);
}
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);
} }
_ => {}
} }
} }
_ => {} });
} Ok(())
} })
});
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(app_state)
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
list_cast_devices, list_cast_devices,
cast_play, cast_play,

View File

@@ -25,6 +25,9 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"externalBin": [
"binaries/radiocast-sidecar"
],
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",