feat(resize/apple): resize overlay — blur + spinner during mid-stream resize
Make a Match-window resize deliberate instead of a stutter: blur the live stream and show a spinner while the host rebuilds its virtual display + encoder and VideoToolbox re-inits on the new-mode IDR. No new protocol — driven entirely by existing client signals. - ResizeIndicator (pure core, unit-tested): START = follower steering, END = a decoded frame at the target size, TIMEOUT = 2.5s safety net for a rejected/capped switch that never yields a new-size frame; re-arms only on a CHANGED target, not a repeated same-size drag. - MatchWindowFollower.onResizeTarget fires the instant the window differs from the live mode (deduped via lastSteered); a new onDecodedSize callback threads each new-mode IDR's coded dims through StreamPump/Stage2Pipeline → SessionPresenter → both stream views. - SessionModel gains @Published resizing (+ resizeTargeted/resizeDecoded, a tick on the 1 Hz stats timer, reset on disconnect); ContentView blurs the stream 16px and overlays ResizeIndicatorView while resizing (the 32px trust-prompt blur is unchanged and takes precedence). tvOS declares the props but never fires the follower (it drives modes via AVDisplayManager), so the overlay stays dormant there. Pure core verified on the Linux toolchain; full AppKit/UIKit build pending on a Mac. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -327,12 +327,25 @@ struct ContentView: View {
|
||||
}()
|
||||
return ZStack {
|
||||
stream(captureEnabled: pendingFingerprint == nil)
|
||||
.blur(radius: pendingFingerprint != nil ? 32 : 0)
|
||||
// Blur the live stream during the trust prompt (heavy) and during a resize (lighter
|
||||
// — the deliberate "hold on" while the host rebuilds its pipeline and the decoder
|
||||
// re-inits on the new-mode IDR). Only the resize blur animates; the trust blur snaps
|
||||
// as before (its own overlay handles the transition).
|
||||
.blur(radius: pendingFingerprint != nil ? 32 : (model.resizing ? 16 : 0))
|
||||
.animation(.easeInOut(duration: 0.22), value: model.resizing)
|
||||
.overlay {
|
||||
if pendingFingerprint != nil {
|
||||
Color.black.opacity(0.45)
|
||||
}
|
||||
}
|
||||
// The resize spinner rides over the (blurred) stream; suppressed under the trust
|
||||
// prompt, which owns the screen. It never hit-tests, so window-drag resizes keep
|
||||
// steering and the next click still reaches the stream.
|
||||
.overlay {
|
||||
if pendingFingerprint == nil {
|
||||
ResizeIndicatorView(active: model.resizing)
|
||||
}
|
||||
}
|
||||
if let fp = pendingFingerprint {
|
||||
TrustCardView(
|
||||
fingerprint: fp,
|
||||
@@ -410,6 +423,16 @@ struct ContentView: View {
|
||||
onSessionEnd: { [weak model] in
|
||||
Task { @MainActor in model?.sessionEnded() }
|
||||
},
|
||||
// Resize overlay START — the follower is main-actor, so this drives the blur
|
||||
// + spinner synchronously the instant the window differs from the live mode.
|
||||
onResizeTarget: { [weak model] w, h in
|
||||
model?.resizeTargeted(width: w, height: h)
|
||||
},
|
||||
// Resize overlay END — the coded dims of each new-mode IDR, reported from the
|
||||
// decode pump thread; hop to the main actor to clear the overlay.
|
||||
onDecodedSize: { [weak model] w, h in
|
||||
Task { @MainActor in model?.resizeDecoded(width: w, height: h) }
|
||||
},
|
||||
endToEndMeter: model.endToEnd,
|
||||
decodeMeter: model.decodeStage,
|
||||
displayMeter: model.displayStage
|
||||
|
||||
Reference in New Issue
Block a user