mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-11 19:07:33 +08:00
[feat] remember remote display resolutions on mobile clients
This commit is contained in:
@@ -56,7 +56,8 @@ final class BackgroundSession implements NativeSession.Listener {
|
||||
}
|
||||
@Override public void data(JSONObject message) {
|
||||
if (view != null) { view.data(message); return; }
|
||||
displayResolutions.receive(message);
|
||||
RemoteDisplayResolutions.Request confirmed=displayResolutions.receive(message);
|
||||
if(confirmed!=null)session.rememberDisplayResolution(confirmed.displayKey,confirmed.mode);
|
||||
if (message.optInt("type", -1) == 12 && videoSettings != null) videoSettings.receive(message);
|
||||
if (message.optInt("type", -1) == 5) {
|
||||
JSONObject service = message.optJSONObject("service_status");
|
||||
|
||||
@@ -651,6 +651,10 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
private void cancelDisplayResolutionTimeout(){if(displayResolutionTimeout!=null)mainHandler.removeCallbacks(displayResolutionTimeout);displayResolutionTimeout=null;}
|
||||
private void updateDisplayResolution(RemoteDisplayResolutions.Mode mode){
|
||||
if(!ready||session==null||!hasNetworkConsent())return;
|
||||
RemoteDisplayResolutions.Display display=displayResolutions.display(selectedDisplay);
|
||||
if(!displayResolutions.pending&&display!=null&&display.supports(mode)&&display.current.same(mode)){
|
||||
session.rememberDisplayResolution(display.key,mode);return;
|
||||
}
|
||||
RemoteDisplayResolutions.Request request=displayResolutions.begin(selectedDisplay,mode);if(request==null)return;
|
||||
cancelDisplayResolutionTimeout();session.displayResolution(request);
|
||||
if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);
|
||||
@@ -677,7 +681,9 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
@Override public void data(JSONObject message){
|
||||
if(session==null)return;
|
||||
if(message.optInt("type",-1)==3||message.optInt("type",-1)==16){
|
||||
displayResolutions.receive(message);if(!displayResolutions.pending)cancelDisplayResolutionTimeout();
|
||||
RemoteDisplayResolutions.Request confirmed=displayResolutions.receive(message);
|
||||
if(confirmed!=null&&session!=null)session.rememberDisplayResolution(confirmed.displayKey,confirmed.mode);
|
||||
if(!displayResolutions.pending)cancelDisplayResolutionTimeout();
|
||||
if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);
|
||||
if(message.optInt("type",-1)==16)return;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ final class NativeSession implements AutoCloseable {
|
||||
private final Context context;
|
||||
private volatile Listener listener;
|
||||
private final SecretStore secrets;
|
||||
private final RemoteResolutionStore resolutionStore;
|
||||
private final String host, scope;
|
||||
private volatile String remote;
|
||||
private String password, identity = "";
|
||||
@@ -80,6 +81,7 @@ final class NativeSession implements AutoCloseable {
|
||||
this.host = host; this.port = port; this.remote = remote; this.password = password;
|
||||
scope = host.toLowerCase(java.util.Locale.ROOT) + ":" + port;
|
||||
secrets = new SecretStore(this.context);
|
||||
resolutionStore = new RemoteResolutionStore(this.context);
|
||||
documents = new SessionDocuments(this.context);
|
||||
}
|
||||
void start() {
|
||||
@@ -184,7 +186,8 @@ final class NativeSession implements AutoCloseable {
|
||||
int status = event.getInt("status");
|
||||
if (status == 1) {
|
||||
if (connected) return; // Repeated transport readiness is the same desktop session.
|
||||
connected = true; main.removeCallbacks(timeout); nReady(handle);
|
||||
connected = true; main.removeCallbacks(timeout);
|
||||
nReady(handle,resolutionStore.load(host,port,remote).toString().getBytes(StandardCharsets.UTF_8));
|
||||
audio = new AudioPlayer();
|
||||
nControl(handle, 2, 1);
|
||||
ui(() -> { listener.connected(); if(!closed){statisticsTick.run();fileTick.run();} });
|
||||
@@ -247,6 +250,9 @@ final class NativeSession implements AutoCloseable {
|
||||
void displayResolution(RemoteDisplayResolutions.Request request){
|
||||
execute(()->{if(!connected||!nDisplayResolution(handle,request.displayId,request.displayKey,request.mode.width,request.mode.height,request.id))ui(()->listener.displayResolutionFailed(request.id));});
|
||||
}
|
||||
void rememberDisplayResolution(String displayKey,RemoteDisplayResolutions.Mode mode){
|
||||
if(!closed&&connected)resolutionStore.remember(host,port,remote,displayKey,mode);
|
||||
}
|
||||
void announcementRequest(JSONObject request,String requestId){
|
||||
byte[] bytes=request.toString().getBytes(StandardCharsets.UTF_8);
|
||||
execute(()->{if(!signalReady||identity.isEmpty()||!nAnnouncementRequest(handle,bytes))ui(()->listener.announcementFailed(requestId));});
|
||||
@@ -343,7 +349,7 @@ final class NativeSession implements AutoCloseable {
|
||||
static native String nCheckMobileUpdate(String version, byte[] releaseJson);
|
||||
private static native long nCreate(NativeSession owner, String host, int port, String identity, String logs, String roots);
|
||||
private static native boolean nConnect(long handle, String identity, String remote, String password);
|
||||
private static native void nReady(long handle);
|
||||
private static native void nReady(long handle,byte[] displayResolutions);
|
||||
private static native void nPointer(long handle, float x, float y, int flag, int wheel);
|
||||
private static native void nKey(long handle, int code, boolean down);
|
||||
private static native void nText(long handle,byte[] text);
|
||||
|
||||
@@ -57,28 +57,30 @@ final class RemoteDisplayResolutions {
|
||||
requested=new Request(display,mode,next==0?1:next);pending=true;failed=false;return requested;
|
||||
}
|
||||
void expire(long id){if(pending&&requested!=null&&requested.id==id){pending=false;failed=true;}}
|
||||
void receive(JSONObject message){
|
||||
Request receive(JSONObject message){
|
||||
try{
|
||||
int type=message.getInt("type");JSONArray values;JSONObject response=null;
|
||||
if(type==3){JSONObject host=message.getJSONObject("host_info");values=host.has("display_resolutions")?host.getJSONArray("display_resolutions"):new JSONArray();}
|
||||
else if(type==16){response=message.getJSONObject("display_resolution");if(requested==null||!requested.matches(response)||!(response.get("accepted") instanceof Boolean))return;values=message.getJSONArray("display_resolutions");}
|
||||
else return;
|
||||
if(values.length()>32)return;
|
||||
else if(type==16){response=message.getJSONObject("display_resolution");if(requested==null||!requested.matches(response)||!(response.get("accepted") instanceof Boolean))return null;values=message.getJSONArray("display_resolutions");}
|
||||
else return null;
|
||||
if(values.length()>32)return null;
|
||||
List<Display> actual=new ArrayList<>();
|
||||
for(int i=0;i<values.length();i++){
|
||||
Display display=new Display(values.getJSONObject(i));
|
||||
for(Display previous:actual)if(previous.id==display.id)return;
|
||||
for(Display previous:actual)if(previous.id==display.id)return null;
|
||||
actual.add(display);
|
||||
}
|
||||
displays=actual;
|
||||
if(response!=null){
|
||||
Display display=display(requested.displayId);pending=false;
|
||||
failed=!response.getBoolean("accepted")||display==null||!display.key.equals(requested.displayKey)||!display.current.same(requested.mode);
|
||||
if(!failed)return requested;
|
||||
}else if(pending){
|
||||
Display display=display(requested.displayId);
|
||||
if(display==null||!display.key.equals(requested.displayKey)||!display.supports(requested.mode))expire(requested.id);
|
||||
}
|
||||
}catch(Exception ignored){ /* Ignore malformed or unrelated control messages. */ }
|
||||
return null;
|
||||
}
|
||||
String feedback(){return pending?L10n.tr("等待电脑端确认…"):failed?L10n.tr("调整失败,请重试。"):"";}
|
||||
private static Mode mode(JSONObject value)throws Exception{return new Mode((int)integer(value,"width",1,32768),(int)integer(value,"height",1,32768));}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Successful selections, scoped to the signaling server, device and native screen. */
|
||||
final class RemoteResolutionStore {
|
||||
private final SharedPreferences preferences;
|
||||
RemoteResolutionStore(Context context) {
|
||||
preferences=context.getSharedPreferences("remote_display_resolutions",Context.MODE_PRIVATE);
|
||||
}
|
||||
private String key(String host,int port,String remote) {
|
||||
return new JSONArray().put(host.trim().toLowerCase(Locale.ROOT)).put(port).put(remote).toString();
|
||||
}
|
||||
private static boolean validKey(String key) {
|
||||
return !key.isEmpty()&&key.getBytes(StandardCharsets.UTF_8).length<=1024&&key.indexOf('\0')<0;
|
||||
}
|
||||
private static int dimension(JSONObject value,String key)throws Exception {
|
||||
Object raw=value.get(key);
|
||||
if(!(raw instanceof Integer)&&!(raw instanceof Long))throw new IllegalArgumentException();
|
||||
long number=((Number)raw).longValue();
|
||||
if(number<1||number>32768)throw new IllegalArgumentException();
|
||||
return (int)number;
|
||||
}
|
||||
synchronized JSONArray load(String host,int port,String remote) {
|
||||
if(remote.isEmpty())return new JSONArray();
|
||||
try {
|
||||
String text=preferences.getString(key(host,port,remote),"[]");
|
||||
if(text==null||text.length()>65536)return new JSONArray();
|
||||
JSONArray values=new JSONArray(text);
|
||||
if(values.length()>32)return new JSONArray();
|
||||
HashSet<String> keys=new HashSet<>();
|
||||
JSONArray result=new JSONArray();
|
||||
for(int i=0;i<values.length();i++) {
|
||||
JSONObject value=values.getJSONObject(i);
|
||||
Object identity=value.get("display_key");
|
||||
if(!(identity instanceof String)||!validKey((String)identity)||!keys.add((String)identity))return new JSONArray();
|
||||
result.put(new JSONObject().put("display_key",identity).put("width",dimension(value,"width")).put("height",dimension(value,"height")));
|
||||
}
|
||||
return result;
|
||||
} catch(Exception ignored) { return new JSONArray(); }
|
||||
}
|
||||
synchronized void remember(String host,int port,String remote,String displayKey,RemoteDisplayResolutions.Mode mode) {
|
||||
if(remote.isEmpty()||!validKey(displayKey)||mode==null||mode.width<1||mode.width>32768||mode.height<1||mode.height>32768)return;
|
||||
try {
|
||||
JSONArray saved=load(host,port,remote),updated=new JSONArray();
|
||||
for(int i=0;i<saved.length();i++) {
|
||||
JSONObject value=saved.getJSONObject(i);
|
||||
if(!displayKey.equals(value.getString("display_key")))updated.put(value);
|
||||
}
|
||||
if(updated.length()>=32)updated.remove(0);
|
||||
updated.put(new JSONObject().put("display_key",displayKey).put("width",mode.width).put("height",mode.height));
|
||||
preferences.edit().putString(key(host,port,remote),updated.toString()).apply();
|
||||
} catch(Exception error) { Log.w("CrossDesk","Could not save remote display resolution",error); }
|
||||
}
|
||||
}
|
||||
@@ -366,10 +366,16 @@ extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nConnect)(
|
||||
const auto join = s->remote + "@" + String(env, password);
|
||||
return MiniRtcJoinConnection(s->controller, join.c_str()) == 0;
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv*, jclass, jlong handle) {
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv* env, jclass, jlong handle, jbyteArray preferences) {
|
||||
auto* s = From(handle); if (!s || !s->controller) return;
|
||||
auto action = MakeHostInformation("CrossDesk Android", {}, true, "1.6.0", HostPlatform::Android);
|
||||
action.i.supports_video_codec_status = true;
|
||||
if (preferences && env->GetArrayLength(preferences) <= 65536) {
|
||||
std::string bytes(env->GetArrayLength(preferences), '\0');
|
||||
env->GetByteArrayRegion(preferences, 0, bytes.size(), reinterpret_cast<jbyte*>(bytes.data()));
|
||||
if (!env->ExceptionCheck())
|
||||
ParseDisplayResolutionPreferences(bytes, action.preferred_display_resolutions);
|
||||
}
|
||||
s->Send(kControlStream, action.to_json()); FreeRemoteAction(action);
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nPointer)(
|
||||
|
||||
@@ -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("调整失败,请重试。") : ''; }
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -180,7 +180,8 @@ typedef struct {
|
||||
- (void)sendAnnouncementRequest:(NSData *)request requestID:(NSString *)requestID
|
||||
NS_SWIFT_NAME(sendAnnouncementRequest(_:requestID:));
|
||||
|
||||
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password;
|
||||
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password
|
||||
displayResolutions:(NSData *)displayResolutions;
|
||||
- (void)disconnect;
|
||||
// Close both session and signaling peers, including automatic reconnection.
|
||||
- (void)stopNetworking;
|
||||
|
||||
@@ -86,6 +86,7 @@ struct RTCState {
|
||||
std::string identity_base;
|
||||
std::string controller_login;
|
||||
std::string pending_remote_id;
|
||||
std::vector<crossdesk::DisplayResolutionPreference> preferred_display_resolutions;
|
||||
uint64_t pending_controller_generation = 0;
|
||||
std::string pending_password;
|
||||
std::string transmission_id;
|
||||
@@ -800,7 +801,14 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password {
|
||||
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password
|
||||
displayResolutions:(NSData *)displayResolutions {
|
||||
std::vector<crossdesk::DisplayResolutionPreference> preferences;
|
||||
if (displayResolutions.length > 0 && displayResolutions.length <= 65536) {
|
||||
const auto *bytes = static_cast<const char *>(displayResolutions.bytes);
|
||||
crossdesk::ParseDisplayResolutionPreferences(
|
||||
std::string(bytes, displayResolutions.length), preferences);
|
||||
}
|
||||
NSString *cleanID = [remoteID
|
||||
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
NSString *cleanPassword = [password
|
||||
@@ -820,6 +828,7 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (generation != self->_controllerGenerationCounter.load()) return;
|
||||
self->_state->pending_remote_id = identifier;
|
||||
self->_state->preferred_display_resolutions = preferences;
|
||||
self->_state->pending_controller_generation = generation;
|
||||
self->_state->pending_password = password_value;
|
||||
[self destroyControllerPeer];
|
||||
@@ -1458,6 +1467,7 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
: "iPhone-" + self->_state->identity_base;
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::host_infomation;
|
||||
action.preferred_display_resolutions = self->_state->preferred_display_resolutions;
|
||||
action.i.platform = crossdesk::HostPlatform::IOS;
|
||||
action.i.supports_privacy_screen = true;
|
||||
action.i.supports_video_codec_status = true;
|
||||
|
||||
@@ -370,6 +370,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
private var appIsActive = false
|
||||
private lazy var audioPlayer = RemoteAudioPlayer()
|
||||
private var activeRemoteID = ""
|
||||
private var resolutionPreferenceKey = ""
|
||||
private var remoteAppVersion = ""
|
||||
private var remoteHostInfoReceived = false
|
||||
private var remoteVersionCheckAttempted = false
|
||||
@@ -610,6 +611,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
}
|
||||
|
||||
private func beginRemoteConnection(_ identifier: String) {
|
||||
guard let configuration = bridgeConfiguration else { return }
|
||||
AppOrientation.endSession()
|
||||
networkStatistics = RemoteNetworkStatistics()
|
||||
resetRemoteHostInfo()
|
||||
@@ -617,6 +619,8 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
resetVideoSettings()
|
||||
cancelPresenceProbe()
|
||||
activeRemoteID = identifier
|
||||
resolutionPreferenceKey = RemoteDisplayResolutionStore.key(host: configuration.host,
|
||||
port: configuration.signalPort, remoteID: identifier)
|
||||
activeDisplayName = ""
|
||||
displaySizes = []
|
||||
connectionRecorded = false
|
||||
@@ -626,7 +630,8 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
sessionVisible = false
|
||||
resetRemoteCursorState()
|
||||
connectionStatus = NSLocalizedString("正在准备连接…", comment: "")
|
||||
bridge.connect(toRemoteID: identifier, password: password)
|
||||
bridge.connect(toRemoteID: identifier, password: password,
|
||||
displayResolutions: RemoteDisplayResolutionStore.message(key: resolutionPreferenceKey))
|
||||
}
|
||||
|
||||
private func beginPresenceProbe(_ identifier: String) {
|
||||
@@ -1044,8 +1049,13 @@ 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 }
|
||||
let display = displayResolutions.display(displayID), display.display_key == displayKey else { return }
|
||||
if !displayResolutions.pending, display.current == resolution, display.modes.contains(resolution) {
|
||||
RemoteDisplayResolutionStore.remember(RemoteDisplayResolutionPreference(display_key: displayKey,
|
||||
width: resolution.width, height: resolution.height), key: resolutionPreferenceKey)
|
||||
return
|
||||
}
|
||||
guard 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)
|
||||
@@ -1059,7 +1069,9 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
// 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 let confirmed = displayResolutions.receive(message) {
|
||||
RemoteDisplayResolutionStore.remember(confirmed, key: resolutionPreferenceKey)
|
||||
}
|
||||
if !displayResolutions.pending {
|
||||
displayResolutionTimeoutTask?.cancel(); displayResolutionTimeoutTask = nil
|
||||
}
|
||||
|
||||
@@ -77,6 +77,42 @@ struct RemoteDisplayResolution: Codable, Equatable, Hashable {
|
||||
var valid: Bool { (1...32768).contains(width) && (1...32768).contains(height) }
|
||||
}
|
||||
|
||||
struct RemoteDisplayResolutionPreference: Codable, Equatable {
|
||||
let display_key: String
|
||||
let width: Int
|
||||
let height: Int
|
||||
var valid: Bool {
|
||||
!display_key.isEmpty && display_key.utf8.count <= 1024 && !display_key.contains("\0") &&
|
||||
RemoteDisplayResolution(width: width, height: height).valid
|
||||
}
|
||||
}
|
||||
|
||||
enum RemoteDisplayResolutionStore {
|
||||
static func key(host: String, port: Int, remoteID: String) -> String {
|
||||
let scope = [host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), String(port), remoteID]
|
||||
guard !remoteID.isEmpty, let data = try? JSONEncoder().encode(scope) else { return "" }
|
||||
return "crossdesk.remoteDisplayResolutions.v1." + data.base64EncodedString()
|
||||
}
|
||||
static func load(key: String, defaults: UserDefaults = .standard) -> [RemoteDisplayResolutionPreference] {
|
||||
guard !key.isEmpty, let data = defaults.data(forKey: key), data.count <= 65536,
|
||||
let values = try? JSONDecoder().decode([RemoteDisplayResolutionPreference].self, from: data),
|
||||
values.count <= 32, values.allSatisfy(\.valid),
|
||||
Set(values.map(\.display_key)).count == values.count else { return [] }
|
||||
return values
|
||||
}
|
||||
static func remember(_ preference: RemoteDisplayResolutionPreference, key: String,
|
||||
defaults: UserDefaults = .standard) {
|
||||
guard !key.isEmpty, preference.valid else { return }
|
||||
var values = load(key: key, defaults: defaults).filter { $0.display_key != preference.display_key }
|
||||
if values.count >= 32 { values.removeFirst() }
|
||||
values.append(preference)
|
||||
if let data = try? JSONEncoder().encode(values) { defaults.set(data, forKey: key) }
|
||||
}
|
||||
static func message(key: String, defaults: UserDefaults = .standard) -> Data {
|
||||
(try? JSONEncoder().encode(load(key: key, defaults: defaults))) ?? Data("[]".utf8)
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoteDisplayResolutionInfo: Decodable {
|
||||
let display_id: Int
|
||||
let display_key: String
|
||||
@@ -128,7 +164,8 @@ struct RemoteDisplayResolutionState {
|
||||
guard pending, requested?.request_id == requestID else { return }
|
||||
pending = false; failed = true
|
||||
}
|
||||
mutating func receive(_ data: Data) {
|
||||
@discardableResult
|
||||
mutating func receive(_ data: Data) -> RemoteDisplayResolutionPreference? {
|
||||
struct Host: Decodable { let display_resolutions: [RemoteDisplayResolutionInfo]? }
|
||||
struct Message: Decodable {
|
||||
let type: Int
|
||||
@@ -136,16 +173,16 @@ struct RemoteDisplayResolutionState {
|
||||
let display_resolution: RemoteDisplayResolutionRequest?
|
||||
let display_resolutions: [RemoteDisplayResolutionInfo]?
|
||||
}
|
||||
guard let message = try? JSONDecoder().decode(Message.self, from: data) else { return }
|
||||
guard let message = try? JSONDecoder().decode(Message.self, from: data) else { return nil }
|
||||
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 }
|
||||
} else { return nil }
|
||||
guard actual.count <= 32, actual.allSatisfy(\.valid),
|
||||
Set(actual.map(\.display_id)).count == actual.count else { return }
|
||||
Set(actual.map(\.display_id)).count == actual.count else { return nil }
|
||||
displays = actual
|
||||
if message.type == 16, let response = message.display_resolution {
|
||||
pending = false
|
||||
@@ -153,11 +190,16 @@ struct RemoteDisplayResolutionState {
|
||||
$0.display_id == response.display_id && $0.display_key == response.display_key &&
|
||||
$0.current == response.resolution
|
||||
}
|
||||
if !failed {
|
||||
return RemoteDisplayResolutionPreference(display_key: response.display_key,
|
||||
width: response.width, height: response.height)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user