| ||||||||
| ||||||||
|
blog / Why Legcord screen sharing sucks Why Legcord screen sharing sucksHey 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 controlsLegcord 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 The simplified process looks something like this:
For audio, another branch is added:
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 beginsSome 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, 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:
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:
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 situationOn 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:
Without those measurements, enabling five more Chromium flags is mostly guesswork. Why audio is even worseVideo 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:
Every operating system represents those routes differently. The feedback problemSuppose 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 VenmicLegcord 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 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 streamsHistorically, 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 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:
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 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 solutionWindows 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 problemOne report from Legcord 1.2.2 showed Discord displaying That looks different from a stream that correctly requests 60 FPS but only delivers 15. An 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 doI 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:
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 likeThe first requirement is better diagnostics. Every useful screensharing report should ideally include: Environment
Video pipeline
Audio pipeline
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 helpThat 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:
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. | ||||||||
|
(c) 2026 Patryk Żurek. All rights reserved. |