Screen Sharing
Capture a display, stream it, and show someone else's — as four separable pieces you can use together or independently.
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.
| Class | Role |
|---|---|
ml::ScreenSource | Grabs frames from a display. Pull-based, no pipeline. |
ml::ScreenSender | Streams a display or window to an RTSP endpoint. |
ml::ScreenReceiver | A component that displays one incoming stream. |
ml::ScreenLayout | Two receivers side by side, with a split ratio. |
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.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);
| Method | Effect |
|---|---|
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();
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.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();
| Method | Effect |
|---|---|
setUrl(rtsp) | Where to publish. |
setCaptureIndex(i) | Capture a whole display. |
setCaptureWindow(id) | Capture one window instead. |
start() / stop() | Begin and end streaming. |
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();
| Method | Effect |
|---|---|
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.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.