Merge remote-tracking branch 'origin/main'
ci / web (push) Failing after 36s
ci / rust (push) Successful in 1m8s
ci / docs-site (push) Failing after 34s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
apple / swift (push) Successful in 1m17s
docker / deploy-docs (push) Successful in 16s
ci / web (push) Failing after 36s
ci / rust (push) Successful in 1m8s
ci / docs-site (push) Failing after 34s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
apple / swift (push) Successful in 1m17s
docker / deploy-docs (push) Successful in 16s
This commit is contained in:
@@ -197,6 +197,17 @@ public final class StreamLayerView: NSView {
|
|||||||
self?.releaseCapture()
|
self?.releaseCapture()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Becoming key RETRIES a still-pending session-start auto-capture — the case where a
|
||||||
|
// session began (reconnect) while this window wasn't key yet, so engageCapture(fromClick:
|
||||||
|
// false) was refused by its key-window guard and, with no retry, capture stayed off and
|
||||||
|
// input dead. This is a no-op once capture engaged (pendingAutoCapture is cleared) and
|
||||||
|
// after a manual ⌘⎋/focus-loss release (the flag is already false), so it does NOT
|
||||||
|
// resurrect the deliberately-rejected "auto-grab on every activation" behavior.
|
||||||
|
windowObservers.append(NotificationCenter.default.addObserver(
|
||||||
|
forName: NSWindow.didBecomeKeyNotification, object: window, queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.attemptPendingCapture()
|
||||||
|
})
|
||||||
attemptPendingCapture()
|
attemptPendingCapture()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +312,13 @@ public final class StreamLayerView: NSView {
|
|||||||
|
|
||||||
private func attemptPendingCapture() {
|
private func attemptPendingCapture() {
|
||||||
guard pendingAutoCapture, window != nil, bounds.width > 0 else { return }
|
guard pendingAutoCapture, window != nil, bounds.width > 0 else { return }
|
||||||
pendingAutoCapture = false // one shot, even if the engage below is refused
|
|
||||||
engageCapture(fromClick: false)
|
engageCapture(fromClick: false)
|
||||||
|
// Clear the one-shot only once it ACTUALLY engaged. If the engage was refused — the
|
||||||
|
// app/window isn't key yet (common right after a reconnect), or the cursor grab raced
|
||||||
|
// app activation — leave it armed so didBecomeKey (or the next layout pass) retries.
|
||||||
|
// This stays scoped to session start: a later manual release (⌘⎋, focus loss) doesn't
|
||||||
|
// re-arm it, so it never resurrects auto-grab-on-activation.
|
||||||
|
if captured { pendingAutoCapture = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func engageCapture(fromClick: Bool) {
|
private func engageCapture(fromClick: Bool) {
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ public final class StreamViewController: UIViewController {
|
|||||||
self.connection = connection
|
self.connection = connection
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
// Fresh session: drop any resign/foreground capture-restore state left over from a
|
||||||
|
// prior session (stop() doesn't clear it). Otherwise a stale `true` could later
|
||||||
|
// re-engage capture on a foreground that the new session never asked for.
|
||||||
|
wasCapturedOnResign = false
|
||||||
// Read the LIVE mode per touch batch — an accepted requestMode() mid-stream
|
// Read the LIVE mode per touch batch — an accepted requestMode() mid-stream
|
||||||
// changes the letterbox, and touches must follow it.
|
// changes the letterbox, and touches must follow it.
|
||||||
streamView.currentHostMode = { [weak connection] in
|
streamView.currentHostMode = { [weak connection] in
|
||||||
@@ -247,7 +251,10 @@ public final class StreamViewController: UIViewController {
|
|||||||
observers.append(NotificationCenter.default.addObserver(
|
observers.append(NotificationCenter.default.addObserver(
|
||||||
forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main
|
forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
guard let self, self.wasCapturedOnResign, self.captureEnabled, self.connection != nil
|
// inputCapture != nil: don't try to restore before this session's capture is wired
|
||||||
|
// up — setForwarding would silently no-op on the nil handlers and leave input dead.
|
||||||
|
guard let self, self.wasCapturedOnResign, self.captureEnabled,
|
||||||
|
self.connection != nil, self.inputCapture != nil
|
||||||
else { return }
|
else { return }
|
||||||
self.setCaptured(true)
|
self.setCaptured(true)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1100,6 +1100,26 @@ pub async fn clock_sync(
|
|||||||
pub mod endpoint {
|
pub mod endpoint {
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
/// Shared QUIC transport tuning for BOTH the host and client endpoints. Keep-alive is the
|
||||||
|
/// load-bearing setting: with quinn's defaults it is OFF, so any quiet stretch on the
|
||||||
|
/// connection (no input, audio muted or stalled, a capture hiccup, a mode change) lets the
|
||||||
|
/// idle timer run out and quinn closes the session — surfacing to the embedder as
|
||||||
|
/// `next_au` → Closed. The native equivalent of Moonlight's ENet keepalive: a small PING
|
||||||
|
/// every `KEEP_ALIVE` keeps the path warm. The interval sits well under `MAX_IDLE` so
|
||||||
|
/// several keepalives can be lost back-to-back (a wifi roam, a brief blip) without a false
|
||||||
|
/// close, while a genuinely dead peer is still detected within `MAX_IDLE`.
|
||||||
|
fn stream_transport() -> Arc<quinn::TransportConfig> {
|
||||||
|
use std::time::Duration;
|
||||||
|
const MAX_IDLE: Duration = Duration::from_secs(20);
|
||||||
|
const KEEP_ALIVE: Duration = Duration::from_secs(4);
|
||||||
|
let mut t = quinn::TransportConfig::default();
|
||||||
|
t.max_idle_timeout(Some(
|
||||||
|
quinn::IdleTimeout::try_from(MAX_IDLE).expect("20s is a valid QUIC idle timeout"),
|
||||||
|
));
|
||||||
|
t.keep_alive_interval(Some(KEEP_ALIVE));
|
||||||
|
Arc::new(t)
|
||||||
|
}
|
||||||
|
|
||||||
/// Server endpoint with a fresh self-signed certificate (tests/dev — production hosts
|
/// Server endpoint with a fresh self-signed certificate (tests/dev — production hosts
|
||||||
/// persist an identity and use [`server_with_identity`] so clients can pin it).
|
/// persist an identity and use [`server_with_identity`] so clients can pin it).
|
||||||
pub fn server(addr: std::net::SocketAddr) -> anyhow_result::Result<quinn::Endpoint> {
|
pub fn server(addr: std::net::SocketAddr) -> anyhow_result::Result<quinn::Endpoint> {
|
||||||
@@ -1142,7 +1162,8 @@ pub mod endpoint {
|
|||||||
.map_err(|e| anyhow_result::Error::msg(format!("server config: {e}")))?;
|
.map_err(|e| anyhow_result::Error::msg(format!("server config: {e}")))?;
|
||||||
let quic_cfg = quinn::crypto::rustls::QuicServerConfig::try_from(rustls_cfg)
|
let quic_cfg = quinn::crypto::rustls::QuicServerConfig::try_from(rustls_cfg)
|
||||||
.map_err(|e| anyhow_result::Error::msg(format!("quic server config: {e}")))?;
|
.map_err(|e| anyhow_result::Error::msg(format!("quic server config: {e}")))?;
|
||||||
let server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_cfg));
|
let mut server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_cfg));
|
||||||
|
server_config.transport_config(stream_transport()); // keep-alive — see stream_transport
|
||||||
Ok(quinn::Endpoint::server(server_config, addr)?)
|
Ok(quinn::Endpoint::server(server_config, addr)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1233,8 +1254,10 @@ pub mod endpoint {
|
|||||||
};
|
};
|
||||||
let quic_cfg = quinn::crypto::rustls::QuicClientConfig::try_from(rustls_cfg)
|
let quic_cfg = quinn::crypto::rustls::QuicClientConfig::try_from(rustls_cfg)
|
||||||
.map_err(|e| anyhow_result::Error::msg(format!("quic client config: {e}")))?;
|
.map_err(|e| anyhow_result::Error::msg(format!("quic client config: {e}")))?;
|
||||||
|
let mut client_cfg = quinn::ClientConfig::new(Arc::new(quic_cfg));
|
||||||
|
client_cfg.transport_config(stream_transport()); // keep-alive — see stream_transport
|
||||||
let mut ep = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())?;
|
let mut ep = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())?;
|
||||||
ep.set_default_client_config(quinn::ClientConfig::new(Arc::new(quic_cfg)));
|
ep.set_default_client_config(client_cfg);
|
||||||
Ok(ep)
|
Ok(ep)
|
||||||
})();
|
})();
|
||||||
(ep, observed)
|
(ep, observed)
|
||||||
|
|||||||
Reference in New Issue
Block a user