mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-17 01:13:19 +08:00
[fix] prevent stale device online status on iOS
This commit is contained in:
@@ -61,7 +61,8 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
displayNames:(NSArray<NSString *> *)displayNames
|
||||
displaySizes:(NSArray<NSValue *> *)displaySizes;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didReceivePresence:(NSDictionary<NSString *, NSNumber *> *)presence;
|
||||
didReceivePresence:(NSDictionary<NSString *, NSNumber *> *)presence
|
||||
snapshot:(BOOL)snapshot;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didReceiveCursorVisible:(BOOL)visible
|
||||
shape:(NSInteger)shape
|
||||
@@ -104,7 +105,9 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
- (void)setHardwareAccelerationEnabled:(BOOL)enabled;
|
||||
- (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy;
|
||||
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs
|
||||
NS_SWIFT_NAME(requestPresence(remoteIDs:));
|
||||
subscribe:(BOOL)subscribe
|
||||
NS_SWIFT_NAME(requestPresence(remoteIDs:subscribe:));
|
||||
- (void)invalidatePresence;
|
||||
|
||||
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password;
|
||||
- (void)disconnect;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -62,6 +63,11 @@ struct CallbackContext {
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct PresenceRequest {
|
||||
uint64_t generation;
|
||||
bool subscription;
|
||||
};
|
||||
|
||||
struct RTCState {
|
||||
PeerPtr *identity_peer = nullptr;
|
||||
PeerPtr *controller_peer = nullptr;
|
||||
@@ -75,6 +81,7 @@ struct RTCState {
|
||||
VideoDegradationPreference video_adaptation_policy =
|
||||
VideoDegradationPreference::MaintainResolution;
|
||||
bool identity_ready = false;
|
||||
std::deque<PresenceRequest> presence_requests;
|
||||
bool identity_recovery_attempted = false;
|
||||
std::string identity_with_password;
|
||||
std::string identity_base;
|
||||
@@ -192,11 +199,15 @@ void DispatchMain(dispatch_block_t block) {
|
||||
} // namespace
|
||||
|
||||
@interface CrossDeskRTCBridge ()
|
||||
- (void)handleSignalState:(CrossDeskSignalState)state role:(PeerRole)role;
|
||||
- (void)handleSignalState:(CrossDeskSignalState)state
|
||||
role:(PeerRole)role
|
||||
generation:(uint64_t)generation;
|
||||
- (void)handleSignalMessage:(const char *)message
|
||||
size:(size_t)size
|
||||
role:(PeerRole)role;
|
||||
- (void)handleProvisionedIdentity:(const std::string &)identity;
|
||||
role:(PeerRole)role
|
||||
generation:(uint64_t)generation;
|
||||
- (void)handleProvisionedIdentity:(const std::string &)identity
|
||||
generation:(uint64_t)generation;
|
||||
- (void)handleConnectionState:(CrossDeskConnectionState)state
|
||||
remoteID:(const std::string &)remoteID
|
||||
generation:(uint64_t)generation;
|
||||
@@ -271,7 +282,8 @@ void OnSignalState(SignalStatus status, const char *, size_t, void *user_data) {
|
||||
(context->role == PeerRole::Identity ||
|
||||
[owner isControllerGenerationActive:context->generation])) {
|
||||
[owner handleSignalState:static_cast<CrossDeskSignalState>(status)
|
||||
role:context->role];
|
||||
role:context->role
|
||||
generation:context->generation];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +291,8 @@ void OnSignalMessage(const char *message, size_t size, void *user_data) {
|
||||
auto *context = static_cast<CallbackContext *>(user_data);
|
||||
CrossDeskRTCBridge *owner = context ? context->owner : nil;
|
||||
if (owner && message && size > 0) {
|
||||
[owner handleSignalMessage:message size:size role:context->role];
|
||||
[owner handleSignalMessage:message size:size role:context->role
|
||||
generation:context->generation];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +318,8 @@ void OnNetworkStats(const char *peer_id, size_t peer_id_size,
|
||||
if (!owner) return;
|
||||
if (context->role == PeerRole::Identity && mode == TraversalMode::UnknownMode &&
|
||||
peer_id && peer_id_size > 0) {
|
||||
[owner handleProvisionedIdentity:std::string(peer_id, peer_id_size)];
|
||||
[owner handleProvisionedIdentity:std::string(peer_id, peer_id_size)
|
||||
generation:context->generation];
|
||||
} else if (context->role == PeerRole::Controller && stats &&
|
||||
[owner isControllerGenerationActive:context->generation]) {
|
||||
[owner handleStats:stats mode:mode];
|
||||
@@ -370,6 +384,9 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
std::atomic<uint64_t> _receivedVideoFrames;
|
||||
std::atomic<uint64_t> _controllerGenerationCounter;
|
||||
std::atomic<uint64_t> _activeControllerGeneration;
|
||||
std::atomic<uint64_t> _identityGeneration;
|
||||
std::atomic<uint64_t> _signalGeneration;
|
||||
std::atomic<uint64_t> _presenceGeneration;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
@@ -396,6 +413,9 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
_receivedVideoFrames.store(0);
|
||||
_controllerGenerationCounter.store(0);
|
||||
_activeControllerGeneration.store(0);
|
||||
_identityGeneration.store(0);
|
||||
_signalGeneration.store(0);
|
||||
_presenceGeneration.store(0);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -517,10 +537,17 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
});
|
||||
}
|
||||
|
||||
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs {
|
||||
- (void)invalidatePresence {
|
||||
_presenceGeneration.fetch_add(1);
|
||||
}
|
||||
|
||||
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs
|
||||
subscribe:(BOOL)subscribe {
|
||||
NSArray<NSString *> *owned_ids = [remoteIDs copy];
|
||||
const uint64_t generation = _presenceGeneration.load();
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (!self->_state->identity_peer || !self->_state->identity_ready ||
|
||||
if (generation != self->_presenceGeneration.load() ||
|
||||
!self->_state->identity_peer || !self->_state->identity_ready ||
|
||||
self->_state->identity_base.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -540,14 +567,19 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
@"type" : @"recent_connections_presence",
|
||||
@"user_id" : user_id ?: @"",
|
||||
@"devices" : devices,
|
||||
@"subscribe" : @(subscribe),
|
||||
};
|
||||
NSData *payload = [NSJSONSerialization dataWithJSONObject:request
|
||||
options:0
|
||||
error:nil];
|
||||
if (!payload) return;
|
||||
SendSignalMessage(self->_state->identity_peer,
|
||||
static_cast<const char *>(payload.bytes),
|
||||
payload.length);
|
||||
if (SendSignalMessage(self->_state->identity_peer,
|
||||
static_cast<const char *>(payload.bytes),
|
||||
payload.length) == 0) {
|
||||
// Both old and new servers return snapshots in request order. Retain the
|
||||
// epoch so a pre-foreground response cannot satisfy a fresh query.
|
||||
self->_state->presence_requests.push_back({generation, bool(subscribe)});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -832,13 +864,15 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
|
||||
- (void)createIdentityPeer {
|
||||
if (_state->identity_peer || _state->signal_host.empty()) return;
|
||||
_state->identity_context.generation = _identityGeneration.fetch_add(1) + 1;
|
||||
Params params = MakeParams(*_state, _state->identity_with_password,
|
||||
&_state->identity_context);
|
||||
_state->identity_peer = CreatePeer(¶ms);
|
||||
if (!_state->identity_peer || Init(_state->identity_peer) != 0) {
|
||||
[self destroyIdentityPeer];
|
||||
[self handleSignalState:CrossDeskSignalStateFailed
|
||||
role:PeerRole::Identity];
|
||||
role:PeerRole::Identity
|
||||
generation:_identityGeneration.load()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,6 +924,9 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
}
|
||||
|
||||
- (void)destroyIdentityPeer {
|
||||
_identityGeneration.fetch_add(1);
|
||||
_presenceGeneration.fetch_add(1);
|
||||
_state->presence_requests.clear();
|
||||
if (_state->identity_peer) {
|
||||
DestroyPeer(&_state->identity_peer);
|
||||
}
|
||||
@@ -920,8 +957,19 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
[self resetVideoDelivery];
|
||||
}
|
||||
|
||||
- (void)handleSignalState:(CrossDeskSignalState)state role:(PeerRole)role {
|
||||
- (void)handleSignalState:(CrossDeskSignalState)state
|
||||
role:(PeerRole)role
|
||||
generation:(uint64_t)generation {
|
||||
// Only the long-lived identity peer drives presence and the home indicator.
|
||||
if (role != PeerRole::Identity || generation != _identityGeneration.load()) {
|
||||
return;
|
||||
}
|
||||
const uint64_t signal_generation = _signalGeneration.fetch_add(1) + 1;
|
||||
_presenceGeneration.fetch_add(1);
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (generation != self->_identityGeneration.load() ||
|
||||
signal_generation != self->_signalGeneration.load()) return;
|
||||
self->_state->presence_requests.clear();
|
||||
if (role == PeerRole::Identity) {
|
||||
self->_state->identity_ready = state == CrossDeskSignalStateConnected;
|
||||
if (state == CrossDeskSignalStateFailed &&
|
||||
@@ -954,6 +1002,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
// its shutdown would incorrectly make the server look disconnected.
|
||||
if (role == PeerRole::Identity) {
|
||||
DispatchMain(^{
|
||||
if (generation != self->_identityGeneration.load() ||
|
||||
signal_generation != self->_signalGeneration.load()) return;
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:
|
||||
@selector(rtcBridge:didChangeSignalState:)]) {
|
||||
@@ -966,13 +1016,18 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
|
||||
- (void)handleSignalMessage:(const char *)message
|
||||
size:(size_t)size
|
||||
role:(PeerRole)role {
|
||||
role:(PeerRole)role
|
||||
generation:(uint64_t)generation {
|
||||
constexpr size_t kMaxPresenceMessageSize = 256 * 1024;
|
||||
if (role != PeerRole::Identity || !message || size == 0 ||
|
||||
if (role != PeerRole::Identity || generation != _identityGeneration.load() ||
|
||||
!message || size == 0 ||
|
||||
size > kMaxPresenceMessageSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t signal_generation = _signalGeneration.load();
|
||||
const uint64_t presence_generation = _presenceGeneration.load();
|
||||
|
||||
NSData *payload = [NSData dataWithBytes:message length:size];
|
||||
id decoded = [NSJSONSerialization JSONObjectWithData:payload
|
||||
options:0
|
||||
@@ -1007,22 +1062,41 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (presence.count == 0) return;
|
||||
|
||||
NSDictionary<NSString *, NSNumber *> *result = [presence copy];
|
||||
DispatchMain(^{
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:
|
||||
@selector(rtcBridge:didReceivePresence:)]) {
|
||||
[delegate rtcBridge:self didReceivePresence:result];
|
||||
const bool response = [type isEqualToString:@"presence"];
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (generation != self->_identityGeneration.load() ||
|
||||
signal_generation != self->_signalGeneration.load() ||
|
||||
!self->_state->identity_ready) return;
|
||||
uint64_t delivery_generation = presence_generation;
|
||||
BOOL snapshot = NO;
|
||||
if (response) {
|
||||
if (self->_state->presence_requests.empty()) return;
|
||||
const auto request = self->_state->presence_requests.front();
|
||||
self->_state->presence_requests.pop_front();
|
||||
delivery_generation = request.generation;
|
||||
snapshot = request.subscription;
|
||||
}
|
||||
if (delivery_generation != self->_presenceGeneration.load()) return;
|
||||
DispatchMain(^{
|
||||
if (generation != self->_identityGeneration.load() ||
|
||||
signal_generation != self->_signalGeneration.load() ||
|
||||
delivery_generation != self->_presenceGeneration.load()) return;
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:
|
||||
@selector(rtcBridge:didReceivePresence:snapshot:)]) {
|
||||
[delegate rtcBridge:self didReceivePresence:result snapshot:snapshot];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleProvisionedIdentity:(const std::string &)identity {
|
||||
- (void)handleProvisionedIdentity:(const std::string &)identity
|
||||
generation:(uint64_t)generation {
|
||||
if (identity.empty()) return;
|
||||
const std::string identity_copy = identity;
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (generation != self->_identityGeneration.load()) return;
|
||||
self->_state->identity_with_password = identity_copy;
|
||||
self->_state->identity_base = BaseIdentity(identity_copy);
|
||||
NSString *host = [NSString stringWithUTF8String:
|
||||
@@ -1038,6 +1112,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
|
||||
}
|
||||
DispatchMain(^{
|
||||
if (generation != self->_identityGeneration.load()) return;
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(rtcBridge:didProvisionIdentity:)]) {
|
||||
[delegate rtcBridge:self didProvisionIdentity:value];
|
||||
|
||||
@@ -28,6 +28,8 @@ struct ContentView: View {
|
||||
.onChange(of: scenePhase) { phase in
|
||||
if phase == .active {
|
||||
session.refreshRecentConnectionPresenceAfterForeground()
|
||||
} else if phase == .background {
|
||||
session.suspendPresenceMonitoring()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,6 +409,23 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
private var remoteCursorSequence: UInt32?
|
||||
private var pendingPresenceRemoteID: String?
|
||||
private var presenceProbeGeneration: UInt64 = 0
|
||||
private var signalConnected = false
|
||||
private var presenceIsForeground = false
|
||||
private var awaitingPresenceSnapshot = true
|
||||
private var presenceUpdatedAt: [String: ContinuousClock.Instant] = [:]
|
||||
private var lastPresenceRefreshAt: ContinuousClock.Instant?
|
||||
private var presenceTimer: Timer?
|
||||
private let presenceClock = ContinuousClock()
|
||||
private static let presenceRefreshInterval: Duration = .seconds(30)
|
||||
private static let presenceMaxAge: Duration = .seconds(60)
|
||||
|
||||
private struct BridgeConfiguration: Equatable {
|
||||
let host: String
|
||||
let signalPort: Int
|
||||
let turnPort: Int
|
||||
let enableSRTP: Bool
|
||||
}
|
||||
private var bridgeConfiguration: BridgeConfiguration?
|
||||
|
||||
var pixelBuffer: CVPixelBuffer? { videoFrame?.pixelBuffer }
|
||||
var frameSize: CGSize { videoFrame?.encodedSize ?? .zero }
|
||||
@@ -426,18 +443,38 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
configureBridge()
|
||||
}
|
||||
|
||||
func configureBridge() {
|
||||
deinit {
|
||||
presenceTimer?.invalidate()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func configureBridge() -> Bool {
|
||||
let host = signalHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let signal = Int(signalPort), let turn = Int(turnPort),
|
||||
!signalHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
(1...65535).contains(signal), (1...65535).contains(turn),
|
||||
!host.isEmpty else {
|
||||
signalStatus = "服务器配置无效"
|
||||
return
|
||||
return false
|
||||
}
|
||||
let configuration = BridgeConfiguration(host: host, signalPort: signal,
|
||||
turnPort: turn, enableSRTP: enableSRTP)
|
||||
if bridgeConfiguration != configuration {
|
||||
bridgeConfiguration = configuration
|
||||
signalConnected = false
|
||||
signalStatus = "正在连接信令服务"
|
||||
stopPresenceMonitoring()
|
||||
invalidatePresence()
|
||||
cancelPendingPresenceConnection()
|
||||
} else if signalConnected {
|
||||
signalStatus = "已连接服务器"
|
||||
}
|
||||
bridge.setHardwareAccelerationEnabled(videoCodecMode == .hardware)
|
||||
bridge.setVideoAdaptationPolicy(videoAdaptationPolicy.bridgeValue)
|
||||
bridge.configure(withSignalHost: signalHost,
|
||||
bridge.configure(withSignalHost: host,
|
||||
signalPort: signal,
|
||||
turnPort: turn,
|
||||
enableSRTP: enableSRTP)
|
||||
return true
|
||||
}
|
||||
|
||||
func connect(password: String, rememberPassword: Bool) {
|
||||
@@ -453,8 +490,11 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
return
|
||||
}
|
||||
remoteID = identifier
|
||||
configureBridge()
|
||||
if recentConnectionPresence[identifier] == true {
|
||||
guard configureBridge() else {
|
||||
connectionStatus = signalStatus
|
||||
return
|
||||
}
|
||||
if isDeviceOnline(identifier) {
|
||||
beginRemoteConnection(identifier)
|
||||
} else {
|
||||
beginPresenceProbe(identifier)
|
||||
@@ -477,7 +517,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
}
|
||||
|
||||
private func beginPresenceProbe(_ identifier: String) {
|
||||
guard signalStatus == "已连接服务器" else {
|
||||
guard signalConnected, presenceIsForeground else {
|
||||
showDeviceOffline()
|
||||
return
|
||||
}
|
||||
@@ -489,7 +529,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
isConnected = false
|
||||
sessionVisible = false
|
||||
connectionStatus = "正在确认设备状态…"
|
||||
bridge.requestPresence(remoteIDs: [identifier])
|
||||
requestPresenceProbe(identifier)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
|
||||
guard let self,
|
||||
@@ -508,6 +548,21 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
pendingPresenceRemoteID = nil
|
||||
}
|
||||
|
||||
private func cancelPendingPresenceConnection() {
|
||||
guard pendingPresenceRemoteID != nil else { return }
|
||||
cancelPresenceProbe()
|
||||
isConnecting = false
|
||||
connectionStatus = "未连接"
|
||||
}
|
||||
|
||||
private func requestPresenceProbe(_ identifier: String) {
|
||||
guard signalConnected, presenceIsForeground else { return }
|
||||
// Older servers ignore subscribe=false and replace the watched list.
|
||||
var identifiers = recentConnections.map(\.remoteID)
|
||||
if !identifiers.contains(identifier) { identifiers.append(identifier) }
|
||||
bridge.requestPresence(remoteIDs: identifiers, subscribe: false)
|
||||
}
|
||||
|
||||
private func showDeviceOffline() {
|
||||
isConnecting = false
|
||||
isConnected = false
|
||||
@@ -569,29 +624,85 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
}
|
||||
|
||||
func isRecentConnectionOnline(_ connection: RecentConnection) -> Bool {
|
||||
recentConnectionPresence[connection.remoteID] == true
|
||||
isDeviceOnline(connection.remoteID)
|
||||
}
|
||||
|
||||
private func isDeviceOnline(_ identifier: String) -> Bool {
|
||||
guard signalConnected, presenceIsForeground, !awaitingPresenceSnapshot,
|
||||
recentConnectionPresence[identifier] == true,
|
||||
let updatedAt = presenceUpdatedAt[identifier] else { return false }
|
||||
return updatedAt.duration(to: presenceClock.now) < Self.presenceMaxAge
|
||||
}
|
||||
|
||||
private func refreshRecentConnectionPresence() {
|
||||
bridge.requestPresence(remoteIDs: recentConnections.map(\.remoteID))
|
||||
guard signalConnected, presenceIsForeground else { return }
|
||||
lastPresenceRefreshAt = presenceClock.now
|
||||
bridge.requestPresence(remoteIDs: recentConnections.map(\.remoteID),
|
||||
subscribe: true)
|
||||
}
|
||||
|
||||
private func invalidatePresence() {
|
||||
bridge.invalidatePresence()
|
||||
recentConnectionPresence = [:]
|
||||
presenceUpdatedAt = [:]
|
||||
awaitingPresenceSnapshot = true
|
||||
lastPresenceRefreshAt = nil
|
||||
}
|
||||
|
||||
private func startPresenceMonitoring() {
|
||||
guard signalConnected, presenceIsForeground, presenceTimer == nil else { return }
|
||||
let timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in
|
||||
self?.maintainPresence()
|
||||
}
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
presenceTimer = timer
|
||||
}
|
||||
|
||||
private func stopPresenceMonitoring() {
|
||||
presenceTimer?.invalidate()
|
||||
presenceTimer = nil
|
||||
}
|
||||
|
||||
private func maintainPresence() {
|
||||
guard signalConnected, presenceIsForeground else { return }
|
||||
let now = presenceClock.now
|
||||
let expired = presenceUpdatedAt.compactMap { identifier, updatedAt in
|
||||
updatedAt.duration(to: now) >= Self.presenceMaxAge ? identifier : nil
|
||||
}
|
||||
if !expired.isEmpty {
|
||||
var updated = recentConnectionPresence
|
||||
for identifier in expired {
|
||||
updated.removeValue(forKey: identifier)
|
||||
presenceUpdatedAt.removeValue(forKey: identifier)
|
||||
}
|
||||
recentConnectionPresence = updated
|
||||
}
|
||||
if lastPresenceRefreshAt.map({ $0.duration(to: now) >= Self.presenceRefreshInterval }) ?? true {
|
||||
refreshRecentConnectionPresence()
|
||||
}
|
||||
}
|
||||
|
||||
func suspendPresenceMonitoring() {
|
||||
presenceIsForeground = false
|
||||
stopPresenceMonitoring()
|
||||
invalidatePresence()
|
||||
cancelPendingPresenceConnection()
|
||||
}
|
||||
|
||||
func refreshRecentConnectionPresenceAfterForeground() {
|
||||
// iOS can suspend the process while it is in the background, so presence
|
||||
// updates received during that time are missed. Treat cached values as
|
||||
// unknown (and therefore offline, like the desktop client) until the
|
||||
// signaling server returns a fresh snapshot.
|
||||
recentConnectionPresence = [:]
|
||||
presenceIsForeground = true
|
||||
invalidatePresence()
|
||||
startPresenceMonitoring()
|
||||
refreshRecentConnectionPresence()
|
||||
if let pendingPresenceRemoteID {
|
||||
bridge.requestPresence(remoteIDs: [pendingPresenceRemoteID])
|
||||
} else {
|
||||
refreshRecentConnectionPresence()
|
||||
requestPresenceProbe(pendingPresenceRemoteID)
|
||||
}
|
||||
}
|
||||
|
||||
func removeRecentConnection(_ connection: RecentConnection) {
|
||||
recentConnections.removeAll { $0.remoteID == connection.remoteID }
|
||||
recentConnectionPresence.removeValue(forKey: connection.remoteID)
|
||||
presenceUpdatedAt.removeValue(forKey: connection.remoteID)
|
||||
RecentConnectionStore.save(recentConnections)
|
||||
RecentConnectionStore.removeThumbnail(fileName: connection.thumbnailFileName)
|
||||
ConnectionCredentialStore.removePassword(for: connection.remoteID)
|
||||
@@ -621,7 +732,6 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
)
|
||||
recentConnections.removeAll { $0.remoteID == activeRemoteID }
|
||||
recentConnections.insert(connection, at: 0)
|
||||
recentConnectionPresence[activeRemoteID] = true
|
||||
RecentConnectionStore.save(recentConnections)
|
||||
refreshRecentConnectionPresence()
|
||||
}
|
||||
@@ -706,6 +816,8 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge,
|
||||
didChange state: CrossDeskSignalState) {
|
||||
signalConnected = state.rawValue == 1
|
||||
invalidatePresence()
|
||||
switch state.rawValue {
|
||||
case 0: signalStatus = "正在连接信令服务"
|
||||
case 1: signalStatus = "已连接服务器"
|
||||
@@ -716,12 +828,15 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
case 6: signalStatus = "TLS 证书校验失败"
|
||||
default: signalStatus = "未知信令状态"
|
||||
}
|
||||
if state.rawValue == 1 {
|
||||
if signalConnected {
|
||||
startPresenceMonitoring()
|
||||
refreshRecentConnectionPresence()
|
||||
if let pendingPresenceRemoteID {
|
||||
bridge.requestPresence(remoteIDs: [pendingPresenceRemoteID])
|
||||
} else {
|
||||
refreshRecentConnectionPresence()
|
||||
requestPresenceProbe(pendingPresenceRemoteID)
|
||||
}
|
||||
} else {
|
||||
stopPresenceMonitoring()
|
||||
cancelPendingPresenceConnection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,9 +849,6 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
case 0: connectionStatus = "正在建立远程连接"
|
||||
case 1:
|
||||
connectionStatus = "已连接"
|
||||
if !remoteID.isEmpty {
|
||||
recentConnectionPresence[remoteID] = true
|
||||
}
|
||||
sessionVisible = true
|
||||
recordSuccessfulConnectionIfNeeded()
|
||||
audioPlayer.setEnabled(audioEnabled)
|
||||
@@ -760,6 +872,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
connectionStatus = "远程设备 ID 不存在"
|
||||
if !remoteID.isEmpty {
|
||||
recentConnectionPresence[remoteID] = false
|
||||
presenceUpdatedAt.removeValue(forKey: remoteID)
|
||||
}
|
||||
sessionVisible = false
|
||||
AppOrientation.update(to: .portrait)
|
||||
@@ -767,6 +880,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
connectionStatus = "远程设备当前不可用"
|
||||
if !remoteID.isEmpty {
|
||||
recentConnectionPresence[remoteID] = false
|
||||
presenceUpdatedAt.removeValue(forKey: remoteID)
|
||||
}
|
||||
sessionVisible = false
|
||||
AppOrientation.update(to: .portrait)
|
||||
@@ -776,18 +890,28 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge, didProvisionIdentity identity: String) {
|
||||
localIdentity = identity.split(separator: "@").first.map(String.init) ?? identity
|
||||
refreshRecentConnectionPresence()
|
||||
if let pendingPresenceRemoteID {
|
||||
bridge.requestPresence(remoteIDs: [pendingPresenceRemoteID])
|
||||
} else {
|
||||
refreshRecentConnectionPresence()
|
||||
requestPresenceProbe(pendingPresenceRemoteID)
|
||||
}
|
||||
}
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge,
|
||||
didReceivePresence presence: [String: NSNumber]) {
|
||||
var updated = recentConnectionPresence
|
||||
for (remoteID, online) in presence {
|
||||
didReceivePresence presence: [String: NSNumber],
|
||||
snapshot: Bool) {
|
||||
guard signalConnected, presenceIsForeground,
|
||||
snapshot || !awaitingPresenceSnapshot else { return }
|
||||
var updated = snapshot ? [:] : recentConnectionPresence
|
||||
if snapshot {
|
||||
presenceUpdatedAt = [:]
|
||||
awaitingPresenceSnapshot = false
|
||||
}
|
||||
let now = presenceClock.now
|
||||
let watched = Set(recentConnections.map(\.remoteID))
|
||||
for (remoteID, online) in presence
|
||||
where watched.contains(remoteID) || pendingPresenceRemoteID == remoteID {
|
||||
updated[remoteID] = online.boolValue
|
||||
presenceUpdatedAt[remoteID] = now
|
||||
}
|
||||
recentConnectionPresence = updated
|
||||
|
||||
|
||||
Reference in New Issue
Block a user