breadcast/breadcastd
Breadway 5e587ce342 Fix mirroring host-selection race, control-channel fragility, teardown
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.
2026-08-15 22:34:21 +08:00
..
src Fix mirroring host-selection race, control-channel fragility, teardown 2026-08-15 22:34:21 +08:00
Cargo.toml Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration 2026-08-03 09:07:21 +08:00