371 lines
15 KiB
JavaScript
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 () {});
|
|
}
|
|
});
|
|
})();
|