useScreenShare
A hook for screen/window sharing using getDisplayMedia
A React hook for screen/window sharing using the getDisplayMedia API. Captures the user's screen, application window, or browser tab with optional system audio.
Source Code
View the full hook implementation in the Hook Source Code section below.
Features
- Screen Capture - Capture monitor, window, or browser tab
- System Audio - Optionally capture system audio (tab/screen audio)
- Display Surface Detection - Know if user selected monitor, window, or browser
- Auto-Stop - Automatically stops when user ends share via browser UI
- SSR Safe - No issues with server-side rendering
Basic Usage
Simple screen share with start/stop controls:
getDisplayMedia is not supported in this browser
"use client";
import { useRef, useEffect } from "react";
import { useScreenShare } from "@repo/hooks/webrtc/use-screen-share";
import { Button } from "@repo/ui/components/button";
import { Monitor, MonitorOff, Loader2 } from "lucide-react";
/* BASIC SCREEN SHARE - Start/Stop Screen Capture */
export const Example1 = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const {
stream,
isSharing,
isLoading,
error,
start,
stop,
displaySurface,
isSupported,
} = useScreenShare();
// Attach stream to video element
useEffect(() => {
if (videoRef.current && stream) {
videoRef.current.srcObject = stream;
}
}, [stream]);
if (!isSupported) {
return (
<div className="text-destructive rounded-lg border border-red-500/50 bg-red-500/10 p-4 text-center text-sm">
getDisplayMedia is not supported in this browser
</div>
);
}
return (
<div className="flex w-full max-w-lg flex-col gap-4">
{/* Screen Preview */}
<div className="bg-muted relative aspect-video overflow-hidden rounded-lg border">
{isSharing ? (
<video
ref={videoRef}
autoPlay
playsInline
muted
className="h-full w-full object-contain"
/>
) : (
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2">
<MonitorOff className="h-12 w-12" />
<span className="text-sm">No screen shared</span>
</div>
)}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
<Loader2 className="h-8 w-8 animate-spin text-white" />
</div>
)}
{/* Display Surface Badge */}
{isSharing && displaySurface && (
<div className="absolute left-3 top-3 rounded-full bg-black/70 px-2.5 py-1 text-xs capitalize text-white">
{displaySurface}
</div>
)}
</div>
{/* Error Message */}
{error && (
<div className="text-destructive rounded-md bg-red-500/10 p-3 text-sm">
{error.message}
</div>
)}
{/* Controls */}
<div className="flex justify-center gap-2">
{isSharing ? (
<Button
variant="destructive"
onClick={stop}
className="gap-2"
>
<MonitorOff className="h-4 w-4" />
Stop Sharing
</Button>
) : (
<Button
onClick={() => start()}
isDisabled={isLoading}
className="gap-2"
>
<Monitor className="h-4 w-4" />
Share Screen
</Button>
)}
</div>
</div>
);
};
Screen Share with Audio
Capture system audio along with the screen:
Click to share with audio
Note: System audio capture is only available when sharing a browser tab or screen with audio permission. Select “Share tab audio” or “Share system audio” in the picker.
"use client";
import { useRef, useEffect } from "react";
import { useScreenShare } from "@repo/hooks/webrtc/use-screen-share";
import { Button } from "@repo/ui/components/button";
import { Monitor, Volume2, VolumeX as VolumeOff } from "lucide-react";
/* SCREEN SHARE WITH AUDIO - Capture System Sound */
export const Example2 = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const { stream, isSharing, start, stop, audioTrack } = useScreenShare();
// Attach stream to video element
useEffect(() => {
if (videoRef.current && stream) {
videoRef.current.srcObject = stream;
}
}, [stream]);
const hasAudio = audioTrack !== null;
return (
<div className="flex w-full max-w-lg flex-col gap-4">
{/* Screen Preview */}
<div className="bg-muted relative aspect-video overflow-hidden rounded-lg border">
{isSharing ? (
<video
ref={videoRef}
autoPlay
playsInline
muted={false}
className="h-full w-full object-contain"
/>
) : (
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2">
<Monitor className="h-12 w-12" />
<span className="text-sm">
Click to share with audio
</span>
</div>
)}
{/* Audio Indicator */}
{isSharing && (
<div
className={`absolute right-3 top-3 flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${
hasAudio
? "bg-green-500 text-white"
: "bg-zinc-600 text-zinc-300"
}`}
>
{hasAudio ? (
<>
<Volume2 className="h-3 w-3" />
Audio On
</>
) : (
<>
<VolumeOff className="h-3 w-3" />
No Audio
</>
)}
</div>
)}
</div>
{/* Info */}
<div className="text-muted-foreground rounded-lg bg-blue-500/10 p-3 text-xs">
<strong className="text-blue-600 dark:text-blue-400">
Note:
</strong>{" "}
System audio capture is only available when sharing a browser
tab or screen with audio permission. Select “Share tab
audio” or “Share system audio” in the picker.
</div>
{/* Controls */}
<div className="flex justify-center gap-2">
{isSharing ? (
<Button variant="destructive" onClick={stop}>
Stop Sharing
</Button>
) : (
<Button
onClick={() => start({ audio: true })}
className="gap-2"
>
<Volume2 className="h-4 w-4" />
Share with Audio
</Button>
)}
</div>
</div>
);
};
Picture-in-Picture Mode
Combine screen share with camera for presentation mode:
Screen + Camera
"use client";
import { useRef, useEffect, useState } from "react";
import { useScreenShare } from "@repo/hooks/webrtc/use-screen-share";
import { useUserMedia } from "@repo/hooks/webrtc/use-user-media";
import { Button } from "@repo/ui/components/button";
import { Monitor, Camera } from "lucide-react";
/* PICTURE-IN-PICTURE - Camera overlay on Screen Share */
export const Example3 = () => {
const screenRef = useRef<HTMLVideoElement>(null);
const cameraRef = useRef<HTMLVideoElement>(null);
const [pipPosition, setPipPosition] = useState<"br" | "bl" | "tr" | "tl">(
"br",
);
// Screen share
const {
stream: screenStream,
isSharing,
start: startScreen,
stop: stopScreen,
} = useScreenShare();
// Camera
const {
stream: cameraStream,
isActive: cameraActive,
start: startCamera,
stop: stopCamera,
} = useUserMedia();
// Attach streams
useEffect(() => {
if (screenRef.current && screenStream) {
screenRef.current.srcObject = screenStream;
}
}, [screenStream]);
useEffect(() => {
if (cameraRef.current && cameraStream) {
cameraRef.current.srcObject = cameraStream;
}
}, [cameraStream]);
// PiP position classes
const positionClasses = {
br: "bottom-3 right-3",
bl: "bottom-3 left-3",
tr: "top-3 right-3",
tl: "top-3 left-3",
};
const handleStartBoth = async () => {
await startScreen();
await startCamera({ video: true, audio: false });
};
const handleStopBoth = () => {
stopScreen();
stopCamera();
};
return (
<div className="flex w-full max-w-lg flex-col gap-4">
{/* Main View */}
<div className="bg-muted relative aspect-video overflow-hidden rounded-lg border">
{/* Screen Share */}
{isSharing ? (
<video
ref={screenRef}
autoPlay
playsInline
muted
className="h-full w-full object-contain"
/>
) : (
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2">
<Monitor className="h-12 w-12" />
<span className="text-sm">Screen + Camera</span>
</div>
)}
{/* Camera PiP Overlay */}
{cameraActive && (
<div
className={`absolute ${positionClasses[pipPosition]} h-24 w-32 cursor-pointer overflow-hidden rounded-lg border-2 border-white shadow-lg transition-all hover:scale-105`}
onClick={() => {
const positions: ("br" | "bl" | "tr" | "tl")[] = [
"br",
"bl",
"tl",
"tr",
];
const current = positions.indexOf(pipPosition);
setPipPosition(
positions.at((current + 1) % positions.length)!,
);
}}
>
<video
ref={cameraRef}
autoPlay
playsInline
muted
className="h-full w-full scale-x-[-1] object-cover"
/>
</div>
)}
</div>
{/* Info */}
{isSharing && cameraActive && (
<p className="text-muted-foreground text-center text-xs">
Click the camera overlay to move it to a different corner
</p>
)}
{/* Controls */}
<div className="flex justify-center gap-2">
{isSharing ? (
<Button variant="destructive" onClick={handleStopBoth}>
Stop All
</Button>
) : (
<Button onClick={handleStartBoth} className="gap-2">
<Monitor className="h-4 w-4" />
<Camera className="h-4 w-4" />
Start PiP Mode
</Button>
)}
</div>
</div>
);
};
API Reference
Hook Signature
function useScreenShare(
defaultOptions?: UseScreenShareOptions,
): UseScreenShareReturn;Options
| Property | Type | Default | Description |
|---|---|---|---|
audio | boolean | false | Include system audio |
video | boolean | MediaTrackConstraints | true | Video constraints |
autoStopOnEnd | boolean | true | Stop when user ends via browser UI |
Return Value
| Property | Type | Description |
|---|---|---|
stream | MediaStream | null | The screen share stream |
isSharing | boolean | Whether actively sharing |
isLoading | boolean | Whether share is being acquired |
error | Error | null | Error if sharing failed |
isSupported | boolean | Whether getDisplayMedia is supported |
start | (options?) => Promise<boolean> | Start screen sharing |
stop | () => void | Stop sharing |
videoTrack | MediaStreamTrack | null | Video track from share |
audioTrack | MediaStreamTrack | null | Audio track (if included) |
displaySurface | string | null | Type: "monitor", "window", or "browser" |
Hook Source Code
import { useState, useEffect, useCallback, useRef } from "react";
/**
* Options for getDisplayMedia
*/
export interface UseScreenShareOptions {
/** Whether to include audio (default: false) */
audio?: boolean | MediaTrackConstraints;
/** Video constraints for screen capture */
video?:
| boolean
| (MediaTrackConstraints & DisplayMediaStreamOptions["video"]);
/** Whether to prefer current tab (Chrome only) */
preferCurrentTab?: boolean;
/** Surface types to allow: "monitor", "window", "browser" */
surfaceTypes?: ("monitor" | "window" | "browser")[];
/** Whether to show system audio option (default: false) */
systemAudio?: "include" | "exclude";
/** Auto-stop when user ends share via browser UI (default: true) */
autoStopOnEnd?: boolean;
}
/**
* Return type for the useScreenShare hook
*/
export interface UseScreenShareReturn {
/** The screen share stream */
stream: MediaStream | null;
/** Whether screen share is active */
isSharing: boolean;
/** Whether share is being acquired */
isLoading: boolean;
/** Error if sharing failed */
error: Error | null;
/** Whether the API is supported */
isSupported: boolean;
/** Start screen sharing */
start: (options?: UseScreenShareOptions) => Promise<boolean>;
/** Stop screen sharing */
stop: () => void;
/** The video track from the share */
videoTrack: MediaStreamTrack | null;
/** The audio track from the share (if included) */
audioTrack: MediaStreamTrack | null;
/** Display surface type: "monitor", "window", or "browser" */
displaySurface: string | null;
}
/**
* A React hook for screen/window sharing using getDisplayMedia.
* Captures the user's screen, application window, or browser tab.
*
* @param defaultOptions - Default options for screen sharing
* @returns UseScreenShareReturn object with stream, states, and controls
*
* @example
* ```tsx
* const { stream, isSharing, start, stop } = useScreenShare();
*
* // Start sharing
* await start();
*
* // Display in video element
* videoRef.current.srcObject = stream;
*
* // Stop when done
* stop();
* ```
*/
export function useScreenShare(
defaultOptions: UseScreenShareOptions = {},
): UseScreenShareReturn {
const [stream, setStream] = useState<MediaStream | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [displaySurface, setDisplaySurface] = useState<string | null>(null);
const optionsRef = useRef<UseScreenShareOptions>(defaultOptions);
// Check if API is supported
const isSupported =
typeof navigator !== "undefined" &&
!!navigator.mediaDevices?.getDisplayMedia;
// Get tracks
const videoTrack = stream?.getVideoTracks()[0] ?? null;
const audioTrack = stream?.getAudioTracks()[0] ?? null;
// Stop sharing
const stop = useCallback(() => {
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
setStream(null);
setDisplaySurface(null);
setError(null);
}, [stream]);
// Start sharing
const start = useCallback(
async (options?: UseScreenShareOptions): Promise<boolean> => {
if (!isSupported) {
setError(new Error("getDisplayMedia is not supported"));
return false;
}
// Merge options
const mergedOptions = { ...optionsRef.current, ...options };
const { autoStopOnEnd = true, ...displayMediaOptions } =
mergedOptions;
setIsLoading(true);
setError(null);
try {
// Stop existing share first
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
const displayStream =
await navigator.mediaDevices.getDisplayMedia({
audio: displayMediaOptions.audio ?? false,
video: displayMediaOptions.video ?? true,
} as DisplayMediaStreamOptions);
// Get display surface type from track settings
const videoTrack = displayStream.getVideoTracks()[0];
if (videoTrack) {
const settings =
videoTrack.getSettings() as MediaTrackSettings & {
displaySurface?: string;
};
setDisplaySurface(settings.displaySurface ?? null);
// Auto-stop when user ends share via browser UI
if (autoStopOnEnd) {
videoTrack.onended = () => {
stop();
};
}
}
setStream(displayStream);
setIsLoading(false);
return true;
} catch (err) {
const error =
err instanceof Error
? err
: new Error("Failed to start screen share");
setError(error);
setStream(null);
setDisplaySurface(null);
setIsLoading(false);
return false;
}
},
[isSupported, stream, stop],
);
// Cleanup on unmount
useEffect(() => {
return () => {
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
};
}, [stream]);
return {
stream,
isSharing: stream !== null,
isLoading,
error,
isSupported,
start,
stop,
videoTrack,
audioTrack,
displaySurface,
};
}
export default useScreenShare;