@desklink/react-native 0.5.7 → 0.5.8

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 CHANGED
@@ -247,6 +247,14 @@ view.
247
247
  dies), so a roam longer than the client's own ~15 s `failed` still returns to
248
248
  the same session; an SDP or candidate failure, which means the negotiation
249
249
  itself is broken, reopens with fresh authority instead of staying stuck.
250
+ A first open whose path never reaches `connected` and never shows a frame
251
+ is different: it fails once with code `never-connected` — on the peer's ICE
252
+ `failed`, or at the 20 s first-frame bound, whichever comes first — closes
253
+ that session and does not reopen, since the same network gives the same
254
+ answer and every reopen restarts the host's capture. Usually the phone has
255
+ no direct route and the app supplied no relay (ICE) servers; `connect()`
256
+ tries again on demand. A first frame that never comes over a path that did
257
+ connect stays a `transport` failure and reopens as before.
250
258
  Every event carries its engine session id where the carrier preserves it,
251
259
  and the session ignores anything naming a session it no longer holds, so a
252
260
  previous generation's queued offer or revocation cannot corrupt or kill a
@@ -288,8 +296,9 @@ view.
288
296
  status by itself: the status follows the picture and the peer's transport, so
289
297
  it reads `reconnecting` only once the picture stops or the stall watchdog
290
298
  fires. While the loop runs, a session that has shown a picture never reports
291
- `failed` or a transport failure; one that never showed a picture reports the
292
- first-frame transport failure after 20 s and the loop stops. A new carrier
299
+ `failed` or a transport failure; one that never showed a picture reports its
300
+ first-frame failure after 20 s (`never-connected` when its path never
301
+ formed) and the loop stops. A new carrier
293
302
  picks up the same session after a short grace for the old ICE pair, sized by
294
303
  how long the picture has been still: stills under 4 s wait out the blip
295
304
  (200 ms), stills past it take one stats beat (50 ms) and ask for the restart
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desklink/react-native",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
package/src/protocol.ts CHANGED
@@ -159,7 +159,14 @@ export type SessionFailure =
159
159
  | { code: 'incompatible-version'; message: string }
160
160
  | { code: 'source-changed'; message: string }
161
161
  | { code: 'revoked'; message: string }
162
+ /** A path that formed and was lost; the session reopens on its own. */
162
163
  | { code: 'transport'; message: string }
164
+ /**
165
+ * A first open whose media path never reached `connected`: no frame, and
166
+ * no reopen, because the same network gives the same answer. Usually the
167
+ * phone has no direct route and the app supplied no relay (ICE) servers.
168
+ */
169
+ | { code: 'never-connected'; message: string }
163
170
  | { code: 'input-unavailable'; message: string }
164
171
  | { code: 'platform'; message: string };
165
172
 
@@ -222,6 +222,13 @@ const UNREACHABLE_DESKTOP =
222
222
  export const FIRST_FRAME_AFTER_MS = 20_000;
223
223
  const NO_PICTURE =
224
224
  'The desktop never sent a picture. On a path that only reaches the desktop through a tunnel or a USB forward, open the session with loopbackTcp: true.';
225
+ /**
226
+ * A first open whose media path never formed: no candidate pair ever answered.
227
+ * Reopening asks the same network the same question, and every reopen makes the
228
+ * host restart its capture, so this ends the session once instead.
229
+ */
230
+ const NEVER_CONNECTED =
231
+ 'No network path to the desktop ever formed. Other networks need a relay route supplied by the app; a tunnel or USB forward needs loopbackTcp: true.';
225
232
 
226
233
  /**
227
234
  * The engine's and host's own refusal tokens, mapped to this package's failure
@@ -407,6 +414,10 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
407
414
  const restartCycles = useRef(0);
408
415
  /** The last ICE state the peer or the engine reported. */
409
416
  const iceConnected = useRef(false);
