Patryk Żurek

Patryk Żurek

thoughts, code, and side projects

blog / Why Legcord screen sharing sucks

Why Legcord screen sharing sucks

June 29, 2026 | legcord, electron, discord, screensharing, webrtc, pipewire, vaapi, macos

Hey all,

Let's get the obvious part out of the way first: screen sharing in Legcord sucks.

It doesn't suck for absolutely everyone. Some people are able to stream windows, select audio, and share for as long as they want without seeing any weirdness at all. For others, the picker won't load. Sometimes only one monitor is listed. Sometimes all previews show Legcord's own window, despite correct labels below them. Sometimes the selected window streams correctly but its audio vanishes into oblivion. On Linux, sharing application audio may also send voices from everyone in the call back into the call. On macOS, the viewer gets a 5 FPS slideshow.

Occasionally it even works the first time, which makes debugging it much more entertaining.

Our extensive list of Github Issues describes problems with source enumeration, thumbnails, monitor detection, system audio, application audio, frame rate, HDR, permissions, power management, and Discord's own stream controls. Reports are filed from Windows, macOS, X11, Wayland, PulseAudio, PipeWire, laptops with multiple GPUs (Optimus or similar), desktops with multiple monitors, and virtually all our distribution formats.

This post is an attempt to explain why the feature is so fragile, why it has never been fixed, and what exactly Legcord does when you click the screensharing button.

More importantly, this post is one enormous cry for help.

I do not have a single magical patch or idea that will make screen sharing reliable on every operating system. The problem crosses Electron, Chromium, WebRTC, Discord's web client, operating-system capture APIs, graphics drivers, audio servers, hardware encoders, portals, permissions, and Legcord's own patches. Properly solving it requires people who understand at least some of those components and have hardware on which the problems can be reproduced. I'm completely burned out from trying to fix it, resorting to streaming from the official client myself.

What Legcord actually controls

Legcord is not a complete independent implementation of Discord.

While it contains zero Discord proprietary code it loads the proprietary Discord's web client inside Electron and adds its own desktop integration, patches, settings, source picker, audio routing, and other native functionality. Most of the call interface still belongs to Discord. WebRTC still performs most of the real-time media work. Chromium still communicates with the operating system's capture and graphics APIs.

When Discord asks the browser to share a display, it invokes the standard getDisplayMedia API. Legcord intercepts this request using Electron's display-media request handler.

The simplified process looks something like this:

Discord's share button
        ↓
navigator.mediaDevices.getDisplayMedia()
        ↓
Legcord's Electron request handler
        ↓
desktopCapturer.getSources()
        ↓
Legcord or the operating system displays a source picker
        ↓
A screen or window is selected
        ↓
Chromium captures frames from the operating system
        ↓
WebRTC converts, scales and encodes those frames
        ↓
Discord sends the encoded stream to everyone watching

For audio, another branch is added:

Application or system output
        ↓
Operating-system audio capture
        ↓
Legcord's selected loopback method
        ↓
Chromium audio track
        ↓
WebRTC processing and synchronisation
        ↓
Discord stream

Legcord's current handler asks Electron for both window and screen sources, passes the result to the renderer so that a picker could display it, waits for the selected source to come back via IPC and then gives it to Electron as a video source with optional loopback audio source.

That sounds like few lines of code because the Legcord part in the process really is fairly small. The actual implementation of screen capture is buried much deeper inside Chromium and all the operating systems.

This is important because the phrase “Legcord's screensharing bug” can refer to several totally unrelated problems.

A missing monitor can be a source-enumeration problem. A correct source with an incorrect thumbnail can be a preview-generation problem. A correct preview followed by a low-frame-rate stream can be a capture, conversion, encoder, or WebRTC scheduling problem. Missing audio can be a permission problem, an unsupported operating-system API, a bad loopback route, or a dead media track. Audio that includes the voices from the call is almost the opposite problem: too much audio has been captured.

There is no single “screen sharing” part to fix. There are multiple that can go berzerk.

The picker can fail before capture even begins

Some recent reports demonstrate this case surprisingly well.

One Windows user reported that every source preview displayed the Legcord window, even though the titles underneath were correct. Selecting one of those entries still streamed the intended application. That means source identification and final capture were probably working while thumbnail generation, thumbnail association, or preview caching was not.

