From 977899b2406408b7864aaff0cd47c13c9841a09a Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 15 Sep 2026 02:01:13 +0800 Subject: [PATCH] [feat] enable iOS punching and preserve connection errors --- .../Bridge/CrossDeskRTCBridge.h | 3 +- .../Bridge/CrossDeskRTCBridge.mm | 25 ++++++--- apps/ios/CrossDeskMobile/ContentView.swift | 15 ++++-- .../CrossDeskMobile/RemoteSessionModel.swift | 54 +++++++++++-------- 4 files changed, 60 insertions(+), 37 deletions(-) diff --git a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h index b2adb70e..ba2d04b9 100644 --- a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h +++ b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h @@ -99,8 +99,7 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) { @property(nonatomic, weak, nullable) id delegate; - (void)configureWithSignalHost:(NSString *)host - signalPort:(NSInteger)signalPort - enableSRTP:(BOOL)enableSRTP; + signalPort:(NSInteger)signalPort; - (void)setHardwareAccelerationEnabled:(BOOL)enabled; - (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy; - (void)requestPresenceForRemoteIDs:(NSArray *)remoteIDs diff --git a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm index 1c65b386..c72b0bd3 100644 --- a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm +++ b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm @@ -75,7 +75,6 @@ struct RTCState { CallbackContext controller_context; std::string signal_host; int signal_port = 0; - bool enable_srtp = false; bool hardware_acceleration = true; VideoDegradationPreference video_adaptation_policy = VideoDegradationPreference::MaintainResolution; @@ -86,6 +85,7 @@ struct RTCState { std::string identity_base; std::string controller_login; std::string pending_remote_id; + uint64_t pending_controller_generation = 0; std::string pending_password; std::string transmission_id; std::string log_path; @@ -336,7 +336,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id, params.native_video_output = true; params.av1_encoding = false; params.turn_mode = TurnMode::TurnAutoUdpTcp; - params.enable_srtp = state.enable_srtp; + params.enable_srtp = true; params.video_content_type = VideoContentType::ScreenContent; params.video_quality = VideoQuality::QualityHigh; params.video_frame_rate = 60; @@ -432,8 +432,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id, } - (void)configureWithSignalHost:(NSString *)host - signalPort:(NSInteger)signalPort - enableSRTP:(BOOL)enableSRTP { + signalPort:(NSInteger)signalPort { NSString *trimmed = [host stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet]; const char *host_c_string = trimmed.UTF8String; @@ -455,7 +454,6 @@ Params MakeParams(const RTCState &state, const std::string &user_id, dispatch_async(_rtcQueue, ^{ const bool unchanged = self->_state->signal_host == host_value && self->_state->signal_port == signalPort && - self->_state->enable_srtp == enableSRTP && self->_state->identity_peer != nullptr; if (unchanged) return; @@ -463,7 +461,6 @@ Params MakeParams(const RTCState &state, const std::string &user_id, [self destroyIdentityPeer]; self->_state->signal_host = host_value; self->_state->signal_port = static_cast(signalPort); - self->_state->enable_srtp = enableSRTP; self->_state->log_path = log_path; self->_state->identity_ready = false; self->_state->identity_recovery_attempted = false; @@ -583,8 +580,14 @@ Params MakeParams(const RTCState &state, const std::string &user_id, password_c_string ? password_c_string : ""; if (identifier.empty()) return; + // Invalidate queued callbacks immediately, before asynchronous teardown. A + // canceled attempt for the same remote must not close a newly started one. + const uint64_t generation = _controllerGenerationCounter.fetch_add(1) + 1; + _activeControllerGeneration.store(0); dispatch_async(_rtcQueue, ^{ + if (generation != self->_controllerGenerationCounter.load()) return; self->_state->pending_remote_id = identifier; + self->_state->pending_controller_generation = generation; self->_state->pending_password = password_value; [self destroyControllerPeer]; if (self->_state->identity_ready && @@ -595,6 +598,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id, } - (void)disconnect { + _controllerGenerationCounter.fetch_add(1); + _activeControllerGeneration.store(0); dispatch_async(_rtcQueue, ^{ self->_state->pending_remote_id.clear(); self->_state->pending_password.clear(); @@ -868,7 +873,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id, _state->identity_base.empty()) { return; } - const uint64_t generation = _controllerGenerationCounter.fetch_add(1) + 1; + const uint64_t generation = _state->pending_controller_generation; + if (generation == 0 || generation != _controllerGenerationCounter.load()) return; _state->controller_context.generation = generation; _activeControllerGeneration.store(generation); _state->controller_login = "C-" + _state->identity_base; @@ -1112,6 +1118,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id, remoteID:(const std::string &)remoteID generation:(uint64_t)generation { if (![self isControllerGenerationCurrent:generation]) return; + if (state != CrossDeskConnectionStateFailed && + ![self isControllerGenerationActive:generation]) return; const std::string remote_copy = remoteID; if (state == CrossDeskConnectionStateConnected) { dispatch_async(_rtcQueue, ^{ @@ -1149,7 +1157,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id, } DispatchMain(^{ if (![self isControllerGenerationCurrent:generation]) return; - if (state == CrossDeskConnectionStateConnected && + if (state != CrossDeskConnectionStateFailed && ![self isControllerGenerationActive:generation]) { return; } @@ -1164,6 +1172,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id, - (BOOL)isControllerGenerationActive:(uint64_t)generation { return generation != 0 && + _controllerGenerationCounter.load() == generation && _activeControllerGeneration.load() == generation; } diff --git a/apps/ios/CrossDeskMobile/ContentView.swift b/apps/ios/CrossDeskMobile/ContentView.swift index 2e760218..02e11caa 100644 --- a/apps/ios/CrossDeskMobile/ContentView.swift +++ b/apps/ios/CrossDeskMobile/ContentView.swift @@ -17,6 +17,14 @@ struct ContentView: View { } .animation(.easeInOut(duration: 0.2), value: session.sessionVisible) .preferredColorScheme(session.sessionVisible ? .dark : .light) + .alert("连接提示", isPresented: Binding( + get: { session.connectionFailureMessage != nil }, + set: { if !$0 { session.dismissConnectionFailure() } } + )) { + Button("确定", role: .cancel) { session.dismissConnectionFailure() } + } message: { + Text(session.connectionFailureMessage ?? "") + } .onAppear { if !session.sessionVisible { AppOrientation.update(to: .portrait) @@ -710,17 +718,14 @@ private struct ServerSettingsView: View { .font(.caption) .foregroundStyle(.tertiary) } - Section("服务器") { + Section { TextField("信令服务器", text: $session.signalHost) .textInputAutocapitalization(.never) .autocorrectionDisabled() TextField("信令端口", text: $session.signalPort) .keyboardType(.numberPad) - } - Section { - Toggle("启用 SRTP", isOn: $session.enableSRTP) } header: { - Text("传输") + Text("服务器") } footer: { Text(session.localIdentity.isEmpty ? "正在获取本机 ID…" diff --git a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift index 3c52dc67..f72b186b 100644 --- a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift +++ b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift @@ -358,7 +358,6 @@ private final class RemoteAudioPlayer { final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDelegate { @Published var signalHost = "api.crossdesk.cn" @Published var signalPort = "9099" - @Published var enableSRTP = false @Published var mouseControlMode = MouseControlMode.saved { didSet { mouseControlMode.save() } } @@ -398,6 +397,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe @Published private(set) var recentConnections = RecentConnectionStore.load() @Published private(set) var recentConnectionPresence: [String: Bool] = [:] @Published private(set) var deviceOfflineAlertVisible = false + @Published private(set) var connectionFailureMessage: String? let bridge = CrossDeskRTCBridge() private let audioPlayer = RemoteAudioPlayer() @@ -424,7 +424,6 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe private struct BridgeConfiguration: Equatable { let host: String let signalPort: Int - let enableSRTP: Bool } private var bridgeConfiguration: BridgeConfiguration? @@ -457,8 +456,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe signalStatus = "服务器配置无效" return false } - let configuration = BridgeConfiguration(host: host, signalPort: signal, - enableSRTP: enableSRTP) + let configuration = BridgeConfiguration(host: host, signalPort: signal) if bridgeConfiguration != configuration { bridgeConfiguration = configuration signalConnected = false @@ -472,8 +470,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe bridge.setHardwareAccelerationEnabled(videoCodecMode == .hardware) bridge.setVideoAdaptationPolicy(videoAdaptationPolicy.bridgeValue) bridge.configure(withSignalHost: host, - signalPort: signal, - enableSRTP: enableSRTP) + signalPort: signal) return true } @@ -484,6 +481,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe } func connect() { + connectionFailureMessage = nil let identifier = remoteID.trimmingCharacters(in: .whitespacesAndNewlines) guard !identifier.isEmpty else { connectionStatus = "请输入远程设备 ID" @@ -576,7 +574,25 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe deviceOfflineAlertVisible = false } + func dismissConnectionFailure() { + guard connectionFailureMessage != nil else { return } + connectionFailureMessage = nil + } + + private func finishConnection(withError message: String) { + // Cleanup may emit Closed. Preserve the terminal reason independently + // of the progress view and invalidate those callbacks in the bridge. + resetConnection() + connectionStatus = message + connectionFailureMessage = message + } + func disconnect() { + resetConnection() + } + + private func resetConnection() { + connectionFailureMessage = nil cancelVideoRecovery() let wasCheckingPresence = pendingPresenceRemoteID != nil cancelPresenceProbe() @@ -882,6 +898,10 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe func rtcBridge(_ bridge: CrossDeskRTCBridge, didChange state: CrossDeskConnectionState, remoteID: String) { + guard isConnecting || isConnected || sessionVisible, + remoteID.isEmpty || remoteID == activeRemoteID else { + return + } isConnected = state.rawValue == 1 if !isConnected { cancelVideoRecovery() @@ -897,35 +917,25 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe bridge.setAudioEnabled(audioEnabled) AppOrientation.update(to: .allButUpsideDown) case 2: connectionStatus = "正在收集 ICE 候选" - case 3: connectionStatus = "网络连接已中断" + case 3: finishConnection(withError: "网络连接已中断,请重新连接。") case 4: - connectionStatus = "P2P/TURN 建链失败" - sessionVisible = false - AppOrientation.update(to: .portrait) + finishConnection(withError: "P2P/TURN 建链失败,请检查网络后重试。") case 5: - connectionStatus = "未连接" - sessionVisible = false - AppOrientation.update(to: .portrait) + finishConnection(withError: "远程连接已结束。") case 6: - connectionStatus = "访问密码错误" - sessionVisible = false - AppOrientation.update(to: .portrait) + finishConnection(withError: "访问密码错误,请重新输入。") case 7: - connectionStatus = "远程设备 ID 不存在" if !remoteID.isEmpty { recentConnectionPresence[remoteID] = false presenceUpdatedAt.removeValue(forKey: remoteID) } - sessionVisible = false - AppOrientation.update(to: .portrait) + finishConnection(withError: "远程设备 ID 不存在,请检查设备 ID。") case 8: - connectionStatus = "远程设备当前不可用" if !remoteID.isEmpty { recentConnectionPresence[remoteID] = false presenceUpdatedAt.removeValue(forKey: remoteID) } - sessionVisible = false - AppOrientation.update(to: .portrait) + finishConnection(withError: "远程设备当前不可用,请稍后重试。") default: connectionStatus = "未知连接状态" } }