(function () { const statusEl = document.getElementById('status'); const topicEl = document.getElementById('topic'); const btnJoin = document.getElementById('btnJoin'); const btnLeave = document.getElementById('btnLeave'); const btnShare = document.getElementById('btnShare'); const btnStopShare = document.getElementById('btnStopShare'); const peersEl = document.getElementById('peers'); const publicKeyEl = document.getElementById('publicKey'); const remoteVideo = document.getElementById('remoteVideo'); const videoPlaceholder = document.getElementById('videoPlaceholder'); const placeholderBlock = document.getElementById('placeholderBlock'); const btnPlay = document.getElementById('btnPlay'); const videoWrap = document.getElementById('videoWrap'); const localPreview = document.getElementById('localPreview'); const previewWrap = document.getElementById('previewWrap'); const streamStateEl = document.getElementById('streamState'); const btnPip = document.getElementById('btnPip'); function setRemoteVideoPlaying(playing) { if (playing) { remoteVideo.style.display = 'block'; placeholderBlock.style.display = 'none'; btnPlay.style.display = 'none'; } else { remoteVideo.style.display = 'none'; if (remoteVideo.srcObject) { placeholderBlock.style.display = 'flex'; videoPlaceholder.textContent = 'Stream paused or ended.'; btnPlay.style.display = ''; } } } remoteVideo.addEventListener('playing', function () { setRemoteVideoPlaying(true); }); remoteVideo.addEventListener('pause', function () { setRemoteVideoPlaying(false); }); remoteVideo.addEventListener('ended', function () { setRemoteVideoPlaying(false); }); btnPlay.addEventListener('click', function () { if (remoteVideo.srcObject) remoteVideo.play(); }); // No public STUN/TURN — fully enclosed P2P; ICE uses only host/local candidates function updateStreamState(label) { streamStateEl.textContent = label === 'live' ? 'Live' : label === 'connecting' ? 'Connecting…' : label === 'failed' ? 'Connection failed.' : label === 'disconnected' ? 'Reconnecting…' : ''; streamStateEl.className = 'bs-pill' + (label === 'live' ? ' bs-pill--live' : ''); } function setStatus(msg, isError) { statusEl.textContent = msg; statusEl.className = 'bs-status' + (isError ? ' error' : ''); } const session = BridgeSwarmExamples.sessionStore('screenshare'); let swarm = null; const connections = []; let sharedStream = null; /** @type {Map} conn -> webrtc state (sharer side) */ const connToSharerPc = new Map(); /** @type {Map} conn -> webrtc state (viewer side) */ const connToViewerPc = new Map(); function updatePeers() { peersEl.textContent = 'Peers: ' + connections.length; } function sendSignaling(conn, msg) { const data = new TextEncoder().encode(JSON.stringify(msg)); conn.write(data).catch(function (err) { console.warn('Signaling write failed:', err); }); } function stopSharing() { if (sharedStream) { sharedStream.getTracks().forEach(function (t) { t.stop(); }); sharedStream = null; } connToSharerPc.forEach(function (state) { state.pc.close(); }); connToSharerPc.clear(); localPreview.srcObject = null; previewWrap.style.display = 'none'; btnShare.disabled = false; btnStopShare.disabled = true; btnStopShare.style.display = 'none'; setStatus('Stopped sharing. You can share again or leave.'); } function setupSharerForConn(conn, stream) { const pc = new RTCPeerConnection({ iceServers: [] }); connToSharerPc.set(conn, { pc: pc }); pc.onconnectionstatechange = function () { if (pc.connectionState === 'failed') { connToSharerPc.delete(conn); pc.close(); } }; pc.onicecandidate = function (e) { if (e.candidate) sendSignaling(conn, { type: 'webrtc-ice', candidate: e.candidate }); }; stream.getTracks().forEach(function (track) { pc.addTrack(track, stream); }); pc.createOffer().then(function (offer) { return pc.setLocalDescription(offer); }).then(function () { sendSignaling(conn, { type: 'webrtc-offer', sdp: pc.localDescription }); }).catch(function (err) { console.error('Sharer createOffer failed:', err); }); conn.on('data', function (data) { try { const msg = JSON.parse(new TextDecoder().decode(data)); if (msg.type === 'webrtc-answer' && msg.sdp) { pc.setRemoteDescription(new RTCSessionDescription(msg.sdp)).catch(function (e) { console.warn(e); }); } else if (msg.type === 'webrtc-ice' && msg.candidate) { pc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(function (e) { console.warn(e); }); } } catch (e) {} }); } async function joinTopic(topic, opts) { if (swarm) return; opts = opts || {}; setStatus('Joining topic "' + topic + '"...'); swarm = new window.BridgeSwarm(session.swarmOptions('bridge-swarm-screenshare', topic, opts)); swarm.on('connection', function (conn, peerInfo) { connections.push({ conn: conn, peerInfo: peerInfo }); updatePeers(); if (sharedStream) { setupSharerForConn(conn, sharedStream); setStatus('Sharing to ' + connections.length + ' peer(s).'); } conn.on('data', function (data) { if (connToSharerPc.has(conn)) return; try { const msg = JSON.parse(new TextDecoder().decode(data)); if (msg.type === 'webrtc-offer' && msg.sdp) { let state = connToViewerPc.get(conn); if (state) { state.pc.close(); connToViewerPc.delete(conn); } const pc = new RTCPeerConnection({ iceServers: [] }); const iceCandidateQueue = []; connToViewerPc.set(conn, { pc: pc, iceCandidateQueue: iceCandidateQueue }); pc.ontrack = function (e) { if (e.streams && e.streams[0]) { remoteVideo.srcObject = e.streams[0]; remoteVideo.style.display = 'block'; placeholderBlock.style.display = 'none'; videoPlaceholder.textContent = 'Connecting…'; btnPlay.style.display = 'none'; updateStreamState('connecting'); btnPip.style.display = ''; btnPip.disabled = !document.pictureInPictureEnabled; } }; pc.oniceconnectionstatechange = function () { if (connToViewerPc.get(conn)?.pc !== pc) return; if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') { updateStreamState('live'); } else if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') { updateStreamState(pc.iceConnectionState === 'failed' ? 'failed' : 'disconnected'); } }; pc.onconnectionstatechange = function () { if (pc.connectionState === 'failed') { remoteVideo.srcObject = null; remoteVideo.style.display = 'none'; placeholderBlock.style.display = 'flex'; videoPlaceholder.textContent = 'Connection failed or lost.'; btnPlay.style.display = 'none'; updateStreamState(''); btnPip.style.display = 'none'; connToViewerPc.delete(conn); pc.close(); } }; pc.onicecandidate = function (e) { if (e.candidate) sendSignaling(conn, { type: 'webrtc-ice', candidate: e.candidate }); }; pc.setRemoteDescription(new RTCSessionDescription(msg.sdp)).then(function () { iceCandidateQueue.forEach(function (c) { pc.addIceCandidate(new RTCIceCandidate(c)).catch(function (e) { console.warn(e); }); }); iceCandidateQueue.length = 0; return pc.createAnswer(); }).then(function (answer) { return pc.setLocalDescription(answer); }).then(function () { sendSignaling(conn, { type: 'webrtc-answer', sdp: pc.localDescription }); }).catch(function (err) { console.error('Viewer createAnswer failed:', err); }); } else if (msg.type === 'webrtc-ice' && msg.candidate) { state = connToViewerPc.get(conn); if (state) { if (state.pc.remoteDescription) { state.pc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(function (e) { console.warn(e); }); } else { state.iceCandidateQueue.push(msg.candidate); } } } } catch (e) {} }); conn.on('end', function () { const i = connections.findIndex(function (c) { return c.conn === conn; }); if (i !== -1) connections.splice(i, 1); updatePeers(); const sharerState = connToSharerPc.get(conn); if (sharerState) { sharerState.pc.close(); connToSharerPc.delete(conn); } const viewerState = connToViewerPc.get(conn); if (viewerState) { viewerState.pc.close(); connToViewerPc.delete(conn); remoteVideo.srcObject = null; remoteVideo.style.display = 'none'; placeholderBlock.style.display = 'flex'; videoPlaceholder.textContent = 'Join a topic. Share your screen, or wait for a peer to share.'; btnPlay.style.display = 'none'; updateStreamState(''); btnPip.style.display = 'none'; } }); conn.on('error', function (err) { console.warn('Peer error:', err); }); }); await swarm.join(topic); await session.resumeConnections(swarm); session.markJoined(swarm, topic); const pubKey = await swarm.getPublicKey(); publicKeyEl.textContent = 'Your public key: ' + pubKey; setStatus('Joined. Share your screen now or wait for viewers.'); btnJoin.disabled = true; btnLeave.disabled = false; btnShare.disabled = false; } BridgeSwarmExamples.waitForBridgeSwarm().then(async function () { window.addEventListener('bridge-swarm-host-disconnect', function onHostDisconnect() { setStatus('Native host disconnected. Refresh or re-join.', true); btnJoin.disabled = true; btnShare.disabled = true; btnStopShare.disabled = true; }, { once: false }); var sess = session.load(); if (sess && sess.active && sess.topic) { topicEl.value = sess.topic; setStatus('Restoring session…'); try { await joinTopic(sess.topic, { swarmId: sess.swarmId }); } catch (err) { session.clear(); setStatus(err.message || 'Could not restore session', true); } return; } setStatus('Join a topic. Share your screen or open in another tab to view.'); }).catch(function (err) { setStatus(err.message, true); }); btnJoin.addEventListener('click', async function () { if (typeof window.BridgeSwarm === 'undefined') { setStatus('Extension not ready yet. Wait a moment and try again.', true); return; } const topic = topicEl.value.trim() || 'bridge-swarm-screenshare'; if (swarm) return; try { await joinTopic(topic); } catch (err) { setStatus('Error: ' + err.message, true); } }); btnLeave.addEventListener('click', async function () { if (!swarm) return; stopSharing(); connToViewerPc.forEach(function (state) { state.pc.close(); }); connToViewerPc.clear(); remoteVideo.srcObject = null; remoteVideo.style.display = 'none'; placeholderBlock.style.display = 'flex'; videoPlaceholder.textContent = 'Join a topic. Share your screen, or wait for a peer to share.'; btnPlay.style.display = 'none'; updateStreamState(''); btnPip.style.display = 'none'; try { const topic = topicEl.value.trim() || 'bridge-swarm-screenshare'; await swarm.leave(topic); await swarm.destroy(); connections.length = 0; updatePeers(); swarm = null; session.clear(); setStatus('Left topic. You can join again.'); btnJoin.disabled = false; btnLeave.disabled = true; btnShare.disabled = true; } catch (err) { console.warn(err); } }); btnShare.addEventListener('click', async function () { if (!swarm) { setStatus('Join a topic first.', true); return; } try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 60 } }, preferCurrentTab: true }); sharedStream = stream; localPreview.srcObject = stream; previewWrap.style.display = 'block'; btnShare.disabled = true; btnStopShare.disabled = false; btnStopShare.style.display = ''; setStatus(connections.length === 0 ? 'Sharing. Waiting for viewers…' : 'Sharing to ' + connections.length + ' peer(s).'); stream.getVideoTracks()[0].addEventListener('ended', function () { stopSharing(); setStatus('Screen share ended (stopped in browser).', false); }); connections.forEach(function (c) { setupSharerForConn(c.conn, stream); }); } catch (err) { setStatus('Screen capture failed: ' + err.message, true); } }); btnStopShare.addEventListener('click', function () { stopSharing(); }); btnPip.addEventListener('click', function () { if (!remoteVideo.srcObject || !document.pictureInPictureEnabled) return; if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(function () {}); } else { remoteVideo.requestPictureInPicture().catch(function () {}); } }); })();