417
+ /** Whether the held native session's path ever reached `connected`. */
418
+ const pathFormed = useRef(false);
419
+ /** Whether any session since the last teardown showed a picture: reopens are for those only. */
420
+ const everPresented = useRef(false);
410
421
  /**
411
422
  * When the peer's own ICE last failed while the session was held, or null
412
423
  * when the current failure is not an ICE one. Kept across the hold so the
@@ -465,10 +476,17 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
465
476
  * `ice` marks a failure of the peer's own media path (its ICE `failed`
466
477
  * state, or a `failure` event whose reason is `ice`). While the session is
467
478
  * held and has not been revoked, that is not the end of the session: the
468
- * reopen effect keeps it for `HOLD_AFTER_FAILED_MS` first. */
479
+ * reopen effect keeps it for `HOLD_AFTER_FAILED_MS` first.
480
+ *
481
+ * A first open whose path never formed is neither: there is nothing to
482
+ * hold and nothing a reopen would change, so it ends as `never-connected`. */
469
483
  const transportFailed = useCallback((message: string = UNREACHABLE_DESKTOP, ice = false): boolean => {
470
484
  if (dialing.current !== null) return false;
471
485
  if (ice && statusRef.current === 'failed') return false;
486
+ if (!everPresented.current && !pathFormed.current && nativeRef.current != null) {
487
+ refuse(NEVER_CONNECTED, 'never-connected');
488
+ return true;
489
+ }
472
490
  refuse(message, 'transport', ice);
473
491
  return true;
474
492
  }, [refuse]);
@@ -671,6 +689,7 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
671
689
  const onTransportState = useCallback((state: string, trackIce = true, skipGrace = false) => {
672
690
  // Synthetic stall reports say nothing about ICE itself.
673
691
  if (trackIce) iceConnected.current = state === 'connected' || state === 'completed';
692
+ if (trackIce && iceConnected.current) pathFormed.current = true;
674
693
  if (heldIceFailureAt.current !== null && statusRef.current === 'failed'
675
694
  && (state === 'connected' || state === 'completed')) {
676
695
  heldIceFailureAt.current = null;
@@ -907,6 +926,7 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
907
926
  pendingCandidates.current = [];
908
927
  pendingCursorEvents.current = [];
909
928
  heldIceFailureAt.current = null;
929
+ pathFormed.current = false;
910
930
  restartGraceUntil.current = 0;
911
931
  restartCycles.current = 0;
912
932
  // A new session has its own media path: the last one's counts and its
@@ -949,6 +969,7 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
949
969
  cancelFirstFrame();
950
970
  stopDialing();
951
971
  restoreToken.current = null;
972
+ everPresented.current = false;
952
973
  const openedRef = opened.current;
953
974
  const owner = signaling.current;
954
975
  discardSession();
@@ -1288,6 +1309,7 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
1288
1309
  // One mark per session: a producer that re-marks (a repaint
1289
1310
  // after renegotiation) must not read a stale picture as live.
1290
1311
  if (presentedRef.current) return;
1312
+ everPresented.current = true;
1291
1313
  attempts.current = 0;
1292
1314
  restartAttempts.current = 0;
1293
1315
  restartCycles.current = 0;
@@ -1449,17 +1471,25 @@ export function useDesktopSession(options: DesktopSessionOptions): DesktopSessio
1449
1471
  heldIceFailureAt.current = null;
1450
1472
  return;
1451
1473
  }
1452
- if (snapshot.failure?.code !== 'transport') return;
1453
- const reopen = () => {
1454
- // The failed session is dead on the engine side. Drop its handle and
1455
- // close the remote session whether or not another attempt is left: a
1456
- // failure the user can retry by hand must not sit behind a stale
1457
- // native handle that makes `connect()` a no-op.
1474
+ // The failed session is dead on the engine side. Drop its handle and
1475
+ // close the remote session whether or not another attempt is left: a
1476
+ // failure the user can retry by hand must not sit behind a stale
1477
+ // native handle that makes `connect()` a no-op.
1478
+ const drop = () => {
1458
1479
  const owner = signaling.current;
1459
1480
  const openedRef = opened.current;
1460
1481
  cancelRestart();
1461
1482
  discardSession();
1462
1483
  void endRemote(openedRef, owner);
1484
+ };
1485
+ // A path that never formed is closed once and not reopened.
1486
+ if (snapshot.failure?.code === 'never-connected') {
1487
+ drop();
1488
+ return;
1489
+ }
1490
+ if (snapshot.failure?.code !== 'transport') return;
1491
+ const reopen = () => {
1492
+ drop();
1463
1493
  // With the carrier dead, the dial loop opens on the first one that comes up.
1464
1494
  if (dialing.current !== null) {
1465
1495
  update({ status: 'reconnecting' });