@pollen-robotics/reachy-mini-sdk 1.8.0 → 1.8.1

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.
Files changed (67) hide show
  1. package/APP_CREATION_GUIDE.md +1795 -0
  2. package/README.md +2 -5
  3. package/dist/animation/distance.d.ts +87 -0
  4. package/dist/animation/distance.d.ts.map +1 -0
  5. package/dist/animation/distance.js +140 -0
  6. package/dist/animation/distance.js.map +1 -0
  7. package/dist/animation/index.d.ts +34 -0
  8. package/dist/animation/index.d.ts.map +1 -0
  9. package/dist/animation/index.js +33 -0
  10. package/dist/animation/index.js.map +1 -0
  11. package/dist/animation/pose.d.ts +40 -0
  12. package/dist/animation/pose.d.ts.map +1 -0
  13. package/dist/animation/pose.js +15 -0
  14. package/dist/animation/pose.js.map +1 -0
  15. package/dist/animation/presets.d.ts +85 -0
  16. package/dist/animation/presets.d.ts.map +1 -0
  17. package/dist/animation/presets.js +82 -0
  18. package/dist/animation/presets.js.map +1 -0
  19. package/dist/animation/safe-return.d.ts +90 -0
  20. package/dist/animation/safe-return.d.ts.map +1 -0
  21. package/dist/animation/safe-return.js +123 -0
  22. package/dist/animation/safe-return.js.map +1 -0
  23. package/dist/lib/reachy-mini.d.ts +99 -0
  24. package/dist/lib/reachy-mini.d.ts.map +1 -1
  25. package/dist/lib/reachy-mini.js +314 -9
  26. package/dist/lib/reachy-mini.js.map +1 -1
  27. package/dist/lib/types.d.ts +27 -0
  28. package/dist/lib/types.d.ts.map +1 -1
  29. package/host/README.md +14 -12
  30. package/host/dist/ReachyHost.d.ts +2 -1
  31. package/host/dist/ReachyHost.d.ts.map +1 -1
  32. package/host/dist/chunks/index-lMs2sCXG.js +439 -0
  33. package/host/dist/chunks/{mountHost-D7cgkU9Q.js → mountHost-DjE5zE9R.js} +11142 -11014
  34. package/host/dist/chunks/{reachy-mini-B1hlBmDQ.js → reachy-mini-GHNS4GSp.js} +494 -307
  35. package/host/dist/components/PickerView.d.ts.map +1 -1
  36. package/host/dist/components/PostOAuthSplash.d.ts +25 -0
  37. package/host/dist/components/PostOAuthSplash.d.ts.map +1 -0
  38. package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
  39. package/host/dist/components/TopBar.d.ts +8 -1
  40. package/host/dist/components/TopBar.d.ts.map +1 -1
  41. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
  42. package/host/dist/embed/index.d.ts +3 -2
  43. package/host/dist/embed/index.d.ts.map +1 -1
  44. package/host/dist/entry/auto.js +2 -2
  45. package/host/dist/entry/embed.js +2 -2
  46. package/host/dist/hooks/useHostBridge.d.ts +6 -0
  47. package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
  48. package/host/dist/hooks/useSdk.d.ts.map +1 -1
  49. package/host/dist/index.js +3 -3
  50. package/host/dist/lib/protocol.d.ts +17 -3
  51. package/host/dist/lib/protocol.d.ts.map +1 -1
  52. package/host/dist/lib/settings.d.ts +3 -2
  53. package/host/dist/lib/settings.d.ts.map +1 -1
  54. package/host/dist/mountHost.d.ts.map +1 -1
  55. package/host/dist/ui/design/IdentityChipBar.d.ts +46 -0
  56. package/host/dist/ui/design/IdentityChipBar.d.ts.map +1 -0
  57. package/host/dist/ui/design/LinkQualityBars.d.ts +32 -0
  58. package/host/dist/ui/design/LinkQualityBars.d.ts.map +1 -0
  59. package/host/dist/ui/design/MetaPill.d.ts +41 -0
  60. package/host/dist/ui/design/MetaPill.d.ts.map +1 -0
  61. package/host/dist/ui/design/TransportChip.d.ts +39 -0
  62. package/host/dist/ui/design/TransportChip.d.ts.map +1 -0
  63. package/package.json +7 -4
  64. package/CHANGELOG.md +0 -188
  65. package/host/APP_AUTHOR_GUIDE.md +0 -646
  66. package/host/SPEC.md +0 -618
  67. package/host/dist/chunks/index-CyLPysJS.js +0 -400
