Settings: choose a STUN server from Google, Cloudflare or a typed address

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 23:13:40 +02:00
co-authored by Claude Opus 5.5
parent 0a290de229
commit 98e44d7082
4 changed files with 43 additions and 6 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ mumh5 keeps the foundation and replaces the experience.
- Sound: on Linux one program or everything except mumh5 itself (through PipeWire, so viewers do not hear the voice chat twice); on Windows the whole system; in a browser what the browser offers - Sound: on Linux one program or everything except mumh5 itself (through PipeWire, so viewers do not hear the voice chat twice); on Windows the whole system; in a browser what the browser offers
- People in the channel see an indicator next to your name, on your tile and in your profile, and click to watch. The tiles above the chat fill a stage you can drag taller or shorter. A stream opens large, with everyone as a strip of tiles below, like a meeting, and can move to a window of its own; in the stacked layout it is just the picture, as wide as the chat. Sounds announce streams and viewers. Viewers set the stream's volume - People in the channel see an indicator next to your name, on your tile and in your profile, and click to watch. The tiles above the chat fill a stage you can drag taller or shorter. A stream opens large, with everyone as a strip of tiles below, like a meeting, and can move to a window of its own; in the stacked layout it is just the picture, as wide as the chat. Sounds announce streams and viewers. Viewers set the stream's volume
- No server setup and no extra account: the setup messages travel through the Mumble server, the stream goes directly between the two clients (WebRTC), up to 8 viewers. Regular Mumble clients do not see streams - No server setup and no extra account: the setup messages travel through the Mumble server, the stream goes directly between the two clients (WebRTC), up to 8 viewers. Regular Mumble clients do not see streams
- Direct connections mean sharer and viewer see each other's IP address; mumh5 says so before the first use. Across the internet both sides need a STUN server. The browser version uses the one built into its proxy; in the desktop app you enter one in Settings, Voice (the proxy's address works there too), and none is contacted unless you do - Direct connections mean sharer and viewer see each other's IP address; mumh5 says so before the first use. Across the internet both sides need a STUN server. The browser version uses the one built into its proxy; in the desktop app you choose one in Settings, Voice (Google, Cloudflare, or any address such as your proxy's), and none is contacted unless you do
- Tested between two desktop instances on one machine with a test picture. Sound capture, real screens, connections across the internet, Windows and the browser build are untested - Tested between two desktop instances on one machine with a test picture. Sound capture, real screens, connections across the internet, Windows and the browser build are untested
### Chat ### Chat
+26 -4
View File
@@ -16,6 +16,21 @@
const s = store.settings; const s = store.settings;
let embedHosts = $state(s.embedHosts.join('\n')); let embedHosts = $state(s.embedHosts.join('\n'));
let testResult = $state(''); let testResult = $state('');
// Public STUN servers to pick from; anything else is typed in
const STUN_PRESETS = [
{ name: 'Google', url: 'stun.l.google.com:19302' },
{ name: 'Cloudflare', url: 'stun.cloudflare.com:3478' }
];
let stunCustom = $state(!!s.stunServer && !STUN_PRESETS.some(p => p.url === s.stunServer));
const stunChoice = $derived(stunCustom ? 'custom' : s.stunServer);
function chooseStun(value: string) {
stunCustom = value === 'custom';
// Keep what was typed when switching to "another server"
if (!stunCustom) s.stunServer = value;
else if (STUN_PRESETS.some(p => p.url === s.stunServer)) s.stunServer = '';
store.saveSettings();
}
// Settings that only work through the upload host are disabled until it is set up // Settings that only work through the upload host are disabled until it is set up
const hasUpload = $derived(!!(s.uploadHost.trim() && s.uploadKey.trim())); const hasUpload = $derived(!!(s.uploadHost.trim() && s.uploadKey.trim()));
const needsUpload = $derived(hasUpload ? undefined : 'Needs an upload host and key'); const needsUpload = $derived(hasUpload ? undefined : 'Needs an upload host and key');
@@ -60,9 +75,16 @@
{#if tab === 'voice'} {#if tab === 'voice'}
<VoiceSettings /> <VoiceSettings />
<h3>Screen sharing</h3> <h3>Screen sharing</h3>
<label for="s-stun">STUN server <span class="opt">(optional)</span></label> <label for="s-stun">STUN server</label>
<input id="s-stun" bind:value={s.stunServer} onchange={() => store.saveSettings()} placeholder="stun.example.com:3478" autocomplete="off" /> <select id="s-stun" value={stunChoice} onchange={e => chooseStun(e.currentTarget.value)}>
<p class="help">Screen sharing connects you directly to the people watching. Across the internet both the person sharing and the people watching need a STUN server here to find a route; it learns your IP address but never sees the stream. Empty means no outside server is contacted, which only works on the same network.</p> <option value="">{isWeb ? 'This site\'s own server' : 'None (same network only)'}</option>
{#each STUN_PRESETS as p (p.url)}<option value={p.url}>{p.name} ({p.url})</option>{/each}
<option value="custom">Another server...</option>
</select>
{#if stunChoice === 'custom'}
<input class="stun-custom" bind:value={s.stunServer} onchange={() => store.saveSettings()} placeholder="stun.example.com:3478" autocomplete="off" aria-label="STUN server address" />
{/if}
<p class="help">Screen sharing connects you directly to the people watching. Across the internet both sides need a STUN server to find a route; it learns your IP address but never sees the stream. {isWeb ? 'By default the site you are on provides it.' : 'With none, no outside server is contacted and streams only work on the same network.'} Networks that block direct connections, like many mobile ones, need more than STUN.</p>
{:else if tab === 'sounds'} {:else if tab === 'sounds'}
<SoundSettings /> <SoundSettings />
{:else if tab === 'appearance'} {:else if tab === 'appearance'}
@@ -123,7 +145,7 @@
.tabs button { padding: 8px 14px; color: var(--text-dim); font-weight: 600; white-space: nowrap; } .tabs button { padding: 8px 14px; color: var(--text-dim); font-weight: 600; white-space: nowrap; }
.tabs button.active { color: var(--text); box-shadow: inset 0 -2px 0 var(--accent); } .tabs button.active { color: var(--text); box-shadow: inset 0 -2px 0 var(--accent); }
h3 { margin: 22px 0 0; font-size: 14px; } h3 { margin: 22px 0 0; font-size: 14px; }
.opt { text-transform: none; font-weight: 400; letter-spacing: 0; color: var(--text-faint); } .stun-custom { margin-top: 6px; }
.help { margin: 10px 0 0; font-size: 13px; color: var(--text-dim); } .help { margin: 10px 0 0; font-size: 13px; color: var(--text-dim); }
.row { display: flex; align-items: flex-end; gap: 8px; } .row { display: flex; align-items: flex-end; gap: 8px; }
.row > div { flex: 1; } .row > div { flex: 1; }
+1 -1
View File
@@ -341,7 +341,7 @@
{/each} {/each}
</div> </div>
{#if watching?.state === 'failed'} {#if watching?.state === 'failed'}
<p class="failed">Could not connect to the stream. Across the internet both of you need a STUN server set in Settings, Voice (the person sharing has to restart the stream after setting it).</p> <p class="failed">Could not connect to the stream. Across the internet both of you need a STUN server, chosen in Settings, Voice (the person sharing has to restart the stream afterwards).</p>
{/if} {/if}
{/if} {/if}
</section> </section>
+15
View File
@@ -334,6 +334,21 @@ try {
await page.getByRole('button', { name: 'Done', exact: true }).click(); await page.getByRole('button', { name: 'Done', exact: true }).click();
console.log('ok: quick audio options on right-click'); console.log('ok: quick audio options on right-click');
// STUN server for screen sharing: none by default, presets, or a typed address
await page.getByTitle('Settings').click();
const stun = page.getByLabel('STUN server', { exact: true });
assert.equal(await stun.inputValue(), '');
await stun.selectOption({ label: 'Cloudflare (stun.cloudflare.com:3478)' });
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem('mumh5.settings')!).stunServer), 'stun.cloudflare.com:3478');
await stun.selectOption('custom');
await page.getByLabel('STUN server address').fill('stun.example.org:3478');
await page.getByLabel('STUN server address').blur();
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem('mumh5.settings')!).stunServer), 'stun.example.org:3478');
await stun.selectOption('');
assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem('mumh5.settings')!).stunServer), '');
await page.getByRole('button', { name: 'Done', exact: true }).click();
console.log('ok: STUN server choices');
// Color schemes apply right away and are remembered // Color schemes apply right away and are remembered
await page.getByTitle('Settings').click(); await page.getByTitle('Settings').click();
await page.getByRole('tab', { name: 'Appearance' }).click(); await page.getByRole('tab', { name: 'Appearance' }).click();