Another Windows report said that the second monitor appeared but the main laptop display did not. This happens before video encoding is even relevant. Chromium either failed to enumerate the display, identified it in an unexpected way, or returned a list that Legcord did not represent correctly.

On Arch Linux, another user could trigger the media request. The logs showed that both video and audio had been requested but the picker itself never appeared. No encoder had been started at that stage. The application was stuck somewhere between Discord requesting media, Electron asking PipeWire or the desktop portal for a source, and Legcord waiting for a result.

Wayland makes this particularly awkward.

Under a modern Wayland session, applications are not allowed to directly capture any window. Screen sharing is negotiated through the desktop portal and PipeWire. Portal provides a trusted system picker and returns a capture stream.

According to Electron's documentation, desktopCapturer.getSources() returns only one source when PipeWire is used. When an application requests both screens and windows, the selected by the portal source will be returned as window capture. This is not a neat fit with Legcord's old expectation that it will enumerate all windows and displays, show all of them in the picker and select one.

Legcord does detect Wayland and enables Chromium's PipeWire WebRTC capturer. It also contains logic acknowledging that the native picker is being used. Even so, the surrounding code still passes through the same general custom-handler architecture. That creates several possible states:

  • the portal is waiting for the user;
  • Electron is waiting for the portal;
  • Legcord is waiting for getSources();
  • Discord is waiting for Legcord's handler;
  • Legcord's renderer is waiting for a source list that PipeWire will not provide in the same form as X11.

I am not claiming that this is definitely the reason every Wayland picker hangs. It is, however, an architectural mismatch worth investigating.

Why a stream can be choppy even when hardware acceleration is enabled

“Hardware acceleration is enabled” is one of the most useless phrases while debugging screensharing.

It may mean that the interface is composited by the GPU. It may mean that canvas rendering is accelerated. It may mean that video decoding is accelerated. It doesn't automatically mean that WebRTC is using the GPU to encode the outgoing screenshare stream.

A screensharing frame may pass through all of these stages:

  1. The operating system captures a desktop surface or application window.
  2. Chromium receives the surface as a GPU texture or a buffer in system memory.
  3. The image is copied, cropped or scaled.
  4. Its colour format is converted into something accepted by the encoder.
  5. WebRTC decides which codec and encoder implementation to use.
  6. The encoder compresses the frame.
  7. WebRTC packetises the result and schedules it for transmission.
  8. Discord applies its own stream configuration, bitrate and quality logic.

Hardware video encoding only helps with stage 6.

If the capture backend captures 10 frames per second, hardware acceleration cannot (at least currently) generate the missing 50 frames. If every frame has to be copied from GPU memory to the CPU and converted before it can be encoded, the hardware encoder sits mostly idle. If WebRTC negotiates a codec that the current driver cannot encode, Chromium may silently use a software implementation. If frames are being dropped by the capturer or by WebRTC's adaptation logic, GPU utilisation can remain low while the stream looks terrible.

High-resolution desktop capture is also a very different beast from playing an ordinary video. A film usually contains motion that the encoder can predict between frames. A desktop can instantly replace enormous regions when a window moves, a page scrolls, a game changes scenes, or text is rendered. A 2560×1440 or 4K desktop at 60 FPS creates a substantial amount of data before compression even starts.

The VA-API situation

On Linux, VA-API is supposed to expose hardware video acceleration through drivers provided by Intel, AMD, and some other vendors. In practice, getting Chromium to use it for a WebRTC encoder depends on the Chromium version, graphics backend, sandbox, driver, codec, distribution packaging, command-line switches, and feature flags.

Legcord has a VA-API option and a preset containing flags related to GPU rasterisation, zero-copy paths, accelerated video encoding and accelerated video decoding. Those get applied by default on every Legcord instance in recent versions, and they're not a longer a "Performance Mode" but their own seperate toggle.

Electron has received reports where Chromium showed a GPU video encoder, multiple VA-API and PipeWire flags were enabled, and screen capture still ran at roughly 10–15 FPS while the encoder remained underutilised. That is exactly the kind of case where the bottleneck is probably before the encoder, between components, or caused by WebRTC's capture and adaptation pipeline rather than raw encoding performance.

