Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 85 additions & 3 deletions tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,85 @@ fn get_livekit_url(app: tauri::AppHandle) -> String {
data.livekit_server_url.clone()
}

#[tauri::command]
async fn create_screenshare_window(
app: tauri::AppHandle,
video_token: String,
) -> Result<(), String> {
if let Some(window) = app.get_webview_window("screenshare") {
let _ = window.show();
let _ = window.set_focus();
return Ok(());
}

let url = format!("screenshare.html?videoToken={}", video_token);

#[allow(unused_mut)]
let mut window_builder =
WebviewWindowBuilder::new(&app, "screenshare", WebviewUrl::App(url.into()))
.title("Screen sharing")
.inner_size(800.0, 450.0)
.resizable(true)
.visible(false)
.transparent(true)
.decorations(false)
.shadow(true)
.always_on_top(false)
.maximizable(false);

#[cfg(target_os = "macos")]
{
window_builder = window_builder.hidden_title(true)
}

let screenshare_window = window_builder
.build()
.map_err(|e| format!("Failed to create screenshare window: {}", e))?;

let window_clone = screenshare_window.clone();

screenshare_window
.run_on_main_thread(move || {
#[cfg(target_os = "macos")]
{
use window_vibrancy::{
apply_vibrancy, NSVisualEffectMaterial, NSVisualEffectState,
};

if let Err(e) = apply_vibrancy(
&window_clone,
NSVisualEffectMaterial::HudWindow,
Some(NSVisualEffectState::Active),
Some(16.0),
) {
log::warn!("Failed to apply vibrancy to screenshare window: {}", e);
}

set_window_corner_radius(&window_clone, 16.0);
}

#[cfg(target_os = "windows")]
{
use window_vibrancy::apply_blur;

if let Err(e) = apply_blur(&window_clone, Some((18, 18, 18, 125))) {
log::warn!("Failed to apply blur to screenshare window: {}", e);
}
}

if let Err(e) = window_clone.show() {
log::error!("Failed to show screenshare window: {}", e);
}

if let Err(e) = window_clone.set_focus() {
log::error!("Failed to focus screenshare window: {}", e);
}
})
.map_err(|e| format!("Failed to run on main thread: {}", e))?;

Ok(())
}

#[tauri::command]
async fn create_camera_window(app: tauri::AppHandle, camera_token: String) -> Result<(), String> {
log::info!("create_camera_window with token: {}", camera_token);
Expand All @@ -519,9 +598,7 @@ async fn create_camera_window(app: tauri::AppHandle, camera_token: String) -> Re

#[cfg(target_os = "macos")]
{
window_builder = window_builder
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay);
window_builder = window_builder.hidden_title(true)
}

let camera_window = window_builder
Expand Down Expand Up @@ -562,6 +639,10 @@ async fn create_camera_window(app: tauri::AppHandle, camera_token: String) -> Re
if let Err(e) = window_clone.show() {
log::error!("Failed to show camera window: {}", e);
}

if let Err(e) = window_clone.set_focus() {
log::error!("Failed to focus camera window: {}", e);
}
})
.map_err(|e| format!("Failed to run on main thread: {}", e))?;

Expand Down Expand Up @@ -934,6 +1015,7 @@ fn main() {
get_camera_permission,
open_camera_settings,
create_camera_window,
create_screenshare_window,
set_sentry_metadata,
call_started,
])
Expand Down
12 changes: 10 additions & 2 deletions tauri/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -407,13 +407,21 @@ body {
}

.screenshare-body {
margin: 0;
height: 100vh;
width: 100vw;
overflow: hidden;
background-color: var(--color-slate-900);
background-color: transparent;
}

.screenshare-body #root {
height: 100%;
width: 100%;
}

.screenshare-video-focus,
.screenshare-video:focus {
outline: 3px solid var(--color-yellow-300);
outline: 2px solid var(--color-yellow-300);
outline-offset: -2px;
}

Expand Down
10 changes: 7 additions & 3 deletions tauri/src/components/SharingScreen/Controls.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { HiOutlineCursorClick } from "react-icons/hi";
import { LiaHandPointerSolid } from "react-icons/lia";
import { useSharingContext } from "@/windows/screensharing/context";
import { TooltipContent, TooltipTrigger, Tooltip, TooltipProvider } from "../ui/tooltip";
import { BiSolidJoystick } from "react-icons/bi";
import useStore from "@/store/store";
import { SegmentedControl } from "../ui/segmented-control";
import { useState } from "react";
import { CustomIcons } from "../ui/icons";
import { cn } from "@/lib/utils";

