ml::docs | Your First App Manifests Events Messaging Plugins Scenes Resources
See all tutorials →
Advanced 5 sections ~12 min C++17/20

Screen Sharing

Capture a display, stream it, and show someone else's — as four separable pieces you can use together or independently.

Introduction

Four Separable Pieces

Malena can capture a screen, send it, and display someone else's — the pieces behind a present-my-desktop feature. They are separable: you can capture without streaming, or display a stream you never captured.

ClassRole
ml::ScreenSourceGrabs frames from a display. Pull-based, no pipeline.
ml::ScreenSenderStreams a display or window to an RTSP endpoint.
ml::ScreenReceiverA component that displays one incoming stream.
ml::ScreenLayoutTwo receivers side by side, with a split ratio.
Screen sharing is behind the MALENA_ENABLE_SCREENSHARE option because the streaming half needs GStreamer. With it off, the classes still compile as no-ops so your code builds unchanged — and your binary stays free of that dependency.
1Capture

Grabbing Frames

ScreenSource is the simplest piece: ask it for a frame and it gives you one.

ml::ScreenSource capture;
capture.setMaxDimension(1280);          // downscale for bandwidth

if (auto frame = capture.grab())
    if (auto jpeg = ml::ScreenSource::encodeJpeg(*frame))
        sendToServer(*jpeg);
MethodEffect
grab()Capture one frame. Returns std::optional<sf::Image>.
setDisplayIndex(i)Choose which display to capture.
setMaxDimension(px)Cap the long edge; frames are scaled down to fit.
encodeJpeg(image)Compress a frame for transmission.
isSupported()Whether capture works on this platform and build.

Capture is a pull, so cost is bounded by how often you ask rather than by a background pipeline. Rate-limit in your own update:

if (_frameClock.getElapsedTime().asMilliseconds() < 1000 / fps) return;
_frameClock.restart();
auto frame = capture.grab();
Check isSupported() and handle a failed grab(). On macOS the first capture triggers the Screen Recording permission prompt, and a refused permission shows up as an empty optional — not an exception. Stop rather than retrying every frame.
2Send

Streaming a Display

ScreenSender pushes a display or a single window to an RTSP URL.

ml::ScreenSender sender;
sender.setUrl("rtsp://10.0.0.1:8554/desk");
sender.setCaptureIndex(0);       // which display
sender.start();
// ...
sender.stop();
MethodEffect
setUrl(rtsp)Where to publish.
setCaptureIndex(i)Capture a whole display.
setCaptureWindow(id)Capture one window instead.
start() / stop()Begin and end streaming.
You need a relay the receivers can reach — MediaMTX is a small, self-contained one. Senders publish to it and receivers subscribe; nothing connects peer to peer.
3Receive

Displaying a Stream

ScreenReceiver is a component, so it lays out and draws like any other.

ml::ScreenReceiver view;
view.setSize({960.f, 540.f});
view.setUrl("rtsp://10.0.0.1:8554/desk");
view.setScaleMode(ml::ScreenReceiver::ScaleMode::Fit);
view.start();
MethodEffect
setUrl(rtsp)Which stream to subscribe to.
start() / stop()Begin and end playback.
setFrozen(bool)Hold the last frame — useful when an instructor wants the class to stop and look.
setScaleMode(mode)How the frame fills the component.
pushFrame(data, size)Feed a frame you obtained yourself, bypassing RTSP entirely.
pushFrame is what lets you use the display half without GStreamer: capture with ScreenSource, ship the JPEG over whatever transport you already have — a WebSocket, say — and push it in. That path works in builds with streaming compiled out.
4Layout

Two Streams Side by Side

ScreenLayout shows two streams at once with a movable split, for comparing a student's screen against a reference.

ml::ScreenLayout split;
split.setSize({1280.f, 720.f});
split.setMode(ml::ScreenLayout::Mode::Split);
split.setRatio(0.5f);
split.setSource(0, "rtsp://10.0.0.1:8554/instructor");
split.setSource(1, "rtsp://10.0.0.1:8554/student-42");
split.setFrozen(1, true);      // hold the right pane
ScreenLayout owns its two panes and links them into the component tree, so they participate in the enable/visibility cascade like any child. If you build a similar composite, link your children the same way — an unlinked child will not receive the cascade and will quietly keep handling input after its parent is disabled.