fix(host/capture): hold the session through a slow compositor switch
apple / swift (push) Successful in 1m1s
ci / docs-site (push) Successful in 54s
apple / screenshots (push) Successful in 5m14s
deb / build-publish (push) Successful in 2m30s
decky / build-publish (push) Successful in 11s
android / android (push) Successful in 4m41s
ci / rust (push) Successful in 4m52s
ci / web (push) Successful in 49s
windows-host / package (push) Successful in 7m54s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m34s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m10s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m7s
apple / swift (push) Successful in 1m1s
ci / docs-site (push) Successful in 54s
apple / screenshots (push) Successful in 5m14s
deb / build-publish (push) Successful in 2m30s
decky / build-publish (push) Successful in 11s
android / android (push) Successful in 4m41s
ci / rust (push) Successful in 4m52s
ci / web (push) Successful in 49s
windows-host / package (push) Successful in 7m54s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m34s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m10s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m7s
A Bazzite/SteamOS Gaming↔Desktop switch tears the old compositor down and can
take 15s+ to bring the new one up — longer than the capture-loss rebuild's
~10s window, so the session failed mid-switch ("disconnect — session failed")
and forced the client to cold-reconnect. Retry the rebuild within a 40s budget
instead of giving up after one round, and re-detect the live compositor on
each attempt so the stream follows the box to whatever session comes up (a new
instance of the same compositor, or a different one — the kind-change case).
The QUIC keepalive runs on its own thread, so the client stays connected
(frozen on the last frame) and the stream resumes when the new output appears,
with no reconnect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2714,15 +2714,76 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
}
|
||||
tracing::warn!(error = %format!("{e:#}"), rebuild = capture_rebuilds,
|
||||
"capture lost — rebuilding pipeline in place");
|
||||
let (new_cap, new_enc, new_frame, new_interval) =
|
||||
build_pipeline_with_retry(&mut vd, cur_mode, bitrate_kbps, bit_depth, plan)
|
||||
.context("rebuild after capture loss")?;
|
||||
// A Bazzite/SteamOS Gaming↔Desktop switch tears the old compositor down and can take
|
||||
// 15s+ to bring the new one up. Don't fail the session over that (the client would
|
||||
// have to cold-reconnect, surfacing a "session failed") — keep retrying within a
|
||||
// generous budget while the QUIC keepalive (its own thread) holds the connection,
|
||||
// RE-DETECTING the live compositor each attempt so we follow the box to whatever
|
||||
// session comes up: a fresh instance of the same compositor, OR a different one
|
||||
// (the kind-change case the session watcher also handles). The client stays
|
||||
// connected, frozen on the last frame, and the stream resumes when the new output
|
||||
// appears — no reconnect.
|
||||
const REBUILD_BUDGET: std::time::Duration = std::time::Duration::from_secs(40);
|
||||
let rebuild_deadline = std::time::Instant::now() + REBUILD_BUDGET;
|
||||
let (new_cap, new_enc, new_frame, new_interval) = loop {
|
||||
// Follow the active session unless an explicit PUNKTFUNK_COMPOSITOR pin forbids
|
||||
// retargeting (then we stick to the pinned backend and just rebuild it).
|
||||
if crate::config::config().compositor.is_none() {
|
||||
let active = crate::vdisplay::detect_active_session();
|
||||
if let Some(c) = crate::vdisplay::compositor_for_kind(active.kind) {
|
||||
crate::vdisplay::apply_session_env(&active);
|
||||
crate::vdisplay::apply_input_env(c);
|
||||
if c != compositor {
|
||||
if matches!(
|
||||
c,
|
||||
crate::vdisplay::Compositor::Kwin
|
||||
| crate::vdisplay::Compositor::Mutter
|
||||
) {
|
||||
crate::vdisplay::settle_desktop_portal(c);
|
||||
}
|
||||
match crate::vdisplay::open(c) {
|
||||
Ok(v) => {
|
||||
tracing::info!(from = compositor.id(), to = c.id(),
|
||||
"capture loss: active session switched compositor — retargeting");
|
||||
vd = v;
|
||||
compositor = c;
|
||||
}
|
||||
Err(e2) => tracing::warn!(error = %format!("{e2:#}"),
|
||||
"capture loss: opening the newly-detected compositor failed — retrying"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match build_pipeline_with_retry(
|
||||
&mut vd,
|
||||
cur_mode,
|
||||
bitrate_kbps,
|
||||
bit_depth,
|
||||
plan,
|
||||
) {
|
||||
Ok(p) => break p,
|
||||
Err(e2) => {
|
||||
if stop.load(Ordering::SeqCst)
|
||||
|| std::time::Instant::now() >= rebuild_deadline
|
||||
{
|
||||
return Err(e2)
|
||||
.context("capture lost — no compositor came up within the rebuild budget");
|
||||
}
|
||||
tracing::warn!(error = %format!("{e2:#}"),
|
||||
"capture lost — new session not up yet, retrying");
|
||||
}
|
||||
}
|
||||
};
|
||||
capturer = new_cap;
|
||||
enc = new_enc;
|
||||
frame = new_frame;
|
||||
interval = new_interval;
|
||||
enc.request_keyframe(); // belt-and-suspenders; a fresh encoder opens on an IDR anyway
|
||||
next = std::time::Instant::now();
|
||||
tracing::info!(
|
||||
compositor = compositor.id(),
|
||||
"capture loss: pipeline rebuilt — stream resumes"
|
||||
);
|
||||
}
|
||||
}
|
||||
if perf && diag_at.elapsed() >= std::time::Duration::from_secs(2) {
|
||||
|
||||
Reference in New Issue
Block a user