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

View File

@@ -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<Option<CommandChild>>,
}
struct AppState {
known_devices: Mutex<HashMap<String, String>>,
}
#[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 mut list: Vec<String> = devices.keys().cloned().collect();
list.sort();
@@ -24,7 +26,9 @@ async fn list_cast_devices(state: State<'_, Arc<AppState>>) -> Result<Vec<String
#[tauri::command]
async fn cast_play(
state: State<'_, Arc<AppState>>,
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<AppState>>, 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<AppState>>,
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::<AppState>();
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,