const player = document.querySelector("#player"); const playButton = document.querySelector("#play"); const audioButton = document.querySelector("#audio"); const reloadButton = document.querySelector("#reload"); const goLiveButton = document.querySelector("#go-live"); const state = document.querySelector("#state"); const message = document.querySelector("#message"); const latency = document.querySelector("#latency"); const peerConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; let signal; let peer; let viewerId = crypto.randomUUID(); let retryTimer; let playPromise; let remoteStream; player.muted = true; player.defaultMuted = true; audioButton.disabled = true; audioButton.textContent = "等待声音"; playButton.addEventListener("click", () => void playWhenReady()); audioButton.addEventListener("click", () => { player.muted = !player.muted; audioButton.textContent = player.muted ? "开启声音" : "关闭声音"; void playWhenReady(); }); reloadButton.addEventListener("click", connect); goLiveButton.addEventListener("click", () => void playWhenReady()); player.addEventListener("playing", () => { console.info("[viewer] media playing"); setPlayButtonVisible(false); }); player.addEventListener("pause", () => { console.info("[viewer] media pause"); setPlayButtonVisible(true); }); player.addEventListener("waiting", () => console.info("[viewer] media waiting")); player.addEventListener("error", () => { console.error("[viewer] media error", player.error); setPlayButtonVisible(true); }); connect(); async function connect() { console.info("[viewer] connect start"); destroyConnection(); setState("connecting", "连接中"); latency.textContent = "WebRTC 连接中"; message.textContent = "正在连接主机。"; const status = await fetch("/api/debug").then((response) => response.json()); console.info("[viewer] status", status); if (!status.hasHostSignal) { setState("idle", "等待直播"); latency.textContent = "等待主机"; message.textContent = "主机还没有开始分享。"; retryTimer = window.setTimeout(connect, 1000); return; } signal = openSignalSocket(); } function openSignalSocket() { const protocol = location.protocol === "https:" ? "wss" : "ws"; const nextSocket = new WebSocket(`${protocol}://${location.host}/signal?role=viewer&viewerId=${viewerId}`); nextSocket.addEventListener("open", () => console.info("[viewer] signal open")); nextSocket.addEventListener("message", (event) => void handleSignalMessage(JSON.parse(event.data))); nextSocket.addEventListener("close", (event) => { console.info("[viewer] signal close", { code: event.code, reason: event.reason }); setState("idle", "直播断开"); latency.textContent = "连接断开"; retryTimer = window.setTimeout(connect, 1000); }); nextSocket.addEventListener("error", (event) => { console.error("[viewer] signal error", event); setState("idle", "连接错误"); }); return nextSocket; } async function handleSignalMessage(data) { console.info("[viewer] signal", data); if (data.type === "ready" && typeof data.viewerId === "string") { viewerId = data.viewerId; return; } if (data.type === "host-left") { setState("idle", "直播结束"); latency.textContent = "主机已断开"; message.textContent = "主机已停止分享。"; destroyPeer(); return; } if (data.type === "offer" && data.sdp) { await acceptOffer(data.sdp); return; } if (data.type === "candidate" && data.candidate && peer) { await peer.addIceCandidate(data.candidate); } } async function acceptOffer(offer) { destroyPeer(); peer = new RTCPeerConnection(peerConfig); remoteStream = new MediaStream(); player.srcObject = remoteStream; peer.addEventListener("track", (event) => { console.info("[viewer] track", { kind: event.track.kind, label: event.track.label }); remoteStream.addTrack(event.track); if (event.track.kind === "audio") { audioButton.disabled = false; audioButton.textContent = player.muted ? "开启声音" : "关闭声音"; } void playWhenReady(); }); peer.addEventListener("icecandidate", (event) => { if (event.candidate) { sendSignal({ type: "candidate", candidate: event.candidate }); } }); peer.addEventListener("connectionstatechange", () => { console.info("[viewer] peer state", peer.connectionState); latency.textContent = `WebRTC ${peer.connectionState}`; if (peer.connectionState === "connected") { setState("live", "播放中"); message.textContent = "正在播放主机分享的桌面。"; } if (["failed", "closed", "disconnected"].includes(peer.connectionState)) { setState("idle", "直播断开"); } }); await peer.setRemoteDescription(offer); const answer = await peer.createAnswer(); await peer.setLocalDescription(answer); sendSignal({ type: "answer", sdp: peer.localDescription }); } function sendSignal(data) { if (signal?.readyState === WebSocket.OPEN) { signal.send(JSON.stringify(data)); } } async function playWhenReady() { if (playPromise) { return playPromise; } try { console.info("[viewer] calling player.play", { readyState: player.readyState, muted: player.muted }); playPromise = player.play(); await playPromise; setPlayButtonVisible(false); } catch (error) { console.warn("[viewer] player.play rejected", error); message.textContent = "浏览器阻止了自动播放,请点击画面上的播放直播。"; setPlayButtonVisible(true); } finally { playPromise = undefined; } } function destroyConnection() { console.info("[viewer] destroyConnection"); window.clearTimeout(retryTimer); retryTimer = undefined; signal?.close(); signal = undefined; destroyPeer(); setPlayButtonVisible(false); } function destroyPeer() { peer?.close(); peer = undefined; remoteStream?.getTracks().forEach((track) => track.stop()); remoteStream = undefined; playPromise = undefined; player.removeAttribute("src"); player.srcObject = null; player.load(); audioButton.disabled = true; audioButton.textContent = "等待声音"; } function setPlayButtonVisible(visible) { playButton.hidden = !visible; } function setState(kind, text) { state.className = `badge ${kind}`; state.textContent = text; }