Skip to content

Commit b8baa8f

Browse files
committed
fix: improve iOS stream diagnostics
1 parent 6056fd3 commit b8baa8f

6 files changed

Lines changed: 327 additions & 15 deletions

File tree

ios/SimDeckStudio.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@
363363
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
364364
CODE_SIGN_ENTITLEMENTS = SimDeckStudio/SimDeckStudio.entitlements;
365365
CODE_SIGN_STYLE = Automatic;
366-
CURRENT_PROJECT_VERSION = 202605172332;
366+
CURRENT_PROJECT_VERSION = 202605180047;
367367
DEVELOPMENT_ASSET_PATHS = "";
368368
DEVELOPMENT_TEAM = CS838V553Y;
369369
ENABLE_PREVIEWS = YES;
@@ -392,7 +392,7 @@
392392
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
393393
CODE_SIGN_ENTITLEMENTS = SimDeckStudio/SimDeckStudio.entitlements;
394394
CODE_SIGN_STYLE = Automatic;
395-
CURRENT_PROJECT_VERSION = 202605172332;
395+
CURRENT_PROJECT_VERSION = 202605180047;
396396
DEVELOPMENT_ASSET_PATHS = "";
397397
DEVELOPMENT_TEAM = CS838V553Y;
398398
ENABLE_PREVIEWS = YES;

