Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s
Some checks failed
dev release / build (push) Failing after 12s
Builds out the full v1 scope: a vendored+patched openscreen subset for low-latency Cast Streaming (Mirroring receiver 0F5096E8) alongside the existing Cast V2/HLS and new DLNA/AVTransport casting paths, breadcastd's Idle/Casting state machine with a private IPC socket, the breadcast GTK4 popup as a thin IPC client, and bread.cast.*/bread.command.cast.* breadd integration (device discovery, start/stop, mirroring lifecycle events). Also adds bakery/systemd/Forgejo CI packaging. Validated end-to-end against a real Chromecast/Google TV: negotiated Cast Streaming session, live pipeline playback, and daemon+GUI click-to-cast/ stop through the actual popup.
This commit is contained in:
parent
887c29002f
commit
8c745d18e0
283 changed files with 36788 additions and 0 deletions
122
breadcastd/src/main.rs
Normal file
122
breadcastd/src/main.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
//! breadcastd — background daemon: mDNS (Cast) and SSDP (DLNA) device
|
||||
//! discovery, both mirroring session lifecycles (Cast Streaming and
|
||||
//! DLNA/AVTransport), and a private IPC socket the `breadcast` GTK4 popup
|
||||
//! talks to. Also does optional breadd event integration for external
|
||||
//! automation (a Hyprland keybind, etc.) — see `bread_events.rs`; that's
|
||||
//! separate from (and does not replace) the IPC socket, since a popup UI
|
||||
//! needs live pushed state that a best-effort pub/sub bus doesn't fit as
|
||||
//! naturally. See `ipc.rs`'s doc comment.
|
||||
|
||||
mod bread_events;
|
||||
mod cast_mirror;
|
||||
mod daemon;
|
||||
mod dlna_mirror;
|
||||
mod ipc;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bread_utils::singleton::{Acquire, try_acquire};
|
||||
use breadcast_core::{CastDevice, DlnaDevice, DlnaDiscovery, DlnaDiscoveryEvent, Discovery, DiscoveryEvent};
|
||||
use daemon::DaemonCommand;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let _guard = match try_acquire(bread_events::APP_ID)? {
|
||||
Acquire::Acquired(guard) => guard,
|
||||
Acquire::HeldByOther(pid) => {
|
||||
tracing::error!(?pid, "breadcastd already running, exiting");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let bread_client = bread_utils::bread_client::BreadClient::connect(bread_events::APP_ID);
|
||||
|
||||
let (events_tx, _events_rx) = tokio::sync::broadcast::channel(64);
|
||||
let daemon_tx = daemon::spawn(events_tx.clone(), bread_client.clone());
|
||||
|
||||
let commands_daemon_tx = daemon_tx.clone();
|
||||
let _commands = bread_client.subscribe("bread.command.cast.**", move |event| {
|
||||
bread_events::handle_command(&event, &commands_daemon_tx);
|
||||
});
|
||||
|
||||
let ipc_daemon_tx = daemon_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = ipc::serve(ipc_daemon_tx, events_tx).await {
|
||||
tracing::error!(error = ?e, "IPC server ended unexpectedly");
|
||||
}
|
||||
});
|
||||
|
||||
let (_discovery, mut cast_events) = Discovery::start()?;
|
||||
let (_dlna_discovery, mut dlna_events) = DlnaDiscovery::start();
|
||||
tracing::info!("breadcastd started, browsing for Cast and DLNA devices");
|
||||
|
||||
// `DiscoveryEvent::Found`/`DlnaDiscoveryEvent::Found` fire on every
|
||||
// re-resolution/re-poll (see `discovery.rs`'s and `dlna/discovery.rs`'s
|
||||
// doc comments), not just the first sighting. Every event is forwarded
|
||||
// to the daemon actor unconditionally (its `HashMap` re-insert is a
|
||||
// harmless no-op for an unchanged device, aside from a redundant but
|
||||
// cheap `device_list_changed` broadcast) — but `bread.cast.device_found`
|
||||
// on the breadd bus is gated on an actual *change*, so anything
|
||||
// subscribed there expecting "on device_found, do X" doesn't fire
|
||||
// repeatedly for the same device.
|
||||
let mut known_cast_devices: HashMap<String, CastDevice> = HashMap::new();
|
||||
let mut known_dlna_devices: HashMap<String, DlnaDevice> = HashMap::new();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = cast_events.recv() => {
|
||||
match event {
|
||||
Some(DiscoveryEvent::Found(device)) => {
|
||||
if known_cast_devices.get(&device.id) != Some(&device) {
|
||||
tracing::info!(?device, "Cast device found");
|
||||
bread_events::emit_device_found(&bread_client, &device.id, &device.name, &device.model, "cast");
|
||||
known_cast_devices.insert(device.id.clone(), device.clone());
|
||||
}
|
||||
let _ = daemon_tx.send(DaemonCommand::CastDeviceFound(device)).await;
|
||||
}
|
||||
Some(DiscoveryEvent::Lost { id }) => {
|
||||
tracing::info!(%id, "Cast device lost");
|
||||
known_cast_devices.remove(&id);
|
||||
let _ = daemon_tx.send(DaemonCommand::CastDeviceLost(id)).await;
|
||||
}
|
||||
// The mDNS browse thread itself ended (daemon shutdown,
|
||||
// an unrecoverable browse error) — that's Cast
|
||||
// discovery's one job gone, not a normal exit.
|
||||
// Returning `Ok(())` from `main` here would exit 0, and
|
||||
// systemd's `Restart=on-failure` would never trigger.
|
||||
None => {
|
||||
tracing::error!("Cast discovery channel closed unexpectedly, exiting");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
event = dlna_events.recv() => {
|
||||
match event {
|
||||
Some(DlnaDiscoveryEvent::Found(device)) => {
|
||||
if known_dlna_devices.get(&device.url) != Some(&device) {
|
||||
tracing::info!(?device, "DLNA device found");
|
||||
bread_events::emit_device_found(&bread_client, &device.url, &device.friendly_name, "DLNA renderer", "dlna");
|
||||
known_dlna_devices.insert(device.url.clone(), device.clone());
|
||||
}
|
||||
let _ = daemon_tx.send(DaemonCommand::DlnaDeviceFound(device)).await;
|
||||
}
|
||||
Some(DlnaDiscoveryEvent::Lost { url }) => {
|
||||
tracing::info!(%url, "DLNA device lost");
|
||||
known_dlna_devices.remove(&url);
|
||||
let _ = daemon_tx.send(DaemonCommand::DlnaDeviceLost(url)).await;
|
||||
}
|
||||
// Same reasoning as the Cast arm above -- `DlnaDiscovery`'s
|
||||
// background task only ends via a panic or this process
|
||||
// dropping every sender, neither of which should happen
|
||||
// while this loop is still the one holding the receiver.
|
||||
None => {
|
||||
tracing::error!("DLNA discovery channel closed unexpectedly, exiting");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue