startResize(e, "top-right")}
- onPointerMove={resizePopout}
- onPointerUp={stopResize}
- onPointerCancel={stopResize}
- />
-
startResize(e, "bottom-right")}
- onPointerMove={resizePopout}
- onPointerUp={stopResize}
- onPointerCancel={stopResize}
- />
-
startResize(e, "bottom-left")}
- onPointerMove={resizePopout}
- onPointerUp={stopResize}
- onPointerCancel={stopResize}
- />
-
- );
-}
-
-export default function Wrapper() {
- const room = getRoom();
- const { pathname } = useLocation();
- const { openMobile } = useSidebar();
- const isMobile = useIsMobile();
- const state = useCall((state) => state.state);
- const callId = useCall((state) => state.callId);
- const watchedStreamParticipantIds = useCall(
- (state) => state.watchedStreamParticipantIds,
- );
- const lastFocusedParticipantId = useCall(
- (state) => state.lastFocusedParticipantId,
- );
- const participant = room.getParticipantByIdentity(
- String(lastFocusedParticipantId),
- );
-
- if (isMobile) {
- return (
-
- );
- }
-
- if (!participant || !lastFocusedParticipantId) {
- return null;
- }
-
- const active =
- !pathname.startsWith("/call") &&
- state === "open" &&
- watchedStreamParticipantIds.includes(Number(participant.identity ?? 0));
-
- return active &&
;
-}
diff --git a/packages/call/src/components/screenshareDialog.tsx b/packages/call/src/components/screenshareDialog.tsx
new file mode 100644
index 0000000..180c63e
--- /dev/null
+++ b/packages/call/src/components/screenshareDialog.tsx
@@ -0,0 +1,335 @@
+import { useEffect, useState } from "react";
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ Label,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ Switch,
+} from "@tensamin/ui";
+import {
+ type DesktopScreenShareAudioOutput,
+ type DesktopScreenShareCapabilities,
+ type DesktopScreenShareSource,
+ useDesktopMedia,
+} from "@tensamin/tauri/context";
+import { toast } from "@tensamin/shared/log";
+import { AppWindow, Loader2, MonitorUp } from "lucide-react";
+import type { ScreenShareCaptureOptions } from "livekit-client";
+import { setScreenShareEnabled, startLinuxDesktopScreenShare } from "../store";
+
+const NONE_AUDIO_OUTPUT = "__none__";
+
+function buildScreenShareOptions(
+ source: DesktopScreenShareSource,
+ capabilities: DesktopScreenShareCapabilities,
+ selectedAudioOutputId: string,
+ shareAudio: boolean,
+): ScreenShareCaptureOptions {
+ const wantsAudio = capabilities.showAudioOutputSelector
+ ? selectedAudioOutputId !== NONE_AUDIO_OUTPUT
+ : capabilities.hasReliableSystemAudio && shareAudio;
+
+ return {
+ audio: wantsAudio
+ ? {
+ autoGainControl: false,
+ echoCancellation: false,
+ noiseSuppression: false,
+ }
+ : false,
+ video: {
+ displaySurface: source.kind === "window" ? "window" : "monitor",
+ },
+ systemAudio: wantsAudio ? "include" : "exclude",
+ surfaceSwitching: "exclude",
+ selfBrowserSurface: "exclude",
+ contentHint: "detail",
+ };
+}
+
+export default function ScreenShareDialog({
+ open,
+ onOpenChange,
+ isScreensharing,
+ portalContainer,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ isScreensharing: boolean;
+ portalContainer?: HTMLElement;
+}) {
+ const {
+ getScreenShareCapabilities,
+ listScreenShareAudioOutputs,
+ listScreenShareSources,
+ } = useDesktopMedia();
+
+ const [loading, setLoading] = useState(false);
+ const [sources, setSources] = useState
([]);
+ const [audioOutputs, setAudioOutputs] = useState<
+ DesktopScreenShareAudioOutput[]
+ >([]);
+ const [capabilities, setCapabilities] =
+ useState(null);
+ const [selectedSourceId, setSelectedSourceId] = useState(null);
+ const [selectedAudioOutputId, setSelectedAudioOutputId] =
+ useState(NONE_AUDIO_OUTPUT);
+ const [shareAudio, setShareAudio] = useState(false);
+
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+
+ let active = true;
+
+ // eslint-disable-next-line
+ setLoading(true);
+ setSelectedSourceId(null);
+ setSelectedAudioOutputId(NONE_AUDIO_OUTPUT);
+ setShareAudio(false);
+
+ Promise.all([listScreenShareSources(), getScreenShareCapabilities()])
+ .then(async ([nextSources, nextCapabilities]) => {
+ if (!active) {
+ return;
+ }
+
+ setSources(nextSources);
+ setCapabilities(nextCapabilities);
+
+ if (nextCapabilities.showAudioOutputSelector) {
+ const nextOutputs = await listScreenShareAudioOutputs();
+
+ if (!active) {
+ return;
+ }
+
+ setAudioOutputs(nextOutputs);
+ } else {
+ setAudioOutputs([]);
+ }
+ })
+ .catch((error) => {
+ console.error("Failed to load desktop share sources", error);
+ toast("error", "Failed to load screen share sources.");
+ })
+ .finally(() => {
+ if (active) {
+ setLoading(false);
+ }
+ });
+
+ return () => {
+ active = false;
+ };
+ }, [
+ getScreenShareCapabilities,
+ listScreenShareAudioOutputs,
+ listScreenShareSources,
+ open,
+ ]);
+
+ const selectedSource =
+ sources.find((source) => source.id === selectedSourceId) ?? null;
+
+ async function startSharing() {
+ if (!selectedSource || !capabilities) {
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ if (capabilities.platform === "linux") {
+ await startLinuxDesktopScreenShare(selectedSource.id);
+ } else {
+ await setScreenShareEnabled(
+ true,
+ buildScreenShareOptions(
+ selectedSource,
+ capabilities,
+ selectedAudioOutputId,
+ shareAudio,
+ ),
+ );
+ }
+ onOpenChange(false);
+ } catch (error) {
+ console.error("Failed to start screen share", error);
+ toast(
+ "error",
+ error instanceof Error
+ ? error.message
+ : "Failed to start screen sharing.",
+ );
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ async function stopSharing() {
+ setLoading(true);
+
+ try {
+ await setScreenShareEnabled(false);
+ onOpenChange(false);
+ } catch (error) {
+ console.error("Failed to stop screen share", error);
+ toast("error", "Failed to stop screen sharing.");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/call/src/components/sidebarBox.tsx b/packages/call/src/components/sidebarBox.tsx
index 1f1840d..294d051 100644
--- a/packages/call/src/components/sidebarBox.tsx
+++ b/packages/call/src/components/sidebarBox.tsx
@@ -1,5 +1,5 @@
import { Lock, LockOpen } from "lucide-react";
-import { openCallPage, useCall, getRoom } from "../store";
+import { openCallPage, useCall } from "../store";
import {
Button,
Card,
@@ -9,8 +9,8 @@ import {
TooltipContent,
TooltipTrigger,
useIsMobile,
-} from "@methanium/ui";
-import MediaShareButton from "./buttons/mediaShare";
+} from "@tensamin/ui";
+import ScreenshareButton from "./buttons/screenshare";
import MuteButton from "./buttons/mute";
import DeafButton from "./buttons/deaf";
import { Room, Track } from "livekit-client";
@@ -18,39 +18,34 @@ import { useEffect, useState } from "react";
import { AreaChart, Area } from "recharts";
import LeaveButton from "./buttons/leave";
-async function getPing(room: Room): Promise {
- const report = await room.localParticipant
- .getTrackPublication(Track.Source.Microphone)
- ?.track?.getRTCStatsReport();
+export default function SidebarBox() {
+ const state = useCall((store) => store.state);
+ const screenRef = useCall((store) => store.screenRef);
+ const isMobile = useIsMobile();
+ const [portalContainer, setPortalContainer] = useState();
- if (!report) return;
+ useEffect(() => {
+ setPortalContainer(screenRef?.current ?? undefined);
+ }, [screenRef]);
- let bestRtt: number | undefined;
-
- report.forEach((stat) => {
- if (
- stat.type === "candidate-pair" &&
- stat.state === "succeeded" &&
- stat.currentRoundTripTime != null
- ) {
- bestRtt = stat.currentRoundTripTime * 1000;
- }
-
- if (stat.type === "remote-inbound-rtp" && stat.roundTripTime != null) {
- bestRtt = stat.roundTripTime * 1000;
- }
- });
-
- if (bestRtt == null || bestRtt <= 0) return;
-
- const roundedRtt = Math.round(bestRtt);
-
- if (roundedRtt <= 0) return;
-
- return roundedRtt;
+ return state === "closed" ? null : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
-function ConnectionBar() {
+function ConnectionBar({ portalContainer }: { portalContainer?: HTMLElement }) {
const state = useCall((store) => store.state);
const isEncrypted = useCall((store) => store.isEncrypted);
const callId = useCall((store) => store.callId);
@@ -58,18 +53,14 @@ function ConnectionBar() {
return (
(
+ render={
- )}
+ }
/>
- Click to open call page
+
+ Click to open call page
+
);
}
-export function TinyPingGraph() {
- const room = getRoom();
+export function TinyPingGraph({
+ portalContainer,
+}: {
+ portalContainer?: HTMLElement;
+}) {
+ const room = useCall((store) => store.room);
const [mapData, setMapData] = useState