Compare commits

...

5 commits

Author SHA1 Message Date
Patrick
73d26986e2 feat: sdp baseline rewrite on windows
enables hw accelerated streams
2026-07-23 22:58:29 +02:00
smartfrigde
fd164d0b2f chore: lint 2026-07-23 21:48:00 +02:00
smartfrigde
0d3f7b0008 fix: linux screensharing tweaks
most notably disabling vaapi disables also software encode/decode
2026-07-23 21:45:03 +02:00
smartfrigde
f64eb87d70 fix: RTP ceiling raise 2026-07-23 20:56:03 +02:00
smartfrigde
20f557e721 feat: hw enabled screensharing on macOS
possibly other platforms too
2026-07-23 19:54:48 +02:00
10 changed files with 640 additions and 80 deletions

View file

@ -91,7 +91,7 @@
"settings-spellcheck": "Spellcheck",
"settings-spellcheck-desc": "Helps you correct misspelled words by highlighting them.",
"settings-vaapi": "VAAPI",
"settings-vaapi-desc": "Use VAAPI (HW acceleration) for video decoding on Linux. This greatly reduces CPU usage during screenshare but may cause issues on some systems. Disable if you experience crashes or black screens during screenshare.",
"settings-vaapi-desc": "Use VAAPI for hardware video encode and decode on Linux (sharer and viewer). Greatly reduces CPU during screenshare, but some GPUs (notably older AMD) produce frozen or blocky streams for viewers — disable to force software OpenH264 encode if that happens.",
"settings-channel": "Discord channel",
"settings-channel-desc": "Use this setting to change current instance of Discord that Legcord is running.",
"settings-bitrateMin": "Minimum bitrate",
@ -113,7 +113,7 @@
"settings-mod-vencord": "Lightweight, and easy to use client mod. Features a built-in store for plugins.",
"settings-mod-equicord": "Forked and born from vencord contributors, featuring a pretty plugin-rich client.",
"settings-prfmMode": "Performance mode",
"settings-prfmMode-desc": "Performance Mode is an experimental feature in Legcord designed to optimize responsiveness and performance based on your needs. The impact may vary depending on your hardware and usage, so we encourage you to try each mode to determine which works best for you.",
"settings-prfmMode-desc": "Optimizes responsiveness and GPU behavior. When Hardware Acceleration is on, Legcord always enables WebRTC hardware encode/decode for screenshare and calls; these modes add broader GPU, latency, or battery tradeoffs on top. Screenshare bitrate is auto-capped for stability — pick resolution and FPS in the share picker.",
"settings-prfmMode-performance": "Performance",
"settings-prfmMode-balanced": "Balanced",
"settings-prfmMode-battery": "Battery",
@ -153,6 +153,8 @@
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
"settings-copyDebugInfo": "Copy Debug Info",
"settings-copyGPUInfo": "Copy GPU Info",
"settings-openWebRTCInternals": "Open WebRTC Internals",
"settings-openGPUInfo": "Open GPU Info",
"settings-clearClientModCache": "Clear client mod cache",
"settings-forceNativeCrash": "Force native crash",
"settings-smoothScroll": "Use smooth scrolling",

View file

