Files
BridgeSwarm/examples/screenshare/app.js
T
Raven Scott f71b01bdb2
CI / Build & Test (push) Successful in 8m7s
Update examples
2026-07-27 02:28:23 -04:00

371 lines
15 KiB
JavaScript

(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<object, { pc: RTCPeerConnection }>} conn -> webrtc state (sharer side) */
const connToSharerPc = new Map();
/** @type {Map<object, { pc: RTCPeerConnection }>} 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 () {});
}
});
})();