feat(apple): stage-2 default + pixel-perfect, decode robustness, UI/rumble polish
Stream reliability - Default to the stage-2 presenter (VTDecompressionSession + CAMetalLayer): it detects and recovers a wedged decoder, where stage-1's AVSampleBufferDisplayLayer freezes hard on a lost HEVC reference frame with no app-side recovery (confirmed Apple limitation). Stage 1 is now a DEBUG-only presenter toggle, plus the automatic no-Metal fallback. - Stage-2 pixel-perfect: render the drawable at the decoded size (shader stays 1:1 = identity) and let the layer's contentsGravity scale via the system compositor — the same path stage-1's videoGravity used — instead of scaling in-shader. - Loss recovery in both pumps is now a persistent awaitingIDR want, retried until an IDR actually lands, so a keyframe request swallowed by the throttle can't strand a frozen frame; 100 ms keyframe throttle to match the Android path. - Fix "Publishing changes from within view updates": defer the HostStore writes out of the .onChange(of: model.phase) callback. - Move AVAudioSession setActive/setCategory off the main thread (async on a shared serial queue) to stop the UI-stall warning. Controllers - Rumble: capped-exponential backoff when the gamecontrollerd.haptics XPC breaks (-4811) so a transient server interruption self-heals instead of cascading; playsHapticsOnly so a controller engine doesn't join the always-active streaming audio session. - Host cards: iPad pointer "magnet" hover effect; iPhone press scale + light haptic. UI / design - Ship Geist (SIL OFL 1.1) as the app font (bundled OTFs + registration), with the license surfaced in Acknowledgements. - Restructure iOS/iPadOS Settings into a category NavigationSplitView; resolution wheel with custom-resolution entry; 10-bit HDR toggle in Display. - Industrial host-card redesign (left-aligned, bold, brand monogram tiles). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ struct ContentView: View {
|
||||
@AppStorage(DefaultsKey.gamepadType) private var gamepadType = 0
|
||||
@AppStorage(DefaultsKey.bitrateKbps) private var bitrateKbps = 0
|
||||
@AppStorage(DefaultsKey.audioChannels) private var audioChannels = 2
|
||||
@AppStorage(DefaultsKey.hdrEnabled) private var hdrEnabled = true
|
||||
@AppStorage(DefaultsKey.fullscreenWhileStreaming) private var fullscreenWhileStreaming = true
|
||||
@AppStorage(DefaultsKey.hudEnabled) private var hudEnabled = true
|
||||
@AppStorage(DefaultsKey.hudPlacement) private var hudPlacement = HUDPlacement.topTrailing.rawValue
|
||||
@@ -68,15 +69,19 @@ struct ContentView: View {
|
||||
// A session actually started — remember it on the card ("Connected … ago"
|
||||
// plus the accent ring on the most recent host).
|
||||
guard let host = model.activeHost else { break }
|
||||
store.markConnected(host.id)
|
||||
// Delegated approval just succeeded: the operator let this device in, so pin the
|
||||
// host's observed fingerprint and remember it as paired — future connects are then
|
||||
// silent (rule 1), exactly like after a PIN/TOFU success. Dismisses the wait prompt.
|
||||
if awaitingApproval?.host.id == host.id {
|
||||
if let fp = model.connection?.hostFingerprint {
|
||||
store.pin(host.id, fingerprint: fp)
|
||||
}
|
||||
awaitingApproval = nil
|
||||
let approvedFingerprint = awaitingApproval?.host.id == host.id
|
||||
? model.connection?.hostFingerprint : nil
|
||||
if awaitingApproval?.host.id == host.id { awaitingApproval = nil }
|
||||
// Persist on the next runloop tick: HostStore is an ObservableObject, and mutating
|
||||
// its @Published from inside .onChange (a view-update callback) trips SwiftUI's
|
||||
// "Publishing changes from within view updates". A one-tick delay is imperceptible.
|
||||
let store = store
|
||||
DispatchQueue.main.async {
|
||||
store.markConnected(host.id)
|
||||
if let approvedFingerprint { store.pin(host.id, fingerprint: approvedFingerprint) }
|
||||
}
|
||||
case .idle:
|
||||
// The delegated-approval connect failed, timed out, or was cancelled — drop the
|
||||
@@ -333,6 +338,7 @@ struct ContentView: View {
|
||||
rawValue: UInt32(clamping: gamepadType)) ?? .auto),
|
||||
bitrateKbps: UInt32(clamping: bitrateKbps),
|
||||
audioChannels: UInt8(clamping: audioChannels),
|
||||
hdrEnabled: hdrEnabled,
|
||||
launchID: launchID,
|
||||
allowTofu: allowTofu,
|
||||
requestAccess: requestAccess)
|
||||
@@ -475,6 +481,7 @@ struct ContentView: View {
|
||||
gamepad: pad,
|
||||
bitrateKbps: bitrate,
|
||||
audioChannels: UInt8(clamping: audioChannels),
|
||||
hdrEnabled: hdrEnabled,
|
||||
autoTrust: true)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user