210 lines
6.3 KiB
JavaScript
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;
|
|
}
|