Initial commit
# Conflicts: # README.md
7
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
5553
src-tauri/Cargo.lock
generated
Normal file
28
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "radio-tauri"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "radio_tauri_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rust_cast = "0.19.0"
|
||||
mdns-sd = "0.17.1"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
|
||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
11
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
29
src-tauri/check_log.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
Checking radio-tauri v0.1.0 (D:\Sites\Work\Radio1\radio-tauri\src-tauri)
|
||||
warning: variable does not need to be mutable
|
||||
--> src\lib.rs:38:9
|
||||
|
|
||||
38 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
||||
| ----^^^^^^
|
||||
| |
|
||||
| help: remove this `mut`
|
||||
|
|
||||
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
|
||||
|
||||
warning: variable does not need to be mutable
|
||||
--> src\lib.rs:75:9
|
||||
|
|
||||
75 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
||||
| ----^^^^^^
|
||||
| |
|
||||
| help: remove this `mut`
|
||||
|
||||
warning: variable does not need to be mutable
|
||||
--> src\lib.rs:99:9
|
||||
|
|
||||
99 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
||||
| ----^^^^^^
|
||||
| |
|
||||
| help: remove this `mut`
|
||||
|
||||
warning: `radio-tauri` (lib) generated 3 warnings (run `cargo fix --lib -p radio-tauri` to apply 3 suggestions)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.78s
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
222
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, 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;
|
||||
|
||||
struct AppState {
|
||||
known_devices: Mutex<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_cast_devices(state: State<'_, Arc<AppState>>) -> Result<Vec<String>, String> {
|
||||
let devices = state.known_devices.lock().unwrap();
|
||||
let mut list: Vec<String> = devices.keys().cloned().collect();
|
||||
list.sort();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cast_play(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
device_name: String,
|
||||
url: String,
|
||||
) -> Result<(), String> {
|
||||
let ip = {
|
||||
let devices = state.known_devices.lock().unwrap();
|
||||
devices
|
||||
.get(&device_name)
|
||||
.cloned()
|
||||
.ok_or("Device not found")?
|
||||
};
|
||||
|
||||
println!("Connecting to {} ({})", device_name, ip);
|
||||
|
||||
// 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(),
|
||||
)
|
||||
} 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)
|
||||
};
|
||||
|
||||
device
|
||||
.connection
|
||||
.connect(&transport_id)
|
||||
.map_err(|e| format!("Failed to connect transport: {:?}", e))?;
|
||||
|
||||
// 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);
|
||||
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))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cast_set_volume(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
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))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let app_state = Arc::new(AppState {
|
||||
known_devices: Mutex::new(HashMap::new()),
|
||||
});
|
||||
|
||||
let state_clone = app_state.clone();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(app_state)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_cast_devices,
|
||||
cast_play,
|
||||
cast_stop,
|
||||
cast_set_volume
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
radio_tauri_lib::run()
|
||||
}
|
||||
36
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "RadioPlayer",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.radio.player",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "RadioPlayer",
|
||||
"width": 360,
|
||||
"height": 720,
|
||||
"resizable": false,
|
||||
"decorations": false,
|
||||
"transparent": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||