@rapidmx/meet-plugin 0.14.0 → 0.15.0
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/apps/meet/_CallControls.tsx +119 -2
- package/apps/meet/_CallView.tsx +59 -122
- package/apps/meet/_DiagnosticsPanel.tsx +65 -7
- package/apps/meet/_ParticipantTile.tsx +82 -9
- package/apps/meet/_ParticipantsDrawer.tsx +10 -7
- package/apps/meet/_icons.tsx +10 -0
- package/apps/shared/relay/AudioPlayer.ts +29 -0
- package/apps/shared/relay/AudioSender.ts +23 -0
- package/apps/shared/relay/RelayClient.ts +52 -1
- package/apps/shared/relay/RelayReceiver.ts +43 -2
- package/apps/shared/relay/RelaySender.ts +23 -1
- package/apps/shared/relay/RelayTransport.ts +19 -0
- package/apps/shared/relay/SilenceMeter.ts +59 -0
- package/apps/shared/webrtc/MeshConnectionManager.ts +25 -3
- package/apps/shared/webrtc/types.ts +71 -2
- package/dist/apps/meet/_CallControls.d.ts +24 -2
- package/dist/apps/meet/_CallControls.js +34 -3
- package/dist/apps/meet/_CallView.d.ts +28 -15
- package/dist/apps/meet/_CallView.js +50 -64
- package/dist/apps/meet/_DiagnosticsPanel.d.ts +5 -3
- package/dist/apps/meet/_DiagnosticsPanel.js +17 -1
- package/dist/apps/meet/_ParticipantTile.d.ts +9 -2
- package/dist/apps/meet/_ParticipantTile.js +36 -5
- package/dist/apps/meet/_ParticipantsDrawer.js +3 -3
- package/dist/apps/meet/_icons.d.ts +4 -0
- package/dist/apps/meet/_icons.js +4 -0
- package/dist/apps/shared/relay/AudioPlayer.d.ts +9 -0
- package/dist/apps/shared/relay/AudioPlayer.js +24 -4
- package/dist/apps/shared/relay/AudioSender.d.ts +8 -0
- package/dist/apps/shared/relay/AudioSender.js +20 -4
- package/dist/apps/shared/relay/RelayClient.d.ts +16 -0
- package/dist/apps/shared/relay/RelayClient.js +44 -1
- package/dist/apps/shared/relay/RelayReceiver.d.ts +5 -0
- package/dist/apps/shared/relay/RelayReceiver.js +38 -6
- package/dist/apps/shared/relay/RelaySender.d.ts +7 -0
- package/dist/apps/shared/relay/RelaySender.js +20 -5
- package/dist/apps/shared/relay/RelayTransport.d.ts +4 -0
- package/dist/apps/shared/relay/RelayTransport.js +14 -0
- package/dist/apps/shared/relay/SilenceMeter.d.ts +25 -0
- package/dist/apps/shared/relay/SilenceMeter.js +55 -0
- package/dist/apps/shared/webrtc/MeshConnectionManager.js +20 -1
- package/dist/apps/shared/webrtc/types.d.ts +66 -2
- package/dist/lib/util/RelayHub.js +15 -2
- package/dist/lib/util/RelayHub.js.map +1 -1
- package/dist/types/util/RelayHub.d.ts +5 -2
- package/package.json +1 -1
|
@@ -14,12 +14,24 @@
|
|
|
14
14
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
15
15
|
* is disabled (`micLocked`) while talking-stick mode is on and this participant isn't the current holder - see
|
|
16
16
|
* `_CallView.tsx`'s doc comment.
|
|
17
|
+
*
|
|
18
|
+
* ## The "…" menu
|
|
19
|
+
*
|
|
20
|
+
* Sits between the grid/focus toggle and Leave, a catch-all for controls that don't need a dedicated button of
|
|
21
|
+
* their own: "Diagnostics" (`onOpenDiagnostics`) and "Settings" (`onOpenSettings`, host-only) each open their own
|
|
22
|
+
* persistent window/modal in `_CallView.tsx` and close this menu; "Connection method" is a four-way
|
|
23
|
+
* `menuitemradio` group (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment)
|
|
24
|
+
* forcing how *this tab's* media reaches everyone else, independent of what anyone else has chosen; "Hide self"/
|
|
25
|
+
* "Show self" (`selfHidden`/`onToggleSelfHidden`) is the only way to bring the local tile back once hidden from
|
|
26
|
+
* grid view, where there's no tile left to offer its own "show" control - see `_CallView.tsx`'s doc comment on
|
|
27
|
+
* where the local tile appears. Otherwise an ordinary menu of this bar's own shape (`openMenu`, closes on
|
|
28
|
+
* Escape/outside click like every other one here).
|
|
17
29
|
*/
|
|
18
30
|
import React, { useEffect, useRef, useState } from "react";
|
|
19
31
|
import { filtersActive } from "../shared/media/filters/filterTypes.js";
|
|
20
32
|
import type { ScreenTransformState } from "../shared/media/filters/ScreenTransform.js";
|
|
21
33
|
import type { LocalMedia, MediaKind } from "../shared/media/useLocalMedia.js";
|
|
22
|
-
import { REACTION_EMOJIS, type MeshParticipant } from "../shared/webrtc/types.js";
|
|
34
|
+
import { REACTION_EMOJIS, type MeshParticipant, type TransportMode } from "../shared/webrtc/types.js";
|
|
23
35
|
import EffectsPanel from "./_EffectsPanel.js";
|
|
24
36
|
import {
|
|
25
37
|
ChevronUpIcon,
|
|
@@ -32,12 +44,21 @@ import {
|
|
|
32
44
|
LeaveIcon,
|
|
33
45
|
MicIcon,
|
|
34
46
|
MicOffIcon,
|
|
47
|
+
OverflowIcon,
|
|
35
48
|
RotateIcon,
|
|
36
49
|
ScreenShareIcon,
|
|
37
50
|
VideoIcon,
|
|
38
51
|
VideoOffIcon,
|
|
39
52
|
} from "./_icons.js";
|
|
40
53
|
|
|
54
|
+
/** The "…" menu's "Connection method" options, in display order - see `TransportMode`'s own doc comment. */
|
|
55
|
+
const TRANSPORT_MODE_OPTIONS: { mode: TransportMode; label: string }[] = [
|
|
56
|
+
{ mode: "auto", label: "Auto" },
|
|
57
|
+
{ mode: "p2p", label: "P2P" },
|
|
58
|
+
{ mode: "relay", label: "Relay" },
|
|
59
|
+
{ mode: "websocket", label: "WebSocket Relay" },
|
|
60
|
+
];
|
|
61
|
+
|
|
41
62
|
export type CallViewMode = "grid" | "focus";
|
|
42
63
|
|
|
43
64
|
export interface CallControlsProps {
|
|
@@ -62,10 +83,20 @@ export interface CallControlsProps {
|
|
|
62
83
|
* doc comment on talking-stick mode. Disables the microphone toggle (not the device-picker chevron, which
|
|
63
84
|
* doesn't change whether anyone can hear them) rather than merely nudging it, unlike a `mute-request`. */
|
|
64
85
|
micLocked?: boolean;
|
|
86
|
+
/** Whether the "…" menu's "Settings" item is offered at all - see this module's doc comment on the menu. */
|
|
87
|
+
isHost: boolean;
|
|
88
|
+
transportMode: TransportMode;
|
|
89
|
+
onSetTransportMode: (mode: TransportMode) => void;
|
|
90
|
+
onOpenDiagnostics: () => void;
|
|
91
|
+
onOpenSettings: () => void;
|
|
92
|
+
/** Whether the local tile is currently hidden - the "…" menu's "Hide self"/"Show self" item reflects and
|
|
93
|
+
* toggles this; see `_CallView.tsx`'s doc comment on where the local tile appears. */
|
|
94
|
+
selfHidden: boolean;
|
|
95
|
+
onToggleSelfHidden: () => void;
|
|
65
96
|
onLeave: () => void;
|
|
66
97
|
}
|
|
67
98
|
|
|
68
|
-
type OpenMenu = MediaKind | "emoji" | "effects" | null;
|
|
99
|
+
type OpenMenu = MediaKind | "emoji" | "effects" | "overflow" | null;
|
|
69
100
|
|
|
70
101
|
const BUTTON = "flex items-center justify-center h-12 rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80";
|
|
71
102
|
const NEUTRAL = "bg-[#3c4043] text-white hover:bg-[#4b4f53]";
|
|
@@ -102,6 +133,13 @@ export default function CallControls({
|
|
|
102
133
|
viewMode,
|
|
103
134
|
onToggleViewMode,
|
|
104
135
|
micLocked,
|
|
136
|
+
isHost,
|
|
137
|
+
transportMode,
|
|
138
|
+
onSetTransportMode,
|
|
139
|
+
onOpenDiagnostics,
|
|
140
|
+
onOpenSettings,
|
|
141
|
+
selfHidden,
|
|
142
|
+
onToggleSelfHidden,
|
|
105
143
|
onLeave,
|
|
106
144
|
}: CallControlsProps) {
|
|
107
145
|
const [openMenu, setOpenMenu] = useState<OpenMenu>(null);
|
|
@@ -307,6 +345,85 @@ export default function CallControls({
|
|
|
307
345
|
{viewMode === "grid" ? <FocusIcon /> : <GridIcon />}
|
|
308
346
|
</button>
|
|
309
347
|
|
|
348
|
+
<div className="sm:relative">
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
className={`${BUTTON} w-12 ${openMenu === "overflow" ? ACTIVE : NEUTRAL}`}
|
|
352
|
+
aria-label="More options"
|
|
353
|
+
aria-haspopup="menu"
|
|
354
|
+
aria-expanded={openMenu === "overflow"}
|
|
355
|
+
onClick={() => toggleMenu("overflow")}
|
|
356
|
+
>
|
|
357
|
+
<OverflowIcon />
|
|
358
|
+
</button>
|
|
359
|
+
{openMenu === "overflow" && (
|
|
360
|
+
<div
|
|
361
|
+
role="menu"
|
|
362
|
+
aria-label="More options"
|
|
363
|
+
className="absolute bottom-full mb-3 right-0 w-56 max-w-[calc(100vw-1rem)] rounded-2xl bg-[#2b2d30] text-white shadow-xl py-1 text-sm"
|
|
364
|
+
>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
role="menuitem"
|
|
368
|
+
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
369
|
+
onClick={() => {
|
|
370
|
+
onOpenDiagnostics();
|
|
371
|
+
setOpenMenu(null);
|
|
372
|
+
}}
|
|
373
|
+
>
|
|
374
|
+
Diagnostics
|
|
375
|
+
</button>
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
role="menuitem"
|
|
379
|
+
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
380
|
+
onClick={() => {
|
|
381
|
+
onToggleSelfHidden();
|
|
382
|
+
setOpenMenu(null);
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
{selfHidden ? "Show self" : "Hide self"}
|
|
386
|
+
</button>
|
|
387
|
+
<div className="my-1 border-t border-white/10" />
|
|
388
|
+
<p className="px-3 pt-1 pb-0.5 text-xs font-semibold uppercase tracking-wide text-white/60">Connection method</p>
|
|
389
|
+
{TRANSPORT_MODE_OPTIONS.map(({ mode, label }) => (
|
|
390
|
+
<button
|
|
391
|
+
key={mode}
|
|
392
|
+
type="button"
|
|
393
|
+
role="menuitemradio"
|
|
394
|
+
aria-checked={transportMode === mode}
|
|
395
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
396
|
+
onClick={() => {
|
|
397
|
+
onSetTransportMode(mode);
|
|
398
|
+
setOpenMenu(null);
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
<span className="w-4 text-[#8ab4f8]" aria-hidden="true">
|
|
402
|
+
{transportMode === mode ? "✓" : ""}
|
|
403
|
+
</span>
|
|
404
|
+
{label}
|
|
405
|
+
</button>
|
|
406
|
+
))}
|
|
407
|
+
{isHost && (
|
|
408
|
+
<>
|
|
409
|
+
<div className="my-1 border-t border-white/10" />
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
role="menuitem"
|
|
413
|
+
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
414
|
+
onClick={() => {
|
|
415
|
+
onOpenSettings();
|
|
416
|
+
setOpenMenu(null);
|
|
417
|
+
}}
|
|
418
|
+
>
|
|
419
|
+
Settings
|
|
420
|
+
</button>
|
|
421
|
+
</>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
426
|
+
|
|
310
427
|
<button type="button" className={`${BUTTON} w-16 bg-[#d93025] text-white hover:bg-[#b3261e]`} aria-label="Leave call" onClick={() => onLeave()}>
|
|
311
428
|
<LeaveIcon />
|
|
312
429
|
</button>
|
package/apps/meet/_CallView.tsx
CHANGED
|
@@ -15,10 +15,24 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The call fills the viewport (`fixed inset-0`) as three rows - a slim header, the tiles (which take whatever room
|
|
17
17
|
* is left and never scroll the page), and the control bar pinned to the bottom - instead of being laid out inside
|
|
18
|
-
* the branded page shell, whose header, footer and padding pushed it off the screen.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* the branded page shell, whose header, footer and padding pushed it off the screen.
|
|
19
|
+
*
|
|
20
|
+
* ## Where the local tile appears
|
|
21
|
+
*
|
|
22
|
+
* In plain grid view (not focus, not presenting), the local tile is an ordinary member of the grid, same size as
|
|
23
|
+
* everyone else's, rather than a floating corner overlay - so with exactly two participants, the local tile is
|
|
24
|
+
* genuinely one of the two grid cells, not a small picture-in-picture over the other person's. The floating corner
|
|
25
|
+
* tile (above the control bar on a narrower window, at the top on a phone, where the bar wraps onto a second row)
|
|
26
|
+
* is reserved for focus/presentation layouts, where there's already a single large tile the local one would
|
|
27
|
+
* otherwise compete with for space. Either way, alone in the call, the local tile simply fills the whole stage
|
|
28
|
+
* (see the `alone` branch of the `stage` computation).
|
|
29
|
+
*
|
|
30
|
+
* `selfHidden` (`handleHideSelf()`/`handleToggleSelfHidden()`) removes the local tile from view entirely, wherever
|
|
31
|
+
* it would otherwise appear - set from the tile's own corner "…" menu (`_ParticipantTile.tsx`'s `onHideSelf`, one
|
|
32
|
+
* directional: there's no tile left to click "show" on once it's gone) or toggled back on from the navbar's own
|
|
33
|
+
* "…" menu (`_CallControls.tsx`), which is the only way to re-show it from grid view, where there'd otherwise be
|
|
34
|
+
* nothing left bearing a "show self" control. Never honored while `alone`: hiding the only tile on screen would
|
|
35
|
+
* just blank it, so the local tile ignores `selfHidden` there and always shows.
|
|
22
36
|
*
|
|
23
37
|
* ## Who is heard
|
|
24
38
|
*
|
|
@@ -51,18 +65,17 @@
|
|
|
51
65
|
* `MeshConnectionManager`'s own doc comment on why that needs no extra code - the header chip then says nobody has
|
|
52
66
|
* the floor, until the host picks someone (or themselves) again.
|
|
53
67
|
*
|
|
54
|
-
* ##
|
|
68
|
+
* ## Diagnostics, connection method and settings
|
|
55
69
|
*
|
|
56
|
-
*
|
|
57
|
-
* `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
63
|
-
* waiting-room toggle) - moved out because none of those are
|
|
64
|
-
*
|
|
65
|
-
* close along with it.
|
|
70
|
+
* All three are opened from `_CallControls.tsx`'s own "…" menu (its doc comment covers the menu itself) via
|
|
71
|
+
* callback props, but owned here: `diagnosticsOpen` renders `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
72
|
+
* window - not a popover like the menus in the control bar, since troubleshooting a connection is something a
|
|
73
|
+
* participant wants to keep open while they poke at the rest of the call, not something that closes the moment
|
|
74
|
+
* they click elsewhere); `transportMode`/`handleSetTransportMode()` forces how *this tab's* media reaches everyone
|
|
75
|
+
* else (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment), independent of what
|
|
76
|
+
* anyone else has chosen; `settingsOpen` renders `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
77
|
+
* own header (mute-on-join, the join password, the waiting-room toggle) - moved out because none of those are
|
|
78
|
+
* about *who* is in the call, which is what the drawer is for.
|
|
66
79
|
*/
|
|
67
80
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
68
81
|
import { playRaisedHandChime } from "../shared/media/chime.js";
|
|
@@ -77,7 +90,7 @@ import type { MeshParticipant, RelayTransportLike, TransportMode } from "../shar
|
|
|
77
90
|
import { createRelayTransport } from "../shared/relay/RelayTransport.js";
|
|
78
91
|
import { GuestSignalingClient } from "../shared/push/GuestSignalingClient.js";
|
|
79
92
|
import CallControls, { type CallViewMode } from "./_CallControls.js";
|
|
80
|
-
import { BatonIcon
|
|
93
|
+
import { BatonIcon } from "./_icons.js";
|
|
81
94
|
import {
|
|
82
95
|
admitParticipant,
|
|
83
96
|
denyParticipant,
|
|
@@ -139,14 +152,6 @@ const MAX_REACTIONS = 12;
|
|
|
139
152
|
/** How often the host's drawer refreshes its own waiting-room list while open. */
|
|
140
153
|
const DEFAULT_WAITING_POLL_MS = 3_000;
|
|
141
154
|
|
|
142
|
-
/** The "…" menu's "Connection method" options, in display order - see `TransportMode`'s own doc comment. */
|
|
143
|
-
const TRANSPORT_MODE_OPTIONS: { mode: TransportMode; label: string }[] = [
|
|
144
|
-
{ mode: "auto", label: "Auto" },
|
|
145
|
-
{ mode: "p2p", label: "P2P" },
|
|
146
|
-
{ mode: "relay", label: "Relay" },
|
|
147
|
-
{ mode: "websocket", label: "WebSocket Relay" },
|
|
148
|
-
];
|
|
149
|
-
|
|
150
155
|
interface Reaction {
|
|
151
156
|
id: number;
|
|
152
157
|
emoji: string;
|
|
@@ -229,13 +234,12 @@ export default function CallView({
|
|
|
229
234
|
const [waitingParticipants, setWaitingParticipants] = useState<WaitingParticipant[]>([]);
|
|
230
235
|
const [talkingStickActive, setTalkingStickActive] = useState(false);
|
|
231
236
|
const [talkingStickHolder, setTalkingStickHolder] = useState<string | undefined>(undefined);
|
|
232
|
-
const [overflowMenuOpen, setOverflowMenuOpen] = useState(false);
|
|
233
237
|
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
|
|
234
238
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
235
239
|
const [transportMode, setTransportModeState] = useState<TransportMode>("auto");
|
|
240
|
+
const [selfHidden, setSelfHidden] = useState(false);
|
|
236
241
|
|
|
237
242
|
const managerRef = useRef<MeshConnectionManager | null>(null);
|
|
238
|
-
const overflowMenuRef = useRef<HTMLDivElement>(null);
|
|
239
243
|
/** The raw capture from `getDisplayMedia()` - only ever used to stop it (releasing the OS's own share
|
|
240
244
|
* indicator) and to notice the participant ending the share from the browser's own UI (`track.onended`). What's
|
|
241
245
|
* actually shown and sent is `screenTransformRef`'s output (`screenStream` state) - see `handleToggleShare()`. */
|
|
@@ -409,31 +413,6 @@ export default function CallView({
|
|
|
409
413
|
}
|
|
410
414
|
}, [talkingStickActive, selfHasTalkingStick]);
|
|
411
415
|
|
|
412
|
-
// The "…" menu closes on Escape or a press anywhere outside it - matching `_CallControls.tsx`'s own menus -
|
|
413
|
-
// but never the windows it opens (diagnostics, settings), which stay open independently (see this module's
|
|
414
|
-
// doc comment on the "…" menu).
|
|
415
|
-
useEffect(() => {
|
|
416
|
-
if (!overflowMenuOpen) {
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const onPointerDown = (event: PointerEvent) => {
|
|
420
|
-
if (!overflowMenuRef.current?.contains(event.target as Node)) {
|
|
421
|
-
setOverflowMenuOpen(false);
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
425
|
-
if (event.key === "Escape") {
|
|
426
|
-
setOverflowMenuOpen(false);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
document.addEventListener("pointerdown", onPointerDown);
|
|
430
|
-
document.addEventListener("keydown", onKeyDown);
|
|
431
|
-
return () => {
|
|
432
|
-
document.removeEventListener("pointerdown", onPointerDown);
|
|
433
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
434
|
-
};
|
|
435
|
-
}, [overflowMenuOpen]);
|
|
436
|
-
|
|
437
416
|
useEffect(() => {
|
|
438
417
|
if (!presenterUid) {
|
|
439
418
|
setActiveSpeakerUid((prev) => pickActiveSpeaker(levels, prev));
|
|
@@ -505,6 +484,17 @@ export default function CallView({
|
|
|
505
484
|
setHandRaised((prev) => !prev);
|
|
506
485
|
}
|
|
507
486
|
|
|
487
|
+
/** Hides the local tile - the tile's own "…" menu calls this directly (one-directional: there is no "show
|
|
488
|
+
* self" from in-grid, since a hidden tile isn't there to click on). Re-showing it is the navbar "…" menu's
|
|
489
|
+
* `handleToggleSelfHidden()` instead. */
|
|
490
|
+
function handleHideSelf() {
|
|
491
|
+
setSelfHidden(true);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function handleToggleSelfHidden() {
|
|
495
|
+
setSelfHidden((prev) => !prev);
|
|
496
|
+
}
|
|
497
|
+
|
|
508
498
|
function handleReaction(emoji: string) {
|
|
509
499
|
if (managerRef.current?.sendReaction(emoji)) {
|
|
510
500
|
showReaction(emoji, "You");
|
|
@@ -573,12 +563,11 @@ export default function CallView({
|
|
|
573
563
|
}
|
|
574
564
|
|
|
575
565
|
/** Forces this tab's own transport policy - see `MeshConnectionManager.setTransportMode()`'s doc comment for
|
|
576
|
-
* what each mode actually does, and
|
|
577
|
-
* per-tab choice rather than a meeting setting. */
|
|
566
|
+
* what each mode actually does, and `_CallControls.tsx`'s doc comment on its "…" menu for why this is a
|
|
567
|
+
* personal, per-tab choice rather than a meeting setting. */
|
|
578
568
|
function handleSetTransportMode(mode: TransportMode) {
|
|
579
569
|
setTransportModeState(mode);
|
|
580
570
|
managerRef.current?.setTransportMode(mode);
|
|
581
|
-
setOverflowMenuOpen(false);
|
|
582
571
|
}
|
|
583
572
|
|
|
584
573
|
/** Admits one pending request - optimistically removed from the drawer's own list (the next poll would drop it
|
|
@@ -673,7 +662,7 @@ export default function CallView({
|
|
|
673
662
|
/>
|
|
674
663
|
);
|
|
675
664
|
|
|
676
|
-
const selfTile = (className?: string) => (
|
|
665
|
+
const selfTile = (className?: string, canHide?: boolean) => (
|
|
677
666
|
<ParticipantTile
|
|
678
667
|
name={selfName}
|
|
679
668
|
stream={media.videoStream}
|
|
@@ -685,6 +674,7 @@ export default function CallView({
|
|
|
685
674
|
// A custom background is a fixed picture, not a live reflection - mirroring it would show it backwards
|
|
686
675
|
// to no one but the participant themselves (see `_ParticipantTile.tsx`'s doc comment).
|
|
687
676
|
mirrored={media.filters.background !== "image"}
|
|
677
|
+
onHideSelf={canHide ? handleHideSelf : undefined}
|
|
688
678
|
className={className}
|
|
689
679
|
/>
|
|
690
680
|
);
|
|
@@ -694,7 +684,8 @@ export default function CallView({
|
|
|
694
684
|
|
|
695
685
|
let stage: React.ReactNode;
|
|
696
686
|
if (alone) {
|
|
697
|
-
// Nobody else yet: the local participant fills the tile area, presenting or not
|
|
687
|
+
// Nobody else yet: the local participant fills the tile area, presenting or not - and is never hideable,
|
|
688
|
+
// since there'd be nothing left to look at (see this module's doc comment on hiding the local tile).
|
|
698
689
|
stage = isPresenting ? (
|
|
699
690
|
<ParticipantTile name={selfName} stream={screenStream} isLocal contain isFocused className="h-full" />
|
|
700
691
|
) : (
|
|
@@ -722,8 +713,11 @@ export default function CallView({
|
|
|
722
713
|
</div>
|
|
723
714
|
);
|
|
724
715
|
} else {
|
|
716
|
+
// Plain grid, nobody presenting: the local tile joins the grid as an ordinary tile (unless hidden) instead
|
|
717
|
+
// of floating in a corner - see this module's doc comment.
|
|
725
718
|
stage = (
|
|
726
719
|
<div className="h-full grid gap-2 auto-rows-fr [grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr))]">
|
|
720
|
+
{!selfHidden && selfTile("h-full", true)}
|
|
727
721
|
{participants.map((p) => remoteTile(p, "h-full"))}
|
|
728
722
|
</div>
|
|
729
723
|
);
|
|
@@ -768,70 +762,6 @@ export default function CallView({
|
|
|
768
762
|
>
|
|
769
763
|
{participants.length + 1}
|
|
770
764
|
</button>
|
|
771
|
-
<div className="relative" ref={overflowMenuRef}>
|
|
772
|
-
<button
|
|
773
|
-
type="button"
|
|
774
|
-
className={`w-9 h-9 flex items-center justify-center rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${overflowMenuOpen ? "bg-[#4b4f53]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`}
|
|
775
|
-
aria-label="More options"
|
|
776
|
-
aria-haspopup="menu"
|
|
777
|
-
aria-expanded={overflowMenuOpen}
|
|
778
|
-
onClick={() => setOverflowMenuOpen((prev) => !prev)}
|
|
779
|
-
>
|
|
780
|
-
<OverflowIcon />
|
|
781
|
-
</button>
|
|
782
|
-
{overflowMenuOpen && (
|
|
783
|
-
<div
|
|
784
|
-
role="menu"
|
|
785
|
-
aria-label="More options"
|
|
786
|
-
className="absolute top-full right-0 mt-2 w-56 rounded-2xl bg-[#2b2d30] text-white shadow-xl py-1 text-sm"
|
|
787
|
-
>
|
|
788
|
-
<button
|
|
789
|
-
type="button"
|
|
790
|
-
role="menuitem"
|
|
791
|
-
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
792
|
-
onClick={() => {
|
|
793
|
-
setDiagnosticsOpen(true);
|
|
794
|
-
setOverflowMenuOpen(false);
|
|
795
|
-
}}
|
|
796
|
-
>
|
|
797
|
-
Diagnostics
|
|
798
|
-
</button>
|
|
799
|
-
<div className="my-1 border-t border-white/10" />
|
|
800
|
-
<p className="px-3 pt-1 pb-0.5 text-xs font-semibold uppercase tracking-wide text-white/60">Connection method</p>
|
|
801
|
-
{TRANSPORT_MODE_OPTIONS.map(({ mode, label }) => (
|
|
802
|
-
<button
|
|
803
|
-
key={mode}
|
|
804
|
-
type="button"
|
|
805
|
-
role="menuitemradio"
|
|
806
|
-
aria-checked={transportMode === mode}
|
|
807
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
808
|
-
onClick={() => handleSetTransportMode(mode)}
|
|
809
|
-
>
|
|
810
|
-
<span className="w-4 text-[#8ab4f8]" aria-hidden="true">
|
|
811
|
-
{transportMode === mode ? "✓" : ""}
|
|
812
|
-
</span>
|
|
813
|
-
{label}
|
|
814
|
-
</button>
|
|
815
|
-
))}
|
|
816
|
-
{isHost && (
|
|
817
|
-
<>
|
|
818
|
-
<div className="my-1 border-t border-white/10" />
|
|
819
|
-
<button
|
|
820
|
-
type="button"
|
|
821
|
-
role="menuitem"
|
|
822
|
-
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
823
|
-
onClick={() => {
|
|
824
|
-
setSettingsOpen(true);
|
|
825
|
-
setOverflowMenuOpen(false);
|
|
826
|
-
}}
|
|
827
|
-
>
|
|
828
|
-
Settings
|
|
829
|
-
</button>
|
|
830
|
-
</>
|
|
831
|
-
)}
|
|
832
|
-
</div>
|
|
833
|
-
)}
|
|
834
|
-
</div>
|
|
835
765
|
</div>
|
|
836
766
|
</header>
|
|
837
767
|
{connectError && (
|
|
@@ -870,16 +800,23 @@ export default function CallView({
|
|
|
870
800
|
viewMode={viewMode}
|
|
871
801
|
onToggleViewMode={() => setViewMode((prev) => (prev === "grid" ? "focus" : "grid"))}
|
|
872
802
|
micLocked={micLocked}
|
|
803
|
+
selfHidden={selfHidden}
|
|
804
|
+
onToggleSelfHidden={handleToggleSelfHidden}
|
|
805
|
+
isHost={isHost}
|
|
806
|
+
transportMode={transportMode}
|
|
807
|
+
onSetTransportMode={handleSetTransportMode}
|
|
808
|
+
onOpenDiagnostics={() => setDiagnosticsOpen(true)}
|
|
809
|
+
onOpenSettings={() => setSettingsOpen(true)}
|
|
873
810
|
onLeave={onLeave}
|
|
874
811
|
/>
|
|
875
812
|
</footer>
|
|
876
813
|
|
|
877
|
-
{!alone && (
|
|
814
|
+
{!alone && !selfHidden && showFocusLayout && (
|
|
878
815
|
<div
|
|
879
816
|
className="absolute z-10 right-3 top-14 w-28 sm:top-auto sm:bottom-24 sm:w-52 aspect-video shadow-xl xl:right-4 xl:bottom-4"
|
|
880
817
|
data-testid="self-view"
|
|
881
818
|
>
|
|
882
|
-
{selfTile("h-full")}
|
|
819
|
+
{selfTile("h-full", true)}
|
|
883
820
|
</div>
|
|
884
821
|
)}
|
|
885
822
|
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* (`MeshParticipant.diagnostics`, refreshed every few seconds by `MeshConnectionManager`'s own poller - see its doc
|
|
11
11
|
* comment). Purely informational and entirely local: nothing here is sent anywhere.
|
|
12
12
|
*
|
|
13
|
-
* `diagnostics` is `undefined` until the first poll completes (shortly after a connection comes up
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* `diagnostics` is `undefined` until the first poll completes (shortly after a connection comes up, or the pair moves
|
|
14
|
+
* to the server relay), shown as "Not available yet" rather than a blank space, so it never looks like the panel
|
|
15
|
+
* forgot to load. For a `"websocket"`-relayed participant the round trip is this browser's own leg to the relay
|
|
16
|
+
* server (the relay has no direct path to the peer), and the relay's own counters follow (`RelayRows`): how much of this browser's microphone audio the capture path
|
|
17
|
+
* delivered and how much of it was digital silence, and how this peer's audio is playing back here.
|
|
16
18
|
*/
|
|
17
19
|
import React from "react";
|
|
18
|
-
import type { MeshParticipant } from "../shared/webrtc/types.js";
|
|
20
|
+
import type { MeshParticipant, RelayDiagnostics } from "../shared/webrtc/types.js";
|
|
19
21
|
import { TRANSPORT_BADGES } from "./_ParticipantTile.js";
|
|
20
22
|
|
|
21
23
|
export interface DiagnosticsPanelProps {
|
|
@@ -64,6 +66,59 @@ function StreamRow({ kind, packetsLost, jitter, bytesSent, bytesReceived }: { ki
|
|
|
64
66
|
);
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/** `part` as a whole-number percentage of `whole`, or "—" when there is no whole yet. */
|
|
70
|
+
function percent(part: number, whole: number): string {
|
|
71
|
+
return whole > 0 ? `${Math.round((part / whole) * 100)}%` : "—";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function plural(count: number, noun: string): string {
|
|
75
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The server relay's own counters (see `RelayDiagnostics`). The capture row is this browser's own microphone, the
|
|
79
|
+
* playback row this peer's audio as heard here - so a mostly silent capture points at the sender, silence arriving in
|
|
80
|
+
* playback with no capture problem points at the other end's capture, and gaps point at the network. */
|
|
81
|
+
function RelayRows({ relay }: { relay: RelayDiagnostics }) {
|
|
82
|
+
const { send, receive } = relay;
|
|
83
|
+
const notSent = send.audio.framesDropped + send.video.framesDropped;
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
{send.audioCaptureWallMs > 0 && (
|
|
87
|
+
<div className={ROW}>
|
|
88
|
+
<span className={LABEL}>Your mic capture</span>
|
|
89
|
+
<span>
|
|
90
|
+
{percent(send.audioCapturedMs, send.audioCaptureWallMs)} of real time · {percent(send.audioSilentMs, send.audioCapturedMs)} silent
|
|
91
|
+
{send.audioEncoderSkippedMs > 0 && ` · ${formatMs(send.audioEncoderSkippedMs / 1000)} skipped`}
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
{notSent > 0 && (
|
|
96
|
+
<div className={ROW}>
|
|
97
|
+
<span className={LABEL}>Not sent</span>
|
|
98
|
+
<span>
|
|
99
|
+
{send.audio.framesDropped} audio · {send.video.framesDropped} video frames
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
{receive && (
|
|
104
|
+
<div className={ROW}>
|
|
105
|
+
<span className={LABEL}>Playback</span>
|
|
106
|
+
<span>
|
|
107
|
+
{`${percent(receive.audio.silentMs, receive.audio.playedMs)} silent · ${plural(receive.audio.gaps, "gap")} ` +
|
|
108
|
+
`(${formatMs(receive.audio.gapMs / 1000)}) · ${formatMs(receive.audio.bufferedMs / 1000)} buffered`}
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
{receive && receive.audio.droppedLate > 0 && (
|
|
113
|
+
<div className={ROW}>
|
|
114
|
+
<span className={LABEL}>Dropped late</span>
|
|
115
|
+
<span>{plural(receive.audio.droppedLate, "packet")}</span>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
67
122
|
export default function DiagnosticsPanel({ selfName, micOn, cameraOn, participants }: DiagnosticsPanelProps) {
|
|
68
123
|
return (
|
|
69
124
|
<div className="text-sm">
|
|
@@ -93,16 +148,19 @@ export default function DiagnosticsPanel({ selfName, micOn, cameraOn, participan
|
|
|
93
148
|
<span className={LABEL}>Connection</span>
|
|
94
149
|
<span title={badge?.title}>{badge?.label ?? (p.transport === "connecting" ? "Connecting…" : "Direct")}</span>
|
|
95
150
|
</div>
|
|
96
|
-
{
|
|
97
|
-
<p className={`${LABEL} py-0.5`}>Not available
|
|
151
|
+
{!p.diagnostics ? (
|
|
152
|
+
<p className={`${LABEL} py-0.5`}>Not available yet.</p>
|
|
98
153
|
) : (
|
|
99
154
|
<>
|
|
100
155
|
<div className={ROW}>
|
|
101
|
-
<span className={LABEL}
|
|
156
|
+
<span className={LABEL} title={p.diagnostics.relay ? "From this browser to the relay server and back" : undefined}>
|
|
157
|
+
{p.diagnostics.relay ? "Round-trip to server" : "Round-trip time"}
|
|
158
|
+
</span>
|
|
102
159
|
<span>{formatMs(p.diagnostics.roundTripTimeSeconds)}</span>
|
|
103
160
|
</div>
|
|
104
161
|
<StreamRow kind="Audio" {...p.diagnostics.audio} />
|
|
105
162
|
<StreamRow kind="Video" {...p.diagnostics.video} />
|
|
163
|
+
{p.diagnostics.relay && <RelayRows relay={p.diagnostics.relay} />}
|
|
106
164
|
</>
|
|
107
165
|
)}
|
|
108
166
|
</div>
|