export function ScreenSharingControls() {
type ScreenSharingControlsProps = {
className?: string;
};

export function ScreenSharingControls({ className }: ScreenSharingControlsProps = {}) {
const { setIsSharingKeyEvents, setIsSharingMouse } = useSharingContext();
const isRemoteControlEnabled = useStore((state) => state.callTokens?.isRemoteControlEnabled);
const [remoteControlStatus, setRemoteControlStatus] = useState<string>("controlling");
Expand All @@ -26,7 +30,7 @@ export function ScreenSharingControls() {

return (
<TooltipProvider>
<div className="w-full pt-2 flex flex-row items-center relative pointer-events-none">
<div className={cn("w-full pt-2 flex flex-row items-center relative pointer-events-none", className)}>
<div className="w-full flex justify-center">
<div className="flex flex-row gap-1 items-center">
<SegmentedControl
Expand Down
23 changes: 17 additions & 6 deletions tauri/src/components/SharingScreen/SharingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const ConsumerComponent = React.memo(() => {
onlySubscribed: true,
});
const localParticipant = useLocalParticipant();
let { isSharingMouse, isSharingKeyEvents, parentKeyTrap } = useSharingContext();
const { isSharingMouse, isSharingKeyEvents, parentKeyTrap, setStreamDimensions } = useSharingContext();
const [wrapperRef, isMouseInside] = useHover();
const { updateCallTokens } = useStore();
const [mouse, mouseRef] = useMouse();
Expand Down Expand Up @@ -263,6 +263,14 @@ const ConsumerComponent = React.memo(() => {
}
}, [track, streamWidth, streamHeight]);

useEffect(() => {
if (track) {
setStreamDimensions({ width: streamWidth, height: streamHeight });
} else {
setStreamDimensions(null);
}
}, [track, streamWidth, streamHeight, setStreamDimensions]);

/*
* We do this because we need a way to retrigger the useEffect below,
* adding the videoRef.current to the dependency array doesn't work because
Expand Down Expand Up @@ -525,7 +533,7 @@ const ConsumerComponent = React.memo(() => {
}, [isMouseInside, isSharingKeyEvents, parentKeyTrap]);

const clearClipboard = useCallback(async () => {
await writeText("");
await writeText("");
}, []);

useEffect(() => {
Expand Down Expand Up @@ -628,10 +636,13 @@ const ConsumerComponent = React.memo(() => {
return (
<div
ref={wrapperRef}
className={cn("w-full screenshare-video rounded-lg overflow-hidden border-solid border-2 relative", {
"screenshare-video-focus": isMouseInside,
"border-slate-200": !isMouseInside,
})}
className={cn(
"w-full screenshare-video rounded-t-lg rounded-b-xl overflow-hidden border-solid border-2 relative",
{
"screenshare-video-focus": isMouseInside,
"border-slate-200": !isMouseInside,
},
)}
tabIndex={-1}
>
{DEBUGGING_VIDEO_TRACK && (
Expand Down
93 changes: 59 additions & 34 deletions tauri/src/components/SharingScreen/utils.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
import { OS } from "@/constants";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { getCurrentWindow, PhysicalSize, LogicalSize } from "@tauri-apps/api/window";
import { currentMonitor } from "@tauri-apps/api/window";
import { PhysicalSize, LogicalSize, currentMonitor } from "@tauri-apps/api/window";

const appWindow = getCurrentWebviewWindow();

/*
* This function resizes the window to fit the stream's aspect ratio.
*
* It assumes that the stream's aspect ratio is greater than 1 (width > height).
* There are two possible scenarios that need to be handled in order to avoid
* the window to overflowing the screen (we could only overflow the heigth because
* we this is calculated from the width using the aspect ratio):
* - The monitor's width is greater than its height, and then
* we need to make sure the stream doesn't overflow the height.
*
* In this case we calculate the max width from the monitor's height
* and don't allow the window to have a width greater than the
* calculated one.
*
* - The monitor's height is greater than its width, and then
* we need to make sure the stream doesn't overflow the width.
*
* In this case we calculate the max height from the monitor's width
* and don't allow the window to have a height greater than the
* calculated one.
*/
export async function resizeWindow(streamWidth: number, streamHeight: number, ref: React.RefObject<HTMLVideoElement>) {
type WindowSizingResult = {
maxContentWidth: number;
maxContentHeight: number;
aspectRatio: number;
streamExtraOffset: number;
};

const calculateWindowSizing = async (streamWidth: number, streamHeight: number): Promise<WindowSizingResult | null> => {
if (streamWidth === 16 && streamHeight === 9) {
return;
return null;
}
const monitor = await currentMonitor();
const monitorWidth = monitor?.size.width || 0;
Expand Down Expand Up @@ -63,24 +48,50 @@ export async function resizeWindow(streamWidth: number, streamHeight: number, re
let maxHeight = Math.floor(monitorHeight - taskbarHeight - streamExtraOffset);
let maxWidth = Math.floor(monitorWidth);

if (maxWidth > 0 && maxHeight > 0) {
if (monitorWidth >= monitorHeight) {
maxWidth = Math.floor(maxHeight * aspectRatio);
} else {
maxHeight = Math.floor(maxWidth / aspectRatio);
}
appWindow.setMaxSize(new LogicalSize(maxWidth, maxHeight + streamExtraOffset));
if (maxWidth <= 0 || maxHeight <= 0) {
return null;
}

if (monitorWidth >= monitorHeight) {
maxWidth = Math.floor(maxHeight * aspectRatio);
} else {
maxHeight = Math.floor(maxWidth / aspectRatio);
}

return {
maxContentWidth: maxWidth,
maxContentHeight: maxHeight,
aspectRatio,
streamExtraOffset,
};
};

/*
* This function resizes the window to fit the stream's aspect ratio.
*
* It assumes that the stream's aspect ratio is greater than 1 (width > height).
* There are two possible scenarios that need to be handled in order to avoid
* the window to overflowing the screen (we could only overflow the height because
* this is calculated from the width using the aspect ratio).
*/
export async function resizeWindow(streamWidth: number, streamHeight: number, ref: React.RefObject<HTMLVideoElement>) {
const sizing = await calculateWindowSizing(streamWidth, streamHeight);
if (!sizing) {
return;
}

const { maxContentWidth, maxContentHeight, aspectRatio, streamExtraOffset } = sizing;
appWindow.setMaxSize(new LogicalSize(maxContentWidth, maxContentHeight + streamExtraOffset));

let size = await appWindow.innerSize();

if (ref.current) {
if (!aspectRatio || isNaN(aspectRatio)) {
return;
}

const newWidth = Math.min(size.width, maxWidth);
const newHeight = Math.min(Math.floor(size.width / aspectRatio), maxHeight) + streamExtraOffset;
const newWidth = Math.min(size.width, maxContentWidth);
const newHeight = Math.min(Math.floor(size.width / aspectRatio), maxContentHeight) + streamExtraOffset;
console.log(`Current size is ${size.width}x${size.height}; New size will be ${newWidth}x${newHeight}`);
appWindow.setSize(
// As the video will be always full window width minus some padding,
Expand All @@ -89,3 +100,17 @@ export async function resizeWindow(streamWidth: number, streamHeight: number, re
);
}
}

export async function setWindowToMaxStreamSize(streamWidth: number, streamHeight: number) {
const sizing = await calculateWindowSizing(streamWidth, streamHeight);
if (!sizing) {
return;
}

const { maxContentWidth, maxContentHeight, streamExtraOffset } = sizing;

await appWindow.setMaxSize(new LogicalSize(maxContentWidth, maxContentHeight + streamExtraOffset));
await appWindow.setSize(
new PhysicalSize(Math.floor(maxContentWidth), Math.floor(maxContentHeight + streamExtraOffset)),
);
}
5 changes: 5 additions & 0 deletions tauri/src/windows/screensharing/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type SharingContextType = {
setVideoToken: (value: string) => void;
parentKeyTrap?: HTMLDivElement;
setParentKeyTrap: (value: HTMLDivElement) => void;
streamDimensions: { width: number; height: number } | null;
setStreamDimensions: (value: { width: number; height: number } | null) => void;
};

const SharingContext = createContext<SharingContextType | undefined>(undefined);
Expand All @@ -30,6 +32,7 @@ export const SharingProvider: React.FC<SharingProviderProps> = ({ children }) =>
const [isSharingKeyEvents, setIsSharingKeyEvents] = useState<boolean>(true);
const [parentKeyTrap, setParentKeyTrap] = useState<HTMLDivElement | undefined>(undefined);
const [videoToken, setVideoToken] = useState<string | null>(null);
const [streamDimensions, setStreamDimensions] = useState<{ width: number; height: number } | null>(null);

return (
<SharingContext.Provider
Expand All @@ -42,6 +45,8 @@ export const SharingProvider: React.FC<SharingProviderProps> = ({ children }) =>
setParentKeyTrap,
videoToken,
setVideoToken,
streamDimensions,
setStreamDimensions,
}}
>
{children}
Expand Down
Loading
Loading