[feat] remember remote display resolutions on mobile clients

This commit is contained in:
dijunkun committed 2026-11-11 15:26:04 +08:00
1 parent 23f48dc761
commit 25fb2a3d7f
15 files changed
+253 -33

No files matched your search

@@ -180,7 +180,8 @@ typedef struct {
- (void)sendAnnouncementRequest:(NSData *)request requestID:(NSString *)requestID
NS_SWIFT_NAME(sendAnnouncementRequest(_:requestID:));
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password;
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password
displayResolutions:(NSData *)displayResolutions;
- (void)disconnect;
// Close both session and signaling peers, including automatic reconnection.
- (void)stopNetworking;
@@ -86,6 +86,7 @@ struct RTCState {
std::string identity_base;
std::string controller_login;
std::string pending_remote_id;
std::vector<crossdesk::DisplayResolutionPreference> preferred_display_resolutions;
uint64_t pending_controller_generation = 0;
std::string pending_password;
std::string transmission_id;
@@ -800,7 +801,14 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
});
}
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password {
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password
displayResolutions:(NSData *)displayResolutions {
std::vector<crossdesk::DisplayResolutionPreference> preferences;
if (displayResolutions.length > 0 && displayResolutions.length <= 65536) {
const auto *bytes = static_cast<const char *>(displayResolutions.bytes);
crossdesk::ParseDisplayResolutionPreferences(
std::string(bytes, displayResolutions.length), preferences);
}
NSString *cleanID = [remoteID
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
NSString *cleanPassword = [password
@@ -820,6 +828,7 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
dispatch_async(_rtcQueue, ^{
if (generation != self->_controllerGenerationCounter.load()) return;
self->_state->pending_remote_id = identifier;
self->_state->preferred_display_resolutions = preferences;
self->_state->pending_controller_generation = generation;
self->_state->pending_password = password_value;
[self destroyControllerPeer];
@@ -1458,6 +1467,7 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
: "iPhone-" + self->_state->identity_base;
RemoteAction action{};
action.type = ControlType::host_infomation;
action.preferred_display_resolutions = self->_state->preferred_display_resolutions;
action.i.platform = crossdesk::HostPlatform::IOS;
action.i.supports_privacy_screen = true;
action.i.supports_video_codec_status = true;
@@ -370,6 +370,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
private var appIsActive = false
private lazy var audioPlayer = RemoteAudioPlayer()
private var activeRemoteID = ""
private var resolutionPreferenceKey = ""
private var remoteAppVersion = ""
private var remoteHostInfoReceived = false
private var remoteVersionCheckAttempted = false
@@ -610,6 +611,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
private func beginRemoteConnection(_ identifier: String) {
guard let configuration = bridgeConfiguration else { return }
AppOrientation.endSession()
networkStatistics = RemoteNetworkStatistics()
resetRemoteHostInfo()
@@ -617,6 +619,8 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
resetVideoSettings()
cancelPresenceProbe()
activeRemoteID = identifier
resolutionPreferenceKey = RemoteDisplayResolutionStore.key(host: configuration.host,
port: configuration.signalPort, remoteID: identifier)
activeDisplayName = ""
displaySizes = []
connectionRecorded = false
@@ -626,7 +630,8 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
sessionVisible = false
resetRemoteCursorState()
connectionStatus = NSLocalizedString("正在准备连接…", comment: "")
bridge.connect(toRemoteID: identifier, password: password)
bridge.connect(toRemoteID: identifier, password: password,
displayResolutions: RemoteDisplayResolutionStore.message(key: resolutionPreferenceKey))
}
private func beginPresenceProbe(_ identifier: String) {
@@ -1044,8 +1049,13 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
func updateDisplayResolution(_ resolution: RemoteDisplayResolution, displayID: Int, displayKey: String) {
guard hasNetworkConsent, isConnected, selectedDisplay == displayID,
displayResolutions.display(displayID)?.display_key == displayKey,
let request = displayResolutions.begin(displayID: selectedDisplay, resolution: resolution) else { return }
let display = displayResolutions.display(displayID), display.display_key == displayKey else { return }
if !displayResolutions.pending, display.current == resolution, display.modes.contains(resolution) {
RemoteDisplayResolutionStore.remember(RemoteDisplayResolutionPreference(display_key: displayKey,
width: resolution.width, height: resolution.height), key: resolutionPreferenceKey)
return
}
guard let request = displayResolutions.begin(displayID: selectedDisplay, resolution: resolution) else { return }
displayResolutionTimeoutTask?.cancel()
bridge.sendDisplayResolution(displayID: request.display_id, displayKey: request.display_key,
width: request.width, height: request.height, requestID: request.request_id)
@@ -1059,7 +1069,9 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
// Host information can arrive before the connection-state callback.
// The bridge already rejects callbacks from previous sessions.
guard hasNetworkConsent else { return }
displayResolutions.receive(message)
if let confirmed = displayResolutions.receive(message) {
RemoteDisplayResolutionStore.remember(confirmed, key: resolutionPreferenceKey)
}
if !displayResolutions.pending {
displayResolutionTimeoutTask?.cancel(); displayResolutionTimeoutTask = nil
}
@@ -77,6 +77,42 @@ struct RemoteDisplayResolution: Codable, Equatable, Hashable {
var valid: Bool { (1...32768).contains(width) && (1...32768).contains(height) }
}
struct RemoteDisplayResolutionPreference: Codable, Equatable {
let display_key: String
let width: Int
let height: Int
var valid: Bool {
!display_key.isEmpty && display_key.utf8.count <= 1024 && !display_key.contains("\0") &&
RemoteDisplayResolution(width: width, height: height).valid
}
}
enum RemoteDisplayResolutionStore {
static func key(host: String, port: Int, remoteID: String) -> String {
let scope = [host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), String(port), remoteID]
guard !remoteID.isEmpty, let data = try? JSONEncoder().encode(scope) else { return "" }
return "crossdesk.remoteDisplayResolutions.v1." + data.base64EncodedString()
}
static func load(key: String, defaults: UserDefaults = .standard) -> [RemoteDisplayResolutionPreference] {
guard !key.isEmpty, let data = defaults.data(forKey: key), data.count <= 65536,
let values = try? JSONDecoder().decode([RemoteDisplayResolutionPreference].self, from: data),
values.count <= 32, values.allSatisfy(\.valid),
Set(values.map(\.display_key)).count == values.count else { return [] }
return values
}
static func remember(_ preference: RemoteDisplayResolutionPreference, key: String,
defaults: UserDefaults = .standard) {
guard !key.isEmpty, preference.valid else { return }
var values = load(key: key, defaults: defaults).filter { $0.display_key != preference.display_key }
if values.count >= 32 { values.removeFirst() }
values.append(preference)
if let data = try? JSONEncoder().encode(values) { defaults.set(data, forKey: key) }
}
static func message(key: String, defaults: UserDefaults = .standard) -> Data {
(try? JSONEncoder().encode(load(key: key, defaults: defaults))) ?? Data("[]".utf8)
}
}
struct RemoteDisplayResolutionInfo: Decodable {
let display_id: Int
let display_key: String
@@ -128,7 +164,8 @@ struct RemoteDisplayResolutionState {
guard pending, requested?.request_id == requestID else { return }
pending = false; failed = true
}
mutating func receive(_ data: Data) {
@discardableResult
mutating func receive(_ data: Data) -> RemoteDisplayResolutionPreference? {
struct Host: Decodable { let display_resolutions: [RemoteDisplayResolutionInfo]? }
struct Message: Decodable {
let type: Int
@@ -136,16 +173,16 @@ struct RemoteDisplayResolutionState {
let display_resolution: RemoteDisplayResolutionRequest?
let display_resolutions: [RemoteDisplayResolutionInfo]?
}
guard let message = try? JSONDecoder().decode(Message.self, from: data) else { return }
guard let message = try? JSONDecoder().decode(Message.self, from: data) else { return nil }
let actual: [RemoteDisplayResolutionInfo]
if message.type == 3, let host = message.host_info {
actual = host.display_resolutions ?? []
} else if message.type == 16, let response = message.display_resolution,
let requested, response.matches(requested), let values = message.display_resolutions {
actual = values
} else { return }
} else { return nil }
guard actual.count <= 32, actual.allSatisfy(\.valid),
Set(actual.map(\.display_id)).count == actual.count else { return }
Set(actual.map(\.display_id)).count == actual.count else { return nil }
displays = actual
if message.type == 16, let response = message.display_resolution {
pending = false
@@ -153,11 +190,16 @@ struct RemoteDisplayResolutionState {
$0.display_id == response.display_id && $0.display_key == response.display_key &&
$0.current == response.resolution
}
if !failed {
return RemoteDisplayResolutionPreference(display_key: response.display_key,
width: response.width, height: response.height)
}
} else if pending, let requested, !actual.contains(where: {
$0.display_id == requested.display_id && $0.display_key == requested.display_key &&
$0.modes.contains(requested.resolution)
}) {
expire(requestID: requested.request_id)
}
return nil
}
}