Claude
Skills
Sign in
Back

screen-share

Included with Lifetime
$97 forever

Screen sharing components for LiveKit video rooms — toggle button, full-width screen share view with PiP camera overlay, and an auto-switching presenter layout. Use this skill when the user says "add screen share", "setup screen sharing", "presenter mode", "screen share view", or "setup screen-share".

Image & Video

What this skill does


# Screen Share

Screen sharing components that extend the `video-room` and `video-ui` skills with a dedicated toggle button, a full-width screen share view with picture-in-picture camera overlay, and an auto-switching presenter layout that detects when someone is sharing their screen and swaps between grid mode and presenter mode.

## Prerequisites

- Next.js app with `src/` directory and App Router
- `video-room` skill installed (LiveKit client, server SDK, types)
- `video-ui` skill installed (VideoRoomProvider, ParticipantGrid, ControlsBar, ParticipantTile)
- No additional packages needed — uses `livekit-client` and `@livekit/components-react` already installed by `video-room` and `video-ui`

## Installation

No additional packages required. All dependencies are already installed by the `video-room` and `video-ui` skills:

```bash
# Already installed:
# livekit-client
# @livekit/components-react
# @livekit/components-styles
```

## What Gets Created

```
src/
├── components/
│   └── video/
│       ├── screen-share-button.tsx     # Toggle button using localParticipant.setScreenShareEnabled
│       ├── screen-share-view.tsx       # Full-width screen share track + PiP camera overlay
│       └── presenter-layout.tsx        # Auto-switches between grid and presenter mode
└── lib/
    └── video/
        └── use-screen-share.ts         # Hook: isScreenSharing, screenShareTrack, toggleScreenShare, screenShareParticipant
```

## Setup Steps

### Step 1: Create `src/lib/video/use-screen-share.ts`

```typescript
"use client";

import { useState, useCallback, useMemo } from "react";
import {
  useTracks,
  useLocalParticipant,
} from "@livekit/components-react";
import { Track, type RemoteTrackPublication, type LocalTrackPublication } from "livekit-client";
import type { TrackReferenceOrPlaceholder } from "@livekit/components-react";

type ScreenShareParticipantInfo = {
  sid: string;
  identity: string;
  name: string;
  isLocal: boolean;
};

type UseScreenShareReturn = {
  /** Whether anyone in the room is currently sharing their screen */
  isScreenSharing: boolean;
  /** Whether the local participant is the one sharing */
  isLocalScreenSharing: boolean;
  /** The screen share track reference, or null if no one is sharing */
  screenShareTrack: TrackReferenceOrPlaceholder | null;
  /** Information about the participant who is sharing, or null */
  screenShareParticipant: ScreenShareParticipantInfo | null;
  /** Toggle screen sharing for the local participant */
  toggleScreenShare: () => Promise<void>;
  /** All screen share tracks (in case multiple participants share simultaneously) */
  allScreenShareTracks: TrackReferenceOrPlaceholder[];
};

export function useScreenShare(): UseScreenShareReturn {
  const { localParticipant } = useLocalParticipant();
  const [isToggling, setIsToggling] = useState(false);

  const screenShareTracks = useTracks(
    [{ source: Track.Source.ScreenShare, withPlaceholder: false }],
    { onlySubscribed: false }
  );

  const primaryScreenShare = screenShareTracks.length > 0 ? screenShareTracks[0] : null;

  const isScreenSharing = screenShareTracks.length > 0;
  const isLocalScreenSharing = localParticipant.isScreenShareEnabled;

  const screenShareParticipant = useMemo((): ScreenShareParticipantInfo | null => {
    if (!primaryScreenShare) return null;

    const participant = primaryScreenShare.participant;
    return {
      sid: participant.sid,
      identity: participant.identity,
      name: participant.name ?? participant.identity,
      isLocal: participant.sid === localParticipant.sid,
    };
  }, [primaryScreenShare, localParticipant.sid]);

  const toggleScreenShare = useCallback(async () => {
    if (isToggling) return;
    setIsToggling(true);
    try {
      await localParticipant.setScreenShareEnabled(
        !localParticipant.isScreenShareEnabled
      );
    } finally {
      setIsToggling(false);
    }
  }, [localParticipant, isToggling]);

  return {
    isScreenSharing,
    isLocalScreenSharing,
    screenShareTrack: primaryScreenShare,
    screenShareParticipant,
    toggleScreenShare,
    allScreenShareTracks: screenShareTracks,
  };
}
```

