deadlock, and OFFER/encode resolution mismatch
Found live-testing on real hardware after the first round of fixes:
1. mDNS resolves one Chromecast on every local address it has (a private
IPv4 and a link-local IPv6 in the common case), as separate
CastDeviceFound events for the same id. The device map was a plain
HashMap::insert, so whichever address resolved last won -- and a bare
fe80:: address has no interface scope attached, so connecting to it
fails outright. This is what "failed to connect and launch the
Mirroring receiver" actually was; the message just didn't say why,
since daemon.rs was converting the anyhow::Error with to_string()
(Display, outermost .context() only) instead of "{e:#}" (full chain).
Fixed both: prefer an already-usable host over a link-local one
instead of always taking the newest resolution, and preserve the full
error chain to the GUI/bread event log.
2. The CASTV2 receive loop (run_io_loop) treated any error from
device.receive() as connection-fatal and ended the whole loop --
including a plain JSON deserialization failure on a single
MEDIA_STATUS message (rust_cast's struct requires an `images` field
this receiver didn't send). That killed the receiver-status/control
channel for the rest of every session, on the very first status
update, while the RTP stream itself kept flowing obliviously.
rust_cast::Error already distinguishes Io/Tls/Dns (actually fatal)
from Serialization/Parsing/etc (a bad message, not a dead socket) --
only end the loop on the former now.
3. CastSession::stop() blocks on an unbounded reply_rx.recv() waiting for
the io thread's device.receiver.stop_app() -- a network round trip
rust_cast gives no way to put a read timeout on. If the receiver ever
stops responding, that never returns, and since the daemon actor
processes one command at a time, a single wedged stop_cast freezes
every future IPC request too, recoverable only by killing the process
-- which is exactly what was observed live. Bounded both that wait and
join_pump's thread joins to 5s; past that, log a warning and tear down
anyway rather than hang forever. The abandoned thread(s) may leak, but
a leak beats an unrecoverable daemon.
4. VideoParams::default() advertised 1920x1080 in the OFFER while
build_video_pipeline_for_streaming actually encodes 1280x720 --
negotiated and actual resolution disagreeing is a real protocol
violation, not just soft video, and a plausible cause of a receiver
decoder corrupting or freezing outright rather than merely looking
worse. Made the OFFER match what's actually sent.
|
||
|---|---|---|
| .forgejo/workflows | ||
| breadcast | ||
| breadcast-caststream-sys | ||
| breadcast-core | ||
| breadcastd | ||
| ci | ||
| contrib | ||
| graphify-out | ||
| vendor/rust_cast-0.21.0 | ||
| .gitignore | ||
| AGENTS.md | ||
| bakery.toml | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CONTRIBUTING.md | ||
| EVENTS.md | ||
| LICENSE | ||
| README.md | ||
breadcast
Cast your screen to a Chromecast, Google TV, or DLNA renderer on the LAN. Two binaries:
breadcastd— background daemon. Discovers Cast (mDNS) and DLNA/UPnP (SSDP) devices, owns the portal screen-capture / encode pipeline, and runs the live session: Cast Streaming (vendored openscreen) or DLNA/AVTransport. One active session at a time.breadcast— GTK4 Layer Shell popup. Thin IPC client ofbreadcastd: pick a device, start or stop a cast. Closing the popup does not interrupt an active session.
This is a bakery product. It is not shipped on the BOS ISO and is not part of the default desktop — install it yourself if you want it.
Install
bakery install breadcast
That puts breadcast and breadcastd on $PATH (usually ~/.local/bin),
installs contrib/breadcastd.service as a systemd user unit, enables it,
and starts the daemon. bakery doctor breadcast checks the system
packages listed in bakery.toml first.
Requirements
- A Wayland compositor with Layer Shell and
xdg-desktop-portalScreenCast (Hyprland is the primary target) - GTK 4.12+ and
gtk4-layer-shell - GStreamer plus
gst-plugin-pipewire,gst-plugins-bad,gst-plugin-va(vah264enc), andgst-plugin-hlssink3 jsoncppandopenssl(runtime deps of the vendored Cast Streaming code)
From source you also need a Rust toolchain (edition 2021).
Build from source
git clone https://git.breadway.dev/Breadway/breadcast
cd breadcast
cargo build --release
Binaries land at target/release/breadcast and target/release/breadcastd.
cp target/release/breadcast target/release/breadcastd ~/.local/bin/
cp contrib/breadcastd.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now breadcastd
Usage
Start the daemon (or let the systemd unit handle it):
breadcastd
Open the device picker:
breadcast
Running breadcast again while it is open closes it (toggle). Click a
device to start mirroring — the portal picker asks which screen to share.
Stop mirroring ends the session. Status is Idle or Casting.
Discovery examples (optional, for debugging the LAN):
cargo run -p breadcast-core --example discover
cargo run -p breadcast-core --example dlna_discover
Hyprland keybind
On stock Hyprland, add the contents of contrib/hyprland.conf to your
hyprland.conf if you want Super+C and the frosted-glass blur:
layerrule = blur, breadcast
layerrule = ignorezero, breadcast
bind = $mainMod, C, exec, breadcast
BOS does not ship this app or a default keybind for it. Add one yourself if you install breadcast on a BOS machine.
bread event integration
breadcastd works the same with or without breadd. When breadd is
running, it publishes bread.cast.* and honors bread.command.cast.*.
See EVENTS.md for the bus contract. bread is not a bakery
dependency.
Theming
breadcast inherits its colour palette from bread-theme.