fix(apple/iOS): immersive streaming — edge-to-edge, no status bar, hidden cursor, native default mode
ci / rust (push) Has been cancelled

Streaming on iPad left the status bar up and the video boxed inside the safe areas, on
top of a 16:9 default mode letterboxing on the 4:3 screen, with the iPadOS cursor
hovering over the video. The session view is now immersive on iOS:

- .ignoresSafeArea + .statusBarHidden + .persistentSystemOverlays(.hidden) for the
  session only (home gets its chrome back on disconnect).
- First run seeds the stream mode from the device's native screen
  (UIScreen.nativeBounds + maximumFramesPerSecond) instead of 1920×1080 — verified
  live: a fresh install negotiated the iPad's 2752×2064 with the host. macOS keeps the
  1080p default (a desktop window is not the screen).
- The iPadOS cursor hides while over the video (UIPointerInteraction .hidden(),
  re-resolved on capture toggles) — the host renders its own cursor from our deltas;
  true pointer lock through UIHostingController remains the documented gap.

Found along the way (host-side, not fixed here): at very high modes a keyframe burst
can fill the UDP send buffer and m3 treats the sendmmsg WouldBlock as fatal
("session ended with error: submit_frame: WouldBlock") instead of backpressuring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:44:29 +02:00
parent 57f7e32c24
commit 154da2dc58
3 changed files with 51 additions and 8 deletions
@@ -40,7 +40,10 @@ struct ContentView: View {
home
}
}
.onAppear { autoConnectIfAsked() }
.onAppear {
seedDefaultModeIfNeeded()
autoConnectIfAsked()
}
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
// On the outer Group so the sheet survives the trust-prompt home transition
// (the "Pair with PIN instead" path disconnects first the host's accept loop
@@ -77,8 +80,15 @@ struct ContentView: View {
}
#if os(macOS)
.frame(minWidth: 640, minHeight: 360)
#endif
.background(Color.black)
#else
// Streaming is immersive: edge-to-edge under the status bar and home
// indicator, both hidden for the session (they return with the hosts grid).
.background(Color.black)
.ignoresSafeArea()
.statusBarHidden(true)
.persistentSystemOverlays(.hidden)
#endif
}
// MARK: - Home (hosts grid)
@@ -243,6 +253,21 @@ struct ContentView: View {
}
}
/// First run on iOS: default the stream mode to this device's native screen so the
/// video fills the display instead of letterboxing 1920×1080 onto a 4:3 iPad. (The
/// compiled-in AppStorage defaults only apply until any value is saved; macOS keeps
/// 1080p a desktop window is not the screen.)
private func seedDefaultModeIfNeeded() {
#if os(iOS)
let defaults = UserDefaults.standard
guard defaults.object(forKey: "punktfunk.width") == nil else { return }
let bounds = UIScreen.main.nativeBounds // portrait-oriented pixels
defaults.set(Int(max(bounds.width, bounds.height)), forKey: "punktfunk.width")
defaults.set(Int(min(bounds.width, bounds.height)), forKey: "punktfunk.height")
defaults.set(UIScreen.main.maximumFramesPerSecond, forKey: "punktfunk.hz")
#endif
}
private func connect(_ host: StoredHost) {
model.connect(
to: host,