### Step 2: Create `src/components/video/screen-share-button.tsx`

```tsx
"use client";

import { Button } from "@/components/ui/button";
import { useScreenShare } from "@/lib/video/use-screen-share";

type ScreenShareButtonProps = {
  className?: string;
  variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link";
  size?: "default" | "sm" | "lg" | "icon";
};

export function ScreenShareButton({
  className,
  variant,
  size = "sm",
}: ScreenShareButtonProps) {
  const { isLocalScreenSharing, toggleScreenShare } = useScreenShare();

  const resolvedVariant = variant ?? (isLocalScreenSharing ? "default" : "secondary");

  return (
    <Button
      variant={resolvedVariant}
      size={size}
      onClick={toggleScreenShare}
      className={className}
      title={isLocalScreenSharing ? "Stop sharing screen" : "Share your screen"}
    >
      <ScreenShareIcon className="mr-2 h-4 w-4" />
      {isLocalScreenSharing ? "Stop Sharing" : "Share Screen"}
    </Button>
  );
}

function ScreenShareIcon({ className }: { className?: string }) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      className={className}
    >
      <path d="M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3" />
      <path d="M8 21h8" />
      <path d="M12 17v4" />
      <path d="m17 8 5-5" />
      <path d="M17 3h5v5" />
    </svg>
  );
}
```

### Step 3: Create `src/components/video/screen-share-view.tsx`

```tsx
"use client";

import { useRef, useEffect, useId } from "react";
import { useTracks, useLocalParticipant } from "@livekit/components-react";
import { Track } from "livekit-client";
import type { TrackReferenceOrPlaceholder } from "@livekit/components-react";
import { Card } from "@/components/ui/card";

type ScreenShareViewProps = {
  /** The screen share track to render. If omitted, auto-detects from room. */
  screenShareTrackRef?: TrackReferenceOrPlaceholder | null;
  /** Whether to show the PiP camera overlay */
  showPiP?: boolean;
  className?: string;
};

export function ScreenShareView({
  screenShareTrackRef,
  showPiP = true,
  className,
}: ScreenShareViewProps) {
  const pipId = useId();
  const screenVideoRef = useRef<HTMLVideoElement>(null);
  const pipVideoRef = useRef<HTMLVideoElement>(null);
  const { localParticipant } = useLocalParticipant();

  // Auto-detect screen share if not provided
  const screenShareTracks = useTracks(
    [{ source: Track.Source.ScreenShare, withPlaceholder: false }],
    { onlySubscribed: false }
  );

  const activeScreenShare = screenShareTrackRef ?? (screenShareTracks.length > 0 ? screenShareTracks[0] : null);

  // Get camera tracks for PiP overlay
  const cameraTracks = useTracks(
    [{ source: Track.Source.Camera, withPlaceholder: false }],
    { onlySubscribed: false }
  );

  // Find the screen share presenter's camera track for PiP
  const presenterCameraTrack = activeScreenShare
    ? cameraTracks.find(
        (t) => t.participant.sid === activeScreenShare.participant.sid
      )
    : null;

  // Attach screen share video
  useEffect(() => {
    const videoEl = screenVideoRef.current;
    const track = activeScreenShare?.publication?.track;
    if (!videoEl || !track) return;

    track.attach(videoEl);
    return () => {
      track.detach(videoEl);
    };
  }, [activeScreenShare]);

  // Attach PiP camera video
  useEffect(() => {
    const videoEl = pipVideoRef.current;
    const track = presenterCameraTrack?.publication?.track;
    if (!videoEl || !track) return;

    track.attach(videoEl);
    return () => {
      track.detach(videoEl);
    };
  }, [presenterCameraTrack]);

  if (!activeScreenShare) {
    return null;
  }
Files: 1
Size: 18.6 KB
Complexity: 29/100
Category: Image & Video

Related in Image & Video