2026-07-07 08:36:01 +01:00

169 lines
4.8 KiB
JavaScript

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;
}