mirror of
https://github.com/smartfrigde/armcord.git
synced 2026-08-19 14:23:23 +00:00
Compare commits
5 commits
758b97e63e
...
73d26986e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d26986e2 | ||
|
|
fd164d0b2f | ||
|
|
0d3f7b0008 | ||
|
|
f64eb87d70 | ||
|
|
20f557e721 |
10 changed files with 640 additions and 80 deletions
|
|
@ -91,7 +91,7 @@
|
||||||
"settings-spellcheck": "Spellcheck",
|
"settings-spellcheck": "Spellcheck",
|
||||||
"settings-spellcheck-desc": "Helps you correct misspelled words by highlighting them.",
|
"settings-spellcheck-desc": "Helps you correct misspelled words by highlighting them.",
|
||||||
"settings-vaapi": "VAAPI",
|
"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": "Discord channel",
|
||||||
"settings-channel-desc": "Use this setting to change current instance of Discord that Legcord is running.",
|
"settings-channel-desc": "Use this setting to change current instance of Discord that Legcord is running.",
|
||||||
"settings-bitrateMin": "Minimum bitrate",
|
"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-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-mod-equicord": "Forked and born from vencord contributors, featuring a pretty plugin-rich client.",
|
||||||
"settings-prfmMode": "Performance mode",
|
"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-performance": "Performance",
|
||||||
"settings-prfmMode-balanced": "Balanced",
|
"settings-prfmMode-balanced": "Balanced",
|
||||||
"settings-prfmMode-battery": "Battery",
|
"settings-prfmMode-battery": "Battery",
|
||||||
|
|
@ -153,6 +153,8 @@
|
||||||
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
|
"settings-skipSplash-desc": "Skips Legcord splash screen when you start up the app.",
|
||||||
"settings-copyDebugInfo": "Copy Debug Info",
|
"settings-copyDebugInfo": "Copy Debug Info",
|
||||||
"settings-copyGPUInfo": "Copy GPU Info",
|
"settings-copyGPUInfo": "Copy GPU Info",
|
||||||
|
"settings-openWebRTCInternals": "Open WebRTC Internals",
|
||||||
|
"settings-openGPUInfo": "Open GPU Info",
|
||||||
"settings-clearClientModCache": "Clear client mod cache",
|
"settings-clearClientModCache": "Clear client mod cache",
|
||||||
"settings-forceNativeCrash": "Force native crash",
|
"settings-forceNativeCrash": "Force native crash",
|
||||||
"settings-smoothScroll": "Use smooth scrolling",
|
"settings-smoothScroll": "Use smooth scrolling",
|
||||||
|
|
|
||||||
2
src/@types/legcordWindow.d.ts
vendored
2
src/@types/legcordWindow.d.ts
vendored
|
|
@ -46,6 +46,8 @@ export interface LegcordWindow {
|
||||||
openCustomIconDialog: () => void;
|
openCustomIconDialog: () => void;
|
||||||
copyDebugInfo: () => void;
|
copyDebugInfo: () => void;
|
||||||
copyGPUInfo: () => void;
|
copyGPUInfo: () => void;
|
||||||
|
openWebRTCInternals: () => void;
|
||||||
|
openGPUInfo: () => void;
|
||||||
setLang(lang: string): () => void;
|
setLang(lang: string): () => void;
|
||||||
addKeybind: (keybind: Keybind) => void;
|
addKeybind: (keybind: Keybind) => void;
|
||||||
toggleKeybind: (id: string) => void;
|
toggleKeybind: (id: string) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { app, powerMonitor } from "electron";
|
import { app, powerMonitor } from "electron";
|
||||||
import isDev from "electron-is-dev";
|
import isDev from "electron-is-dev";
|
||||||
|
|
@ -60,12 +60,13 @@ const memory: Preset = {
|
||||||
disableFeatures: [],
|
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 = {
|
const voip: Preset = {
|
||||||
switches: [
|
switches: [
|
||||||
["enable-gpu-rasterization"],
|
["enable-gpu-rasterization"],
|
||||||
["enable-zero-copy"],
|
["enable-zero-copy"],
|
||||||
["ignore-gpu-blocklist"],
|
["ignore-gpu-blocklist"],
|
||||||
|
["enable-accelerated-video-decode"],
|
||||||
["force_high_performance_gpu"],
|
["force_high_performance_gpu"],
|
||||||
["disable-background-timer-throttling"],
|
["disable-background-timer-throttling"],
|
||||||
["disable-renderer-backgrounding"],
|
["disable-renderer-backgrounding"],
|
||||||
|
|
@ -77,8 +78,6 @@ const voip: Preset = {
|
||||||
"WebRtcHWEncoding",
|
"WebRtcHWEncoding",
|
||||||
"AcceleratedVideoDecoder",
|
"AcceleratedVideoDecoder",
|
||||||
"AcceleratedVideoEncoder",
|
"AcceleratedVideoEncoder",
|
||||||
"AcceleratedVideoDecodeLinuxGL",
|
|
||||||
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
|
||||||
"ZeroCopyDesktopCapture",
|
"ZeroCopyDesktopCapture",
|
||||||
],
|
],
|
||||||
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
|
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
|
||||||
|
|
@ -107,11 +106,14 @@ const smoothExperiment: Preset = {
|
||||||
["enable-gpu-rasterization"],
|
["enable-gpu-rasterization"],
|
||||||
["enable-zero-copy"],
|
["enable-zero-copy"],
|
||||||
["ignore-gpu-blocklist"],
|
["ignore-gpu-blocklist"],
|
||||||
|
["enable-accelerated-video-decode"],
|
||||||
["disable-background-timer-throttling"],
|
["disable-background-timer-throttling"],
|
||||||
["disable-renderer-backgrounding"],
|
["disable-renderer-backgrounding"],
|
||||||
["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"],
|
["enable-hardware-overlays", "single-fullscreen,single-on-top,underlay"],
|
||||||
["force_high_performance_gpu"],
|
["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: [
|
enableFeatures: [
|
||||||
"EnableDrDc",
|
"EnableDrDc",
|
||||||
|
|
@ -120,10 +122,10 @@ const smoothExperiment: Preset = {
|
||||||
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
|
"ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes",
|
||||||
"UseSkiaRenderer",
|
"UseSkiaRenderer",
|
||||||
"WebAssemblyLazyCompilation",
|
"WebAssemblyLazyCompilation",
|
||||||
"AcceleratedVideoDecodeLinuxGL",
|
"WebRtcHWEncoding",
|
||||||
|
"WebRtcHWDecoding",
|
||||||
"AcceleratedVideoEncoder",
|
"AcceleratedVideoEncoder",
|
||||||
"AcceleratedVideoDecoder",
|
"AcceleratedVideoDecoder",
|
||||||
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
|
||||||
"ZeroCopyDesktopCapture",
|
"ZeroCopyDesktopCapture",
|
||||||
],
|
],
|
||||||
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
|
disableFeatures: ["Vulkan", "UseChromeOSDirectVideoDecoder"],
|
||||||
|
|
@ -141,23 +143,174 @@ const battery: Preset = {
|
||||||
disableFeatures: [],
|
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: [
|
switches: [
|
||||||
["ignore-gpu-blocklist"],
|
["ignore-gpu-blocklist"],
|
||||||
["enable-gpu-rasterization"],
|
|
||||||
["enable-zero-copy"],
|
["enable-zero-copy"],
|
||||||
["force_high_performance_gpu"],
|
["enable-accelerated-video-decode"],
|
||||||
["use-gl", "desktop"],
|
["enable-gpu-memory-buffer-video-frames"],
|
||||||
],
|
|
||||||
enableFeatures: [
|
|
||||||
"AcceleratedVideoDecodeLinuxGL",
|
|
||||||
"AcceleratedVideoEncoder",
|
|
||||||
"AcceleratedVideoDecoder",
|
|
||||||
"AcceleratedVideoDecodeLinuxZeroCopyGL",
|
|
||||||
],
|
],
|
||||||
|
enableFeatures: ["WebRtcHWDecoding", "AcceleratedVideoDecoder", "ZeroCopyDesktopCapture", "CanvasOopRasterization"],
|
||||||
disableFeatures: ["UseChromeOSDirectVideoDecoder"],
|
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)
|
* Load custom flags from JSON file in user data directory (cached after first load)
|
||||||
* Path:
|
* Path:
|
||||||
|
|
@ -305,10 +458,10 @@ export function getPreset(): Preset | undefined {
|
||||||
console.log("No performance modes set");
|
console.log("No performance modes set");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getConfig("vaapi")) {
|
// Platform-specific video encode/decode (macOS VideoToolbox / Win HW / Linux VAAPI).
|
||||||
console.log("VAAPI flags enabled");
|
// Shared voip/smoothScreenshare presets must not carry Linux-only or cross-platform
|
||||||
preset = preset ? mergePresets(preset, vaapi) : vaapi;
|
// encode flags that would undermine the macOS stack we just fixed.
|
||||||
}
|
preset = applyPlatformVideoStack(preset);
|
||||||
|
|
||||||
if (preset) {
|
if (preset) {
|
||||||
return mergeWithCustomFlags(preset);
|
return mergeWithCustomFlags(preset);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { Game } from "arrpc";
|
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 isDev from "electron-is-dev";
|
||||||
import type { Keybind } from "../@types/keybind.js";
|
import type { Keybind } from "../@types/keybind.js";
|
||||||
import type { Settings } from "../@types/settings.js";
|
import type { Settings } from "../@types/settings.js";
|
||||||
|
|
@ -71,6 +71,36 @@ function ifExistsRead(path: string): string | undefined {
|
||||||
|
|
||||||
let ipcRegistered = false;
|
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 {
|
export function registerIpc(passedWindow: BrowserWindow): void {
|
||||||
if (ipcRegistered) return;
|
if (ipcRegistered) return;
|
||||||
ipcRegistered = true;
|
ipcRegistered = true;
|
||||||
|
|
@ -355,6 +385,12 @@ export function registerIpc(passedWindow: BrowserWindow): void {
|
||||||
ipcMain.on("copyGPUInfo", () => {
|
ipcMain.on("copyGPUInfo", () => {
|
||||||
clipboard.writeText(JSON.stringify(app.getGPUFeatureStatus()));
|
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", () => {
|
ipcMain.on("openCustomIconDialog", () => {
|
||||||
dialog
|
dialog
|
||||||
.showOpenDialog({
|
.showOpenDialog({
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ contextBridge.exposeInMainWorld("legcord", {
|
||||||
openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"),
|
openCustomIconDialog: () => ipcRenderer.send("openCustomIconDialog"),
|
||||||
copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"),
|
copyDebugInfo: () => ipcRenderer.send("copyDebugInfo"),
|
||||||
copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"),
|
copyGPUInfo: () => ipcRenderer.send("copyGPUInfo"),
|
||||||
|
openWebRTCInternals: () => ipcRenderer.send("openWebRTCInternals"),
|
||||||
|
openGPUInfo: () => ipcRenderer.send("openGPUInfo"),
|
||||||
dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput,
|
dumpFlags: () => ipcRenderer.sendSync("dumpFlags") as AppliedFlagsOutput,
|
||||||
},
|
},
|
||||||
touchbar: {
|
touchbar: {
|
||||||
|
|
|
||||||
|
|
@ -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 ~1–2.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
|
// 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.
|
// (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
|
// 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() {
|
async function load() {
|
||||||
await sleep(5000).then(() => {
|
await sleep(5000).then(() => {
|
||||||
const original = navigator.mediaDevices.getDisplayMedia;
|
// Venmic audio injection lives in the Shelter screenshare getDisplayMedia patch.
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// dirty hack to make clicking notifications focus Legcord
|
// dirty hack to make clicking notifications focus Legcord
|
||||||
addScript(`
|
addScript(`
|
||||||
(() => {
|
(() => {
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,15 @@ if (!app.requestSingleInstanceLock() && getConfig("multiInstance") === false) {
|
||||||
enableFeatures.add("MacLoopbackAudioForScreenShare");
|
enableFeatures.add("MacLoopbackAudioForScreenShare");
|
||||||
enableFeatures.add("MacSckSystemAudioLoopbackOverride");
|
enableFeatures.add("MacSckSystemAudioLoopbackOverride");
|
||||||
enableFeatures.add("MacCatapSystemAudioLoopbackCapture");
|
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
|
// work around chrome 66 disabling autoplay by default
|
||||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||||
|
|
|
||||||
|
|
@ -38,20 +38,47 @@ export async function patchNavigator(requestAudio = false) {
|
||||||
const stream = await original.call(this, opts);
|
const stream = await original.call(this, opts);
|
||||||
const video = stream.getVideoTracks()[0];
|
const video = stream.getVideoTracks()[0];
|
||||||
|
|
||||||
const width = store.resolution * (16 / 9);
|
const width = Math.round(store.resolution * (16 / 9));
|
||||||
const height = store.resolution;
|
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 = {
|
const stream_constraints: MediaTrackConstraints = {
|
||||||
frameRate: store.fps,
|
frameRate: { ideal: store.fps, min: Math.min(15, store.fps) },
|
||||||
width: width,
|
width: { min: 640, ideal: width, max: width },
|
||||||
height: height,
|
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) {
|
if (video) {
|
||||||
|
try {
|
||||||
|
video.contentHint = contentHint;
|
||||||
|
} catch {
|
||||||
|
// contentHint is best-effort
|
||||||
|
}
|
||||||
|
|
||||||
video
|
video
|
||||||
.applyConstraints(stream_constraints)
|
.applyConstraints(stream_constraints)
|
||||||
.then(() => {
|
.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) => {
|
.catch((error) => {
|
||||||
console.error("Failed to apply video constraints:", error);
|
console.error("Failed to apply video constraints:", error);
|
||||||
|
|
|
||||||
|
|
@ -14,33 +14,277 @@ const {
|
||||||
} = shelter;
|
} = shelter;
|
||||||
|
|
||||||
store.fps ??= 30; // set default
|
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() {
|
const BITRATE_CEILING = 25_000_000;
|
||||||
// @ts-expect-error fix types
|
const REAPPLY_DELAYS_MS = [1000, 3000, 8000] as const;
|
||||||
const mediaConnections = [...MediaEngineStore.getMediaEngine().connections];
|
|
||||||
// @ts-expect-error fix types
|
/** Discord-like Go Live targets (bits/s) by height and fps band. */
|
||||||
const currentUserId = UserStore.getCurrentUser().id;
|
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 width = Math.round(store.resolution * (16 / 9));
|
||||||
const height = store.resolution;
|
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(
|
const params = streamConnection.videoStreamParameters?.[0];
|
||||||
width * height * store.fps * 0.08, // width * height * fps * bits per pixel value (apprx.)
|
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);
|
||||||
|
|
||||||
|
log(
|
||||||
|
`Patched current user's stream (${reason}) with resolution: (${width}x${height}) ${store.fps}FPS @ ${formatMbps(targetBitrate)} (min ${formatMbps(bitrateMin)} / max ${formatMbps(bitrateMax)}).`,
|
||||||
);
|
);
|
||||||
const streamConnection = mediaConnections.find((connection) => connection.streamUserId === currentUserId);
|
return true;
|
||||||
if (streamConnection) {
|
}
|
||||||
streamConnection.videoStreamParameters[0].maxFrameRate = store.fps;
|
|
||||||
streamConnection.videoStreamParameters[0].maxResolution.height = height;
|
function scheduleStreamQualityReapplies(): void {
|
||||||
streamConnection.videoStreamParameters[0].maxResolution.width = width;
|
clearReapplyTimers();
|
||||||
streamConnection.videoQualityManager.goliveMaxQuality.bitrateMin =
|
const generation = reapplyGeneration;
|
||||||
calculatedTargetBitrate - calculatedTargetBitrate * 0.05; // remove 5% of target bitrate for ground bitrate
|
for (const delay of REAPPLY_DELAYS_MS) {
|
||||||
streamConnection.videoQualityManager.goliveMaxQuality.bitrateMax =
|
const timer = setTimeout(() => {
|
||||||
calculatedTargetBitrate + calculatedTargetBitrate * 0.25; // add 25% of target bitrate for ceiling bitrate
|
if (generation !== reapplyGeneration) return;
|
||||||
streamConnection.videoQualityManager.goliveMaxQuality.bitrateTarget = calculatedTargetBitrate;
|
if (!getLocalStreamConnection()) return;
|
||||||
log(
|
patchStreamQuality(`reapply@${delay}ms`);
|
||||||
`Patched current user's stream with resolution: (${width}x${height}) ${store.fps}FPS @ ${calculatedTargetBitrate / (1000 * 1000)}Mbps.`,
|
}, 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) {
|
function onStreamEnd(dispatch: StreamDispatch) {
|
||||||
if (!dispatch.streamKey) return;
|
if (!dispatch.streamKey) return;
|
||||||
const owner = dispatch.streamKey.split(":").at(-1);
|
const owner = dispatch.streamKey.split(":").at(-1);
|
||||||
// @ts-expect-error fix types
|
// @ts-expect-error Discord UserStore typings
|
||||||
const currentUserId = UserStore.getCurrentUser().id;
|
const currentUserId = UserStore.getCurrentUser().id as string;
|
||||||
if (dispatch.reason === "user_requested" && owner === currentUserId) {
|
if (dispatch.reason === "user_requested" && owner === currentUserId) {
|
||||||
window.legcord.screenshare.venmicStop();
|
window.legcord.screenshare.venmicStop();
|
||||||
}
|
}
|
||||||
|
if (owner === currentUserId) {
|
||||||
|
loggedEncoderForCurrentStream = false;
|
||||||
|
clearReapplyTimers();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onLoad() {
|
export function onLoad() {
|
||||||
|
|
@ -93,6 +341,7 @@ export function onLoad() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onUnload() {
|
export function onUnload() {
|
||||||
|
clearReapplyTimers();
|
||||||
dispatcher.unsubscribe("MEDIA_ENGINE_VIDEO_SOURCE_QUALITY_CHANGED", onStreamQualityChange);
|
dispatcher.unsubscribe("MEDIA_ENGINE_VIDEO_SOURCE_QUALITY_CHANGED", onStreamQualityChange);
|
||||||
dispatcher.unsubscribe("STREAM_DELETE", onStreamEnd);
|
dispatcher.unsubscribe("STREAM_DELETE", onStreamEnd);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,9 +151,12 @@ export function SettingsPage() {
|
||||||
t["settings-storageFolder"],
|
t["settings-storageFolder"],
|
||||||
t["settings-copyDebugInfo"],
|
t["settings-copyDebugInfo"],
|
||||||
t["settings-copyGPUInfo"],
|
t["settings-copyGPUInfo"],
|
||||||
|
t["settings-openWebRTCInternals"],
|
||||||
|
t["settings-openGPUInfo"],
|
||||||
t["settings-clearClientModCache"],
|
t["settings-clearClientModCache"],
|
||||||
"venmic",
|
"venmic",
|
||||||
"VAAPI",
|
"VAAPI",
|
||||||
|
"WebRTC",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -980,6 +983,16 @@ export function SettingsPage() {
|
||||||
{t["settings-copyGPUInfo"]}
|
{t["settings-copyGPUInfo"]}
|
||||||
</Button>
|
</Button>
|
||||||
</SearchableSetting>
|
</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"]}>
|
<SearchableSetting keywords={[t["settings-clearClientModCache"], "cache"]}>
|
||||||
<Button
|
<Button
|
||||||
size={ButtonSizes.MAX}
|
size={ButtonSizes.MAX}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue