Skip to content

Commit bab811e

Browse files
joeblauclaude
andauthored
fix: cap the Video settings summary to the device ceiling (follow-up to #17) (#29)
The 1080p60 unlock (#28) gated the Quality and Frame Rate pickers by StreamCapability but left the collapsed Video launcher summary interpolating the raw persisted videoQuality/frameRate. On a device whose ceiling is below a stored value — e.g. a settings blob restored via iCloud/backup from a more capable phone — the row rendered "1080p · … · 60 fps" while the pickers and the actual encoded stream were correctly capped, reintroducing the exact over-promise #17 set out to remove. Show the effective (min'd) values so the summary matches what streams. Also pins two StreamCore contracts an adversarial review flagged as untested: the StreamCapability core-count boundary at exactly 6 (5 cores + ample RAM -> 30 fps) and the zero-dimension encodeSize fallback at a 1080 ceiling and under clamping. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5ca4bb2 commit bab811e

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

Stream/SettingsView.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,13 @@ struct SettingsView: View {
325325
? "\(settings.selectedProtocol.displayName) · \(settings.isSecure ? "Secure" : "Unencrypted")"
326326
: "\(settings.selectedProtocol.displayName) · Not configured"
327327
case .video:
328-
return "\(settings.videoQuality)p · \(bitrateLabel(settings.videoBitrate)) · \(settings.frameRate) fps"
328+
// Show the EFFECTIVE (device-capped) values, matching the Quality and
329+
// Frame Rate pickers — so the collapsed row never over-promises a
330+
// resolution/fps the device won't actually stream (e.g. a settings blob
331+
// restored from a more capable phone).
332+
let quality = min(settings.videoQuality, capability.maxShortEdge)
333+
let fps = min(settings.frameRate, capability.maxFrameRate)
334+
return "\(quality)p · \(bitrateLabel(settings.videoBitrate)) · \(fps) fps"
329335
case .backup:
330336
return "Off"
331337
case .audio:

StreamCoreTests/StreamSettingsTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ import StreamCore
7575
#expect(size.height == 1280)
7676
}
7777

78+
@Test("Zero-dimension fallback honors a 1080 ceiling (true 1080×1920 canvas)")
79+
func zeroFallbackAt1080Ceiling() {
80+
let size = settings(videoQuality: 1080).encodeSize(forOrientedWidth: 0, height: 0, maxShortEdge: 1080)
81+
#expect(size.width == 1080)
82+
#expect(size.height == 1920)
83+
}
84+
85+
@Test("Zero-dimension fallback still clamps to the device ceiling")
86+
func zeroFallbackClampsToCeiling() {
87+
// A 1080p pick on a 720-ceiling device must fall back to 720×1280, proving
88+
// the ceiling clamps the fallback branch too (not just the scaled path).
89+
let size = settings(videoQuality: 1080).encodeSize(forOrientedWidth: 0, height: 0, maxShortEdge: 720)
90+
#expect(size.width == 720)
91+
#expect(size.height == 1280)
92+
}
93+
7894
@Test("Aspect ratio is preserved within a pixel of rounding")
7995
func preservesAspectRatio() {
8096
let size = settings().encodeSize(forOrientedWidth: 1170, height: 2532, maxShortEdge: 1080)
@@ -153,6 +169,14 @@ import StreamCore
153169
#expect(cap.maxFrameRate == 30)
154170
}
155171

172+
@Test("The core-count boundary sits exactly at 6 (5 cores + ample RAM → 30 fps)")
173+
func coreCountBoundaryPinnedAtSix() {
174+
// Pins the >= 6 gate to the exact edge so a regression to >= 5 is caught.
175+
let cap = StreamCapability.device(processorCount: 5, physicalMemory: 8_000_000_000)
176+
#expect(cap.maxShortEdge == 1080)
177+
#expect(cap.maxFrameRate == 30)
178+
}
179+
156180
@Test("Memory-tier boundaries map to the intended profile")
157181
func memoryBoundaries() {
158182
// Exactly at the high floor → 1080p60.

0 commit comments

Comments
 (0)