e.to_string() on an anyhow::Error only prints the outermost .context()
message; the underlying cause (why connect_app/build_pipeline/etc.
actually failed) was being silently dropped before it ever reached the
GUI or bread event log. Use "{e:#}" instead.
Three independent problems found by reading the mirroring paths end to end.
1. Segfault on Cast session teardown. CastStreamSender::SchedulePoll
self-reschedules every 100ms with a raw `this` and was never cancelled,
so the one task that can be scheduled to run *after* an already-queued
teardown task would dereference the just-reset `environment` unique_ptr
(Environment::task_runner() dereferences a member immediately) --- a hard
null deref on openscreen's TaskRunner thread. TaskRunnerImpl's shutdown
has an explicit flushing phase and PlatformClientPosix::ShutDown()'s quit
task queues behind whatever is already pending, so this is a race the
teardown path can lose. Latch a `shutting_down` atomic before posting
teardown and check it in the poll and in every other posted task.
2. Encoder bitrate collapsing to the floor within seconds. The control loop
set `target = 0.85 * estimate` once a second unconditionally. openscreen's
BandwidthEstimator deliberately under-estimates capacity whenever the
transmit rate is below it and documents the required TCP-like response;
multiplying the target by <=0.85 every second instead walks 4000 kbps past
1500 in ~6s and pins it at the floor on a healthy LAN. Replaced with
proper AIMD (hold on a zero/unknown estimate, back off below it, probe up
10%/s otherwise), clamped to 1000..8000 kbps, with unit tests.
3. DLNA/HLS latency. Segment length is max(target-duration, GOP), so
target-duration=2 with a 2s GOP gave 2s segments, ~6s of renderer buffer,
plus 3 segments of stale video waited for before handover. 1s segments
(GOP halved to make that reachable), shorter playlist, and wait for 2
segments instead of 3.
Also hardened two paths into openscreen's fatal OSP_CHECK on strictly
increasing RTP timestamps: pull_encoded_frame no longer substitutes 0 for a
missing PTS (it skips the buffer), and facade.cc drops non-monotonic capture
times at the FFI boundary. Either could previously abort the daemon outright.
CastMirrorSession now owns the Arc<CastStreamSender> instead of leaving its
lifetime to whichever detached pump thread dropped the last clone, so the
blocking FFI destroy happens at a defined point in stop() with the pump
joins ordered around it.
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.