mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 01:13:19 +08:00
[feat] enable iOS punching and preserve connection errors
This commit is contained in:
@@ -99,8 +99,7 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
@property(nonatomic, weak, nullable) id<CrossDeskRTCBridgeDelegate> 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<NSString *> *)remoteIDs
|
||||
|
||||
@@ -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<int>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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…"
|
||||
|
||||
@@ -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 = "未知连接状态"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user