ios/SimDeckStudio/App/AppModel.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ final class AppModel {
5252
private static let selectedEndpointKey = "selectedEndpoint"
5353
private static let streamConfigKey = "streamConfig"
5454
private static let hapticsEnabledKey = "hapticsEnabled"
55+
private static let touchOverlayVisibleKey = "touchOverlayVisible"
5556
private static let lastFrameCacheDirectoryName = "LastStreamFrames"
5657

5758
var endpoint: SimDeckEndpoint?
@@ -70,6 +71,8 @@ final class AppModel {
7071
var chromeImage: UIImage?
7172
var chromeScreenMask: UIImage?
7273
var streamDiagnostics = StreamDiagnostics()
74+
var streamReconnects = 0
75+
var streamReconnectReason = ""
7376
var bootingSimulatorID: String?
7477
var streamDisplayToken = 0
7578
var hasCurrentStreamFrame = false
@@ -80,6 +83,11 @@ final class AppModel {
8083
UserDefaults.standard.set(hapticsEnabled, forKey: Self.hapticsEnabledKey)
8184
}
8285
}
86+
var touchOverlayVisible = AppModel.loadTouchOverlayVisible() {
87+
didSet {
88+
UserDefaults.standard.set(touchOverlayVisible, forKey: Self.touchOverlayVisibleKey)
89+
}
90+
}
8391

8492
@ObservationIgnored private var streamClient: WebRTCClient?
8593
@ObservationIgnored private var hasAutoConnected = false
@@ -449,7 +457,7 @@ final class AppModel {
449457
self.scheduleStreamReconnect(reason: reason)
450458
}
451459
}
452-
let loadedChromeAssets = await chromeAssets(api: api, endpoint: endpoint, simulatorID: selectedSimulatorID)
460+
let loadedChromeAssets = await chromeAssets(api: api, endpoint: endpoint, simulatorID: selectedSimulatorID, forceRefresh: true)
453461
guard isCurrentStreamRequest(generation, simulatorID: selectedSimulatorID) else {
454462
client.disconnect()
455463
return false
@@ -502,7 +510,7 @@ final class AppModel {
502510
status = "Loading device chrome."
503511

504512
let api = SimDeckAPI(endpoint: endpoint)
505-
let loadedChromeAssets = await chromeAssets(api: api, endpoint: endpoint, simulatorID: selectedSimulatorID)
513+
let loadedChromeAssets = await chromeAssets(api: api, endpoint: endpoint, simulatorID: selectedSimulatorID, forceRefresh: true)
506514
guard isCurrentStreamRequest(generation, simulatorID: selectedSimulatorID) else { return }
507515
applyChromeAssets(loadedChromeAssets)
508516
status = selectedSimulator?.isBooted == true ? "Ready." : "Ready to boot."
@@ -767,6 +775,12 @@ final class AppModel {
767775
updateStreamConfig { $0.quality = quality }
768776
}
769777

778+
func setTouchOverlayVisible(_ isVisible: Bool) {
779+
guard touchOverlayVisible != isVisible else { return }
780+
touchOverlayVisible = isVisible
781+
hapticSelection()
782+
}
783+
770784
private func autoConnectIfNeeded(_ endpoint: SimDeckEndpoint) async {
771785
guard !hasAutoConnected, !isAutoConnecting, self.endpoint == nil, authEndpoint == nil else { return }
772786
await autoConnectToAvailableEndpointIfNeeded(preferredEndpoint: endpoint)
@@ -843,6 +857,8 @@ final class AppModel {
843857
self.selectedSimulatorID != nil,
844858
self.selectedSimulator?.isBooted == true {
845859
attempt += 1
860+
self.streamReconnects += 1
861+
self.streamReconnectReason = reason
846862
self.lastReconnectStartedAt = Date()
847863
self.status = attempt == 1
848864
? (reason == "foreground" ? "Resuming stream." : "Recovering stream.")
@@ -941,17 +957,19 @@ final class AppModel {
941957
private func chromeAssets(
942958
api: SimDeckAPI,
943959
endpoint: SimDeckEndpoint,
944-
simulatorID: String
960+
simulatorID: String,
961+
forceRefresh: Bool = false
945962
) async -> ChromeAssets {
946-
if let cached = cachedChromeAssets(endpoint: endpoint, simulatorID: simulatorID) {
963+
if !forceRefresh, let cached = cachedChromeAssets(endpoint: endpoint, simulatorID: simulatorID) {
947964
return cached
948965
}
949966

950967
let loadedProfile = try? await api.chromeProfile(udid: simulatorID)
951-
let loadedImage = try? await api.chromeImage(udid: simulatorID)
968+
let assetStamp = loadedProfile?.assetStamp
969+
let loadedImage = try? await api.chromeImage(udid: simulatorID, stamp: assetStamp)
952970
let loadedScreenMask: UIImage?
953971
if loadedProfile?.hasScreenMask == true {
954-
loadedScreenMask = try? await api.screenMaskImage(udid: simulatorID)
972+
loadedScreenMask = try? await api.screenMaskImage(udid: simulatorID, stamp: assetStamp)
955973
} else {
956974
loadedScreenMask = nil
957975
}
@@ -1406,6 +1424,10 @@ final class AppModel {
14061424
UserDefaults.standard.object(forKey: hapticsEnabledKey) as? Bool ?? true
14071425
}
14081426

1427+
private static func loadTouchOverlayVisible() -> Bool {
1428+
UserDefaults.standard.object(forKey: touchOverlayVisibleKey) as? Bool ?? true
1429+
}
1430+
14091431
func hapticSelection() {
14101432
guard hapticsEnabled else { return }
14111433
UISelectionFeedbackGenerator().selectionChanged()

ios/SimDeckStudio/App/Models.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,27 +162,52 @@ struct PrivateDisplayInfo: Hashable, Decodable, Sendable {
162162

163163
struct StreamDiagnostics: Hashable, Sendable {
164164
var codec: String = ""
165+
var width: UInt64 = 0
166+
var height: UInt64 = 0
165167
var receivedPackets: UInt64 = 0
166168
var decodedFrames: UInt64 = 0
169+
var renderedFrames: UInt64 = 0
170+
var decoderDroppedFrames: UInt64 = 0
171+
var presentationDroppedFrames: UInt64 = 0
167172
var droppedFrames: UInt64 = 0
168173
var packetsLost: UInt64 = 0
174+
var latestPacketGapMs: Double = 0
169175
var latestFrameGapMs: Double = 0
176+
var packetFps: Double = 0
170177
var decodedFps: Double = 0
171178
var renderedFps: Double = 0
179+
var peerConnectionState: String = ""
180+
var iceConnectionState: String = ""
181+
var iceGatheringState: String = ""
182+
var signalingState: String = ""
172183
var selectedCandidatePair: String = ""
173184
var timestamp = Date()
174185

175186
init() {}
176187

177188
init(stats: [String: Any]) {
178189
codec = stats["codec"] as? String ?? ""
190+
width = StreamDiagnostics.uintValue(stats["width"])
191+
height = StreamDiagnostics.uintValue(stats["height"])
179192
receivedPackets = StreamDiagnostics.uintValue(stats["receivedPackets"])
180193
decodedFrames = StreamDiagnostics.uintValue(stats["decodedFrames"])
194+
renderedFrames = StreamDiagnostics.uintValue(stats["renderedFrames"])
195+
decoderDroppedFrames = StreamDiagnostics.uintValue(stats["decoderDroppedFrames"])
196+
presentationDroppedFrames = StreamDiagnostics.uintValue(stats["presentationDroppedFrames"])
181197
droppedFrames = StreamDiagnostics.uintValue(stats["droppedFrames"])
198+
if decoderDroppedFrames == 0 {
199+
decoderDroppedFrames = droppedFrames
200+
}
182201
packetsLost = StreamDiagnostics.uintValue(stats["packetsLost"])
202+
latestPacketGapMs = StreamDiagnostics.doubleValue(stats["latestPacketGapMs"])
183203
latestFrameGapMs = StreamDiagnostics.doubleValue(stats["latestFrameGapMs"])
204+
packetFps = StreamDiagnostics.doubleValue(stats["packetFps"])
184205
decodedFps = StreamDiagnostics.doubleValue(stats["decodedFps"])
185206
renderedFps = StreamDiagnostics.doubleValue(stats["appFps"])
207+
peerConnectionState = stats["peerConnectionState"] as? String ?? stats["status"] as? String ?? ""
208+
iceConnectionState = stats["iceConnectionState"] as? String ?? ""
209+
iceGatheringState = stats["iceGatheringState"] as? String ?? ""
210+
signalingState = stats["signalingState"] as? String ?? ""
186211
selectedCandidatePair = stats["selectedCandidatePair"] as? String ?? ""
187212
timestamp = Date()
188213
}
@@ -225,6 +250,22 @@ struct ChromeProfile: Hashable, Decodable, Sendable {
225250
let chromeStyle: String?
226251
let hasScreenMask: Bool?
227252
let buttons: [ChromeButtonProfile]?
253+
254+
var assetStamp: String {
255+
[
256+
totalWidth,
257+
totalHeight,
258+
screenX,
259+
screenY,
260+
screenWidth,
261+
screenHeight,
262+
cornerRadius
263+
]
264+
.map { value in
265+
value.isFinite ? String(Int((value * 1000).rounded())) : "0"
266+
}
267+
.joined(separator: "x")
268+
}
228269
}
229270

230271
struct ChromeButtonProfile: Hashable, Decodable, Sendable {

ios/SimDeckStudio/Networking/SimDeckAPI.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ struct SimDeckAPI: Sendable {
7676
)
7777
}
7878

79-
func chromeImage(udid: String) async throws -> UIImage {
79+
func chromeImage(udid: String, stamp: String? = nil) async throws -> UIImage {
8080
let data = try await request(
8181
path: "/api/simulators/\(udid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? udid)/chrome.png",
8282
method: "GET",
8383
body: Optional<String>.none,
8484
timeout: 10,
85+
queryItems: Self.assetQueryItems(stamp: stamp),
8586
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData
8687
)
8788
guard let image = UIImage(data: data) else {
@@ -90,12 +91,13 @@ struct SimDeckAPI: Sendable {
9091
return image
9192
}
9293

93-
func screenMaskImage(udid: String) async throws -> UIImage {
94+
func screenMaskImage(udid: String, stamp: String? = nil) async throws -> UIImage {
9495
let data = try await request(
9596
path: "/api/simulators/\(udid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? udid)/screen-mask.png",
9697
method: "GET",
9798
body: Optional<String>.none,
9899
timeout: 10,
100+
queryItems: Self.assetQueryItems(stamp: stamp),
99101
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData
100102
)
101103
guard let image = UIImage(data: data) else {
@@ -113,9 +115,10 @@ struct SimDeckAPI: Sendable {
113115
method: String = "GET",
114116
body: (some Encodable)? = Optional<String>.none,
115117
timeout: TimeInterval = 10,
118+
queryItems: [URLQueryItem] = [],
116119
cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
117120
) async throws -> T {
118-
let data = try await request(path: path, method: method, body: body, timeout: timeout, cachePolicy: cachePolicy)
121+
let data = try await request(path: path, method: method, body: body, timeout: timeout, queryItems: queryItems, cachePolicy: cachePolicy)
119122
if data.isEmpty, T.self == EmptyResponse.self {
120123
return EmptyResponse() as! T
121124
}
@@ -127,13 +130,15 @@ struct SimDeckAPI: Sendable {
127130
method: String,
128131
body: (some Encodable)?,
129132
timeout: TimeInterval,
133+
queryItems: [URLQueryItem] = [],
130134
cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
131135
) async throws -> Data {
132136
let (data, _) = try await requestWithHTTPResponse(
133137
path: path,
134138
method: method,
135139
body: body,
136140
timeout: timeout,
141+
queryItems: queryItems,
137142
cachePolicy: cachePolicy
138143
)
139144
return data
@@ -144,9 +149,10 @@ struct SimDeckAPI: Sendable {
144149
method: String,
145150
body: (some Encodable)?,
146151
timeout: TimeInterval,
152+
queryItems: [URLQueryItem] = [],
147153
cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
148154
) async throws -> (Data, HTTPURLResponse) {
149-
var request = URLRequest(url: url(for: path), cachePolicy: cachePolicy, timeoutInterval: timeout)
155+
var request = URLRequest(url: url(for: path, queryItems: queryItems), cachePolicy: cachePolicy, timeoutInterval: timeout)
150156
request.httpMethod = method
151157
request.setValue("application/json", forHTTPHeaderField: "Accept")
152158
request.setValue(originHeaderValue, forHTTPHeaderField: "Origin")
@@ -194,15 +200,23 @@ struct SimDeckAPI: Sendable {
194200
return components.url?.absoluteString.trimmingTrailingSlashes() ?? baseURL.absoluteString
195201
}
196202

197-
private func url(for path: String) -> URL {
203+
private func url(for path: String, queryItems: [URLQueryItem] = []) -> URL {
198204
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
199205
return baseURL.appendingPathComponent(path)
200206
}
201207
let prefix = components.path.trimmingTrailingSlashes()
202208
let suffix = path.hasPrefix("/") ? path : "/\(path)"
203209
components.path = "\(prefix)\(suffix)"
210+
if !queryItems.isEmpty {
211+
components.queryItems = (components.queryItems ?? []) + queryItems
212+
}
204213
return components.url ?? baseURL.appendingPathComponent(path)
205214
}
215+
216+
private static func assetQueryItems(stamp: String?) -> [URLQueryItem] {
217+
guard let stamp = stamp?.nilIfBlank else { return [] }
218+
return [URLQueryItem(name: "stamp", value: stamp)]
219+
}
206220
}
207221

208222
private struct EmptyResponse: Codable {}

ios/SimDeckStudio/Views/ContentView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,12 @@ private struct ServerTitleButton: View {
190190
}
191191
.padding(.leading, 14)
192192
.padding(.trailing, 12)
193-
.padding(.vertical, 8)
193+
.padding(.vertical, 5)
194194
.contentShape(Capsule())
195195
}
196196
.buttonStyle(.plain)
197197
.frame(minWidth: 170, maxWidth: 240)
198+
.frame(height: 42)
198199
.modifier(GlassCapsuleModifier(interactive: true))
199200
}
200201

0 commit comments

Comments
 (0)