@ -46,6 +46,8 @@ export interface LegcordWindow {
openCustomIconDialog: () => void;
copyDebugInfo: () => void;
copyGPUInfo: () => void;
openWebRTCInternals: () => void;
openGPUInfo: () => void;
setLang(lang: string): () => void;
addKeybind: (keybind: Keybind) => void;
toggleKeybind: (id: string) => void;

View file

@ -1,4 +1,4 @@
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { app, powerMonitor } from "electron";
import isDev from "electron-is-dev";
@ -60,12 +60,13 @@ const memory: Preset = {
disableFeatures: [],
};
/** Favor voice/video call quality with HW WebRTC encode/decode. */
/** Favor voice/video call quality. Platform-specific HW encode is layered on later. */
const voip: Preset = {
switches: [
["enable-gpu-rasterization"],
["enable-zero-copy"],
["ignore-gpu-blocklist"],
["enable-accelerated-video-decode"],
["force_high_performance_gpu"],
["disable-background-timer-throttling"],
["disable-renderer-backgrounding"],
@ -77,8 +78,6 @@ const voip: Preset = {
"WebRtcHWEncoding",
"AcceleratedVideoDecoder",
"AcceleratedVideoEncoder",
"AcceleratedVideoDecodeLinuxGL",
"AcceleratedVideoDecodeLinuxZeroCopyGL",
"ZeroCopyDesktopCapture",
],
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
@ -107,11 +106,14 @@ const smoothExperiment: Preset = {
["enable-gpu-rasterization"],
["enable-zero-copy"],
["ignore-gpu-blocklist"],
["enable-accelerated-video-decode"],
["disable-background-timer-throttling"],
["disable-renderer-backgrounding"],
["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"],
["force_high_performance_gpu"],
["use-gl", "desktop"],
// Do NOT set use-gl=desktop here. On Electron 43+/macOS, Chromium only allows
// ANGLE (metal/opengl); use-gl=desktop fails GPU init and then disables all
// HW acceleration (including VideoToolbox encode) after repeated crashes.
],
enableFeatures: [
"EnableDrDc",
@ -120,10 +122,10 @@ const smoothExperiment: Preset = {
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
"UseSkiaRenderer",
"WebAssemblyLazyCompilation",
"AcceleratedVideoDecodeLinuxGL",
"WebRtcHWEncoding",
"WebRtcHWDecoding",
"AcceleratedVideoEncoder",
"AcceleratedVideoDecoder",
"AcceleratedVideoDecodeLinuxZeroCopyGL",
"ZeroCopyDesktopCapture",
],
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
@ -141,23 +143,174 @@ const battery: Preset = {
disableFeatures: [],
};
const vaapi: Preset = {
/**
* Shared WebRTC / screenshare baseline (no platform encode backend).
* Encode is added by macVideoToolbox / winVideoEncode / linux vaapi|software.
*/
const webrtcHwCommon: Preset = {
switches: [
["ignore-gpu-blocklist"],
["enable-gpu-rasterization"],
["enable-zero-copy"],
["force_high_performance_gpu"],
["use-gl", "desktop"],
],
enableFeatures: [
"AcceleratedVideoDecodeLinuxGL",
"AcceleratedVideoEncoder",
"AcceleratedVideoDecoder",
"AcceleratedVideoDecodeLinuxZeroCopyGL",
["enable-accelerated-video-decode"],
["enable-gpu-memory-buffer-video-frames"],
],
enableFeatures: ["WebRtcHWDecoding", "AcceleratedVideoDecoder", "ZeroCopyDesktopCapture", "CanvasOopRasterization"],
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
};
/** Windows Media Foundation / Chromium HW encode path. */
const winVideoEncode: Preset = {
switches: [
// Legacy Chromium switches still honored by Electron's WebRTC stack
["webrtc-hw-encoding"],
["webrtc-hw-decoding"],
],
enableFeatures: [
"WebRtcHWEncoding",
"AcceleratedVideoEncoder",
// Off by default on Windows; without it CBP (Discord's 42e01f) stays on OpenH264.
// SDP munge prefers Baseline, but keep CBP HW as a fallback if negotiation reverts.
"PlatformH264CbpEncoding",
],
disableFeatures: [],
};
/**
* Linux: force software OpenH264 encode when VAAPI is off.
* AMD VCE via VaapiVideoEncodeAccelerator can freeze Discord screenshare for viewers
* even when chrome://gpu lists encode profiles.
*/
const linuxSoftwareVideoEncode: Preset = {
switches: [],
enableFeatures: [],
disableFeatures: ["AcceleratedVideoEncoder", "VaapiVideoEncoder", "WebRtcHWEncoding"],
};
/** macOS VideoToolbox HW encode/decode (Intel + Apple Silicon). */
const macVideoToolbox: Preset = {
switches: [
["ignore-gpu-blocklist"],
// After use-gl=desktop / other GPU init failures, Chromium may keep GPU disabled
// due to "frequent crashes" even once the bad flag is gone — clear that limit.
["disable-gpu-process-crash-limit"],
["enable-zero-copy"],
["enable-accelerated-video-decode"],
// Legacy Chromium switches still honored by Electron's WebRTC stack
["webrtc-hw-encoding"],
["webrtc-hw-decoding"],
["enable-gpu-memory-buffer-video-frames"],
],
enableFeatures: [
"MacosVideoToolbox",
"VideoToolboxVideoDecoder",
"WebRtcHWEncoding",
"WebRtcHWDecoding",
"AcceleratedVideoEncoder",
"AcceleratedVideoDecoder",
"ZeroCopyDesktopCapture",
// Helps enumerate platform HW encoders used by WebRTC on Apple GPUs
"PlatformHEVCEncoderSupport",
],
disableFeatures: [],
};
/** Linux VA-API encode/decode (only applied when vaapi setting is on). */
const linuxVaapi: Preset = {
switches: [
["ignore-gpu-blocklist"],
["disable-gpu-process-crash-limit"],
["enable-gpu-rasterization"],
["enable-zero-copy"],
["enable-accelerated-video-decode"],
["force_high_performance_gpu"],
// ANGLE+OpenGL required for AcceleratedVideoDecodeLinuxGL on Wayland.
// Do NOT use use-gl=desktop (removed in Electron 43+ / breaks GPU init).
["use-gl", "angle"],
["use-angle", "gl"],
["enable-gpu-memory-buffer-video-frames"],
],
enableFeatures: [
"VaapiIgnoreDriverChecks",
"VaapiVideoDecoder",
"VaapiVideoEncoder",
"AcceleratedVideoEncoder",
"AcceleratedVideoDecoder",
"AcceleratedVideoDecodeLinuxGL",
"AcceleratedVideoDecodeLinuxZeroCopyGL",
"WebRtcHWEncoding",
"WebRtcHWDecoding",
"ZeroCopyDesktopCapture",
"CanvasOopRasterization",
],
// Vulkan is incompatible with ozone wayland and breaks VAAPI GL interop.
disableFeatures: ["UseChromeOSDirectVideoDecoder", "Vulkan"],
};
/**
* Fedora/RPM Fusion ships H.264/HEVC VA-API in dri-freeworld (patent-encumbered), while
* stock /usr/lib64/dri only exposes MPEG2/JPEG. Prefer freeworld so chrome://gpu lists
* real encode/decode profiles instead of an empty Video Acceleration Information block.
*/
export function configureLinuxVaapiEnvironment(): void {
if (process.platform !== "linux") return;
const candidates = [
"/usr/lib64/dri-freeworld",
"/usr/lib64/dri-nonfree",
"/usr/lib/dri-freeworld",
"/usr/lib/dri-nonfree",
];
const preferred = candidates.filter((dir) => existsSync(dir));
if (preferred.length === 0) return;
const stock = ["/usr/lib64/dri", "/usr/lib/dri"].filter((dir) => existsSync(dir));
const parts = [...preferred, ...stock];
const existing = process.env.LIBVA_DRIVERS_PATH;
process.env.LIBVA_DRIVERS_PATH = existing ? `${parts.join(":")}:${existing}` : parts.join(":");
console.log(`VAAPI: using LIBVA_DRIVERS_PATH=${process.env.LIBVA_DRIVERS_PATH}`);
}
/** Strip Linux encode features that shared presets may have enabled. */
function withoutLinuxHwEncode(preset: Preset): Preset {
const block = new Set(["AcceleratedVideoEncoder", "VaapiVideoEncoder", "WebRtcHWEncoding"]);
return {
switches: preset.switches,
enableFeatures: preset.enableFeatures.filter((f) => !block.has(f)),
disableFeatures: [...new Set([...preset.disableFeatures, ...block])],
};
}
/**
* Apply the platform-tested WebRTC / screenshare encode+decode stack.
* macOS VideoToolbox; Windows Chromium HW encode; Linux VAAPI toggle.
*/
function applyPlatformVideoStack(base: Preset | undefined): Preset | undefined {
if (!getConfig("hardwareAcceleration")) return base;
const preset = base ? mergePresets(base, webrtcHwCommon) : webrtcHwCommon;
console.log("WebRTC HW baseline enabled");
switch (process.platform) {
case "darwin":
console.log("macOS VideoToolbox HW encode/decode flags enabled");
return mergePresets(preset, macVideoToolbox);
case "win32":
console.log("Windows HW video encode flags enabled");
return mergePresets(preset, winVideoEncode);
case "linux":
if (getConfig("vaapi")) {
console.log("Linux VAAPI HW encode/decode flags enabled");
configureLinuxVaapiEnvironment();
return mergePresets(preset, linuxVaapi);
}
console.log("Linux VAAPI off — forcing software WebRTC video encode");
return withoutLinuxHwEncode(mergePresets(preset, linuxSoftwareVideoEncode));
default:
// Other Unix-likes: keep decode baseline; enable generic HW encode.
return mergePresets(preset, winVideoEncode);
}
}
/**
* Load custom flags from JSON file in user data directory (cached after first load)
* Path:
@ -305,10 +458,10 @@ export function getPreset(): Preset | undefined {
console.log("No performance modes set");
}
if (getConfig("vaapi")) {
console.log("VAAPI flags enabled");
preset = preset ? mergePresets(preset, vaapi) : vaapi;
}
// Platform-specific video encode/decode (macOS VideoToolbox / Win HW / Linux VAAPI).
// Shared voip/smoothScreenshare presets must not carry Linux-only or cross-platform
// encode flags that would undermine the macOS stack we just fixed.
preset = applyPlatformVideoStack(preset);
if (preset) {
return mergeWithCustomFlags(preset);

View file

@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import type { Game } from "arrpc";
import { app, type BrowserWindow, clipboard, dialog, ipcMain, shell } from "electron";
import { app, BrowserWindow, clipboard, dialog, ipcMain, shell } from "electron";
import isDev from "electron-is-dev";
import type { Keybind } from "../@types/keybind.js";
import type { Settings } from "../@types/settings.js";
@ -71,6 +71,36 @@ function ifExistsRead(path: string): string | undefined {
let ipcRegistered = false;
const chromeInternalsWindows = new Map<string, BrowserWindow>();
function openChromeInternalsPage(url: "chrome://webrtc-internals/" | "chrome://gpu/", title: string): void {
const existing = chromeInternalsWindows.get(url);
if (existing && !existing.isDestroyed()) {
if (existing.isMinimized()) existing.restore();
existing.focus();
return;
}
const win = new BrowserWindow({
width: 1100,
height: 800,
minWidth: 640,
minHeight: 480,
title,
autoHideMenuBar: true,
webPreferences: {
sandbox: true,
nodeIntegration: false,
contextIsolation: true,
},
});
void win.loadURL(url);
chromeInternalsWindows.set(url, win);
win.on("closed", () => {
chromeInternalsWindows.delete(url);
});
}
export function registerIpc(passedWindow: BrowserWindow): void {
if (ipcRegistered) return;
ipcRegistered = true;
@ -355,6 +385,12 @@ export function registerIpc(passedWindow: BrowserWindow): void {
ipcMain.on("copyGPUInfo", () => {
clipboard.writeText(JSON.stringify(app.getGPUFeatureStatus()));
});
ipcMain.on("openWebRTCInternals", () => {
openChromeInternalsPage("chrome://webrtc-internals/", "WebRTC Internals");
});
ipcMain.on("openGPUInfo", () => {
openChromeInternalsPage("chrome://gpu/", "GPU");
});
ipcMain.on("openCustomIconDialog", () => {
dialog
.showOpenDialog({

View file

@ -51,6 +51,8 @@ contextBridge.exposeInMainWorld("legcord", {
openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"),
copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"),
copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"),
openWebRTCInternals: () => ipcRenderer.send("openWebRTCInternals"),
openGPUInfo: () => ipcRenderer.send("openGPUInfo"),
dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput,
},
touchbar: {

View file

@ -30,6 +30,106 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
}
}
// Raise Chromium's ~2500kbps screenshare SDP cap before Discord binds RTCPeerConnection.
// Shelter plugins load too late / MediaEngine may keep the original method reference.
// On macOS/Windows also rewrite H.264 Constrained Baseline → Baseline on local *and* remote SDP:
// Discord's Go Live answer forces profile-level-id=42e01f (OpenH264); local-only munging
// is overwritten by setRemoteDescription, so the answer must be rewritten too.
// Without this, Windows burns CPU on OpenH264 even when Media Foundation HW encode is available.
{
const bitrateScript = document.createElement("script");
bitrateScript.textContent = `(function () {
var CAP = "80000";
// Modest floor/start (kbps) so GCC probes above Discord's ~12.5 Mbps screenshare default
// without fighting congestion control. Max remains the hard ceiling.
var MIN_BR = "3000";
var START_BR = "3000";
// VideoToolbox (macOS) and Media Foundation (Windows) HW encode avoid OpenH264 CBP.
var preferHwH264 = ${process.platform === "darwin" || process.platform === "win32" ? "true" : "false"};
function setOrAppendFmtpParam(sdp, key, value) {
var re = new RegExp(key + "=\\\\d+", "g");
if (sdp.indexOf(key + "=") !== -1) {
return sdp.replace(re, key + "=" + value);
}
return sdp.replace(/(a=fmtp:\\d+ [^\\r\\n]*)/g, function (line) {
if (line.indexOf(key + "=") !== -1) return line;
return line + ";" + key + "=" + value;
});
}
function mungeSdp(sdp) {
if (!sdp || typeof sdp !== "string") return sdp;
var out = sdp;
// Chromium uses OpenH264 for Constrained Baseline (42e0xx). Rewrite to Baseline
// (4200xx) so VideoToolbox / Media Foundation HW H.264 can be selected.
// See discuss-webrtc: CBP uses software encoder for historical reasons.
if (preferHwH264) {
out = out.replace(/profile-level-id=42e0([0-9a-fA-F]{2})/gi, "profile-level-id=4200$1");
}
out = setOrAppendFmtpParam(out, "x-google-max-bitrate", CAP);
out = setOrAppendFmtpParam(out, "x-google-min-bitrate", MIN_BR);
out = setOrAppendFmtpParam(out, "x-google-start-bitrate", START_BR);
return out;
}
function wrapDescription(desc) {
if (!desc || !desc.sdp) return desc;
var sdp = mungeSdp(desc.sdp);
if (sdp === desc.sdp) return desc;
try {
return new RTCSessionDescription({ type: desc.type, sdp: sdp });
} catch (e) {
return Object.assign({}, desc, { sdp: sdp });
}
}
var proto = window.RTCPeerConnection && window.RTCPeerConnection.prototype;
if (!proto) return;
var origSLD = proto.setLocalDescription;
proto.setLocalDescription = function (desc) {
var args = Array.prototype.slice.call(arguments);
if (args.length > 0) args[0] = wrapDescription(args[0]);
return origSLD.apply(this, args);
};
var origSRD = proto.setRemoteDescription;
proto.setRemoteDescription = function (desc) {
var args = Array.prototype.slice.call(arguments);
if (args.length > 0) args[0] = wrapDescription(args[0]);
return origSRD.apply(this, args);
};
var origOffer = proto.createOffer;
proto.createOffer = function () {
var self = this;
var args = arguments;
return Promise.resolve(origOffer.apply(self, args)).then(function (offer) {
return wrapDescription(offer) || offer;
});
};
var origAnswer = proto.createAnswer;
if (origAnswer) {
proto.createAnswer = function () {
var self = this;
var args = arguments;
return Promise.resolve(origAnswer.apply(self, args)).then(function (answer) {
return wrapDescription(answer) || answer;
});
};
}
// Do NOT patch RTCRtpSender.setParameters to force high maxBitrate — that fights
// Discord/WebRTC congestion control and collapses streams to tiny resolutions.
console.log("[Legcord] Early WebRTC screenshare SDP patch installed" + (preferHwH264 ? " (H264 CBP→Baseline on local+remote)" : ""));
})();`;
if (document.documentElement) {
document.documentElement.prepend(bitrateScript);
} else {
const observer = new MutationObserver(() => {
if (document.documentElement) {
observer.disconnect();
document.documentElement.prepend(bitrateScript);
}
});
observer.observe(document, { childList: true });
}
}
// Fix: Chromium on macOS ignores video deviceId when passed as an "ideal" constraint
// (plain string), always returning the first camera. Discord passes deviceId this way.
// This patch promotes "ideal" to "exact", stops active tracks before switching so macOS
@ -132,42 +232,9 @@ const version = ipcRenderer.sendSync("displayVersion") as string;
}
}
export async function getVirtmic() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioDevice = devices.find(({ label }) => label === "vencord-screen-share");
return audioDevice?.deviceId;
} catch (_error) {
return null;
}
}
async function load() {
await sleep(5000).then(() => {
const original = navigator.mediaDevices.getDisplayMedia;
navigator.mediaDevices.getDisplayMedia = async function (opts) {
const stream = await original.call(this, opts);
const id = await getVirtmic();
if (id) {
const audio = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: {
exact: id,
},
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false,
},
});
audio.getAudioTracks().forEach((t) => {
stream.addTrack(t);
});
}
return stream;
};
// Venmic audio injection lives in the Shelter screenshare getDisplayMedia patch.
// dirty hack to make clicking notifications focus Legcord
addScript(`
(() => {

View file

@ -186,6 +186,15 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
enableFeatures.add("MacLoopbackAudioForScreenShare");
enableFeatures.add("MacSckSystemAudioLoopbackOverride");
enableFeatures.add("MacCatapSystemAudioLoopbackCapture");
// VideoToolbox path for WebRTC H.264 (Intel + Apple Silicon)
enableFeatures.add("MacosVideoToolbox");
enableFeatures.add("VideoToolboxVideoDecoder");
if (getConfig("hardwareAcceleration")) {
app.commandLine.appendSwitch("webrtc-hw-encoding");
trackSwitch("webrtc-hw-encoding");
app.commandLine.appendSwitch("webrtc-hw-decoding");
trackSwitch("webrtc-hw-decoding");
}
}
// work around chrome 66 disabling autoplay by default
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");

View file

@ -38,20 +38,47 @@ export async function patchNavigator(requestAudio = false) {
const stream = await original.call(this, opts);
const video = stream.getVideoTracks()[0];
const width = store.resolution * (16 / 9);
const width = Math.round(store.resolution * (16 / 9));
const height = store.resolution;
// Prefer smoothness at 30+ FPS; detail/text trades FPS for sharpness (hurts Go Live badly).
const contentHint = store.fps >= 30 ? "motion" : "detail";
const stream_constraints: MediaTrackConstraints = {
frameRate: store.fps,
width: width,
height: height,
frameRate: { ideal: store.fps, min: Math.min(15, store.fps) },
width: { min: 640, ideal: width, max: width },
height: { min: 480, ideal: height, max: height },
// @ts-expect-error non-standard but used by Chromium desktop capture
advanced: [{ width, height }],
// crop-and-scale so capture matches the picker resolution. "none" kept native
// panel size (e.g. 2304x1440) and forced software/HW encode of full desktop.
// @ts-expect-error Chromium supports resizeMode on display tracks
resizeMode: "crop-and-scale",
};
if (video) {
try {
video.contentHint = contentHint;
} catch {
// contentHint is best-effort
}
video
.applyConstraints(stream_constraints)
.then(() => {
console.log(`Stream modified -> (${width}x${height}) ${store.fps}FPS`);
const settings = video.getSettings();
console.log(
`Stream modified -> requested (${width}x${height}) ${store.fps}FPS hint=${contentHint}; actual (${settings.width ?? "?"}x${settings.height ?? "?"}) ${settings.frameRate ?? "?"}FPS`,
);
if (
typeof settings.width === "number" &&
typeof settings.height === "number" &&
(settings.width > width * 1.25 || settings.height > height * 1.25)
) {
console.warn(
`[Screenshare] Capture is larger than requested (${settings.width}x${settings.height} vs ${width}x${height}); encode may still downscale in software.`,
);
}
})
.catch((error) => {
console.error("Failed to apply video constraints:", error);

View file

@ -14,33 +14,277 @@ const {
} = shelter;
store.fps ??= 30; // set default
store.resolution ??= 720; // set default
store.resolution ??= 1080; // 1080p is a safer default than 2K on integrated GPUs
function onStreamQualityChange() {
// @ts-expect-error fix types
const mediaConnections = [...MediaEngineStore.getMediaEngine().connections];
// @ts-expect-error fix types
const currentUserId = UserStore.getCurrentUser().id;
const BITRATE_CEILING = 25_000_000;
const REAPPLY_DELAYS_MS = [1000, 3000, 8000] as const;
/** Discord-like Go Live targets (bits/s) by height and fps band. */
function targetBitrateFor(height: number, fps: number): number {
const highFps = fps > 30;
const table: Record<number, [number, number]> = {
480: [2_500_000, 3_500_000],
720: [4_000_000, 6_000_000],
1080: [8_000_000, 10_000_000],
1440: [12_000_000, 15_000_000],
2160: [18_000_000, 25_000_000],
};
const nearest = Object.keys(table)
.map(Number)
.sort((a, b) => Math.abs(a - height) - Math.abs(b - height))[0];
const [low, high] = table[nearest] ?? [4_000_000, 6_000_000];
return Math.min(highFps ? high : low, BITRATE_CEILING);
}
type StreamConnection = {
pc?: RTCPeerConnection;
peerConnection?: RTCPeerConnection;
_pc?: RTCPeerConnection;
streamUserId?: string;
videoStreamParameters?: Array<{
maxFrameRate?: number;
maxResolution?: { type: string; width: number; height: number };
maxPixelCount?: number;
maxBitrate?: number;
}>;
videoQualityManager?: {
goliveMaxQuality?: {
bitrateMin?: number;
bitrateMax?: number;
bitrateTarget?: number;
};
};
};
let loggedEncoderForCurrentStream = false;
let reapplyTimers: ReturnType<typeof setTimeout>[] = [];
let reapplyGeneration = 0;
function clearReapplyTimers(): void {
for (const t of reapplyTimers) clearTimeout(t);
reapplyTimers = [];
reapplyGeneration++;
}
function getLocalStreamConnection(): StreamConnection | undefined {
// @ts-expect-error Discord MediaEngine typings
const mediaConnections = [...MediaEngineStore.getMediaEngine().connections] as StreamConnection[];
// @ts-expect-error Discord UserStore typings
const currentUserId = UserStore.getCurrentUser().id as string;
return mediaConnections.find((connection) => connection.streamUserId === currentUserId);
}
function getOutboundVideoSender(streamConnection: StreamConnection): RTCRtpSender | undefined {
const pc = streamConnection.pc ?? streamConnection.peerConnection ?? streamConnection._pc;
return pc?.getSenders?.().find((s) => s.track?.kind === "video");
}
/** Prefer H.264 profiles that map to platform HW encode (not OpenH264 CBP). */
function preferPlatformHwH264(streamConnection: StreamConnection): void {
const platform = window.legcord.platform;
if (platform !== "darwin" && platform !== "win32") return;
try {
const sender = getOutboundVideoSender(streamConnection);
if (!sender?.setCodecPreferences || typeof RTCRtpSender.getCapabilities !== "function") return;
const caps = RTCRtpSender.getCapabilities("video");
if (!caps?.codecs?.length) return;
const rank = (codec: RTCRtpCodec) => {
const mime = codec.mimeType.toLowerCase();
const fmtp = (codec.sdpFmtpLine ?? "").toLowerCase();
if (!mime.includes("h264")) {
if (mime.includes("vp9")) return 10;
if (mime.includes("vp8")) return 11;
return 20;
}
// Avoid Constrained Baseline → OpenH264 software path
if (/profile-level-id=42e0/.test(fmtp)) return 5;
// Baseline / Main / High → VideoToolbox (macOS) or Media Foundation (Windows)
if (/profile-level-id=4200/.test(fmtp)) return 0;
if (/profile-level-id=4d00/.test(fmtp)) return 1;
if (/profile-level-id=6400/.test(fmtp)) return 2;
return 3;
};
const preferred = [...caps.codecs].sort((a, b) => rank(a) - rank(b));
sender.setCodecPreferences(preferred);
const backend = platform === "darwin" ? "VideoToolbox" : "MediaFoundation";
log(
`Preferred ${backend} H264 codecs: ${preferred
.slice(0, 6)
.map((c) => `${c.mimeType}${c.sdpFmtpLine ? ` (${c.sdpFmtpLine})` : ""}`)
.join(", ")}`,
);
} catch (e) {
console.warn("[Screenshare] Failed to prefer platform HW H264:", e);
}
}
function formatMbps(bps: number | undefined | null): string {
if (bps == null || Number.isNaN(bps)) return "?";
return `${(bps / 1_000_000).toFixed(1)}Mbps`;
}
/** Cap RTP framerate / bitrate ceiling without forcing scale (Discord adapts spatial layers). */
async function applySenderEncodeLimits(
streamConnection: StreamConnection,
_height: number,
fps: number,
maxBitrate: number,
): Promise<void> {
try {
const sender = getOutboundVideoSender(streamConnection);
if (!sender?.getParameters || !sender.setParameters) return;
const params = sender.getParameters();
if (!params.encodings?.length) {
params.encodings = [{}];
}
const before = params.encodings.map((e) => e.maxBitrate);
for (const encoding of params.encodings) {
// Raise whenever Discord/Chromium left a lower ceiling — never fight intentional
// downscales by forcing scaleResolutionDownBy.
const current = encoding.maxBitrate;
if (current == null || current < maxBitrate) {
encoding.maxBitrate = maxBitrate;
}
encoding.maxFramerate = fps;
}
await sender.setParameters(params);
const after = sender.getParameters().encodings?.map((e) => e.maxBitrate) ?? [];
log(
`Applied RTP sender limits: maxBitrate ${before.map(formatMbps).join(",")}${after.map(formatMbps).join(",")} (ceiling ${formatMbps(maxBitrate)}) maxFramerate=${fps}`,
);
} catch (e) {
console.warn("[Screenshare] Failed to apply RTP sender encode limits:", e);
}
}
async function logEncoderImplementation(streamConnection: StreamConnection): Promise<void> {
try {
const sender = getOutboundVideoSender(streamConnection);
if (!sender?.getStats) return;
// Encoder stats appear shortly after the stream starts
await new Promise((r) => setTimeout(r, 1500));
// Connection may have been replaced; prefer a fresh local stream handle
const live = getLocalStreamConnection() ?? streamConnection;
const liveSender = getOutboundVideoSender(live) ?? sender;
const encodingParams = liveSender.getParameters?.().encodings?.[0];
const golive = live.videoQualityManager?.goliveMaxQuality;
const stats = await liveSender.getStats();
let availableOutgoingBitrate: number | undefined;
for (const report of stats.values()) {
if (report.type === "candidate-pair" && "availableOutgoingBitrate" in report) {
const nominated = "nominated" in report ? Boolean(report.nominated) : false;
const state = "state" in report ? String(report.state) : "";
if (nominated || state === "succeeded") {
availableOutgoingBitrate = Number(report.availableOutgoingBitrate);
if (nominated) break;
}
}
}
for (const report of stats.values()) {
if (report.type !== "outbound-rtp" || report.kind !== "video") continue;
const impl = "encoderImplementation" in report ? String(report.encoderImplementation) : "unknown";
const mime = "mimeType" in report ? String(report.mimeType) : "unknown";
const scale =
"scalabilityMode" in report
? String(report.scalabilityMode)
: "frameWidth" in report && "frameHeight" in report
? `${report.frameWidth}x${report.frameHeight}`
: "";
const targetBitrate = "targetBitrate" in report ? Number(report.targetBitrate) : undefined;
const qualityLimitationReason =
"qualityLimitationReason" in report ? String(report.qualityLimitationReason) : "?";
const qualityLimitationDurations =
"qualityLimitationDurations" in report && report.qualityLimitationDurations
? JSON.stringify(report.qualityLimitationDurations)
: "?";
log(
[
`Screenshare encoder: implementation=${impl} codec=${mime} ${scale}`.trim(),
`targetBitrate=${formatMbps(targetBitrate)}`,
`availableOutgoingBitrate=${formatMbps(availableOutgoingBitrate)}`,
`qualityLimitationReason=${qualityLimitationReason}`,
`qualityLimitationDurations=${qualityLimitationDurations}`,
`encoding.maxBitrate=${formatMbps(encodingParams?.maxBitrate)}`,
`encoding.maxFramerate=${encodingParams?.maxFramerate ?? "?"}`,
`goliveMaxQuality min/target/max=${formatMbps(golive?.bitrateMin)}/${formatMbps(golive?.bitrateTarget)}/${formatMbps(golive?.bitrateMax)}`,
].join(" | "),
);
return;
}
log("Screenshare encoder: no outbound-rtp video stats yet");
} catch (e) {
console.warn("[Screenshare] Failed to read encoder stats:", e);
}
}
function patchStreamQuality(reason: string): boolean {
const streamConnection = getLocalStreamConnection();
if (!streamConnection) return false;
const width = Math.round(store.resolution * (16 / 9));
const height = store.resolution;
const targetBitrate = targetBitrateFor(height, store.fps);
const bitrateMin = Math.round(targetBitrate * 0.8);
const bitrateMax = Math.min(Math.round(targetBitrate * 1.2), BITRATE_CEILING);
const params = streamConnection.videoStreamParameters?.[0];
if (params) {
params.maxFrameRate = store.fps;
params.maxResolution = { type: "fixed", width, height };
params.maxPixelCount = width * height;
params.maxBitrate = bitrateMax;
}
const golive = streamConnection.videoQualityManager?.goliveMaxQuality;
if (golive) {
golive.bitrateMin = bitrateMin;
golive.bitrateMax = bitrateMax;
golive.bitrateTarget = targetBitrate;
}
void applySenderEncodeLimits(streamConnection, height, store.fps, bitrateMax);
preferPlatformHwH264(streamConnection);
const calculatedTargetBitrate = Math.round(
width * height * store.fps * 0.08, // width * height * fps * bits per pixel value (apprx.)
);
const streamConnection = mediaConnections.find((connection) => connection.streamUserId === currentUserId);
if (streamConnection) {
streamConnection.videoStreamParameters[0].maxFrameRate = store.fps;
streamConnection.videoStreamParameters[0].maxResolution.height = height;
streamConnection.videoStreamParameters[0].maxResolution.width = width;
streamConnection.videoQualityManager.goliveMaxQuality.bitrateMin =
calculatedTargetBitrate - calculatedTargetBitrate * 0.05; // remove 5% of target bitrate for ground bitrate
streamConnection.videoQualityManager.goliveMaxQuality.bitrateMax =
calculatedTargetBitrate + calculatedTargetBitrate * 0.25; // add 25% of target bitrate for ceiling bitrate
streamConnection.videoQualityManager.goliveMaxQuality.bitrateTarget = calculatedTargetBitrate;
log(
`Patched current user's stream with resolution: (${width}x${height}) ${store.fps}FPS @ ${calculatedTargetBitrate / (1000 * 1000)}Mbps.`,
`Patched current user's stream (${reason}) with resolution: (${width}x${height}) ${store.fps}FPS @ ${formatMbps(targetBitrate)} (min ${formatMbps(bitrateMin)} / max ${formatMbps(bitrateMax)}).`,
);
return true;
}
function scheduleStreamQualityReapplies(): void {
clearReapplyTimers();
const generation = reapplyGeneration;
for (const delay of REAPPLY_DELAYS_MS) {
const timer = setTimeout(() => {
if (generation !== reapplyGeneration) return;
if (!getLocalStreamConnection()) return;
patchStreamQuality(`reapply@${delay}ms`);
}, delay);
reapplyTimers.push(timer);
}
}
function onStreamQualityChange() {
if (!patchStreamQuality("quality-changed")) return;
scheduleStreamQualityReapplies();
if (!loggedEncoderForCurrentStream) {
loggedEncoderForCurrentStream = true;
const streamConnection = getLocalStreamConnection();
if (streamConnection) void logEncoderImplementation(streamConnection);
}
}
@ -51,11 +295,15 @@ interface StreamDispatch {
function onStreamEnd(dispatch: StreamDispatch) {
if (!dispatch.streamKey) return;
const owner = dispatch.streamKey.split(":").at(-1);
// @ts-expect-error fix types
const currentUserId = UserStore.getCurrentUser().id;
// @ts-expect-error Discord UserStore typings
const currentUserId = UserStore.getCurrentUser().id as string;
if (dispatch.reason === "user_requested" && owner === currentUserId) {
window.legcord.screenshare.venmicStop();
}
if (owner === currentUserId) {
loggedEncoderForCurrentStream = false;
clearReapplyTimers();
}
}
export function onLoad() {
@ -93,6 +341,7 @@ export function onLoad() {
}
export function onUnload() {
clearReapplyTimers();
dispatcher.unsubscribe("MEDIA_ENGINE_VIDEO_SOURCE_QUALITY_CHANGED", onStreamQualityChange);
dispatcher.unsubscribe("STREAM_DELETE", onStreamEnd);
}

View file

@ -151,9 +151,12 @@ export function SettingsPage() {
t["settings-storageFolder"],
t["settings-copyDebugInfo"],
t["settings-copyGPUInfo"],
t["settings-openWebRTCInternals"],
t["settings-openGPUInfo"],
t["settings-clearClientModCache"],
"venmic",
"VAAPI",
"WebRTC",
],
};
@ -980,6 +983,16 @@ export function SettingsPage() {
{t["settings-copyGPUInfo"]}
</Button>
</SearchableSetting>
<SearchableSetting keywords={[t["settings-openWebRTCInternals"], "WebRTC", "internals"]}>
<Button size={ButtonSizes.MAX} onClick={window.legcord.settings.openWebRTCInternals}>
{t["settings-openWebRTCInternals"]}
</Button>
</SearchableSetting>
<SearchableSetting keywords={[t["settings-openGPUInfo"], "GPU", "chrome"]}>
<Button size={ButtonSizes.MAX} onClick={window.legcord.settings.openGPUInfo}>
{t["settings-openGPUInfo"]}
</Button>
</SearchableSetting>
<SearchableSetting keywords={[t["settings-clearClientModCache"], "cache"]}>
<Button
size={ButtonSizes.MAX}