From 20f557e72110d9b9703281747c03cc13bd98fd52 Mon Sep 17 00:00:00 2001 From: smartfrigde <37928912+smartfrigde@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:54:48 +0200 Subject: [PATCH 1/5] feat: hw enabled screensharing on macOS possibly other platforms too --- assets/lang/en-US.json | 6 +- src/@types/legcordWindow.d.ts | 2 + src/common/flags.ts | 78 +++++++- src/discord/ipc.ts | 38 +++- src/discord/preload/bridge.ts | 2 + src/discord/preload/patches.mts | 124 ++++++++---- src/main.ts | 9 + .../components/ScreensharePicker.tsx | 35 +++- src/shelter/screenshare/index.tsx | 181 ++++++++++++++++-- src/shelter/settings/pages/SettingsPage.tsx | 13 ++ 10 files changed, 431 insertions(+), 57 deletions(-) diff --git a/assets/lang/en-US.json b/assets/lang/en-US.json index 0199d66..7dd124a 100644 --- a/assets/lang/en-US.json +++ b/assets/lang/en-US.json @@ -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). Enables driver-check bypass and WebRTC HW encode paths that greatly reduce CPU during screenshare. May cause crashes or black screens on some GPUs — disable 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", diff --git a/src/@types/legcordWindow.d.ts b/src/@types/legcordWindow.d.ts index 9cb9ebd..2becc98 100644 --- a/src/@types/legcordWindow.d.ts +++ b/src/@types/legcordWindow.d.ts @@ -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; diff --git a/src/common/flags.ts b/src/common/flags.ts index 67ba5b5..47a927a 100644 --- a/src/common/flags.ts +++ b/src/common/flags.ts @@ -66,6 +66,7 @@ const voip: Preset = { ["enable-gpu-rasterization"], ["enable-zero-copy"], ["ignore-gpu-blocklist"], + ["enable-accelerated-video-decode"], ["force_high_performance_gpu"], ["disable-background-timer-throttling"], ["disable-renderer-backgrounding"], @@ -107,11 +108,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,6 +124,8 @@ const smoothExperiment: Preset = { "ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes", "UseSkiaRenderer", "WebAssemblyLazyCompilation", + "WebRtcHWEncoding", + "WebRtcHWDecoding", "AcceleratedVideoDecodeLinuxGL", "AcceleratedVideoEncoder", "AcceleratedVideoDecoder", @@ -141,19 +147,76 @@ const battery: Preset = { disableFeatures: [], }; +/** + * Cross-platform WebRTC / screenshare HW encode baseline. + * Applied whenever hardwareAcceleration is on, including performanceMode "none". + */ +const webrtcHw: Preset = { + switches: [ + ["ignore-gpu-blocklist"], + ["enable-zero-copy"], + ["enable-accelerated-video-decode"], + ["enable-gpu-memory-buffer-video-frames"], + ], + enableFeatures: [ + "WebRtcHWEncoding", + "WebRtcHWDecoding", + "AcceleratedVideoEncoder", + "AcceleratedVideoDecoder", + "ZeroCopyDesktopCapture", + "CanvasOopRasterization", + ], + disableFeatures: ["UseChromeOSDirectVideoDecoder"], +}; + +/** 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 (teams-for-linux #1324 + modern Chromium names). */ const vaapi: Preset = { switches: [ ["ignore-gpu-blocklist"], ["enable-gpu-rasterization"], ["enable-zero-copy"], + ["enable-accelerated-video-decode"], ["force_high_performance_gpu"], ["use-gl", "desktop"], ], enableFeatures: [ - "AcceleratedVideoDecodeLinuxGL", + "VaapiIgnoreDriverChecks", "AcceleratedVideoEncoder", "AcceleratedVideoDecoder", + "AcceleratedVideoDecodeLinuxGL", "AcceleratedVideoDecodeLinuxZeroCopyGL", + "WebRtcHWEncoding", + "WebRtcHWDecoding", + "ZeroCopyDesktopCapture", + "CanvasOopRasterization", ], disableFeatures: ["UseChromeOSDirectVideoDecoder"], }; @@ -310,6 +373,17 @@ export function getPreset(): Preset | undefined { preset = preset ? mergePresets(preset, vaapi) : vaapi; } + // Always enable WebRTC HW encode/decode when GPU acceleration is on (incl. performanceMode "none") + if (getConfig("hardwareAcceleration")) { + console.log("WebRTC HW encode/decode baseline enabled"); + preset = preset ? mergePresets(preset, webrtcHw) : webrtcHw; + + if (process.platform === "darwin") { + console.log("macOS VideoToolbox HW encode/decode flags enabled"); + preset = mergePresets(preset, macVideoToolbox); + } + } + if (preset) { return mergeWithCustomFlags(preset); } diff --git a/src/discord/ipc.ts b/src/discord/ipc.ts index 127e36d..04e3419 100644 --- a/src/discord/ipc.ts +++ b/src/discord/ipc.ts @@ -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(); + +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({ diff --git a/src/discord/preload/bridge.ts b/src/discord/preload/bridge.ts index 09bad96..825cc27 100644 --- a/src/discord/preload/bridge.ts +++ b/src/discord/preload/bridge.ts @@ -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: { diff --git a/src/discord/preload/patches.mts b/src/discord/preload/patches.mts index 361406c..2d8c59b 100644 --- a/src/discord/preload/patches.mts +++ b/src/discord/preload/patches.mts @@ -30,6 +30,95 @@ 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 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. +{ + const bitrateScript = document.createElement("script"); + bitrateScript.textContent = `(function () { + var CAP = "80000"; + var isMac = ${process.platform === "darwin" ? "true" : "false"}; + 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 / platform HW H.264 can be selected on macOS. + // See discuss-webrtc: CBP uses software encoder for historical reasons. + if (isMac) { + out = out.replace(/profile-level-id=42e0([0-9a-fA-F]{2})/gi, "profile-level-id=4200$1"); + } + if (/x-google-max-bitrate=\\d+/.test(out)) { + out = out.replace(/x-google-max-bitrate=\\d+/g, "x-google-max-bitrate=" + CAP); + } else { + out = out.replace(/(a=fmtp:\\d+ [^\\r\\n]*)/g, function (line) { + if (line.indexOf("x-google-max-bitrate") !== -1) return line; + return line + ";x-google-max-bitrate=" + CAP; + }); + } + 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" + (isMac ? " (macOS 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 +221,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(` (() => { diff --git a/src/main.ts b/src/main.ts index 49b65ae..b9a7a4c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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"); diff --git a/src/shelter/screenshare/components/ScreensharePicker.tsx b/src/shelter/screenshare/components/ScreensharePicker.tsx index cbc9638..f8493b2 100644 --- a/src/shelter/screenshare/components/ScreensharePicker.tsx +++ b/src/shelter/screenshare/components/ScreensharePicker.tsx @@ -38,20 +38,45 @@ 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 }], + // @ts-expect-error Chromium supports resizeMode on display tracks + resizeMode: "none", }; 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); diff --git a/src/shelter/screenshare/index.tsx b/src/shelter/screenshare/index.tsx index 7ea0c53..8f03b02 100644 --- a/src/shelter/screenshare/index.tsx +++ b/src/shelter/screenshare/index.tsx @@ -14,7 +14,150 @@ const { } = shelter; store.fps ??= 30; // set default -store.resolution ??= 720; // set default +store.resolution ??= 1080; // 1080p is a safer default than 2K while OpenH264 is in use + +const BITRATE_CEILING = 25_000_000; + +/** 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 = { + 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); +} + +let loggedEncoderForCurrentStream = false; + +function getOutboundVideoSender(streamConnection: { + pc?: RTCPeerConnection; + peerConnection?: RTCPeerConnection; + _pc?: RTCPeerConnection; +}): 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 VideoToolbox HW encode (not OpenH264 CBP). */ +function preferMacVideoToolboxH264(streamConnection: { + pc?: RTCPeerConnection; + peerConnection?: RTCPeerConnection; + _pc?: RTCPeerConnection; +}): void { + if (window.legcord.platform !== "darwin") 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; + if (/profile-level-id=4200/.test(fmtp)) return 0; // Baseline → VideoToolbox + if (/profile-level-id=4d00/.test(fmtp)) return 1; // Main + if (/profile-level-id=6400/.test(fmtp)) return 2; // High + return 3; + }; + + const preferred = [...caps.codecs].sort((a, b) => rank(a) - rank(b)); + sender.setCodecPreferences(preferred); + log( + `Preferred macOS VideoToolbox H264 codecs: ${preferred + .slice(0, 6) + .map((c) => `${c.mimeType}${c.sdpFmtpLine ? ` (${c.sdpFmtpLine})` : ""}`) + .join(", ")}`, + ); + } catch (e) { + console.warn("[Screenshare] Failed to prefer VideoToolbox H264:", e); + } +} + +/** Cap RTP framerate / bitrate ceiling without forcing scale (Discord adapts spatial layers). */ +async function applySenderEncodeLimits( + streamConnection: { + pc?: RTCPeerConnection; + peerConnection?: RTCPeerConnection; + _pc?: RTCPeerConnection; + }, + _height: number, + fps: number, + maxBitrate: number, +): Promise { + try { + const sender = getOutboundVideoSender(streamConnection); + if (!sender?.getParameters || !sender.setParameters) return; + + const params = sender.getParameters(); + if (!params.encodings?.length) { + params.encodings = [{}]; + } + + for (const encoding of params.encodings) { + // Only raise Chromium's default ~2.5Mbps ceiling — never fight adaptive downscales + const current = encoding.maxBitrate; + if (current == null || current === 0 || (current >= 2_000_000 && current <= 2_500_000)) { + encoding.maxBitrate = maxBitrate; + } + encoding.maxFramerate = fps; + // Do not set scaleResolutionDownBy — it fought Discord BWE and crushed quality to 320–640p + } + + await sender.setParameters(params); + log( + `Applied RTP sender limits: maxBitrate ceiling≈${(maxBitrate / 1_000_000).toFixed(1)}Mbps maxFramerate=${fps}`, + ); + } catch (e) { + console.warn("[Screenshare] Failed to apply RTP sender encode limits:", e); + } +} + +async function logEncoderImplementation(streamConnection: { + pc?: RTCPeerConnection; + peerConnection?: RTCPeerConnection; + _pc?: RTCPeerConnection; +}): Promise { + try { + const sender = getOutboundVideoSender(streamConnection); + if (!sender?.getStats) return; + + // Encoder stats appear shortly after the stream starts + await new Promise((r) => setTimeout(r, 1500)); + const stats = await sender.getStats(); + 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}` + : ""; + log(`Screenshare encoder: implementation=${impl} codec=${mime} ${scale}`.trim()); + return; + } + log("Screenshare encoder: no outbound-rtp video stats yet"); + } catch (e) { + console.warn("[Screenshare] Failed to read encoder stats:", e); + } +} function onStreamQualityChange() { // @ts-expect-error fix types @@ -24,23 +167,32 @@ function onStreamQualityChange() { 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 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; + const params = streamConnection.videoStreamParameters[0]; + params.maxFrameRate = store.fps; + params.maxResolution = { type: "fixed", width, height }; + params.maxPixelCount = width * height; + params.maxBitrate = bitrateMax; + + streamConnection.videoQualityManager.goliveMaxQuality.bitrateMin = bitrateMin; + streamConnection.videoQualityManager.goliveMaxQuality.bitrateMax = bitrateMax; + streamConnection.videoQualityManager.goliveMaxQuality.bitrateTarget = targetBitrate; + + void applySenderEncodeLimits(streamConnection, height, store.fps, bitrateMax); + preferMacVideoToolboxH264(streamConnection); + log( - `Patched current user's stream with resolution: (${width}x${height}) ${store.fps}FPS @ ${calculatedTargetBitrate / (1000 * 1000)}Mbps.`, + `Patched current user's stream with resolution: (${width}x${height}) ${store.fps}FPS @ ${(targetBitrate / 1_000_000).toFixed(1)}Mbps (min ${(bitrateMin / 1_000_000).toFixed(1)} / max ${(bitrateMax / 1_000_000).toFixed(1)}).`, ); + if (!loggedEncoderForCurrentStream) { + loggedEncoderForCurrentStream = true; + void logEncoderImplementation(streamConnection); + } } } @@ -56,6 +208,9 @@ function onStreamEnd(dispatch: StreamDispatch) { if (dispatch.reason === "user_requested" && owner === currentUserId) { window.legcord.screenshare.venmicStop(); } + if (owner === currentUserId) { + loggedEncoderForCurrentStream = false; + } } export function onLoad() { diff --git a/src/shelter/settings/pages/SettingsPage.tsx b/src/shelter/settings/pages/SettingsPage.tsx index b08d78e..f8ca06d 100644 --- a/src/shelter/settings/pages/SettingsPage.tsx +++ b/src/shelter/settings/pages/SettingsPage.tsx @@ -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"]} + + + + + +