A useful VA-API setting must therefore do more than append flags. In a perfect world Legcord would verify:

  • which codec Discord negotiated;
  • which encoder implementation WebRTC selected;
  • whether the implementation is hardware-backed;
  • whether frames stay in GPU memory;
  • how many frames arrive from the capture backend;
  • how many are discarded before encoding;
  • how long colour conversion and encoding take;
  • whether the requested and delivered frame rates match.

Without those measurements, enabling five more Chromium flags is mostly guesswork.

Why audio is even worse

Video capture begins with a visible object: a monitor or a window.

System audio does not.

The microphone is an input device, so a browser can request it like any other recording source. The sound coming from applications is normally being sent in the opposite direction, toward an output device. Sharing it requires an OS-specific loopback mechanism that records some part of the output graph and turns it into a new input stream.

The phrase “share audio” can mean several different things:

  • capture everything sent to the default speakers;
  • capture only the selected application;
  • capture several applications but exclude Discord;
  • capture a monitor's HDMI output;
  • capture a virtual sink;
  • capture a game while excluding the user's microphone;
  • capture system audio while preventing remote participants from hearing themselves.

Every operating system represents those routes differently.

The feedback problem

Suppose Legcord captures everything being sent to the default speakers.

The game is sent to the speakers, so it is captured.

A browser video is sent to the speakers, so it is captured.

The voices of everyone in the Discord call are also sent to the speakers, so they are captured too.

Legcord then sends that captured output back into the same call. A participant speaks, their voice reaches your computer, the loopback capturer records it, and the stream sends it back to them a moment later. They hear themselves as an echo.

This is not merely a theoretical mistake in Legcord. Electron has had a reproducible report of desktop audio capture causing remote conference participants to hear their own voices, with the issue marked as blocked by upstream Chromium behavior.

Preventing it requires application-aware routing rather than blindly recording the final output device. The implementation needs to identify the streams belonging to the shared application and exclude the stream used by Discord's own audio service.

That is much easier to describe than to implement reliably.

Linux: PipeWire, PulseAudio and Venmic

Legcord contains a native Linux integration based on Venmic. Instead of only asking Chromium for a generic loopback of the entire system output, Venmic can inspect the PipeWire graph and link selected application streams into the source used for screen sharing.

Legcord asks Electron for its child-process metrics and locates Chromium’s out-of-process Audio Service. It uses that process’s PID to identify and exclude Legcord’s own audio-output nodes from Venmic’s PipeWire routing. Venmic itself separately creates a virtual PipeWire node named vencord-screen-share, into which the selected application streams are linked.

This is considerably more advanced than simply recording the speakers.

It is also fragile.

Correct me if I'm wrong but from my understanding:

PipeWire is a graph of nodes, ports and links which can appear or disappear while the application is running. Applications can restart audio streams when output device changes. Chromium can restart its audio-service process. Sandboxed applications can limit which process information is available. Compatibility layers can expose PulseAudio streams through PipeWire. Games can create several streams with various properties. Web browsers can start several streams in one process. Virtual mixer can make final source hard to identify.

Legcord currently depends in part on the process ID and properties attached to those nodes. If the call audio is produced by a different process than the one Legcord found, or if Chromium recreates its audio process after the exclusion list was built, Discord's own output can leak into the shared route.

That is one plausible explanation for the Linux report where selecting an application or Venmic source also shared the voices of everyone in the call. It is not proven by the report alone, but it matches the class of failure the routing code is trying to prevent.

Audio also needs a stable clock. Video frames and audio samples may be generated by different devices with clocks that drift slightly. WebRTC has to resample, buffer and synchronise them. Rebuilding part of the PipeWire graph or switching devices can briefly interrupt the stream, change latency, or cause accumulated samples to be played too quickly. That can present itself as audio jumping, becoming loud for a moment, going silent, or slowly moving out of sync.

macOS: permissions, operating-system versions and silent dead streams

Historically, macOS was the worst platform for desktop audio capture.

On macOS Monterey (12) and earlier, Electron cannot natively capture system audio through the ordinary browser media APIs. Apple required applications to provide a signed kernel extension for that kind of access. Chromium does not ship one, and neither does Legcord.

On those systems, the practical workaround is a virtual audio device such as BlackHole or Soundflower. The user routes system audio into that device and shares it as an input. That is not a good out-of-the-box experience, but it is an operating-system limitation rather than a flag Legcord forgot to enable.

