Screen sharing #17

Merged
kibi merged 3 commits from screen-sharing into main 2026-10-01 23:01:00 +00:00
3 changed files with 10 additions and 4 deletions
Showing only changes of commit e41a2cfd70 - Show all commits
+4
View File
@@ -43,6 +43,9 @@ class ScreenShare {
host = $state.raw<Session | null>(null); host = $state.raw<Session | null>(null);
stream = $state.raw<MediaStream | null>(null); stream = $state.raw<MediaStream | null>(null);
kind = $state<ShareKind>('screen'); kind = $state<ShareKind>('screen');
// Show our own camera mirrored to ourselves, as a mirror would. Not a camera pointing away
// from us (a phone's back camera), and never what the others get.
mirror = $state(false);
viewers = $state(0); viewers = $state(0);
// Streams we receive: one screen at a time, and any number of cameras next to it // Streams we receive: one screen at a time, and any number of cameras next to it
watches = $state.raw<Watching[]>([]); watches = $state.raw<Watching[]>([]);
@@ -142,6 +145,7 @@ class ScreenShare {
} }
this.usesVirtualMic = usesVirtualMic; this.usesVirtualMic = usesVirtualMic;
this.kind = kind; this.kind = kind;
this.mirror = kind === 'camera' && stream.getVideoTracks()[0]?.getSettings().facingMode !== 'environment';
this.host = s; this.host = s;
this.stream = stream; this.stream = stream;
this.send(s, this.others(s), SHARE.on, kind); this.send(s, this.others(s), SHARE.on, kind);
+3 -1
View File
@@ -220,7 +220,7 @@
<span class="label">Preview</span> <span class="label">Preview</span>
<div class="preview"> <div class="preview">
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video bind:this={video} autoplay playsinline muted></video> <video bind:this={video} class:mirror={kind === 'camera' && preview?.getVideoTracks()[0]?.getSettings().facingMode !== 'environment'} autoplay playsinline muted></video>
{#if !preview}<p>Nothing chosen yet</p>{/if} {#if !preview}<p>Nothing chosen yet</p>{/if}
</div> </div>
@@ -280,6 +280,8 @@
.name { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .name { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview { position: relative; background: #000; aspect-ratio: 16 / 9; max-height: 240px; margin: 0 auto; width: 100%; } .preview { position: relative; background: #000; aspect-ratio: 16 / 9; max-height: 240px; margin: 0 auto; width: 100%; }
.preview video { width: 100%; height: 100%; object-fit: contain; display: block; } .preview video { width: 100%; height: 100%; object-fit: contain; display: block; }
/* Your own camera as a mirror shows you; a camera facing away is left as it is */
.preview video.mirror { transform: scaleX(-1); }
.preview p { position: absolute; inset: 0; margin: 0; display: flex; align-items: center; justify-content: center; color: #aaa; font-size: 13px; } .preview p { position: absolute; inset: 0; margin: 0; display: flex; align-items: center; justify-content: center; color: #aaa; font-size: 13px; }
.check { display: flex; align-items: center; gap: 8px; text-transform: none; font-weight: 400; letter-spacing: 0; font-size: 14px; color: var(--text); margin-top: 12px; } .check { display: flex; align-items: center; gap: 8px; text-transform: none; font-weight: 400; letter-spacing: 0; font-size: 14px; color: var(--text); margin-top: 12px; }
.check input { width: auto; } .check input { width: auto; }
+3 -3
View File
@@ -267,7 +267,7 @@
{:else} {:else}
<div class="pic" role="presentation" oncontextmenu={streamMenu} <div class="pic" role="presentation" oncontextmenu={streamMenu}
ontouchstart={touchStart} ontouchend={touchEnd} ontouchmove={touchEnd} ontouchcancel={touchEnd}> ontouchstart={touchStart} ontouchend={touchEnd} ontouchmove={touchEnd} ontouchcancel={touchEnd}>
<video bind:this={spotVideo} use:media={focused} autoplay playsinline muted ondblclick={fullscreen}></video> <video bind:this={spotVideo} class:mirror={focus === self.session && share.mirror} use:media={focused} autoplay playsinline muted ondblclick={fullscreen}></video>
{#if focus !== self.session && focusWatch && focusWatch.state !== 'live'} {#if focus !== self.session && focusWatch && focusWatch.state !== 'live'}
<p class="pending" class:bad={focusWatch.state === 'failed'}>{focusWatch.state === 'failed' <p class="pending" class:bad={focusWatch.state === 'failed'}>{focusWatch.state === 'failed'
? 'Could not connect to the stream. Across the internet both of you need a STUN server; networks that block direct connections need a relay. Both are in Settings, Voice.' ? 'Could not connect to the stream. Across the internet both of you need a STUN server; networks that block direct connections need a relay. Both are in Settings, Voice.'
@@ -298,7 +298,7 @@
aria-label={stream ? `${focus === u.session ? 'Hide' : 'Show'} the ${sharing || 'stream'} of ${store.displayName(u)}` : sharing && !isSelf ? `Watch the ${sharing} of ${store.displayName(u)}` : store.displayName(u)}> aria-label={stream ? `${focus === u.session ? 'Hide' : 'Show'} the ${sharing || 'stream'} of ${store.displayName(u)}` : sharing && !isSelf ? `Watch the ${sharing} of ${store.displayName(u)}` : store.displayName(u)}>
{#if cam} {#if cam}
<!-- svelte-ignore a11y_media_has_caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video class:mirror={isSelf} use:media={cam} autoplay playsinline muted></video> <video class:mirror={isSelf && share.mirror} use:media={cam} autoplay playsinline muted></video>
{:else} {:else}
<Avatar name={u.name} size={avatar} speaking={talking(u.session)} /> <Avatar name={u.name} size={avatar} speaking={talking(u.session)} />
{/if} {/if}
@@ -414,7 +414,7 @@
/* A camera keeps its own shape inside the tile, whatever size the tile is: nothing is cut /* A camera keeps its own shape inside the tile, whatever size the tile is: nothing is cut
off, a phone held upright gets bars at the sides. Your own is mirrored, like a mirror. */ off, a phone held upright gets bars at the sides. Your own is mirrored, like a mirror. */
.tile video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; } .tile video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; }
.tile video.mirror { transform: scaleX(-1); } .tile video.mirror, .spot video.mirror { transform: scaleX(-1); }
.icon-btn.live, .icon-btn.live:hover { color: var(--speaking); } .icon-btn.live, .icon-btn.live:hover { color: var(--speaking); }
.camgrid { position: relative; display: flex; flex-direction: column; flex: none; } .camgrid { position: relative; display: flex; flex-direction: column; flex: none; }
/* Name tag over the lower left corner */ /* Name tag over the lower left corner */