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:
@@ -0,0 +1,52 @@
|
||||
import XCTest
|
||||
|
||||
@testable import PunktfunkKit
|
||||
|
||||
final class ResizeIndicatorTests: XCTestCase {
|
||||
func testInactiveUntilSteered() {
|
||||
var r = ResizeIndicator()
|
||||
XCTAssertFalse(r.active)
|
||||
// A decoded frame with nothing pending is a no-op (session start / steady state).
|
||||
r.decoded(width: 1920, height: 1080)
|
||||
XCTAssertFalse(r.active)
|
||||
}
|
||||
|
||||
func testSteeringActivatesAndDecodedTargetClears() {
|
||||
var r = ResizeIndicator()
|
||||
r.steering(width: 2560, height: 1440, now: 0)
|
||||
XCTAssertTrue(r.active)
|
||||
// A frame at a DIFFERENT size (the old mode still draining) doesn't clear it.
|
||||
r.decoded(width: 1920, height: 1080)
|
||||
XCTAssertTrue(r.active)
|
||||
// The target frame lands → clear.
|
||||
r.decoded(width: 2560, height: 1440)
|
||||
XCTAssertFalse(r.active)
|
||||
}
|
||||
|
||||
func testTimeoutClearsWhenTargetNeverArrives() {
|
||||
var r = ResizeIndicator(timeout: 2.5)
|
||||
r.steering(width: 2560, height: 1440, now: 10)
|
||||
r.tick(now: 12) // 2 s < timeout — still up
|
||||
XCTAssertTrue(r.active)
|
||||
r.tick(now: 12.6) // 2.6 s ≥ timeout — a rejected/capped switch clears
|
||||
XCTAssertFalse(r.active)
|
||||
}
|
||||
|
||||
func testDragReArmsTimeoutOnEachNewTarget() {
|
||||
var r = ResizeIndicator(timeout: 2.5)
|
||||
r.steering(width: 2000, height: 1200, now: 0)
|
||||
r.steering(width: 2200, height: 1200, now: 2) // target changed → since re-armed to 2
|
||||
r.tick(now: 4) // only 2 s since the last change — still up (drag isn't a timeout)
|
||||
XCTAssertTrue(r.active)
|
||||
r.tick(now: 4.6) // 2.6 s since the last change → clears
|
||||
XCTAssertFalse(r.active)
|
||||
}
|
||||
|
||||
func testSteadyDragDoesNotResetTimeout() {
|
||||
var r = ResizeIndicator(timeout: 2.5)
|
||||
r.steering(width: 2560, height: 1440, now: 0)
|
||||
r.steering(width: 2560, height: 1440, now: 1) // SAME target → since stays 0
|
||||
r.tick(now: 2.6) // 2.6 s since the ORIGINAL steer → clears (not reset by the repeat)
|
||||
XCTAssertFalse(r.active)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user