macOS 13 introduced newer APIs that allow applications to capture desktop audio without a kernel extension. Newer Chromium and Electron releases can use them, but that introduced another set of requirements.

On macOS 14.2 and newer, Electron requires the application bundle to contain an NSAudioCaptureUsageDescription entry in its Info.plist. Starting with the Electron 39 development cycle, Chromium's newer CoreAudio Tap path became the default for desktop audio capture.

The particularly nasty part is how it fails.

Electron documents that when the permission description is missing, starting the audio stream still appears to succeed. The application receives a media track, but it is a dead track containing no useful audio. No warning or error is displayed.

That is almost perfectly designed to produce an issue report saying:

I enabled share audio. The stream started. There was no error. Nobody can hear anything.

Legcord 1.2.4 uses Electron 41, so it is well beyond the Electron version where the new CoreAudio path became the default. The stable branch's macOS packaging configuration contains microphone and camera descriptions but does not contain NSAudioCaptureUsageDescription.

The current development branch does contain it thanks to this PR.

That means development branch has a fix for one specific packaging problem that can cause silent audio on macOS 14.2 and later. It does not prove that all macOS audio reports are caused by the same thing and it helps nobody on macOS 12 and earlier, but it is a useful fix once it reaches the stable branch.

macOS permissions introduce another complication: the application may have microphone permission, screen-recording permission, and system-audio permission as separate states. Updating or moving an unsigned or differently signed application bundle can cause macOS to associate permissions with the wrong build. A user can therefore have video but no audio, audio but no video, or permissions that appear enabled in System Settings but no longer match the binary currently running.

Windows is not the solution

Windows generally has better-supported system loopback capture (longest audio sharing implementation, predating Linux by almost a year), but that does not make the pipeline simple.

The system can expose desktop capture through different Chromium and Windows APIs depending on the version and configuration. Hybrid-GPU laptops may render a window on one adapter while Electron is running on another. Protected surfaces may not provide thumbnails. Minimized or occluded applications can stop updating. HDR displays introduce colour spaces and pixel formats that ordinary SDR WebRTC pipelines may mishandle.

The report where every thumbnail displayed Legcord but the correct application was ultimately streamed is especially interesting. It suggests that thumbnail capture and the final video source do not necessarily follow the same path. Electron's source list contains identifiers, names and generated thumbnails. Legcord uses those thumbnails for its picker, but Chromium later opens the selected identifier for the real stream.

The preview can therefore be wrong while the final capture is right.

All of that might be caused by some sandboxing introduced by Microsoft?

The bad FPS problem may not be only a performance problem

One report from Legcord 1.2.2 showed Discord displaying undefinedFPS when the user selected 1080p at 60 FPS.

That looks different from a stream that correctly requests 60 FPS but only delivers 15. An undefinedFPS label suggests that one of Discord's expected configuration values, experiment values, patched functions, or quality-option mappings did not exist in the form Legcord expected.

Because Legcord loads the live Discord web application, Discord can change that internal code independently of a Legcord release. A patch that works against one build can stop matching after Discord changes a module, renames a property, or moves an experiment.

This is another reason the feature is difficult to maintain. We are not only supporting three operating systems and several graphics stacks. We are integrating with a remote application that can update at any time.

We're trying our best to keep track with Discord's screensharing code updates.

We do our patches using Shelter with a solution I found over 2 years ago now.

Another KEY observation I made with my friend Zach, was that Legcord screenshare goes laggy when app is minimized/working in background. If I recall correctly, I added some flags to remedy that, however this requires a lot more testing and investigating. That might just be it! Who knows...

What Legcord already tries to do

I do not want this post to give impression that screen sharing was implemented by enabling it in Electron and hoping for the best

Legcord already contains a considerable amount of custom platform-specific hacks:

  • a custom Electron display-media request handler;
  • a source picker connected through IPC;
  • support for Electron's native macOS picker;
  • PipeWire WebRTC capture flags on Wayland;
  • PulseAudio loopback support on Linux;
  • Venmic integration for application-specific PipeWire routing;
  • exclusion rules intended to keep Discord's audio out of the shared stream;
  • performance and screensharing presets;
  • macOS loopback-related Chromium features in the development branch;
  • configuration for multiple audio-loopback methods;
  • detection of Electron's separate audio-service process.

Problem is that each layer relies on APIs and internal behavior which continues to evolve.