@@ -7,6 +7,39 @@ import { oauthHandleRedirectIfPresent, oauthLoginUrl, } from '@huggingface/hub';
7
7
  import { degToRad, rpyToMatrix } from './math.js';
8
8
  import { consumeFragmentCredentials, readPreselectedRobotIdFromUrl, sdpHasAudioSendRecv, } from './url-helpers.js';
9
9
  import { UPLOAD_CHUNK_SIZE, UPLOAD_BUFFERED_HIGH_WATER, UPLOAD_BUFFERED_LOW_WATER, hasCompressionStream, makeUploadId, bytesToBase64, gzipBase64, clampVolume, } from './upload-helpers.js';
10
+ // ─── Internal constants ──────────────────────────────────────────────────────
11
+ /**
12
+ * How long we tolerate `iceConnectionState === 'disconnected'` before
13
+ * surfacing it as an error. The spec defines this state as transient
14
+ * (browsers keep STUN keep-alives running and usually heal in 1-2 s
15
+ * on WiFi blips, AP roams, brief 4G dropouts). Consumers watching
16
+ * `iceStateChange` directly should outlive this window before
17
+ * showing any fatal UI.
18
+ */
19
+ const ICE_DISCONNECT_GRACE_MS = 3000;
20
+ /**
21
+ * Grace before treating `iceConnectionState === 'failed'` as terminal.
22
+ * The spec says `failed` IS terminal, but we've observed real
23
+ * `failed → connected` flips on rapid AP roams and iOS BT route
24
+ * changes — 1 s of debounce absorbs those without noticeably
25
+ * delaying a real failure.
26
+ */
27
+ const ICE_FAILED_GRACE_MS = 1000;
28
+ /**
29
+ * Ceiling on how long we'll keep `_armIceGraceOnVisibility` waiting
30
+ * for the tab to come back. The daemon's `webrtcsink` runs a STUN
31
+ * consent-freshness check (RFC 7675, ~30 s default) and unilaterally
32
+ * tears its side of the session down past that window, releasing the
33
+ * producer slot on central. If the user backgrounded the tab for
34
+ * longer than this, running another 3 s foreground grace is a lie —
35
+ * the underlying transport is gone, nothing can recover. Give up
36
+ * straight away so the host shows the real "session expired" UX
37
+ * instead of a fake "Reconnecting…" badge that's never going to
38
+ * heal. 60 s gives a 2× margin over the daemon-side timeout — long
39
+ * enough to absorb a "phone in pocket for 45 s" case, short enough
40
+ * to be honest with the user.
41
+ */
42
+ const MAX_VISIBILITY_DEFER_MS = 60_000;
10
43
  export class ReachyMini extends EventTarget {
11
44
  // ─── Config ──────────────────────────────────────────────────────────
12
45
  _signalingUrl;
@@ -62,6 +95,19 @@ export class ReachyMini extends EventTarget {
62
95
  _sessionReject = null;
63
96
  _iceConnected = false;
64
97
  _dcOpen = false;
98
+ // ─── Resilience: ICE-blip debounce + network awareness ──────────────
99
+ // Backs `_scheduleIceGrace` / `_armIceGraceOnVisibility` and the
100
+ // `networkOnline` / `networkOffline` / `networkChange` forwarders.
101
+ // All three handler slots are scoped to the lifetime of a live
102
+ // session (installed in `startSession`, cleared in
103
+ // `stopSession` / `disconnect` / `_handleEndSession` /
104
+ // `_failSessionRejected`).
105
+ _iceGraceTimer = null;
106
+ _iceGraceReason = null;
107
+ _pendingVisibilityHandler = null;
108
+ _onlineHandler = null;
109
+ _offlineHandler = null;
110
+ _connectionChangeHandler = null;
65
111
  // ─── Motion completion plumbing (wake_up / goto_sleep) ───────────────
66
112
  _pendingMotionCompletions = {
67
113
  wake_up: [],
@@ -418,6 +464,9 @@ export class ReachyMini extends EventTarget {
418
464
  this._pc = new RTCPeerConnection({
419
465
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
420
466
  });
467
+ // Scope `networkOnline` / `networkOffline` / `networkChange`
468
+ // event forwarding to the lifetime of this session.
469
+ this._installNetworkListeners();
421
470
  return new Promise((resolve, reject) => {
422
471
  this._sessionResolve = resolve;
423
472
  this._sessionReject = reject;
@@ -454,21 +503,38 @@ export class ReachyMini extends EventTarget {
454
503
  const s = this._pc?.iceConnectionState;
455
504
  if (!s)
456
505
  return;
506
+ // Public, granular event: every transition is visible to
507
+ // consumers so they can render finer UX (e.g. a transient
508
+ // "Reconnecting…" badge during `disconnected`) without
509
+ // having to attach their own handler to `_pc`.
510
+ this._emit('iceStateChange', { state: s });
457
511
  if (s === 'connected' || s === 'completed') {
512
+ // Healed — cancel any pending grace from a previous blip.
513
+ this._clearIceGrace();
458
514
  this._iceConnected = true;
459
515
  this._checkSessionReady();
516
+ return;
460
517
  }
461
- else if (s === 'failed') {
462
- const err = new Error('ICE connection failed');
463
- if (this._sessionReject) {
464
- this._sessionReject(err);
465
- this._sessionResolve = null;
466
- this._sessionReject = null;
518
+ if (s === 'disconnected') {
519
+ // TRANSIENT per spec debounce before escalating.
520
+ // If the tab is hidden, JS timers are throttled and
521
+ // would fire unpredictably late, so defer the grace
522
+ // window to the next foreground frame.
523
+ if (typeof document !== 'undefined' && document.hidden) {
524
+ this._armIceGraceOnVisibility();
525
+ }
526
+ else {
527
+ this._scheduleIceGrace(ICE_DISCONNECT_GRACE_MS, 'disconnected');
467
528
  }
468
- this._emit('error', { source: 'webrtc', error: err });
529
+ return;
469
530
  }
470
- else if (s === 'disconnected') {
471
- this._emit('error', { source: 'webrtc', error: new Error('ICE disconnected') });
531
+ if (s === 'failed') {
532
+ // Terminal per spec, but in practice we've seen
533
+ // `failed → connected` on rapid AP roams / BT route
534
+ // changes on iOS. Give the ICE agent a short window
535
+ // to surprise us before rejecting the session.
536
+ this._scheduleIceGrace(ICE_FAILED_GRACE_MS, 'failed');
537
+ return;
472
538
  }
473
539
  };
474
540
  this._pc.ondatachannel = (e) => {
@@ -495,6 +561,10 @@ export class ReachyMini extends EventTarget {
495
561
  : `Session rejected: ${msg.reason || 'unknown reason'}`);
496
562
  err.reason = msg.reason ?? null;
497
563
  err.activeApp = msg.activeApp ?? null;
564
+ // Resilience teardown BEFORE closing `_pc` so a queued grace
565
+ // callback can't dereference a dead handle.
566
+ this._clearIceGrace();
567
+ this._uninstallNetworkListeners();
498
568
  if (this._pc) {
499
569
  this._pc.close();
500
570
  this._pc = null;
@@ -542,6 +612,10 @@ export class ReachyMini extends EventTarget {
542
612
  }
543
613
  this._logSubscribers.clear();
544
614
  this._rejectPendingMotionCompletions(new Error('Session stopped'));
615
+ // Tear down resilience plumbing BEFORE closing `_pc` so a
616
+ // queued grace callback can't dereference a dead handle.
617
+ this._clearIceGrace();
618
+ this._uninstallNetworkListeners();
545
619
  if (this._sessionReject) {
546
620
  this._sessionReject(new Error('Session stopped'));
547
621
  this._sessionResolve = null;
@@ -612,6 +686,9 @@ export class ReachyMini extends EventTarget {
612
686
  }
613
687
  this._logSubscribers.clear();
614
688
  this._rejectPendingMotionCompletions(new Error('Disconnected'));
689
+ // Mirrors the resilience teardown in `stopSession()`.
690
+ this._clearIceGrace();
691
+ this._uninstallNetworkListeners();
615
692
  if (this._sessionReject) {
616
693
  this._sessionReject(new Error('Disconnected'));
617
694
  this._sessionResolve = null;
@@ -649,7 +726,229 @@ export class ReachyMini extends EventTarget {
649
726
  this._state = 'disconnected';
650
727
  this._emit('disconnected', { reason: 'user' });
651
728
  }
729
+ // ─── Resilience: ICE-blip debounce + network awareness ───────────────
730
+ //
731
+ // Both halves below are intentionally generic (they don't know about
732
+ // motion, audio, or the FSM): they just smooth out browser-level
733
+ // events so the consumer's own state machine doesn't get torn down
734
+ // by routine WiFi/4G/screen-off noise.
735
+ /**
736
+ * Cancel any pending ICE grace timer and visibility handler. Called
737
+ * on a healed `connected`/`completed` transition AND from the
738
+ * lifecycle teardown paths so a callback can't fire after `_pc`
739
+ * is closed.
740
+ */
741
+ _clearIceGrace() {
742
+ if (this._iceGraceTimer !== null) {
743
+ clearTimeout(this._iceGraceTimer);
744
+ this._iceGraceTimer = null;
745
+ }
746
+ this._iceGraceReason = null;
747
+ if (this._pendingVisibilityHandler && typeof document !== 'undefined') {
748
+ document.removeEventListener('visibilitychange', this._pendingVisibilityHandler);
749
+ }
750
+ this._pendingVisibilityHandler = null;
751
+ }
752
+ /**
753
+ * Start a grace window. After `ms`, re-check the live ICE state:
754
+ * - If we healed back to `connected`/`completed`, the timer was
755
+ * already cancelled in `oniceconnectionstatechange`, so we
756
+ * never get here.
757
+ * - If we're still in the originally-observed bad state (or
758
+ * worse), surface the error and reject any pending session
759
+ * promise. The original code path is preserved verbatim so
760
+ * downstream consumers see the same `error` payload shape.
761
+ */
762
+ _scheduleIceGrace(ms, reason) {
763
+ // Coalesce: if a grace is already pending and the reason hasn't
764
+ // changed, keep the original timer so a flurry of identical
765
+ // transitions doesn't reset the clock. If the reason changed
766
+ // (typically `disconnected` → `failed`, but also the reverse on
767
+ // some Android WebViews), replace the timer with the new
768
+ // (reason, ms) pair — the latest signal wins.
769
+ if (this._iceGraceTimer !== null) {
770
+ if (this._iceGraceReason === reason)
771
+ return;
772
+ clearTimeout(this._iceGraceTimer);
773
+ }
774
+ this._iceGraceReason = reason;
775
+ this._iceGraceTimer = setTimeout(() => {
776
+ this._iceGraceTimer = null;
777
+ const r = this._iceGraceReason;
778
+ this._iceGraceReason = null;
779
+ const s = this._pc?.iceConnectionState;
780
+ if (s === 'connected' || s === 'completed')
781
+ return; // healed
782
+ if (r === 'disconnected' && s === 'disconnected') {
783
+ this._emit('error', {
784
+ source: 'webrtc',
785
+ error: new Error(`ICE stuck in 'disconnected' for > ${ms}ms`),
786
+ });
787
+ return;
788
+ }
789
+ if (r === 'failed' || s === 'failed') {
790
+ const err = new Error('ICE connection failed');
791
+ if (this._sessionReject) {
792
+ this._sessionReject(err);
793
+ this._sessionResolve = null;
794
+ this._sessionReject = null;
795
+ }
796
+ this._emit('error', { source: 'webrtc', error: err });
797
+ }
798
+ }, ms);
799
+ }
800
+ /**
801
+ * `disconnected` while the tab is hidden. JS timers are throttled
802
+ * in background tabs (Chrome clamps to ~1 Hz, Safari can pause
803
+ * altogether), so a foreground grace timer would either miss the
804
+ * window or fire long after the connection healed. Wait for the
805
+ * tab to come back, then re-evaluate.
806
+ */
807
+ _armIceGraceOnVisibility() {
808
+ if (this._pendingVisibilityHandler)
809
+ return;
810
+ const deferredAt = Date.now();
811
+ const handler = () => {
812
+ if (typeof document !== 'undefined' && document.hidden)
813
+ return;
814
+ document.removeEventListener('visibilitychange', handler);
815
+ this._pendingVisibilityHandler = null;
816
+ if (!this._pc)
817
+ return;
818
+ const s = this._pc.iceConnectionState;
819
+ if (s === 'connected' || s === 'completed')
820
+ return; // healed in bg
821
+ // Ceiling: if the user backgrounded past the daemon's
822
+ // ICE-consent freshness window the session is gone from
823
+ // the daemon's side regardless of what `_pc` reports
824
+ // locally. Running another foreground grace would tell
825
+ // the user "Reconnecting…" for a recovery that can never
826
+ // happen. Escalate immediately so the host renders the
827
+ // real "session expired" UX. See MAX_VISIBILITY_DEFER_MS.
828
+ if (Date.now() - deferredAt > MAX_VISIBILITY_DEFER_MS) {
829
+ const err = new Error('Session expired while tab was backgrounded');
830
+ if (this._sessionReject) {
831
+ this._sessionReject(err);
832
+ this._sessionResolve = null;
833
+ this._sessionReject = null;
834
+ }
835
+ this._emit('error', { source: 'webrtc', error: err });
836
+ return;
837
+ }
838
+ if (s === 'failed') {
839
+ this._scheduleIceGrace(ICE_FAILED_GRACE_MS, 'failed');
840
+ return;
841
+ }
842
+ // Still disconnected when we came back — give it a normal
843
+ // foreground grace window now that timers fire reliably.
844
+ this._scheduleIceGrace(ICE_DISCONNECT_GRACE_MS, 'disconnected');
845
+ };
846
+ document.addEventListener('visibilitychange', handler);
847
+ this._pendingVisibilityHandler = handler;
848
+ }
849
+ /**
850
+ * Install browser-level network listeners and forward them as
851
+ * public `networkOnline` / `networkOffline` / `networkChange`
852
+ * events on this instance. Idempotent: called from
853
+ * `startSession()`, removed by `_uninstallNetworkListeners` on
854
+ * teardown. Reachable only when there's a live `window`
855
+ * (defensive guard for SSR / test environments).
856
+ *
857
+ * `online` / `offline` are semantically about CONNECTIVITY:
858
+ * "does the OS think we can reach the internet". They flip
859
+ * symmetrically.
860
+ *
861
+ * `connection.change` (NetworkInformation API, Chrome / Android
862
+ * WebView only) is semantically about the TRANSPORT: it fires
863
+ * on Wi-Fi → 4G swaps, AP roams, etc. without necessarily going
864
+ * through `offline`. We forward it as its own `networkChange`
865
+ * event rather than aliasing it onto `networkOnline`, so
866
+ * consumers don't have to guess whether they're seeing a real
867
+ * connectivity recovery or a silent transport swap.
868
+ */
869
+ _installNetworkListeners() {
870
+ if (this._onlineHandler || typeof window === 'undefined')
871
+ return;
872
+ const onOnline = () => this._emit('networkOnline', {});
873
+ const onOffline = () => this._emit('networkOffline', {});
874
+ window.addEventListener('online', onOnline);
875
+ window.addEventListener('offline', onOffline);
876
+ this._onlineHandler = onOnline;
877
+ this._offlineHandler = onOffline;
878
+ const conn = navigator.connection;
879
+ if (conn && typeof conn.addEventListener === 'function') {
880
+ const onChange = () => this._emit('networkChange', {
881
+ effectiveType: conn.effectiveType,
882
+ downlink: conn.downlink,
883
+ rtt: conn.rtt,
884
+ saveData: conn.saveData,
885
+ });
886
+ conn.addEventListener('change', onChange);
887
+ this._connectionChangeHandler = onChange;
888
+ }
889
+ }
890
+ /** Counterpart to `_installNetworkListeners`. */
891
+ _uninstallNetworkListeners() {
892
+ if (typeof window !== 'undefined') {
893
+ if (this._onlineHandler) {
894
+ window.removeEventListener('online', this._onlineHandler);
895
+ }
896
+ if (this._offlineHandler) {
897
+ window.removeEventListener('offline', this._offlineHandler);
898
+ }
899
+ }
900
+ const conn = navigator.connection;
901
+ if (conn && this._connectionChangeHandler && typeof conn.removeEventListener === 'function') {
902
+ conn.removeEventListener('change', this._connectionChangeHandler);
903
+ }
904
+ this._onlineHandler = null;
905
+ this._offlineHandler = null;
906
+ this._connectionChangeHandler = null;
907
+ }
652
908
  // ─── Commands ────────────────────────────────────────────────────────
909
+ /**
910
+ * Atomic raw-units pose update over the data channel. Channels you
911
+ * omit are held at their last commanded value (per-axis, independent).
912
+ *
913
+ * **Head pose is in the WORLD frame.** The daemon's IK splits the
914
+ * requested head world-yaw between body rotation and the stewart
915
+ * platform, subject to the mechanical limit
916
+ * `|head_yaw_world − body_yaw| ≤ 65°`.
917
+ *
918
+ * **If you want the head to FOLLOW the body** (tank-style rotation):
919
+ * a `setTarget({ body_yaw })` on its own does NOT rotate the head —
920
+ * the head's commanded world yaw is unchanged, so its gaze stays
921
+ * pinned in world frame while the body turns under it. To make the
922
+ * head turn with the body, include a `head` matrix in the SAME call
923
+ * with the body-yaw delta added to the head RPY's yaw:
924
+ *
925
+ * ```ts
926
+ * // Body-yaw drag handler: tank-couple the head so it follows.
927
+ * const delta = newBodyDeg - lastCommandedBodyDeg;
928
+ * const nextHeadYaw = lastCommandedHeadYawDeg + delta;
929
+ * robot.setTarget({
930
+ * head: rpyToMatrix(headRoll, headPitch, nextHeadYaw).flat(),
931
+ * body_yaw: degToRad(newBodyDeg),
932
+ * });
933
+ * lastCommandedHeadYawDeg = nextHeadYaw;
934
+ * lastCommandedBodyDeg = newBodyDeg;
935
+ * ```
936
+ *
937
+ * **Baseline must be the last COMMANDED value, not telemetry.** For
938
+ * continuous-input controllers (slider drag, joystick), do not use
939
+ * `state.head` from the `state` event as the baseline for incremental
940
+ * commands — telemetry lags one WebRTC round-trip, so cumulative
941
+ * deltas computed against it stall (every iteration in a rapid drag
942
+ * adds the same `delta` to the same stale baseline → the head fails
943
+ * to keep up). Track the last-commanded RPY in your own buffer.
944
+ *
945
+ * @param head Flat row-major 4×4 matrix (16 finite numbers) in
946
+ * the world frame. Omit to hold the previous head target.
947
+ * @param antennas `[rightRad, leftRad]` (radians). Omit to hold.
948
+ * @param body_yaw Signed radians. Omit to hold.
949
+ * @returns `true` if the command was queued on the data channel,
950
+ * `false` if the channel is not open.
951
+ */
653
952
  setTarget({ head, antennas, body_yaw } = {}) {
654
953
  const cmd = { type: 'set_full_target' };
655
954
  if (head !== undefined) {
@@ -718,6 +1017,9 @@ export class ReachyMini extends EventTarget {
718
1017
  playSound(file) {
719
1018
  return this._sendCommand({ type: 'play_sound', file });
720
1019
  }
1020
+ clearIncomingAudio() {
1021
+ return this._sendCommand({ type: 'clear_incoming_audio' });
1022
+ }
721
1023
  setMotorMode(mode) {
722
1024
  return this._sendCommand({ type: 'set_motor_mode', mode });
723
1025
  }
@@ -1232,6 +1534,9 @@ export class ReachyMini extends EventTarget {
1232
1534
  const err = new Error(friendly || `Session ended before it could start: ${reason || 'unknown reason'}`);
1233
1535
  err.reason = reason ?? null;
1234
1536
  this._emit('sessionRejected', { reason, activeApp: null });
1537
+ // Resilience teardown alongside the PC close path.
1538
+ this._clearIceGrace();
1539
+ this._uninstallNetworkListeners();
1235
1540
  if (this._pc) {
1236
1541
  this._pc.close();
1237
1542
  this._pc = null;