mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-12 03:28:20 +08:00
[feat] add macOS and mobile resolution controls with disconnect restore
This commit is contained in:
1 parent
a3159b198e
commit
34b59de15d
50 files changed
+1086
-36
No files matched your search
@@ -100,6 +100,10 @@ typedef struct {
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didReceivePresence:(NSDictionary<NSString *, NSNumber *> *)presence
|
||||
snapshot:(BOOL)snapshot;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didReceiveDisplayResolutions:(NSData *)message;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didFailDisplayResolutionRequest:(uint32_t)requestID;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
didReceiveAnnouncementMessage:(NSData *)message;
|
||||
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
|
||||
@@ -163,6 +167,12 @@ typedef struct {
|
||||
preference:(NSInteger)preference
|
||||
requestID:(uint32_t)requestID
|
||||
NS_SWIFT_NAME(sendVideoSettings(quality:frameRate:preference:requestID:));
|
||||
- (void)sendDisplayResolution:(NSInteger)displayID
|
||||
displayKey:(NSString *)displayKey
|
||||
width:(NSInteger)width
|
||||
height:(NSInteger)height
|
||||
requestID:(uint32_t)requestID
|
||||
NS_SWIFT_NAME(sendDisplayResolution(displayID:displayKey:width:height:requestID:));
|
||||
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs
|
||||
subscribe:(BOOL)subscribe
|
||||
NS_SWIFT_NAME(requestPresence(remoteIDs:subscribe:));
|
||||
|
||||
@@ -711,6 +711,35 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendDisplayResolution:(NSInteger)displayID
|
||||
displayKey:(NSString *)displayKey
|
||||
width:(NSInteger)width
|
||||
height:(NSInteger)height
|
||||
requestID:(uint32_t)requestID {
|
||||
if (displayID < 0 || displayID >= crossdesk::kMaxResolutionDisplays ||
|
||||
width <= 0 || width > 32768 || height <= 0 || height > 32768 || !requestID) return;
|
||||
NSString *key = [displayKey copy];
|
||||
const uint64_t generation = _activeControllerGeneration.load();
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (![self isControllerGenerationActive:generation] ||
|
||||
!self->_state->controller_peer) return;
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::display_resolution;
|
||||
action.resolution_change = {int(displayID), key.UTF8String ?: "",
|
||||
{int(width), int(height)}, requestID, false};
|
||||
const auto message = action.to_json();
|
||||
if (message.empty() || MiniRtcSendReliableDataFrame(self->_state->controller_peer,
|
||||
message.data(), message.size(), kControlStream) != 0) {
|
||||
DispatchMain(^{
|
||||
if (![self isControllerGenerationActive:generation]) return;
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(rtcBridge:didFailDisplayResolutionRequest:)])
|
||||
[delegate rtcBridge:self didFailDisplayResolutionRequest:requestID];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)notifyVideoSettings:(crossdesk::VideoSettings)settings
|
||||
generation:(uint64_t)generation {
|
||||
DispatchMain(^{
|
||||
@@ -1738,6 +1767,18 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action.type == ControlType::display_resolution_status ||
|
||||
action.type == ControlType::host_infomation) {
|
||||
const auto message = action.to_json();
|
||||
NSData *payload = [NSData dataWithBytes:message.data() length:message.size()];
|
||||
DispatchMain(^{
|
||||
if (![self isControllerGenerationActive:generation]) return;
|
||||
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
|
||||
if ([delegate respondsToSelector:@selector(rtcBridge:didReceiveDisplayResolutions:)])
|
||||
[delegate rtcBridge:self didReceiveDisplayResolutions:payload];
|
||||
});
|
||||
if (action.type == ControlType::display_resolution_status) return;
|
||||
}
|
||||
if (action.type == ControlType::video_settings_status) {
|
||||
[self notifyVideoSettings:action.vs generation:generation];
|
||||
return;
|
||||
|
||||
@@ -322,6 +322,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
didSet { UserDefaults.standard.set(autoLandscape, forKey: AppOrientation.autoLandscapeKey) }
|
||||
}
|
||||
@Published private(set) var videoSettings = RemoteVideoSettingsState()
|
||||
@Published private(set) var displayResolutions = RemoteDisplayResolutionState()
|
||||
@Published var privacyScreen = RemotePrivacyScreen()
|
||||
@Published private(set) var privacyClock = ProcessInfo.processInfo.systemUptime
|
||||
private var privacyTimer: Timer?
|
||||
@@ -384,6 +385,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
private var videoWasBackgrounded = false
|
||||
private var videoRecoveryTask: Task<Void, Never>?
|
||||
private var videoSettingsTimeoutTask: Task<Void, Never>?
|
||||
private var displayResolutionTimeoutTask: Task<Void, Never>?
|
||||
private var pendingPresenceRemoteID: String?
|
||||
private var presenceProbeGeneration: UInt64 = 0
|
||||
@Published private(set) var signalConnected = false
|
||||
@@ -1040,7 +1042,40 @@ 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 }
|
||||
displayResolutionTimeoutTask?.cancel()
|
||||
bridge.sendDisplayResolution(displayID: request.display_id, displayKey: request.display_key,
|
||||
width: request.width, height: request.height, requestID: request.request_id)
|
||||
displayResolutionTimeoutTask = Task { @MainActor [weak self] in
|
||||
do { try await Task.sleep(nanoseconds: 10_000_000_000) } catch { return }
|
||||
self?.displayResolutions.expire(requestID: request.request_id)
|
||||
}
|
||||
}
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceiveDisplayResolutions message: Data) {
|
||||
// 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 !displayResolutions.pending {
|
||||
displayResolutionTimeoutTask?.cancel(); displayResolutionTimeoutTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge, didFailDisplayResolutionRequest requestID: UInt32) {
|
||||
guard hasNetworkConsent, isConnected else { return }
|
||||
displayResolutions.expire(requestID: requestID)
|
||||
if !displayResolutions.pending {
|
||||
displayResolutionTimeoutTask?.cancel(); displayResolutionTimeoutTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func resetVideoSettings() {
|
||||
displayResolutionTimeoutTask?.cancel(); displayResolutionTimeoutTask = nil
|
||||
displayResolutions = RemoteDisplayResolutionState()
|
||||
privacyTimer?.invalidate(); privacyTimer = nil
|
||||
privacyScreen = RemotePrivacyScreen()
|
||||
privacyClock = ProcessInfo.processInfo.systemUptime
|
||||
|
||||
@@ -1488,6 +1488,39 @@ private struct FloatingVideoSettings: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(NSLocalizedString("被控端分辨率", comment: ""))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
let display = session.displayResolutions.display(session.selectedDisplay)
|
||||
Menu {
|
||||
ForEach(Array(Set(display?.modes ?? [])).sorted {
|
||||
$0.width == $1.width ? $0.height > $1.height : $0.width > $1.width
|
||||
}, id: \.self) { mode in
|
||||
Button {
|
||||
if let display {
|
||||
session.updateDisplayResolution(mode, displayID: display.display_id, displayKey: display.display_key)
|
||||
}
|
||||
} label: {
|
||||
if mode == display?.current { Label(mode.label, systemImage: "checkmark") }
|
||||
else { Text(mode.label) }
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(display?.current.label ?? NSLocalizedString("设备不支持", comment: ""))
|
||||
Spacer()
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
}
|
||||
.font(.subheadline).padding(8)
|
||||
.background(Color.secondary.opacity(0.1), in: RoundedRectangle(cornerRadius: 7))
|
||||
}
|
||||
.disabled(!session.isConnected || session.displayResolutions.pending || (display?.modes.isEmpty ?? true))
|
||||
if session.displayResolutions.pending {
|
||||
Text(NSLocalizedString("等待电脑端确认…", comment: "")).font(.caption).foregroundStyle(.secondary)
|
||||
} else if session.displayResolutions.failed {
|
||||
Text(NSLocalizedString("调整失败,请重试。", comment: "")).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
settingRow(NSLocalizedString("画面质量", comment: ""), options: [NSLocalizedString("低", comment: ""), NSLocalizedString("中", comment: ""), NSLocalizedString("高", comment: "")],
|
||||
values: [0, 1, 2], selection: Binding(
|
||||
get: { session.videoSettings.selection.quality },
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
// Values match crossdesk::VideoSettings on the reliable control stream.
|
||||
struct RemoteVideoSettings: Equatable {
|
||||
var quality = 2
|
||||
@@ -66,3 +68,96 @@ struct RemoteVideoSettingsState {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RemoteDisplayResolution: Codable, Equatable, Hashable {
|
||||
let width: Int
|
||||
let height: Int
|
||||
var label: String { "\(width) × \(height)" }
|
||||
var valid: Bool { (1...32768).contains(width) && (1...32768).contains(height) }
|
||||
}
|
||||
|
||||
struct RemoteDisplayResolutionInfo: Decodable {
|
||||
let display_id: Int
|
||||
let display_key: String
|
||||
let width: Int
|
||||
let height: Int
|
||||
let modes: [RemoteDisplayResolution]
|
||||
var current: RemoteDisplayResolution { RemoteDisplayResolution(width: width, height: height) }
|
||||
var valid: Bool {
|
||||
(0..<32).contains(display_id) && !display_key.isEmpty &&
|
||||
display_key.utf8.count <= 1024 && !display_key.contains("\0") &&
|
||||
current.valid && modes.count <= 256 && modes.allSatisfy(\.valid)
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoteDisplayResolutionRequest: Decodable, Equatable {
|
||||
let display_id: Int
|
||||
let display_key: String
|
||||
let width: Int
|
||||
let height: Int
|
||||
let request_id: UInt32
|
||||
var accepted = false
|
||||
var resolution: RemoteDisplayResolution { RemoteDisplayResolution(width: width, height: height) }
|
||||
func matches(_ other: Self) -> Bool {
|
||||
display_id == other.display_id && display_key == other.display_key &&
|
||||
resolution == other.resolution && request_id == other.request_id
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoteDisplayResolutionState {
|
||||
private(set) var displays: [RemoteDisplayResolutionInfo] = []
|
||||
private(set) var requested: RemoteDisplayResolutionRequest?
|
||||
private(set) var pending = false
|
||||
private(set) var failed = false
|
||||
|
||||
func display(_ id: Int) -> RemoteDisplayResolutionInfo? {
|
||||
displays.first { $0.display_id == id }
|
||||
}
|
||||
mutating func begin(displayID: Int, resolution: RemoteDisplayResolution) -> RemoteDisplayResolutionRequest? {
|
||||
guard !pending, let display = display(displayID), display.modes.contains(resolution),
|
||||
display.current != resolution else { return nil }
|
||||
let next = (requested?.request_id ?? 0) &+ 1
|
||||
let request = RemoteDisplayResolutionRequest(display_id: displayID,
|
||||
display_key: display.display_key, width: resolution.width,
|
||||
height: resolution.height, request_id: next == 0 ? 1 : next)
|
||||
requested = request; pending = true; failed = false
|
||||
return request
|
||||
}
|
||||
mutating func expire(requestID: UInt32) {
|
||||
guard pending, requested?.request_id == requestID else { return }
|
||||
pending = false; failed = true
|
||||
}
|
||||
mutating func receive(_ data: Data) {
|
||||
struct Host: Decodable { let display_resolutions: [RemoteDisplayResolutionInfo]? }
|
||||
struct Message: Decodable {
|
||||
let type: Int
|
||||
let host_info: Host?
|
||||
let display_resolution: RemoteDisplayResolutionRequest?
|
||||
let display_resolutions: [RemoteDisplayResolutionInfo]?
|
||||
}
|
||||
guard let message = try? JSONDecoder().decode(Message.self, from: data) else { return }
|
||||
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 }
|
||||
guard actual.count <= 32, actual.allSatisfy(\.valid),
|
||||
Set(actual.map(\.display_id)).count == actual.count else { return }
|
||||
displays = actual
|
||||
if message.type == 16, let response = message.display_resolution {
|
||||
pending = false
|
||||
failed = !response.accepted || !actual.contains {
|
||||
$0.display_id == response.display_id && $0.display_key == response.display_key &&
|
||||
$0.current == response.resolution
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "Im App Store ist derzeit kein Update verfügbar";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "Diese App wurde im App Store der aktuellen Region nicht gefunden";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "Die neue Version erfordert iOS %1$@ oder neuer";
|
||||
"被控端分辨率" = "Auflösung des entfernten Bildschirms";
|
||||
"设备不支持" = "Nicht unterstützt";
|
||||
"等待电脑端确认…" = "Warten auf Bestätigung vom Computer...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "No update is currently available in the App Store";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "This app could not be found in the App Store for the current region";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "The new version requires iOS %1$@ or later";
|
||||
"被控端分辨率" = "Remote display resolution";
|
||||
"设备不支持" = "Unsupported";
|
||||
"等待电脑端确认…" = "Waiting for confirmation from the computer...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "No hay actualizaciones disponibles en el App Store por ahora";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "No se encontró esta app en el App Store de la región actual";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "La nueva versión requiere iOS %1$@ o posterior";
|
||||
"被控端分辨率" = "Resolución de la pantalla remota";
|
||||
"设备不支持" = "No compatible";
|
||||
"等待电脑端确认…" = "Esperando confirmación del ordenador...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "Aucune mise à jour disponible dans l’App Store pour le moment";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "Cette app est introuvable dans l’App Store de la région actuelle";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "La nouvelle version nécessite iOS %1$@ ou une version ultérieure";
|
||||
"被控端分辨率" = "Résolution de l’écran distant";
|
||||
"设备不支持" = "Non pris en charge";
|
||||
"等待电脑端确认…" = "En attente de confirmation de l'ordinateur...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "現在 App Store に利用可能なアップデートはありません";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "現在の地域の App Store でこのアプリが見つかりませんでした";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "新しいバージョンには iOS %1$@ 以降が必要です";
|
||||
"被控端分辨率" = "接続先の画面解像度";
|
||||
"设备不支持" = "サポートされていません";
|
||||
"等待电脑端确认…" = "コンピューターからの確認を待っています...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "현재 App Store에 사용 가능한 업데이트가 없습니다";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "현재 지역의 App Store에서 이 앱을 찾을 수 없습니다";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "새 버전에는 iOS %1$@ 이상이 필요합니다";
|
||||
"被控端分辨率" = "원격 화면 해상도";
|
||||
"设备不支持" = "지원되지 않음";
|
||||
"等待电脑端确认…" = "컴퓨터의 확인을 기다리는 중...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "Nenhuma atualização disponível na App Store no momento";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "Este app não foi encontrado na App Store da região atual";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "A nova versão requer iOS %1$@ ou posterior";
|
||||
"被控端分辨率" = "Resolução da tela remota";
|
||||
"设备不支持" = "Não suportado";
|
||||
"等待电脑端确认…" = "Aguardando confirmação do computador...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "Nenhuma atualização disponível na App Store no momento";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "Este app não foi encontrado na App Store da região atual";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "A nova versão requer iOS %1$@ ou posterior";
|
||||
"被控端分辨率" = "Resolução da tela remota";
|
||||
"设备不支持" = "Não suportado";
|
||||
"等待电脑端确认…" = "Aguardando confirmação do computador...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "В App Store пока нет доступных обновлений";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "Не удалось найти приложение в App Store для текущего региона";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "Для новой версии требуется iOS %1$@ или новее";
|
||||
"被控端分辨率" = "Разрешение удалённого экрана";
|
||||
"设备不支持" = "Не поддерживается";
|
||||
"等待电脑端确认…" = "Ожидание подтверждения от компьютера...";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "App Store 暂无可用更新";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "暂时无法在当前地区的 App Store 查询到此应用";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "新版本需要 iOS %1$@ 或更高版本";
|
||||
"被控端分辨率" = "被控端分辨率";
|
||||
"设备不支持" = "设备不支持";
|
||||
"等待电脑端确认…" = "等待电脑端确认…";
|
||||
@@ -285,3 +285,6 @@
|
||||
"App Store 暂无可用更新" = "App Store 暫無可用更新";
|
||||
"暂时无法在当前地区的 App Store 查询到此应用" = "暫時無法在目前地區的 App Store 查詢到此 App";
|
||||
"新版本需要 iOS %1$@ 或更高版本" = "新版本需要 iOS %1$@ 或以上版本";
|
||||
"被控端分辨率" = "被控端解析度";
|
||||
"设备不支持" = "裝置不支援";
|
||||
"等待电脑端确认…" = "等待電腦端確認…";
|
||||
Reference in new issue
Block a user