diff --git a/src/lib/share.svelte.ts b/src/lib/share.svelte.ts index 2d00b8b..5c28a49 100644 --- a/src/lib/share.svelte.ts +++ b/src/lib/share.svelte.ts @@ -230,9 +230,15 @@ class ScreenShare { this.watchPc?.close(); const pc = new RTCPeerConnection({ iceServers: this.iceServers() }); this.watchPc = pc; - pc.addEventListener('track', e => { if (this.watchPc === pc) this.setWatchState('live', e.streams[0]); }); + // The track is announced with the description, before any route exists: the stream only + // counts as live once the connection is up, otherwise the viewer stares at a black picture + pc.addEventListener('track', e => { + if (this.watchPc === pc) this.setWatchState(pc.connectionState === 'connected' ? 'live' : 'connecting', e.streams[0]); + }); pc.addEventListener('connectionstatechange', () => { - if (pc.connectionState === 'failed' && this.watchPc === pc) this.setWatchState('failed'); + if (this.watchPc !== pc) return; + if (pc.connectionState === 'connected') this.setWatchState('live'); + else if (pc.connectionState === 'failed') this.setWatchState('failed'); }); try { await pc.setRemoteDescription({ type: 'offer', sdp }); diff --git a/src/ui/VoiceStage.svelte b/src/ui/VoiceStage.svelte index 893672e..f17d0d4 100644 --- a/src/ui/VoiceStage.svelte +++ b/src/ui/VoiceStage.svelte @@ -101,10 +101,14 @@ const talking = (s: number) => !!voice.talking[s] || (s === self?.session && voice.transmitting); - // The same stream can sit in a tile and on the stage at once + // Mobile browsers do not always honour autoplay for a stream attached later, so play is asked for too function media(node: HTMLMediaElement, stream: MediaStream | null) { - node.srcObject = stream; - return { update(next: MediaStream | null) { if (node.srcObject !== next) node.srcObject = next; } }; + const attach = (s: MediaStream | null) => { + node.srcObject = s; + if (s) node.play().catch(() => {}); + }; + attach(stream); + return { update(next: MediaStream | null) { if (node.srcObject !== next) attach(next); } }; } // Sound comes from one hidden player, so it plays once however many pictures are shown @@ -124,12 +128,23 @@ // Right-click on the picture let showStats = $state(false); - const streamMenu = (e: MouseEvent) => menu.show(e, [ + const streamItems = () => [ { label: 'Open in a separate window', action: popOut }, { label: 'Full screen', action: fullscreen }, { separator: true, label: '' }, { label: 'Stats for nerds', checked: showStats, action: () => (showStats = !showStats) } - ]); + ]; + const streamMenu = (e: MouseEvent) => menu.show(e, streamItems()); + // Touch screens have no right-click: holding a finger on the picture opens the same menu + let hold: ReturnType | undefined; + function touchStart(e: TouchEvent) { + const touch = e.touches[0]; + clearTimeout(hold); + if (e.touches.length !== 1) return; + const { clientX, clientY } = touch; + hold = setTimeout(() => menu.showAt(clientX, clientY, streamItems()), 550); + } + const touchEnd = () => clearTimeout(hold); let frame = $state(); function fullscreen() { @@ -217,8 +232,14 @@ {#if popped}

Shown in a separate window.

{:else} -