zebra-spaces: 'share gameplay' on spotlit game tile — publishes the tab as a screen-share

Speakers can now broadcast their gameplay to the room. The spotlit
game tile's meta bar gets a 'share gameplay' button (next to where
fullscreen/etc live for other tile kinds). Click:
- sfuPublishScreen({ preferCurrentTab: true })  — Chromium picker
  auto-selects the current tab; Firefox ignores the hint and shows
  the normal picker (user just clicks 'this tab')
- existing SFU screen-share pipeline takes over from there — listeners
  and other speakers see the tab via their subscribe leg, with the
  game iframe visible at full size in their spotlight
- button label flips to 'stop sharing' while live; click again unpubs

Only canSpeak(myRole) gets the button — listeners are audience only.

The game iframe still loads + plays for the local user the same way
(the share is just a screen-capture of what's already on screen), so
there's no double-iframe or weird audio routing. Audience sees a
screen-share with the game running in real time.
This commit is contained in:
Russell Ballestrini 2026-06-02 12:16:19 -04:00
parent e3a62be500
commit 5c26bc3232
No known key found for this signature in database

View file

@ -1701,7 +1701,29 @@ function setSpotlight(kind, pubHex){
iframe.loading = 'eager';
const meta = document.createElement('div'); meta.className = 'screen-meta';
const who = document.createElement('span'); who.textContent = 'game: ' + g.label;
meta.appendChild(who);
const ctl = document.createElement('span');
/* 'share gameplay' captures the user's current tab via getDisplayMedia
* and publishes it through the same SFU screen-share path so audience
* members see the game live. Chromium's preferCurrentTab hint makes
* the picker default to this tab. Only speakers can publish — listeners
* can still watch the host's gameplay if someone shares. */
if (canSpeak(myRole)){
const shareBtn = document.createElement('button');
shareBtn.className = 'small';
const refreshLabel = () => { shareBtn.textContent = sfuScreenPC ? 'stop sharing' : 'share gameplay'; };
refreshLabel();
shareBtn.onclick = (ev) => {
ev.stopPropagation();
if (sfuScreenPC){
sfuUnpublishScreen().then(refreshLabel);
} else {
logLine('', 'share gameplay: pick this tab in the picker');
sfuPublishScreen({ preferCurrentTab: true }).then(refreshLabel).catch(()=>refreshLabel());
}
};
ctl.appendChild(shareBtn);
}
meta.appendChild(who); meta.appendChild(ctl);
big.appendChild(iframe); big.appendChild(meta);
$('spotlight').appendChild(big);
const thumb = thumbElementFor('game', pubHex);
@ -1865,7 +1887,7 @@ async function sfuPublish(){
}
/* ----- screen share ----- */
async function sfuPublishScreen(){
async function sfuPublishScreen(opts){
if (sfuScreenPC || !myKeys || !roomID) return;
let stream;
try {
@ -1874,11 +1896,16 @@ async function sfuPublishScreen(){
* gracefully; nothing is rejected. The constraint matters for the audio
* side: without channelCount:2 + sampleRate:48000, getDisplayMedia on
* Chrome can hand back mono 16kHz, which kills music quality. */
stream = await navigator.mediaDevices.getDisplayMedia({
const constraints = {
video: { width:{ideal:1920}, height:{ideal:1080}, frameRate:{ideal:30} },
audio: { echoCancellation:false, noiseSuppression:false, autoGainControl:false,
channelCount:2, sampleRate:48000 }
});
channelCount:2, sampleRate:48000 },
};
/* Chromium-only hint — when sharing a game we want the picker to
* default to the current tab so the user can just click 'share'
* and the iframe content (gameplay) goes out without hunting. */
if (opts && opts.preferCurrentTab) constraints.preferCurrentTab = true;
stream = await navigator.mediaDevices.getDisplayMedia(constraints);
} catch(e){ logLine('err','screen share cancelled: '+e.message); return; }
sfuScreenStream = stream;
const vTracks = stream.getVideoTracks(), aTracks = stream.getAudioTracks();
@ -3380,8 +3407,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">aec8e1a791c0723464e7405f71b11ae7</span><br>
sha256 <span class="stamp-sha">baa9354fbc0ff538dcb65e510ae7c6e1b01a5032e8e44c8ff51b0c76f821f4cf</span><br>
md5 <span class="stamp-md5">7a6f4dd81ede2811d219eabc4603f026</span><br>
sha256 <span class="stamp-sha">e728807c95404767e824dba0a87e9ad8446d739316c0c8f809e4befc0b9566d5</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>