[feat] remember remote display resolutions on mobile clients

This commit is contained in:
dijunkun committed 2026-11-11 15:26:04 +08:00
1 parent 23f48dc761
commit 25fb2a3d7f
15 files changed
+253 -33

No files matched your search

@@ -1,5 +1,5 @@
import { tr } from './Localization';
import { RemoteDisplayResolutions, ResolutionDisplay, ResolutionMode, ResolutionRequest, resolutionLabel } from './RemoteDisplayResolutions';
import { RemoteDisplayResolutions, RemoteResolutionStore, ResolutionDisplay, ResolutionMode, ResolutionRequest, ResolutionPreference, resolutionLabel } from './RemoteDisplayResolutions';
import { RemotePrivacyScreen, PrivacyStatus } from './RemotePrivacyScreen';
import bridge from 'libcrossdesk.so';
import { common, bundleManager } from '@kit.AbilityKit';
@@ -12,6 +12,7 @@ import { AnnouncementInbox, AnnouncementItem, AnnouncementRequest, AnnouncementR
import { VideoSettings, VideoValues, NetworkStatistics, NetworkReport, RecentPresence, PresenceMessage, asciiKey } from './ControllerModels';
interface Command {
displayResolutions?: ResolutionPreference[];
displayId?: number; displayKey?: string; width?: number; height?: number;
op: string; session?: number; connection?: number; server?: string; port?: number; identity?: string;
logs?: string; certificates?: string; directory?: string; previewDirectory?: string; remote?: string; password?: string; version?: string;
@@ -106,6 +107,8 @@ export class MobileController {
video: VideoSettings = new VideoSettings();
private resolutions: RemoteDisplayResolutions = new RemoteDisplayResolutions();
private resolutionTimeout: number = -1;
private resolutionScope: string = '';
private resolutionStore: RemoteResolutionStore;
private privacyScreen: RemotePrivacyScreen = new RemotePrivacyScreen();
network: NetworkStatistics = new NetworkStatistics();
presence: RecentPresence;
@@ -141,6 +144,8 @@ export class MobileController {
private privacy: string = '';
constructor(context: common.UIAbilityContext) {
this.context = context; this.store = new MobileStore(context);
this.resolutionStore = new RemoteResolutionStore((key: string) => this.store.text(key, '[]'),
(key: string, value: string) => this.store.put(key, value));
this.state.consent = this.store.flag('consent'); this.state.server = this.store.text('server', 'api.crossdesk.cn');
this.state.port = this.store.text('port', '9099'); this.state.remote = this.store.text('lastRemote');
this.state.relative = this.store.flag('relative', true); this.state.videoPreference = this.store.number('videoPreference', 1);
@@ -248,6 +253,7 @@ export class MobileController {
begin(password: string): void {
if (!this.state.consent || !/^[0-9]{9}$/.test(this.state.remote) || !password || password.length > 128 || password.includes('@')) { this.toast(tr("请输入有效的访问密码")); return; }
this.joinSent = false; this.connection++; this.connectingId = this.state.remote; this.pendingPassword = password;
this.resolutionScope = this.scope();
this.state.connecting = true; this.state.ready = false; this.state.modal = 'connecting'; this.state.notice = '';
this.state.muted = false; this.state.platform = ''; this.state.hostName = ''; this.state.receivedPath = this.state.receivedName = this.state.transfer = '';
this.state.remoteClipboard = ''; this.state.displays = []; this.state.width = 16; this.state.height = 9; this.state.hasFrame = false;
@@ -265,7 +271,8 @@ export class MobileController {
private joinSent: boolean = false;
private joinIfReady(): void {
if (!this.state.connecting || this.joinSent || this.state.signal !== 1 || !this.state.identity) return;
this.joinSent = true; this.send({ op: 'connect', connection: this.connection, remote: this.connectingId, password: this.pendingPassword });
this.joinSent = true; this.send({ op: 'connect', connection: this.connection, remote: this.connectingId, password: this.pendingPassword,
displayResolutions: this.resolutionStore.load(this.resolutionScope, this.connectingId) });
}
end(reason: string): void {
this.release(); this.send({ op: 'disconnect' }); this.platform.disconnect();
@@ -310,6 +317,12 @@ export class MobileController {
if (!this.state.ready || !this.state.consent) return;
const display = this.resolutions.display(this.state.selectedDisplay);
if (!display || display.display_id !== displayId || display.display_key !== displayKey) return;
if (!this.resolutions.pending && display.width === mode.width && display.height === mode.height &&
display.modes.some((value: ResolutionMode) => value.width === mode.width && value.height === mode.height)) {
this.resolutionStore.remember(this.resolutionScope, this.connectingId,
{ display_key: displayKey, width: mode.width, height: mode.height });
return;
}
const request = this.resolutions.begin(displayId, mode);
if (!request) return;
this.send({ op: 'displayResolution', displayId: request.display_id, displayKey: request.display_key,
@@ -532,7 +545,10 @@ export class MobileController {
if (!this.video.pending && this.settingsTimeout >= 0) clearTimeout(this.settingsTimeout); this.syncVideo(); return;
}
if (message.type === 16 && message.display_resolution && message.display_resolutions) {
this.resolutions.receive(message.display_resolution, message.display_resolutions); this.syncResolutions(); return;
const confirmed = this.resolutions.receive(message.display_resolution, message.display_resolutions);
if (confirmed) this.resolutionStore.remember(this.resolutionScope, this.connectingId, confirmed);
if (!this.resolutions.pending && this.resolutionTimeout >= 0) clearTimeout(this.resolutionTimeout);
this.syncResolutions(); return;
}
if (message.host_info) {
const info = message.host_info; this.state.platform = info.platform; this.state.hostName = info.host_name;
@@ -1,7 +1,8 @@
import { tr } from './Localization';
export interface ResolutionMode { width: number; height: number; }
export interface ResolutionDisplay extends ResolutionMode {
export interface ResolutionPreference extends ResolutionMode { display_key: string; }
export interface ResolutionDisplay extends ResolutionPreference {
display_id: number; display_key: string; width: number; height: number; modes: ResolutionMode[];
}
export interface ResolutionRequest extends ResolutionMode {
@@ -13,6 +14,51 @@ function validMode(mode: ResolutionMode): boolean {
return !!mode && Number.isInteger(mode.width) && Number.isInteger(mode.height) &&
mode.width > 0 && mode.width <= 32768 && mode.height > 0 && mode.height <= 32768;
}
export function validResolutionPreference(value: ResolutionPreference): boolean {
if (!validMode(value) || typeof value.display_key !== 'string' || !value.display_key.length ||
value.display_key.length > 1024 || value.display_key.includes('\0')) return false;
const bytes = Array.from(value.display_key).reduce((n: number, c: string) => {
const point = c.codePointAt(0) ?? 0;
return n + (point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4);
}, 0);
return bytes <= 1024;
}
export function readResolutionPreferences(text: string): ResolutionPreference[] {
if (text.length > 65536) return [];
try {
const values = JSON.parse(text) as ResolutionPreference[];
if (!Array.isArray(values) || values.length > 32) return [];
const keys: string[] = [];
const result: ResolutionPreference[] = [];
for (const value of values) {
if (!validResolutionPreference(value) || keys.includes(value.display_key)) return [];
keys.push(value.display_key);
result.push({ display_key: value.display_key, width: value.width, height: value.height });
}
return result;
} catch { return []; }
}
export class RemoteResolutionStore {
private read: (key: string) => string;
private write: (key: string, value: string) => void;
constructor(read: (key: string) => string, write: (key: string, value: string) => void) {
this.read = read; this.write = write;
}
load(scope: string, id: string): ResolutionPreference[] {
if (!id) return [];
try { return readResolutionPreferences(this.read(`resolutions:${JSON.stringify([scope, id])}`)); }
catch { return []; }
}
remember(scope: string, id: string, preference: ResolutionPreference): void {
if (!id || !validResolutionPreference(preference)) return;
try {
const values = this.load(scope, id).filter((value: ResolutionPreference) => value.display_key !== preference.display_key);
if (values.length >= 32) values.shift();
values.push({ display_key: preference.display_key, width: preference.width, height: preference.height });
this.write(`resolutions:${JSON.stringify([scope, id])}`, JSON.stringify(values));
} catch { console.warn('Could not save remote display resolution'); }
}
}
export class RemoteDisplayResolutions {
displays: ResolutionDisplay[] = [];
requested?: ResolutionRequest;
@@ -35,13 +81,7 @@ export class RemoteDisplayResolutions {
const ids: number[] = [];
for (const d of displays) {
if (!d || !Number.isInteger(d.display_id) || d.display_id < 0 || d.display_id >= 32 || ids.includes(d.display_id) ||
typeof d.display_key !== 'string' || !d.display_key.length || d.display_key.length > 1024 || d.display_key.includes('\0') ||
!validMode(d) || !Array.isArray(d.modes) || d.modes.length > 256 || !d.modes.every((m: ResolutionMode) => validMode(m))) return false;
const bytes = Array.from(d.display_key).reduce((n: number, c: string) => {
const point = c.codePointAt(0) ?? 0;
return n + (point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4);
}, 0);
if (bytes > 1024) return false;
!validResolutionPreference(d) || !Array.isArray(d.modes) || d.modes.length > 256 || !d.modes.every((m: ResolutionMode) => validMode(m))) return false;
ids.push(d.display_id);
}
return true;
@@ -54,13 +94,14 @@ export class RemoteDisplayResolutions {
d.display_id === request.display_id && d.display_key === request.display_key &&
d.modes.some((m: ResolutionMode) => sameMode(m, request)))) this.expire(request.request_id);
}
receive(response: ResolutionRequest, displays: ResolutionDisplay[]): void {
receive(response: ResolutionRequest, displays: ResolutionDisplay[]): ResolutionPreference | undefined {
const request = this.requested;
if (!request || !response || response.request_id !== request.request_id || response.display_id !== request.display_id ||
response.display_key !== request.display_key || !sameMode(response, request) || typeof response.accepted !== 'boolean' || !this.valid(displays)) return;
response.display_key !== request.display_key || !sameMode(response, request) || typeof response.accepted !== 'boolean' || !this.valid(displays)) return undefined;
this.displays = displays; this.pending = false;
this.failed = !response.accepted || !displays.some((d: ResolutionDisplay) =>
d.display_id === response.display_id && d.display_key === response.display_key && sameMode(d, response));
return this.failed ? undefined : { display_key: response.display_key, width: response.width, height: response.height };
}
feedback(): string { return this.pending ? tr("等待电脑端确认…") : this.failed ? tr("调整失败,请重试。") : ''; }
}
+5
View File
@@ -7,6 +7,7 @@
#include <mobile_file_transfer_session.h>
#include <mobile_video_timing.h>
#include <display_stream_id.h>
#include <display_resolution_preferences.h>
#include <libyuv.h>
#include <algorithm>
#include <cmath>
@@ -144,6 +145,7 @@ class MobileSession {
s->connecting_ = false; s->connected_ = true;
auto info = MakeHostInformation("CrossDesk HarmonyOS Mobile", {}, true, s->version_, HostPlatform::HarmonyOS);
info.i.supports_video_codec_status = true;
info.preferred_display_resolutions = s->preferred_display_resolutions_;
s->Send(info); FreeRemoteAction(info);
RemoteAction audio{}; audio.type = audio_capture; audio.a = !s->muted_; s->Send(audio);
s->audio_.Start();
@@ -238,6 +240,7 @@ class MobileSession {
CancelPreview();
if (controller_) { MiniRtcLeaveConnection(controller_,remote_.c_str()); MiniRtcDestroyPeer(&controller_); }
files_.reset(); renderer_.Clear(); audio_.Stop();
preferred_display_resolutions_.clear();
if (active) Event({{"type","ended"},{"reason",reason}},true);
++connection_; display_=0; frame_id_=0;
}
@@ -274,6 +277,7 @@ class MobileSession {
else if (op=="connect") {
if (!online_||login_.empty()) throw std::runtime_error("Not signed in");
Disconnect(""); connection_=v.at("connection").get<uint64_t>();
ReadDisplayResolutionPreferences(v.value("displayResolutions", json::array()), preferred_display_resolutions_);
remote_=v.at("remote").get<std::string>(); const auto password=v.at("password").get<std::string>();
if (remote_.size()!=9||!std::all_of(remote_.begin(),remote_.end(),::isdigit)||password.size()>128||password.find('@')!=std::string::npos)
throw std::runtime_error("Invalid remote credentials");
@@ -405,6 +409,7 @@ class MobileSession {
std::unique_ptr<Context> identity_context_,control_context_;
std::unique_ptr<mobile_controller::FileTransferSession> files_;
std::string server_,logs_,login_,controller_login_,remote_,directory_,preview_directory_,preview_path_,version_;
std::vector<DisplayResolutionPreference> preferred_display_resolutions_;
int port_=9099,signal_status_=-1;
uint64_t signal_generation_=0;
std::set<int> keys_;