Cosmo Realtime SDK
Multimodal

Screen share

Stream the user's screen as a live video track so the agent can see the application they're working in.

Screen share lets the agent watch the live screen and talk about what it sees — walking a user through an unfamiliar app, reviewing a document together, debugging alongside a developer.

The screen travels as a LiveKit video track (like camera video), not as control-channel messages. The browser or OS handles the capture-permission prompt. Screen share needs the WebRTC transport: on the websocket transport, starting a share refuses with video_unsupported.

Start a screen share

await session.startScreenShare();   // browser shows the picker
// … the agent now sees the shared surface …
await session.stopScreenShare();

Track the lifecycle for your UI:

const state = client.getScreenShareState();
// { kind: 'inactive' } | { kind: 'requesting' }
// | { kind: 'active'; startedAt: number }
// | { kind: 'error'; error: { code: 'screen_denied' | 'screen_start_failed'; message: string } }
switch (state.kind) {
  case 'active':
    showSharingSince(state.startedAt);
    break;
  case 'error':
    showShareError(state.error.message);
    break;
}

The state is a discriminated object, not a bare string — switch on state.kind, and the active and error arms carry their payloads (startedAt, the failing error).

To show the person what they are sharing, render the SDK's own capture — don't call getDisplayMedia a second time (a second capture prompts again, and anything captured outside the SDK never reaches the agent):

// Plain TypeScript: the stream is on the session while a share is active.
const stream = session.getScreenShareStream();  // MediaStream | null

// React: the hook pairs the lifecycle state with the stream.
function SharePreview() {
  const { state, stream } = useScreenShare();
  const videoRef = useRef<HTMLVideoElement | null>(null);
  useEffect(() => {
    if (videoRef.current) videoRef.current.srcObject = stream;
  }, [stream]);
  if (state.kind !== 'active') return null;
  return <video ref={videoRef} autoPlay playsInline muted />;
}

The SDK owns the stream's lifecycle — end it with session.stopScreenShare(), never by stopping its tracks.

A denied picker surfaces as a screen_denied screen-share state — treat it as a normal user choice, not a failure. getVisionInputStatus() tells you whether the model has a fresh frame, which is the honest signal for a "👁 agent can see your screen" indicator.

Python drives capture itself — the SDK publishes the track, you supply the frames.

MethodBehavior
start_screen_share(*, width=1920, height=1080)Creates the track. The publish is deferred to the first pushed frame so dimensions resolve from the source. Idempotent — calling it again restarts an active share.
push_screen_share_frame(frame)Pushes one rtc.VideoFrame. A no-op if no share is active.
stop_screen_share()Unpublishes the track. Idempotent.

There's no OS capture helper, since a server process rarely has a display. Feed frames from whatever surface you render — a headless browser, a rendered canvas, or a file. See Video for a worked example.

Swift splits capture from transport, because on Apple platforms you own the capture pipeline (ScreenCaptureKit on macOS, ReplayKit on iOS):

try await session.startScreenShare()          // create the track

// From your capture callback (any thread):
session.pushScreenShareFrame(sampleBuffer)    // nonisolated, safe from capture threads

await session.stopScreenShare()

The track publishes on the first frame you push, not at startScreenShare() — so a capture pipeline that never produces frames never publishes an empty track. Two hooks matter in production:

  • setScreenShareFrameProcessor(_:) — transform frames before they leave the device (redaction, watermarking, cropping a region).
  • onScreenShareFailed(_:) — observe SFU rejections or codec failures without tearing down the session.

Screen content is the most sensitive input modality — it can contain anything the user has open.

  • Capture the narrowest surface the task needs: a window or tab rather than the whole display (the browser picker offers this; on macOS, filter in your ScreenCaptureKit config).
  • Redact before transport where you control frames (Swift's frame processor); in the browser, prefer sharing a specific tab.
  • Say clearly in your UI when the screen is visible to the agent, and make stopping one tap. store_video: false keeps shared screens out of persisted artifacts while the conversation still records; store_recording: false stops every class at once — see Recording and privacy.

For the agent to not just see but point at and click things on-screen, see Screen tools.

On this page