feat: Implement an Electron-based broadcasting client with screen/window and audio source selection, including Pipewire integration, and add auto-unmute to the viewer.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
client/config.json
|
||||||
|
|||||||
176
client/index.html
Normal file
176
client/index.html
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Screenshare Broadcaster</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #0f172a;
|
||||||
|
--text-primary: #f8fafc;
|
||||||
|
--text-secondary: #94a3b8;
|
||||||
|
--accent-color: #3b82f6;
|
||||||
|
--glass-bg: rgba(30, 41, 59, 0.7);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
h1 { margin-top: 0; }
|
||||||
|
.controls {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
input, button, select {
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
width: 90%;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
button:hover { background: #2563eb; transform: translateY(-2px); }
|
||||||
|
button:disabled { background: #475569; cursor: not-allowed; transform: none; }
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.status { color: var(--text-secondary); margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
width: 90%;
|
||||||
|
margin: 0 auto 0.5rem auto;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Source Grid */
|
||||||
|
.sources-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 0 auto 1.5rem auto;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
.source-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
.source-item:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.source-item.selected {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
.source-item img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
.source-item span {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Panel */
|
||||||
|
.stats-panel {
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 90%;
|
||||||
|
margin: 1rem auto;
|
||||||
|
text-align: left;
|
||||||
|
display: none;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #10b981; /* green text */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Broadcaster Client</h1>
|
||||||
|
|
||||||
|
<div class="controls" id="controlsPanel">
|
||||||
|
<label class="label">Server Connection</label>
|
||||||
|
<input type="text" id="serverUrl" placeholder="Server URL (e.g. http://localhost:3000)" value="http://localhost:3000">
|
||||||
|
<input type="password" id="serverPassword" placeholder="Stream Password">
|
||||||
|
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; width: 90%; margin: 10px auto;">
|
||||||
|
<h3 style="margin:0;">Media Sources</h3>
|
||||||
|
<button id="getSourcesBtn" style="width:auto; margin:0; padding: 0.4rem 0.8rem;">Refresh Devices</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="label">Visual Source (Screen/Window)</label>
|
||||||
|
<div id="sourcesGrid" class="sources-grid"></div>
|
||||||
|
|
||||||
|
<label class="label">Audio Source (Microphone/Pipewire Virtual Sinks)</label>
|
||||||
|
<select id="audioSelect"></select>
|
||||||
|
|
||||||
|
<button id="startBtn" disabled style="margin-top: 1.5rem;">Start Broadcast</button>
|
||||||
|
<div class="status" id="statusText">Not Broadcasting</div>
|
||||||
|
<button id="stopBtn" style="display:none; background:#ef4444;">Stop Broadcast</button>
|
||||||
|
|
||||||
|
<div class="stats-panel" id="statsPanel">
|
||||||
|
<div><strong>Resolution:</strong> <span id="statsRes">0x0</span></div>
|
||||||
|
<div><strong>FPS:</strong> <span id="statsFps">0</span></div>
|
||||||
|
<div><strong>Upstream:</strong> <span id="statsBitrate">0 kbps</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<video id="localVideo" autoplay playsinline muted></video>
|
||||||
|
|
||||||
|
<!-- Use socket.io client script locally installed via npm -->
|
||||||
|
<script src="./node_modules/socket.io-client/dist/socket.io.js"></script>
|
||||||
|
<script src="renderer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
83
client/main.js
Normal file
83
client/main.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const { app, BrowserWindow, desktopCapturer, ipcMain } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const PipewireHelper = require('./pipewire');
|
||||||
|
|
||||||
|
// Ensure Chromium tries to use PipeWire for screen sharing on Linux
|
||||||
|
app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer');
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 1000,
|
||||||
|
height: 800,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
win.loadFile('index.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
// Setup the virtual microphone as soon as the app starts
|
||||||
|
await PipewireHelper.createVirtualMic();
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('will-quit', async () => {
|
||||||
|
await PipewireHelper.destroyVirtualMic();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle IPC request from renderer to get screen/audio sources
|
||||||
|
ipcMain.handle('get-sources', async () => {
|
||||||
|
const inputSources = await desktopCapturer.getSources({
|
||||||
|
types: ['window', 'screen'],
|
||||||
|
fetchWindowIcons: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return inputSources.map(source => ({
|
||||||
|
id: source.id,
|
||||||
|
name: source.name,
|
||||||
|
thumbnail: source.thumbnail.toDataURL()
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Pipewire specific audio isolation requests
|
||||||
|
ipcMain.handle('get-audio-apps', async () => {
|
||||||
|
return await PipewireHelper.getAudioApplications();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('link-app-audio', async (event, appName) => {
|
||||||
|
return await PipewireHelper.linkApplicationToMic(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle saving and loading the config.json profile
|
||||||
|
const fs = require('fs');
|
||||||
|
const configPath = path.join(__dirname, 'config.json');
|
||||||
|
|
||||||
|
ipcMain.handle('get-config', () => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
}
|
||||||
|
} catch (e) { console.error("Could not read config", e); }
|
||||||
|
return { serverUrl: 'http://localhost:3000', serverPassword: '' };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('save-config', (event, newConfig) => {
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||||
|
});
|
||||||
888
client/package-lock.json
generated
Normal file
888
client/package-lock.json
generated
Normal file
@@ -0,0 +1,888 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"electron": "^40.6.0",
|
||||||
|
"socket.io-client": "^4.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/get": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"env-paths": "^2.2.0",
|
||||||
|
"fs-extra": "^8.1.0",
|
||||||
|
"got": "^11.8.5",
|
||||||
|
"progress": "^2.0.3",
|
||||||
|
"semver": "^6.2.0",
|
||||||
|
"sumchecker": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"global-agent": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sindresorhus/is": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@szmarczak/http-timer": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"defer-to-connect": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/cacheable-request": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/http-cache-semantics": "*",
|
||||||
|
"@types/keyv": "^3.1.4",
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/responselike": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/http-cache-semantics": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/keyv": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "24.10.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz",
|
||||||
|
"integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/responselike": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/yauzl": {
|
||||||
|
"version": "2.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
|
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boolean": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "0.2.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||||
|
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable-lookup": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cacheable-request": {
|
||||||
|
"version": "7.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||||
|
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clone-response": "^1.0.2",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"http-cache-semantics": "^4.0.0",
|
||||||
|
"keyv": "^4.0.0",
|
||||||
|
"lowercase-keys": "^2.0.0",
|
||||||
|
"normalize-url": "^6.0.1",
|
||||||
|
"responselike": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clone-response": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/decompress-response/node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/defer-to-connect": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-properties": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-node": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/electron": {
|
||||||
|
"version": "40.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron/-/electron-40.6.0.tgz",
|
||||||
|
"integrity": "sha512-ett8W+yOFGDuM0vhJMamYSkrbV3LoaffzJd9GfjI96zRAxyrNqUSKqBpf/WGbQCweDxX2pkUCUfrv4wwKpsFZA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@electron/get": "^2.0.0",
|
||||||
|
"@types/node": "^24.9.0",
|
||||||
|
"extract-zip": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"electron": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.20.55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.18.3",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/env-paths": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-error": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/extract-zip": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"get-stream": "^5.1.0",
|
||||||
|
"yauzl": "^2.10.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"extract-zip": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.17.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/yauzl": "^2.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fd-slicer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6 <7 || >=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-stream": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pump": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-agent": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"boolean": "^3.0.1",
|
||||||
|
"es6-error": "^4.1.1",
|
||||||
|
"matcher": "^3.0.0",
|
||||||
|
"roarr": "^2.15.3",
|
||||||
|
"semver": "^7.3.2",
|
||||||
|
"serialize-error": "^7.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-agent/node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/globalthis": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"define-properties": "^1.2.1",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/got": {
|
||||||
|
"version": "11.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||||
|
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@sindresorhus/is": "^4.0.0",
|
||||||
|
"@szmarczak/http-timer": "^4.0.5",
|
||||||
|
"@types/cacheable-request": "^6.0.1",
|
||||||
|
"@types/responselike": "^1.0.0",
|
||||||
|
"cacheable-lookup": "^5.0.3",
|
||||||
|
"cacheable-request": "^7.0.2",
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"http2-wrapper": "^1.0.0-beta.5.2",
|
||||||
|
"lowercase-keys": "^2.0.0",
|
||||||
|
"p-cancelable": "^2.0.0",
|
||||||
|
"responselike": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-cache-semantics": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/http2-wrapper": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"quick-lru": "^5.1.1",
|
||||||
|
"resolve-alpn": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-buffer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/json-stringify-safe": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/keyv": {
|
||||||
|
"version": "4.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-buffer": "3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lowercase-keys": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/matcher": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/normalize-url": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-cancelable": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/progress": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quick-lru": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-alpn": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/responselike": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lowercase-keys": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/roarr": {
|
||||||
|
"version": "2.15.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||||
|
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"boolean": "^3.0.1",
|
||||||
|
"detect-node": "^2.0.4",
|
||||||
|
"globalthis": "^1.0.1",
|
||||||
|
"json-stringify-safe": "^5.0.1",
|
||||||
|
"semver-compare": "^1.0.0",
|
||||||
|
"sprintf-js": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver-compare": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/serialize-error": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"type-fest": "^0.13.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
|
||||||
|
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.4.1",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
|
||||||
|
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sprintf-js": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/sumchecker": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "0.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||||
|
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yauzl": {
|
||||||
|
"version": "2.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||||
|
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-crc32": "~0.2.3",
|
||||||
|
"fd-slicer": "~1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
client/package.json
Normal file
17
client/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron ."
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"electron": "^40.6.0",
|
||||||
|
"socket.io-client": "^4.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
148
client/pipewire.js
Normal file
148
client/pipewire.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
const { exec } = require('child_process');
|
||||||
|
const util = require('util');
|
||||||
|
const execAsync = util.promisify(exec);
|
||||||
|
|
||||||
|
const VIRT_MIC_NAME = 'simplescreenshare-audio';
|
||||||
|
|
||||||
|
// Pipewire helper class to isolate application audio
|
||||||
|
class PipewireHelper {
|
||||||
|
|
||||||
|
// Create a virtual microphone (a null-audio-sink) that Electron can listen to
|
||||||
|
static async createVirtualMic() {
|
||||||
|
try {
|
||||||
|
await execAsync(`pw-cli destroy ${VIRT_MIC_NAME}`).catch(() => {}); // Cleanup old
|
||||||
|
|
||||||
|
const cmd = `pw-cli create-node adapter '{ factory.name=support.null-audio-sink node.name=${VIRT_MIC_NAME} node.description="SimpleScreenshare Audio" media.class=Audio/Source/Virtual object.linger=1 audio.position=[FL,FR] }'`;
|
||||||
|
const { stdout } = await execAsync(cmd);
|
||||||
|
console.log("Created virtual mic:", stdout.trim());
|
||||||
|
|
||||||
|
// Wait a moment for Pipewire to register it
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create virtual mic:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the virtual microphone
|
||||||
|
static async destroyVirtualMic() {
|
||||||
|
try {
|
||||||
|
// Find node ID
|
||||||
|
const { stdout } = await execAsync(`pw-cli dump short Node`);
|
||||||
|
const lines = stdout.split('\\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes(VIRT_MIC_NAME)) {
|
||||||
|
const id = line.split(',')[0].trim();
|
||||||
|
await execAsync(`pw-cli destroy ${id}`);
|
||||||
|
console.log(`Destroyed virtual mic (ID: ${id})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to destroy virtual mic:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all running applications currently playing audio
|
||||||
|
static async getAudioApplications() {
|
||||||
|
try {
|
||||||
|
// pw-dump outputs the entire graph as JSON, increase buffer!
|
||||||
|
const { stdout } = await execAsync('pw-dump', { maxBuffer: 1024 * 1024 * 50 });
|
||||||
|
const dump = JSON.parse(stdout);
|
||||||
|
|
||||||
|
// Filter out audio output streams (applications playing sound)
|
||||||
|
const apps = dump.filter(node =>
|
||||||
|
node.info &&
|
||||||
|
node.info.props &&
|
||||||
|
node.info.props['media.class'] === 'Stream/Output/Audio' &&
|
||||||
|
node.info.props['application.name']
|
||||||
|
).map(node => ({
|
||||||
|
id: node.id,
|
||||||
|
name: node.info.props['application.name'],
|
||||||
|
mediaName: node.info.props['media.name'] || 'Unknown Stream'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Deduplicate by application name and return
|
||||||
|
const uniqueApps = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const app of apps) {
|
||||||
|
if (!seen.has(app.name)) {
|
||||||
|
seen.add(app.name);
|
||||||
|
uniqueApps.push(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueApps;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get audio applications:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link a target application's output to our Virtual Microphone
|
||||||
|
static async linkApplicationToMic(targetAppName) {
|
||||||
|
try {
|
||||||
|
// 1. Get the dump with massive buffer
|
||||||
|
const { stdout: dumpOut } = await execAsync('pw-dump', { maxBuffer: 1024 * 1024 * 50 });
|
||||||
|
const dump = JSON.parse(dumpOut);
|
||||||
|
|
||||||
|
// 2. Find the target application node
|
||||||
|
const targetNode = dump.find(node =>
|
||||||
|
node.info &&
|
||||||
|
node.info.props &&
|
||||||
|
node.info.props['media.class'] === 'Stream/Output/Audio' &&
|
||||||
|
node.info.props['application.name'] === targetAppName
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Find our virtual mic node
|
||||||
|
const micNode = dump.find(node =>
|
||||||
|
node.info &&
|
||||||
|
node.info.props &&
|
||||||
|
node.info.props['node.name'] === VIRT_MIC_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetNode || !micNode) {
|
||||||
|
console.error("Could not find target node or virtual mic node");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Linking ${targetAppName} (ID: ${targetNode.id}) to ${VIRT_MIC_NAME} (ID: ${micNode.id})`);
|
||||||
|
|
||||||
|
// 4. Find the Ports for both nodes
|
||||||
|
const ports = dump.filter(n => n.type === 'PipeWire:Interface:Port');
|
||||||
|
|
||||||
|
// Target App Output Ports
|
||||||
|
const targetPorts = ports.filter(p => p.info.props['node.id'] === targetNode.id && p.info.direction === 'output');
|
||||||
|
const targetFL = targetPorts.find(p => p.info.props['audio.channel'] === 'FL');
|
||||||
|
const targetFR = targetPorts.find(p => p.info.props['audio.channel'] === 'FR');
|
||||||
|
|
||||||
|
// Virtual Mic Input Ports
|
||||||
|
const micPorts = ports.filter(p => p.info.props['node.id'] === micNode.id && p.info.direction === 'input');
|
||||||
|
const micFL = micPorts.find(p => p.info.props['audio.channel'] === 'FL');
|
||||||
|
const micFR = micPorts.find(p => p.info.props['audio.channel'] === 'FR');
|
||||||
|
|
||||||
|
if (!targetFL || !targetFR || !micFL || !micFR) {
|
||||||
|
console.error("Could not find stereo ports for linking");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Link them using port aliases!
|
||||||
|
const targetFlAlias = targetFL.info.props['port.alias'] || targetFL.info.props['object.path'] || targetFL.id;
|
||||||
|
const targetFrAlias = targetFR.info.props['port.alias'] || targetFR.info.props['object.path'] || targetFR.id;
|
||||||
|
const micFlAlias = micFL.info.props['port.alias'] || micFL.info.props['object.path'] || micFL.id;
|
||||||
|
const micFrAlias = micFR.info.props['port.alias'] || micFR.info.props['object.path'] || micFR.id;
|
||||||
|
|
||||||
|
await execAsync(`pw-link "${targetFlAlias}" "${micFlAlias}"`).catch(e => console.log("pw-link output:", e.message));
|
||||||
|
await execAsync(`pw-link "${targetFrAlias}" "${micFrAlias}"`).catch(e => console.log("pw-link output:", e.message));
|
||||||
|
|
||||||
|
console.log("Successfully linked audio.");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to link application:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PipewireHelper;
|
||||||
9
client/preload.js
Normal file
9
client/preload.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
getSources: () => ipcRenderer.invoke('get-sources'),
|
||||||
|
getAudioApps: () => ipcRenderer.invoke('get-audio-apps'),
|
||||||
|
linkAppAudio: (appName) => ipcRenderer.invoke('link-app-audio', appName),
|
||||||
|
getConfig: () => ipcRenderer.invoke('get-config'),
|
||||||
|
saveConfig: (config) => ipcRenderer.invoke('save-config', config)
|
||||||
|
});
|
||||||
368
client/renderer.js
Normal file
368
client/renderer.js
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
const serverUrlInput = document.getElementById('serverUrl');
|
||||||
|
const serverPasswordInput = document.getElementById('serverPassword');
|
||||||
|
const sourcesGrid = document.getElementById('sourcesGrid');
|
||||||
|
const audioSelect = document.getElementById('audioSelect');
|
||||||
|
const getSourcesBtn = document.getElementById('getSourcesBtn');
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
const stopBtn = document.getElementById('stopBtn');
|
||||||
|
const localVideo = document.getElementById('localVideo');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
const statsPanel = document.getElementById('statsPanel');
|
||||||
|
|
||||||
|
let socket;
|
||||||
|
let peerConnections = {};
|
||||||
|
let activeStream;
|
||||||
|
let selectedVideoSourceId = null;
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: "stun:localhost:3478" },
|
||||||
|
{ urls: "turn:localhost:3478", username: "myuser", credential: "mypassword" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Get Desktop Sources from Main Process and populate raw select tags
|
||||||
|
// Also enumerate native audio devices from navigator!
|
||||||
|
getSourcesBtn.addEventListener('click', async () => {
|
||||||
|
sourcesGrid.innerHTML = '<div style="color:var(--text-secondary); width:100%;">Loading sources...</div>';
|
||||||
|
audioSelect.innerHTML = '<option value="">Loading audio devices...</option>';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
selectedVideoSourceId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// --- Fetch Virtual Video Sources ---
|
||||||
|
const sources = await window.electronAPI.getSources();
|
||||||
|
sourcesGrid.innerHTML = '';
|
||||||
|
sources.forEach(source => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'source-item';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = source.thumbnail;
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.innerText = source.name;
|
||||||
|
label.title = source.name;
|
||||||
|
|
||||||
|
item.appendChild(img);
|
||||||
|
item.appendChild(label);
|
||||||
|
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.source-item').forEach(i => i.classList.remove('selected'));
|
||||||
|
item.classList.add('selected');
|
||||||
|
selectedVideoSourceId = source.id;
|
||||||
|
startPreview(source.id);
|
||||||
|
startBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
sourcesGrid.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Fetch Application Audio Sources via built Pipewire Helper ---
|
||||||
|
const audioApps = await window.electronAPI.getAudioApps();
|
||||||
|
audioSelect.innerHTML = '<option value="none">No Audio (Video Only)</option>';
|
||||||
|
audioApps.forEach(app => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
// We pass the actual application name into the value so the main process can find it via pw-dump
|
||||||
|
option.value = app.name;
|
||||||
|
option.text = `${app.name} (${app.mediaName})`;
|
||||||
|
audioSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we don't disable start button here, it would be enabled before user clicked a grid item
|
||||||
|
startBtn.disabled = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
sourcesGrid.innerHTML = '<div style="color:red; width:100%;">Error loading sources</div>';
|
||||||
|
audioSelect.innerHTML = '<option value="none">Error loading audio</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Preview Stream Logic ---
|
||||||
|
let previewStream = null;
|
||||||
|
|
||||||
|
async function startPreview(videoSourceId) {
|
||||||
|
// Cleanup previous preview
|
||||||
|
if (previewStream) {
|
||||||
|
previewStream.getTracks().forEach(t => t.stop());
|
||||||
|
previewStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!videoSourceId) {
|
||||||
|
localVideo.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
previewStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: false,
|
||||||
|
video: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'desktop',
|
||||||
|
chromeMediaSourceId: videoSourceId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Removed 1080p ideal limit to prevent Chromium from green-padding non-16:9 window captures!
|
||||||
|
const videoTrack = previewStream.getVideoTracks()[0];
|
||||||
|
await videoTrack.applyConstraints({ frameRate: { ideal: 60 } });
|
||||||
|
|
||||||
|
localVideo.srcObject = previewStream;
|
||||||
|
localVideo.style.display = 'block';
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to start preview stream:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Start Broadcast
|
||||||
|
startBtn.addEventListener('click', async () => {
|
||||||
|
const url = serverUrlInput.value;
|
||||||
|
const password = serverPasswordInput.value;
|
||||||
|
const videoSourceId = selectedVideoSourceId;
|
||||||
|
const targetAppName = audioSelect.value;
|
||||||
|
|
||||||
|
if (!videoSourceId || !url || !password) {
|
||||||
|
alert("Please fill out URL, Password, and select a visual source.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save credentials for next time
|
||||||
|
window.electronAPI.saveConfig({ serverUrl: url, serverPassword: password });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stop the preview grab so we can grab the real stream cleanly
|
||||||
|
if (previewStream) {
|
||||||
|
previewStream.getTracks().forEach(t => t.stop());
|
||||||
|
previewStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: false,
|
||||||
|
video: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'desktop',
|
||||||
|
chromeMediaSourceId: videoSourceId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoTrack = stream.getVideoTracks()[0];
|
||||||
|
await videoTrack.applyConstraints({ frameRate: { ideal: 60 } });
|
||||||
|
|
||||||
|
// If user selected an application, grab the Virtual Mic input and link the app to it!
|
||||||
|
if (targetAppName && targetAppName !== 'none') {
|
||||||
|
const linked = await window.electronAPI.linkAppAudio(targetAppName);
|
||||||
|
if (linked) {
|
||||||
|
// Now that the pipewire graph is linked, we just need to read from our Virtual Mic sink!
|
||||||
|
// Chromium registers this as a standard Input device
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const virtMic = devices.find(d => d.kind === 'audioinput' && d.label.toLowerCase().includes('simplescreenshare'));
|
||||||
|
|
||||||
|
if (virtMic) {
|
||||||
|
const audioStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {
|
||||||
|
deviceId: { exact: virtMic.deviceId },
|
||||||
|
echoCancellation: false,
|
||||||
|
autoGainControl: false,
|
||||||
|
noiseSuppression: false,
|
||||||
|
googAutoGainControl: false,
|
||||||
|
googEchoCancellation: false,
|
||||||
|
googNoiseSuppression: false,
|
||||||
|
googHighpassFilter: false,
|
||||||
|
channelCount: 2,
|
||||||
|
sampleRate: 48000
|
||||||
|
},
|
||||||
|
video: false
|
||||||
|
});
|
||||||
|
stream.addTrack(audioStream.getAudioTracks()[0]);
|
||||||
|
} else {
|
||||||
|
console.warn("Virtual mic device not found in navigator enumeration");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Failed to link application audio. Broadcasting video only.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeStream = stream;
|
||||||
|
localVideo.srcObject = stream;
|
||||||
|
localVideo.style.display = 'block';
|
||||||
|
|
||||||
|
connectAndBroadcast(url, password);
|
||||||
|
|
||||||
|
startBtn.style.display = 'none';
|
||||||
|
stopBtn.style.display = 'inline-block';
|
||||||
|
statsPanel.style.display = 'block';
|
||||||
|
statusText.innerText = `Broadcasting to ${url}`;
|
||||||
|
|
||||||
|
// Auto stop if user closes the requested window
|
||||||
|
stream.getVideoTracks()[0].onended = stopSharing;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Stream capture error:", e);
|
||||||
|
alert("Failed to capture screen. See console for details.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function connectAndBroadcast(url, password) {
|
||||||
|
socket = io(url);
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
socket.emit('broadcaster', password);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('authError', (msg) => {
|
||||||
|
alert(msg);
|
||||||
|
stopSharing();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('viewer', id => {
|
||||||
|
if (!activeStream) return;
|
||||||
|
|
||||||
|
const peerConnection = new RTCPeerConnection(config);
|
||||||
|
peerConnections[id] = peerConnection;
|
||||||
|
|
||||||
|
activeStream.getTracks().forEach(track => {
|
||||||
|
const sender = peerConnection.addTrack(track, activeStream);
|
||||||
|
if (track.kind === 'video') {
|
||||||
|
const params = sender.getParameters();
|
||||||
|
if (!params.encodings) params.encodings = [{}];
|
||||||
|
params.encodings[0].maxBitrate = 10000000;
|
||||||
|
sender.setParameters(params).catch(e => console.error(e));
|
||||||
|
} else if (track.kind === 'audio') {
|
||||||
|
const params = sender.getParameters();
|
||||||
|
if (!params.encodings) params.encodings = [{}];
|
||||||
|
params.encodings[0].maxBitrate = 510000; // max Opus bitrate
|
||||||
|
sender.setParameters(params).catch(e => console.error(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
peerConnection.onicecandidate = event => {
|
||||||
|
if (event.candidate) {
|
||||||
|
socket.emit('candidate', id, event.candidate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
peerConnection.createOffer().then(sdp => {
|
||||||
|
if (window.RTCRtpSender && window.RTCRtpSender.getCapabilities) {
|
||||||
|
const caps = window.RTCRtpSender.getCapabilities('video');
|
||||||
|
if (caps && caps.codecs) {
|
||||||
|
const h264 = caps.codecs.filter(c => c.mimeType.toLowerCase() === 'video/h264' || c.mimeType.toLowerCase() === 'video/vp8');
|
||||||
|
const transceivers = peerConnection.getTransceivers();
|
||||||
|
transceivers.forEach(t => {
|
||||||
|
if (t.receiver.track.kind === 'video') t.setCodecPreferences(h264);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebRTC defaults to voice-optimized ~32kbps mono. Let's force high-fidelity stereo!
|
||||||
|
let sdpLines = sdp.sdp.split('\r\n');
|
||||||
|
let opusPayloadType = null;
|
||||||
|
for (let i = 0; i < sdpLines.length; i++) {
|
||||||
|
if (sdpLines[i].includes('a=rtpmap:') && sdpLines[i].includes('opus/48000/2')) {
|
||||||
|
const match = sdpLines[i].match(/a=rtpmap:(\d+) /);
|
||||||
|
if (match) opusPayloadType = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (opusPayloadType) {
|
||||||
|
let fmtpFound = false;
|
||||||
|
for (let i = 0; i < sdpLines.length; i++) {
|
||||||
|
if (sdpLines[i].startsWith(`a=fmtp:${opusPayloadType}`)) {
|
||||||
|
// Completely overwrite the opus config for pristine stereo
|
||||||
|
sdpLines[i] = `a=fmtp:${opusPayloadType} minptime=10;useinbandfec=1;maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate=510000;cbr=1`;
|
||||||
|
fmtpFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!fmtpFound) {
|
||||||
|
sdpLines.push(`a=fmtp:${opusPayloadType} minptime=10;useinbandfec=1;maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate=510000;cbr=1`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sdp.sdp = sdpLines.join('\r\n');
|
||||||
|
|
||||||
|
return peerConnection.setLocalDescription(sdp);
|
||||||
|
}).then(() => {
|
||||||
|
socket.emit('offer', id, peerConnection.localDescription);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('answer', (id, description) => {
|
||||||
|
if (peerConnections[id]) peerConnections[id].setRemoteDescription(description);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('candidate', (id, candidate) => {
|
||||||
|
if (peerConnections[id]) peerConnections[id].addIceCandidate(new RTCIceCandidate(candidate));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnectPeer', id => {
|
||||||
|
if (peerConnections[id]) {
|
||||||
|
peerConnections[id].close();
|
||||||
|
delete peerConnections[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSharing() {
|
||||||
|
if (activeStream) {
|
||||||
|
activeStream.getTracks().forEach(t => t.stop());
|
||||||
|
activeStream = null;
|
||||||
|
}
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
Object.values(peerConnections).forEach(pc => pc.close());
|
||||||
|
peerConnections = {};
|
||||||
|
|
||||||
|
localVideo.style.display = 'none';
|
||||||
|
statsPanel.style.display = 'none';
|
||||||
|
startBtn.style.display = 'inline-block';
|
||||||
|
stopBtn.style.display = 'none';
|
||||||
|
statusText.innerText = 'Not Broadcasting';
|
||||||
|
}
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', stopSharing);
|
||||||
|
|
||||||
|
// --- Stats Monitoring Loop ---
|
||||||
|
let lastBytesSent = 0;
|
||||||
|
let lastTimestamp = 0;
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
if (!activeStream || Object.keys(peerConnections).length === 0) return;
|
||||||
|
|
||||||
|
// Get stats from the first active peer connection
|
||||||
|
const pc = Object.values(peerConnections)[0];
|
||||||
|
if (!pc) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await pc.getStats();
|
||||||
|
stats.forEach(report => {
|
||||||
|
if (report.type === 'outbound-rtp' && report.kind === 'video') {
|
||||||
|
const fps = report.framesPerSecond || 0;
|
||||||
|
const bytesSent = report.bytesSent || 0;
|
||||||
|
const timestamp = report.timestamp;
|
||||||
|
const res = `${report.frameWidth || 0}x${report.frameHeight || 0}`;
|
||||||
|
|
||||||
|
let bitrate = 0;
|
||||||
|
if (lastTimestamp && lastBytesSent) {
|
||||||
|
const timeDiff = timestamp - lastTimestamp; // ms
|
||||||
|
const bytesDiff = bytesSent - lastBytesSent;
|
||||||
|
// convert bytes/ms to kbps: (bytes * 8 / 1000) / (timeDiff / 1000) => (bytes * 8) / timeDiff
|
||||||
|
bitrate = Math.round((bytesDiff * 8) / timeDiff);
|
||||||
|
}
|
||||||
|
lastBytesSent = bytesSent;
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
|
||||||
|
document.getElementById('statsFps').innerText = fps;
|
||||||
|
document.getElementById('statsRes').innerText = res;
|
||||||
|
document.getElementById('statsBitrate').innerText = bitrate + ' kbps';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) { console.error("Stats error", e); }
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Initial load of sources & config
|
||||||
|
window.electronAPI.getConfig().then(cfg => {
|
||||||
|
if (cfg.serverUrl) serverUrlInput.value = cfg.serverUrl;
|
||||||
|
if (cfg.serverPassword) serverPasswordInput.value = cfg.serverPassword;
|
||||||
|
});
|
||||||
|
getSourcesBtn.click();
|
||||||
12
client/test-devices.js
Normal file
12
client/test-devices.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const { app, BrowserWindow } = require('electron');
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
const win = new BrowserWindow({ show: false });
|
||||||
|
win.webContents.executeJavaScript(`
|
||||||
|
navigator.mediaDevices.getUserMedia({audio:true}).then(() =>
|
||||||
|
navigator.mediaDevices.enumerateDevices()
|
||||||
|
).then(devices => devices.map(d => d.label))
|
||||||
|
`).then(labels => {
|
||||||
|
console.log("LABELS:", labels.filter(l => l.toLowerCase().includes('screenshare')));
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<span class="dot pulse"></span>
|
<span class="dot pulse"></span>
|
||||||
<span>Connecting to Broadcaster</span>
|
<span>Connecting to Broadcaster</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p style="margin-top:20px; font-size: 0.9rem; color: var(--text-secondary);">Click anywhere to enable audio once connected</p>
|
||||||
</div>
|
</div>
|
||||||
<video id="remoteVideo" autoplay playsinline controls></video>
|
<video id="remoteVideo" autoplay playsinline controls></video>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ socket.on('offer', (id, description) => {
|
|||||||
overlay.classList.add('hidden');
|
overlay.classList.add('hidden');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auto-unmute when the user interacts with the document to bypass browser// Auto-unmute when the user interacts with the document to bypass browser
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
remoteVideo.muted = false;
|
||||||
|
// Set sink to max possible volume explicitly avoiding browser gain staging
|
||||||
|
remoteVideo.volume = 1.0;
|
||||||
|
}, {once: true});
|
||||||
|
|
||||||
peerConnection.onicecandidate = event => {
|
peerConnection.onicecandidate = event => {
|
||||||
if (event.candidate) {
|
if (event.candidate) {
|
||||||
socket.emit('candidate', id, event.candidate);
|
socket.emit('candidate', id, event.candidate);
|
||||||
|
|||||||
Reference in New Issue
Block a user