const startButton = document.querySelector("#start"); const stopButton = document.querySelector("#stop"); const preview = document.querySelector("#preview"); const state = document.querySelector("#state"); const message = document.querySelector("#message"); const peerConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; let stream; let signal; const peers = new Map(); startButton.addEventListener("click", startSharing); stopButton.addEventListener("click", stopSharing); async function startSharing() { console.info("[host] startSharing clicked"); startButton.disabled = true; setState("connecting", "准备中"); message.textContent = "请选择要分享的桌面、窗口或应用。"; try { stream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 30 }, audio: { echoCancellation: false, noiseSuppression: false, sampleRate: 48000, }, }); console.info("[host] getDisplayMedia resolved", describeStream(stream)); preview.srcObject = stream; signal = openSignalSocket(); stream.getTracks().forEach((track) => { track.addEventListener("ended", () => { console.info("[host] track ended", describeTrack(track)); stopSharing(); }); }); stopButton.disabled = false; setState("live", "推流中"); message.textContent = `观看地址:${location.origin}/viewer`; } catch (error) { console.error("[host] startSharing failed", error); stopSharing(); message.textContent = error instanceof Error ? error.message : "启动分享失败。"; } } function openSignalSocket() { const protocol = location.protocol === "https:" ? "wss" : "ws"; const nextSocket = new WebSocket(`${protocol}://${location.host}/signal?role=host`); nextSocket.addEventListener("open", () => console.info("[host] signal open")); nextSocket.addEventListener("message", (event) => void handleSignalMessage(JSON.parse(event.data))); nextSocket.addEventListener("close", (event) => console.info("[host] signal close", { code: event.code, reason: event.reason })); nextSocket.addEventListener("error", (event) => console.error("[host] signal error", event)); return nextSocket; } async function handleSignalMessage(data) { console.info("[host] signal", data); if (data.type === "viewer-join" && typeof data.viewerId === "string") { await createPeer(data.viewerId); return; } if (data.type === "answer" && typeof data.viewerId === "string") { const peer = peers.get(data.viewerId); if (peer) { await peer.setRemoteDescription(data.sdp); } return; } if (data.type === "candidate" && typeof data.viewerId === "string") { const peer = peers.get(data.viewerId); if (peer && data.candidate) { await peer.addIceCandidate(data.candidate); } return; } if (data.type === "viewer-left" && typeof data.viewerId === "string") { closePeer(data.viewerId); } } async function createPeer(viewerId) { closePeer(viewerId); const peer = new RTCPeerConnection(peerConfig); peers.set(viewerId, peer); stream.getTracks().forEach((track) => peer.addTrack(track, stream)); peer.addEventListener("icecandidate", (event) => { if (event.candidate) { sendSignal({ type: "candidate", to: viewerId, candidate: event.candidate }); } }); peer.addEventListener("connectionstatechange", () => { console.info("[host] peer state", { viewerId, state: peer.connectionState }); if (["failed", "closed", "disconnected"].includes(peer.connectionState)) { closePeer(viewerId); } }); const offer = await peer.createOffer(); await peer.setLocalDescription(offer); sendSignal({ type: "offer", to: viewerId, sdp: peer.localDescription }); } function sendSignal(data) { if (signal?.readyState === WebSocket.OPEN) { signal.send(JSON.stringify(data)); } } function closePeer(viewerId) { const peer = peers.get(viewerId); if (peer) { peer.close(); peers.delete(viewerId); } } function stopSharing() { console.info("[host] stopSharing", { peers: peers.size, signalState: signal?.readyState }); for (const viewerId of peers.keys()) { closePeer(viewerId); } signal?.close(); stream?.getTracks().forEach((track) => track.stop()); signal = undefined; stream = undefined; preview.srcObject = null; startButton.disabled = false; stopButton.disabled = true; setState("idle", "未推流"); } function describeStream(nextStream) { return { tracks: nextStream.getTracks().map(describeTrack), }; } function describeTrack(track) { return { kind: track.kind, label: track.label, enabled: track.enabled, muted: track.muted, readyState: track.readyState, settings: track.getSettings(), }; } function setState(kind, text) { state.className = `badge ${kind}`; state.textContent = text; }