media-sever/public/viewer.js
2026-07-07 08:36:01 +01:00

210 lines
6.3 KiB
JavaScript

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