[feat] add remote host version checks and update prompts

This commit is contained in:
dijunkun
2026-09-28 13:54:39 +08:00
parent f347fc2387
commit 51d392ef48
25 changed files with 891 additions and 392 deletions
@@ -61,6 +61,7 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
height:(NSInteger)height;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveHostName:(NSString *)hostName
appVersion:(NSString *)appVersion
displayNames:(NSArray<NSString *> *)displayNames
displaySizes:(NSArray<NSValue *> *)displaySizes;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
@@ -101,6 +102,11 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
@property(nonatomic, weak, nullable) id<CrossDeskRTCBridgeDelegate> delegate;
// Uses the same release/patch ordering as the desktop controller.
+ (nullable NSString *)availableUpdateForAppVersion:(NSString *)appVersion
releaseJSON:(NSData *)releaseJSON
NS_SWIFT_NAME(availableUpdate(appVersion:releaseJSON:));
- (void)configureWithSignalHost:(NSString *)host
signalPort:(NSInteger)signalPort;
- (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy;
@@ -18,6 +18,7 @@
#include <vector>
#include <display_stream_id.h>
#include <app_version.h>
#include <file_transfer_format.h>
#include <remote_action.h>
#include <stream_names.h>
@@ -330,7 +331,8 @@ void DispatchMain(dispatch_block_t block) {
- (void)handleData:(const char *)data
size:(size_t)size
sourceID:(const char *)sourceID
sourceIDSize:(size_t)sourceIDSize;
sourceIDSize:(size_t)sourceIDSize
generation:(uint64_t)generation;
- (void)handleStats:(const MiniRtcNetTrafficStats *)stats mode:(TraversalMode)mode;
- (void)sendMessage:(const std::string &)message
reliable:(BOOL)reliable
@@ -377,7 +379,8 @@ void OnDataBuffer(const char *data, size_t size, const char *, size_t,
[owner handleData:data
size:size
sourceID:source_id
sourceIDSize:source_id_size];
sourceIDSize:source_id_size
generation:context->generation];
}
}
@@ -488,6 +491,16 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
std::atomic<uint64_t> _presenceGeneration;
}
+ (nullable NSString *)availableUpdateForAppVersion:(NSString *)appVersion
releaseJSON:(NSData *)releaseJSON {
const auto latest = crossdesk::ParseVersionInfoJSON(std::string_view(
static_cast<const char *>(releaseJSON.bytes), releaseJSON.length));
if (!latest) return nil;
const auto update = crossdesk::AvailableAppUpdate(appVersion.UTF8String ?: "",
*latest);
return update ? [NSString stringWithUTF8String:update->c_str()] : nil;
}
- (instancetype)init {
self = [super init];
if (self) {
@@ -1264,6 +1277,10 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
CopyCString(action.i.host_name, sizeof(action.i.host_name),
controller_name);
action.i.host_name_size = std::strlen(action.i.host_name);
NSString *appVersion = [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
CopyCString(action.i.app_version, sizeof(action.i.app_version),
appVersion.UTF8String ?: "");
action.i.display_list = nullptr;
action.i.display_num = 0;
action.i.left = nullptr;
@@ -1515,7 +1532,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
- (void)handleData:(const char *)data
size:(size_t)size
sourceID:(const char *)sourceID
sourceIDSize:(size_t)sourceIDSize {
sourceIDSize:(size_t)sourceIDSize
generation:(uint64_t)generation {
if (!data || size == 0 || !sourceID) return;
const std::string source(sourceID, sourceIDSize);
if (source == kControlStream) {
@@ -1528,6 +1546,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
initWithBytes:action.i.host_name
length:action.i.host_name_size
encoding:NSUTF8StringEncoding];
NSString *app_version = crossdesk::IsValidAppVersion(action.i.app_version)
? [NSString stringWithUTF8String:action.i.app_version] : nil;
NSMutableArray<NSString *> *display_names =
[NSMutableArray arrayWithCapacity:action.i.display_num];
NSMutableArray<NSValue *> *display_sizes =
@@ -1548,12 +1568,14 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
}
crossdesk::FreeRemoteAction(action);
DispatchMain(^{
if (![self isControllerGenerationActive:generation]) return;
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:
@selector(rtcBridge:didReceiveHostName:displayNames:
@selector(rtcBridge:didReceiveHostName:appVersion:displayNames:
displaySizes:)]) {
[delegate rtcBridge:self
didReceiveHostName:host_name ?: @""
appVersion:app_version ?: @""
displayNames:display_names
displaySizes:display_sizes];
}
@@ -359,10 +359,17 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
@Published private(set) var recentConnectionPresence: [String: Bool] = [:]
@Published private(set) var deviceOfflineAlertVisible = false
@Published private(set) var connectionFailureMessage: String?
@Published private(set) var remoteUpdateMessage: String?
let bridge = CrossDeskRTCBridge()
private let audioPlayer = RemoteAudioPlayer()
private var activeRemoteID = ""
private var remoteAppVersion = ""
private var remoteHostInfoReceived = false
private var remoteVersionCheckAttempted = false
private var remoteVersionCheckGeneration = UUID()
private var remoteVersionCheckTask: URLSessionDataTask?
private static let remoteUpdateNotice = "被控端版本过低,请升级到最新版本。"
private var pendingRememberPassword = false
private var connectionRecorded = false
private var shouldCaptureThumbnail = false
@@ -406,6 +413,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
deinit {
presenceTimer?.invalidate()
remoteVersionCheckTask?.cancel()
}
@discardableResult
@@ -460,6 +468,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
private func beginRemoteConnection(_ identifier: String) {
resetRemoteVersionCheck()
cancelVideoRecovery()
cancelPresenceProbe()
activeRemoteID = identifier
@@ -552,6 +561,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
private func resetConnection() {
resetRemoteVersionCheck()
connectionFailureMessage = nil
cancelVideoRecovery()
let wasCheckingPresence = pendingPresenceRemoteID != nil
@@ -576,11 +586,62 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
func retry() {
resetRemoteVersionCheck()
cancelVideoRecovery()
bridge.disconnect()
connect()
}
func dismissRemoteUpdate() {
remoteUpdateMessage = nil
}
private func resetRemoteVersionCheck() {
remoteVersionCheckTask?.cancel()
remoteVersionCheckTask = nil
remoteVersionCheckGeneration = UUID()
remoteVersionCheckAttempted = false
remoteAppVersion = ""
remoteHostInfoReceived = false
remoteUpdateMessage = nil
}
private func checkRemoteVersionIfNeeded() {
guard isConnected, remoteHostInfoReceived,
!remoteVersionCheckAttempted else { return }
remoteVersionCheckAttempted = true
if remoteAppVersion.isEmpty {
remoteUpdateMessage = Self.remoteUpdateNotice
return
}
let generation = remoteVersionCheckGeneration
let appVersion = remoteAppVersion
// Only public release metadata is fetched; no peer ID or version is sent.
let url = URL(string: "https://version.crossdesk.cn/version.json")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData,
timeoutInterval: 15)
remoteVersionCheckTask = URLSession.shared.dataTask(with: request) {
[weak self] data, response, error in
let latest: String?
if error == nil, let data,
(response as? HTTPURLResponse)?.statusCode == 200 {
latest = CrossDeskRTCBridge.availableUpdate(appVersion: appVersion,
releaseJSON: data)
} else {
latest = nil
}
DispatchQueue.main.async { [weak self] in
guard let self,
self.remoteVersionCheckGeneration == generation,
self.isConnected else { return }
self.remoteVersionCheckTask = nil
guard latest != nil else { return }
self.remoteUpdateMessage = Self.remoteUpdateNotice
}
}
remoteVersionCheckTask?.resume()
}
func savedPassword(for remoteID: String) -> String {
ConnectionCredentialStore.password(for: remoteID) ?? ""
}
@@ -879,6 +940,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
case 1:
connectionStatus = "已连接"
sessionVisible = true
checkRemoteVersionIfNeeded()
recordSuccessfulConnectionIfNeeded()
audioPlayer.setEnabled(audioEnabled)
bridge.setAudioEnabled(audioEnabled)
@@ -966,8 +1028,12 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
func rtcBridge(_ bridge: CrossDeskRTCBridge,
didReceiveHostName hostName: String,
appVersion: String,
displayNames remoteDisplayNames: [String],
displaySizes remoteDisplaySizes: [NSValue]) {
remoteHostInfoReceived = true
remoteAppVersion = appVersion
checkRemoteVersionIfNeeded()
updateActiveConnectionName(hostName)
let names = remoteDisplayNames.enumerated().map { index, displayName in
let name = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -147,6 +147,14 @@ struct RemoteSessionView: View {
.zIndex(50)
}
}
.alert("升级提示", isPresented: Binding(
get: { session.remoteUpdateMessage != nil },
set: { if !$0 { session.dismissRemoteUpdate() } }
)) {
Button("知道了", role: .cancel) { session.dismissRemoteUpdate() }
} message: {
Text(session.remoteUpdateMessage ?? "")
}
.onDisappear {
keyboardInputVisible = false
viewport = RemoteViewportState()
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -241,12 +241,12 @@
{
"id": "inih",
"name": "inih / INIReader",
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
"version": "24d9d9deefb95ede2cdb18e77cb01350101f70f8",
"license": "BSD-3-Clause",
"source": {
"kind": "repository",
"url": "https://github.com/kunkundi/minirtc",
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
"revision": "24d9d9deefb95ede2cdb18e77cb01350101f70f8"
},
"documents": [
{
@@ -527,12 +527,12 @@
{
"id": "minirtc",
"name": "MiniRTC",
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
"version": "24d9d9deefb95ede2cdb18e77cb01350101f70f8",
"license": "LGPL-3.0-only",
"source": {
"kind": "repository",
"url": "https://github.com/kunkundi/minirtc",
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
"revision": "24d9d9deefb95ede2cdb18e77cb01350101f70f8"
},
"documents": [
{
@@ -833,12 +833,12 @@
{
"id": "webrtc",
"name": "WebRTC-derived code in MiniRTC",
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
"version": "24d9d9deefb95ede2cdb18e77cb01350101f70f8",
"license": "BSD-3-Clause and patent grant",
"source": {
"kind": "repository",
"url": "https://github.com/kunkundi/minirtc",
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
"revision": "24d9d9deefb95ede2cdb18e77cb01350101f70f8"
},
"documents": [
{
@@ -898,7 +898,7 @@
]
}
],
"bundle_sha256": "09376251bbda3f5a1f7112f15dcbb1259fef7898f85b45aacafa767348aafdc2",
"bundle_sha256": "58702163ac79d8821007b1ae44ef054cf1d9a3cec9d6334b852f0bbe99b5af01",
"toolchain": {
"xmake_version": "3.1.1",
"xmake_repository_revision": "121109352da272cd1a3d4ed52f2dcda34f839955"