Screen sharing #7
@@ -80,7 +80,7 @@ mumh5 keeps the foundation and replaces the experience.
|
||||
|
||||
- Share a screen or a window with the mumh5 users in your channel. A dialog lets you choose what to show, with a live preview, which sound goes with it and the quality, before anything is sent
|
||||
- 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 sharer's tile shows the picture; a click makes it large with everyone else in a strip below, like a meeting, and it can move to a window of its own. 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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
+15
-14
@@ -12,8 +12,8 @@
|
||||
import ResizeHandle from './ResizeHandle.svelte';
|
||||
|
||||
// People in your voice channel as tiles. Shown on windows too narrow for the member list
|
||||
// (always), and on any window while someone in the channel shares a screen: a sharer's tile
|
||||
// carries the picture, and clicking it puts it on a large stage with the tiles as a strip.
|
||||
// (always), and on any window while someone in the channel shares a screen. A stream is shown
|
||||
// large above the tiles, which become a strip; the tiles themselves never carry the picture.
|
||||
// videoOnly: just the picture, as wide as the chat (stacked layout, where the channel list
|
||||
// above already shows who is here)
|
||||
let { always = false, videoOnly = false }: { always?: boolean; videoOnly?: boolean } = $props();
|
||||
@@ -34,7 +34,7 @@
|
||||
return watching?.session === s ? watching.stream : null;
|
||||
}
|
||||
|
||||
// Tiles with a picture come first, then other people who share, so streams sit at the top left
|
||||
// People with a stream we can show come first, then others who share
|
||||
const rank = (s: number) => streamOf(s) ? 0 : session.sharing[s] ? 1 : 2;
|
||||
const people = $derived([...inChannel].sort((a, b) => rank(a.session) - rank(b.session)));
|
||||
|
||||
@@ -52,7 +52,14 @@
|
||||
lastWatch = now;
|
||||
}
|
||||
});
|
||||
$effect(() => { if (focus === self?.session && !mine) focus = null; });
|
||||
// Our own stream opens large when it starts, unless another one is on the stage
|
||||
let hadMine = false;
|
||||
$effect(() => {
|
||||
const has = !!mine;
|
||||
if (has && !hadMine && focus == null && self) focus = self.session;
|
||||
else if (!has && focus === self?.session) focus = null;
|
||||
hadMine = has;
|
||||
});
|
||||
// Without tiles there is nothing to choose: the watched stream, else our own
|
||||
$effect(() => { if (videoOnly) focus = watching ? watching.session : mine ? self?.session ?? null : null; });
|
||||
|
||||
@@ -264,17 +271,12 @@
|
||||
{@const sharing = !!session.sharing[u.session]}
|
||||
{@const isSelf = u.session === self.session}
|
||||
<li style:width="{shownW}px">
|
||||
<button class="tile" class:talking={talking(u.session)} class:self={isSelf} class:video={!!stream} class:focused={focus === u.session}
|
||||
<button class="tile" class:talking={talking(u.session)} class:self={isSelf} class:focused={focus === u.session}
|
||||
onclick={() => tileClick(u.session)}
|
||||
oncontextmenu={e => menu.show(e, userMenu(u))}
|
||||
title={stream ? (focus === u.session ? 'Back to tiles' : 'Show large') : sharing && !isSelf ? 'Sharing their screen. Click to watch' : store.displayName(u)}
|
||||
aria-label={stream ? `${focus === u.session ? 'Shrink' : 'Enlarge'} the screen of ${store.displayName(u)}` : sharing && !isSelf ? `Watch the screen of ${store.displayName(u)}` : store.displayName(u)}>
|
||||
{#if stream}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video use:media={stream} autoplay playsinline muted></video>
|
||||
{:else}
|
||||
<Avatar name={u.name} size={avatar} speaking={talking(u.session)} />
|
||||
{/if}
|
||||
title={stream ? (focus === u.session ? 'Hide the stream' : 'Show the stream') : sharing && !isSelf ? 'Sharing their screen. Click to watch' : store.displayName(u)}
|
||||
aria-label={stream ? `${focus === u.session ? 'Hide' : 'Show'} the screen of ${store.displayName(u)}` : sharing && !isSelf ? `Watch the screen of ${store.displayName(u)}` : store.displayName(u)}>
|
||||
<Avatar name={u.name} size={avatar} speaking={talking(u.session)} />
|
||||
<span class="who">{store.displayName(u)}</span>
|
||||
{#if watching?.session === u.session && watching.state !== 'live'}
|
||||
<span class="note">{watching.state === 'connecting' ? 'Connecting...' : 'Could not connect'}</span>
|
||||
@@ -315,7 +317,6 @@
|
||||
.tile:hover { background: var(--bg-hover); }
|
||||
.tile.talking { border-color: var(--speaking); }
|
||||
.tile.focused { border-color: var(--accent); }
|
||||
.tile video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; }
|
||||
/* Name tag over the lower left corner */
|
||||
.who { position: absolute; left: 4px; bottom: 4px; max-width: calc(100% - 8px); padding: 1px 6px; font-size: 12px; color: #fff; background: rgb(0 0 0 / 0.6);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
@@ -74,14 +74,16 @@ try {
|
||||
await pickAndStart();
|
||||
await alice.page.locator('.stage .status', { hasText: '0 watching' }).waitFor();
|
||||
await alice.page.locator('.sidebar .row.user.self').getByTitle('You are sharing your screen').waitFor();
|
||||
// The sharer's own tile shows what is being sent; clicking it makes it large
|
||||
await alice.page.waitForFunction(() => (document.querySelector('.stage .tile.self video') as HTMLVideoElement | null)?.videoWidth! > 0, null, { timeout: 15000 });
|
||||
await alice.page.locator('.stage .tile.self').click();
|
||||
// The sharer sees what is being sent, large by default; the tiles themselves never carry the picture
|
||||
const ownView = alice.page.getByRole('region', { name: 'Screen share' });
|
||||
await ownView.getByText('Your screen').waitFor();
|
||||
await alice.page.waitForFunction(() => (document.querySelector('.spot video') as HTMLVideoElement | null)?.videoWidth! > 0, null, { timeout: 15000 });
|
||||
assert.equal(await alice.page.locator('.stage .tile video').count(), 0);
|
||||
await ownView.getByRole('button', { name: 'Back to tiles' }).click();
|
||||
await ownView.waitFor({ state: 'detached' });
|
||||
console.log('ok: sharing started from the dialog, own tile shows the stream');
|
||||
await alice.page.locator('.stage .tile.self').click();
|
||||
await ownView.waitFor();
|
||||
console.log('ok: sharing started from the dialog, own stream shown large');
|
||||
|
||||
// Stacked layout: only the picture, as wide as the chat, and the layout stays stacked on narrow windows
|
||||
const setLayout = async (name: string) => {
|
||||
@@ -152,13 +154,13 @@ try {
|
||||
await bob.page.waitForFunction(() => (document.querySelector('.spot video') as HTMLVideoElement | null)?.videoWidth! > 0);
|
||||
console.log('ok: stream in a separate window, back when it is closed');
|
||||
|
||||
// Like a meeting: back to tiles keeps the picture in the tile, a click makes it large again
|
||||
// Back to tiles hides the picture; the sharer's tile brings it back
|
||||
await view.getByRole('button', { name: 'Back to tiles' }).click();
|
||||
await view.waitFor({ state: 'detached' });
|
||||
await bob.page.waitForFunction(() => (document.querySelector('.stage .tile.video video') as HTMLVideoElement | null)?.videoWidth! > 0);
|
||||
assert.equal(await bob.page.locator('.stage .tile video').count(), 0);
|
||||
await tile.click();
|
||||
await view.waitFor();
|
||||
console.log('ok: stream in the tile, large on click');
|
||||
console.log('ok: large view hidden and shown again from the tile');
|
||||
|
||||
// Leaving and coming back
|
||||
await view.getByRole('button', { name: 'Stop watching' }).click();
|
||||
|
||||
Reference in New Issue
Block a user