Electron has to update its Chromium version frequently and that affects screen capturing behavior. Portals can vary between desktop environments (Gnome had issues for a long time). Apple can randomly reset Legcords permissions. Discord can change its internal streaming code without announcing it to third-party clients (cause why would they), causing to break our custom scripts.

We simply don't have enough power from our level of integration to make a good implementation with all the neccesary checks.

(I wish someone proved me wrong on that one)

What a proper fix would look like

The first requirement is better diagnostics.

Every useful screensharing report should ideally include:

Environment

  • Legcord version and distribution format (especially on Linux, we wanna know if it's a Flatpak/Deb/AppImage etc.);
  • Electron and Chromium versions;
  • operating system and exact release;
  • X11 or Wayland on Linux;
  • desktop environment and portal implementation;
  • GPU model and driver version (but don't expect any of the devs to have a matching GPU to fully debug your issue);
  • whether hardware acceleration and VA-API are enabled/working (copy GPU info button in Legcord settings);
  • monitor count, resolutions, refresh rates and HDR state;
  • source type: window, monitor or application;
  • requested resolution and frame rate.

Video pipeline

  • capture backend selected by Chromium;
  • actual input frame rate;
  • source dimensions and pixel format;
  • frames received, encoded and dropped;
  • codec negotiated by Discord;
  • encoder implementation selected by WebRTC;
  • whether the encoder is hardware or software;
  • average encode time;
  • target and actual bitrate;
  • WebRTC quality-limitation reason.

Audio pipeline

  • selected loopback method;
  • selected PipeWire or operating-system nodes;
  • included and excluded process IDs;
  • whether the Electron audio-service PID changed;
  • sample rate and channel count;
  • whether samples are arriving on the media track;
  • whether the track is muted, silent or disconnected;
  • system permission state;
  • route changes made while the stream was running.

Most useful tools will likely include Chromium's WebRTC internals page, Chromium tracing, Electron process logs, Devtools console logs, PipeWire graph dump, media track statistics and POSSIBLY small Electron reproduction.

A standalone reproduction is especially valuable. If a 100-line Electron application exhibits the same problem, we can report it upstream without requiring Electron maintainers to understand Legcord, Discord or our patches. If the small application works while Legcord fails, we know the bug is probably in our integration.

This is a huge cry for help

That is the main the reason why I am writing this.

Legcord needs help from people familiar with Electron, Chromium, WebRTC, PipeWire, PulseAudio, CoreAudio, ScreenCaptureKit, Windows capture APIs, media codecs, VA-API, graphics drivers, or any of the desktop environments involved.

You do not need to understand the entire pipeline.

A useful contribution could be:

  • producing a minimal Electron reproduction of one failure;
  • testing the development branch on several macOS versions;
  • verifying whether the new macOS usage description fixes dead audio;
  • recording WebRTC statistics during a choppy stream;
  • determining which encoder Chromium actually selects;
  • tracing a Wayland request from the portal to PipeWire;
  • improving request-scoped IPC in the source picker;
  • making Venmic exclusions survive audio-process restarts;
  • documenting a reproducible hardware and driver combination;
  • comparing Legcord with another Electron client on the same machine;
  • reviewing the implementation and explaining where my assumptions are wrong.
  • investigating Discord internals (Flux events and whatnot) using bundled Shelter

Even a detailed issue report can help, provided it contains more than “audio doesn't work.”

Screen sharing is one of those features that looks simple from the outside because the interface is one button. Behind that button is a real-time media system assembled from components maintained by several different projects, each with its own platform abstractions, release cycle and idea of what “supported” means.

Legcord currently tries to connect all of those components with a relatively small amount of code and quite a lot of hope.

Some obvious improvements can already be made. The macOS permission fix in the development branch should be released and tested. The picker needs request-specific IPC and much better logging. PipeWire routing needs more robust identification of Discord's own output. Actual WebRTC statistics need to replace guesswork about whether a stream is hardware accelerated.

Those changes will help, but they will not solve every platform and every machine.

PRs that redesign the screensharing page for the 100th time should not be the main focus. And please refrain from submitting fully vibecoded PRs with no thought process/engineering behind them.

So yes I'm very much aware that Legcord screen sharing sucks!

I would very much like it to suck less.

The source is open, the issue tracker is open, and any help from a complete implementation to one good trace is welcome.

more posts | rss feed