@desklink/react-native 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -12
- package/android/build.gradle +0 -1
- package/android/src/main/java/expo/modules/desklink/DesklinkModule.kt +4 -0
- package/android/src/main/java/expo/modules/desklink/DesktopSession.kt +20 -12
- package/android/src/main/java/expo/modules/desklink/DesktopView.kt +18 -3
- package/app.plugin.js +224 -1
- package/package.json +1 -1
- package/src/DesktopView.ios.tsx +15 -6
- package/src/DesktopView.tsx +8 -2
- package/src/native.ios.ts +12 -7
- package/src/native.web.ts +12 -2
- package/src/protocol.ts +10 -2
- package/src/useDesktopSession.ts +396 -136
- package/src/DesktopView.ios.spec.tsx +0 -529
- package/src/controls.spec.tsx +0 -205
package/src/native.ios.ts
CHANGED
|
@@ -95,8 +95,13 @@ export function preferH264(sdp: string): string {
|
|
|
95
95
|
return lines.join('\r\n');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
/**
|
|
99
|
+
* One native failure, with its `reason` so the hook can tell an ICE failure
|
|
100
|
+
* (the peer's path died, which the session can be held across) from an SDP or
|
|
101
|
+
* candidate failure (the negotiation itself is broken, which stays terminal).
|
|
102
|
+
*/
|
|
103
|
+
function failure(id: string, error: unknown, reason: 'ice' | 'sdp' | 'candidate' | 'control') {
|
|
104
|
+
emit(id, 'failure', { code: 'transport', reason, message: error instanceof Error ? error.message : String(error) });
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export const nativeDesklink: NativeDesklinkModule = {
|
|
@@ -121,7 +126,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
121
126
|
};
|
|
122
127
|
peer.oniceconnectionstatechange = () => {
|
|
123
128
|
emit(id, 'ice', { state: peer.iceConnectionState.toUpperCase() });
|
|
124
|
-
if (peer.iceConnectionState === 'failed') failure(id, 'the connection to the desktop was lost');
|
|
129
|
+
if (peer.iceConnectionState === 'failed') failure(id, 'the connection to the desktop was lost', 'ice');
|
|
125
130
|
};
|
|
126
131
|
peer.ontrack = (event) => {
|
|
127
132
|
if (session.closed || event.track.kind !== 'video') return;
|
|
@@ -148,7 +153,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
148
153
|
setRemoteDescription(id, type, sdp) {
|
|
149
154
|
const session = sessions.get(id);
|
|
150
155
|
if (!session) return false;
|
|
151
|
-
if (type !== 'offer') { failure(id, 'the engine must send an offer'); return false; }
|
|
156
|
+
if (type !== 'offer') { failure(id, 'the engine must send an offer', 'sdp'); return false; }
|
|
152
157
|
void (async () => {
|
|
153
158
|
try {
|
|
154
159
|
await session.peer.setRemoteDescription({ type: 'offer', sdp: preferH264(sdp.replace(PLAYOUT_DELAY, '')) });
|
|
@@ -158,7 +163,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
158
163
|
const answer = await session.peer.createAnswer();
|
|
159
164
|
await session.peer.setLocalDescription(answer);
|
|
160
165
|
if (!session.closed) emit(id, 'answer', { sdp: session.peer.localDescription?.sdp ?? answer.sdp });
|
|
161
|
-
} catch (error) { if (!session.closed) failure(id, error); }
|
|
166
|
+
} catch (error) { if (!session.closed) failure(id, error, 'sdp'); }
|
|
162
167
|
})();
|
|
163
168
|
return true;
|
|
164
169
|
},
|
|
@@ -167,7 +172,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
167
172
|
if (!session) return false;
|
|
168
173
|
const ice = { candidate, sdpMid, sdpMLineIndex: sdpMLineIndex ?? 0 };
|
|
169
174
|
if (!session.remoteSet) session.candidates.push(ice);
|
|
170
|
-
else void session.peer.addIceCandidate(ice).catch((error) => failure(id, error));
|
|
175
|
+
else void session.peer.addIceCandidate(ice).catch((error) => failure(id, error, 'candidate'));
|
|
171
176
|
return true;
|
|
172
177
|
},
|
|
173
178
|
sendControl(id, message) {
|
|
@@ -178,7 +183,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
178
183
|
if (!session.input && ['pointer', 'wheel', 'key', 'text', 'clipboard_read', 'clipboard_write'].includes(String(control.kind))) return false;
|
|
179
184
|
session.channel.send(JSON.stringify({ ...control, seq: ++session.seq }));
|
|
180
185
|
return true;
|
|
181
|
-
} catch (error) { failure(id, error); return false; }
|
|
186
|
+
} catch (error) { failure(id, error, 'control'); return false; }
|
|
182
187
|
},
|
|
183
188
|
setInputEnabled(id, enabled) {
|
|
184
189
|
const session = sessions.get(id);
|
package/src/native.web.ts
CHANGED
|
@@ -127,6 +127,8 @@ interface WebSession {
|
|
|
127
127
|
chordsDown: Set<string>;
|
|
128
128
|
/** Re-measure what the keyboard covers; null until a surface is attached. */
|
|
129
129
|
followKeyboard: (() => void) | null;
|
|
130
|
+
/** Whether the first frame was already reported; renegotiation must not re-mark a stale picture. */
|
|
131
|
+
presented: boolean;
|
|
130
132
|
/** Candidates that arrived before the offer; applied once it is set. */
|
|
131
133
|
remoteDescriptionSet: boolean;
|
|
132
134
|
pendingCandidates: RTCIceCandidateInit[];
|
|
@@ -960,6 +962,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
960
962
|
compositionSent: '',
|
|
961
963
|
composing: false,
|
|
962
964
|
chordsDown: new Set<string>(),
|
|
965
|
+
presented: false,
|
|
963
966
|
remoteDescriptionSet: false,
|
|
964
967
|
pendingCandidates: [],
|
|
965
968
|
detach: null,
|
|
@@ -982,6 +985,12 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
982
985
|
// track and produce nothing.
|
|
983
986
|
const presented = (): void => {
|
|
984
987
|
if (sessions.get(id) !== session) return;
|
|
988
|
+
// Readiness is the first frame. Renegotiation re-fires ontrack
|
|
989
|
+
// and the compositor repaints the frame it already holds, and a
|
|
990
|
+
// repaint is not a new frame: a second mark would read a stale
|
|
991
|
+
// picture as live and reset the recovery budget mid-outage.
|
|
992
|
+
if (session.presented) return;
|
|
993
|
+
session.presented = true;
|
|
985
994
|
video.style.opacity = '1';
|
|
986
995
|
emit(id, 'presented', {});
|
|
987
996
|
};
|
|
@@ -1009,7 +1018,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
1009
1018
|
peer.onconnectionstatechange = () => {
|
|
1010
1019
|
if (sessions.get(id) !== session) return;
|
|
1011
1020
|
if (peer.connectionState === 'failed') {
|
|
1012
|
-
emit(id, 'failure', { code: 'transport', message: 'the connection to the desktop was lost' });
|
|
1021
|
+
emit(id, 'failure', { code: 'transport', reason: 'ice', message: 'the connection to the desktop was lost' });
|
|
1013
1022
|
}
|
|
1014
1023
|
};
|
|
1015
1024
|
peer.oniceconnectionstatechange = () => {
|
|
@@ -1026,7 +1035,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
1026
1035
|
const session = sessions.get(id);
|
|
1027
1036
|
if (session === undefined) return false;
|
|
1028
1037
|
if (type !== 'offer') {
|
|
1029
|
-
emit(id, 'failure', { code: 'transport', message: 'the engine must send an offer' });
|
|
1038
|
+
emit(id, 'failure', { code: 'transport', reason: 'sdp', message: 'the engine must send an offer' });
|
|
1030
1039
|
return false;
|
|
1031
1040
|
}
|
|
1032
1041
|
void (async () => {
|
|
@@ -1043,6 +1052,7 @@ export const nativeDesklink: NativeDesklinkModule = {
|
|
|
1043
1052
|
} catch (error) {
|
|
1044
1053
|
emit(id, 'failure', {
|
|
1045
1054
|
code: 'transport',
|
|
1055
|
+
reason: 'sdp',
|
|
1046
1056
|
message: error instanceof Error ? error.message : 'the desktop refused our answer',
|
|
1047
1057
|
});
|
|
1048
1058
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -103,6 +103,8 @@ export interface CursorSample {
|
|
|
103
103
|
* the new session corrupts or kills a healthy recovery.
|
|
104
104
|
*/
|
|
105
105
|
export type SessionEvent =
|
|
106
|
+
/** Optional carrier notification, distinct from the native peer's `closed`. */
|
|
107
|
+
| { kind: 'carrier-closed'; sessionId?: never }
|
|
106
108
|
| { kind: 'description'; description: RtcDescription; sessionId?: string }
|
|
107
109
|
| { kind: 'candidate'; candidate: RtcCandidate; sessionId?: string }
|
|
108
110
|
| ({ kind: 'cursor' } & CursorSample)
|
|
@@ -120,15 +122,21 @@ export type SessionEvent =
|
|
|
120
122
|
* The package calls these and nothing else, so an application can carry them
|
|
121
123
|
* over its existing encrypted request path, a socket, or an in-process bridge.
|
|
122
124
|
* `subscribe` delivers engine notifications until the returned function runs.
|
|
125
|
+
* The application's authorize() resolves only once this carrier is open. Its
|
|
126
|
+
* rejection rules (transport vs. refusal) are in the README's Signaling section.
|
|
123
127
|
*/
|
|
124
128
|
export interface Signaling {
|
|
125
129
|
/**
|
|
126
|
-
* Send one request.
|
|
130
|
+
* Send one request. Reject pending and future requests with `code: 'transport'`
|
|
131
|
+
* once the carrier is gone; never queue requests on a dead carrier.
|
|
132
|
+
* A rejection may carry a stable `code` — the engine's or
|
|
127
133
|
* the host's own token — which the package reads to classify a refusal, so a
|
|
128
134
|
* refusal is not retried as though it were a network failure.
|
|
129
135
|
*/
|
|
130
136
|
request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
131
137
|
subscribe(handler: (event: SessionEvent) => void): () => void;
|
|
138
|
+
/** Optional: end this carrier now. Called once the package has declared it dead. */
|
|
139
|
+
close?(): void;
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
export type SessionStatus =
|
|
@@ -173,7 +181,7 @@ export interface SessionSnapshot {
|
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
/**
|
|
176
|
-
* Control-channel messages. `seq` is monotonic per
|
|
184
|
+
* Control-channel messages. `seq` is monotonic per session; the engine
|
|
177
185
|
* refuses a repeat, so a replayed gesture cannot move the pointer twice.
|
|
178
186
|
*/
|
|
179
187
|
export type ControlMessage =
|