2024-05-29 18:49:28 +08:00

267 lines
9.2 KiB
HTML

{% extends "_layout.html" %}
{% block title %}ChatBox{% endblock %}
{% block pagestylesheet %}
<link rel="stylesheet" href="/static/index.css" />
{% endblock %}
{% block content %}
<div id="app">
<div id="message-list">
</div>
<div id="inputs">
<select id="peers">
</select>
<input id="message-input" />
</div>
</div>
{% endblock %}
{% block pagescripts %}
<script>
(() => {
let message_list = document.getElementById("message-list")
let peers = document.getElementById("peers")
let inputbox = document.getElementById("message-input")
let search = new URLSearchParams(location.search);
let sender = search.get("name") || "";
let room = search.get("room") || "public";
let peerMap = new Map();
let channelMap = new Map();
let ws;
let wsReconnectInterval = 5000;
const MessageBootstrap = "Bootstrap";
const MessageDiscoverRequest = "DiscoverRequest";
const MessageDiscoverResponse = "DiscoverResponse";
const MessageSessionOffer = "SessionOffer";
const MessageSessionAnswer = "SessionAnswer";
const MessageICECandidate = "IceCandidate";
const display = (message) => {
let newNode = document.createElement("div")
newNode.innerHTML = `${new Date().toTimeString()}: ${message}`;
message_list.appendChild(newNode)
}
inputbox.addEventListener("keyup", (e) => {
if (!(e.key === 'Enter' || e.keyCode === 13)) {
return
}
if (!inputbox.value || inputbox.value.trim().length == 0) {
return
}
let channel = channelMap.get(peers.value)
if (!channel) {
return
}
console.log(`You -> ${peers.value}: ${inputbox.value}`)
display(`You -> ${peers.value}: ${inputbox.value}`)
channel.send(inputbox.value)
inputbox.value = ""
})
const addPeer = (peerName) => {
let newNode = document.createElement("option")
newNode.id = peerName
newNode.setAttribute("value", peerName)
newNode.innerText = peerName;
newNode.innerHTML = peerName;
peers.appendChild(newNode)
}
const removePeer = (peerName) => {
let el = document.getElementById(peerName);
if(el) el.remove()
}
let timeout;
let reconnect = () => {
if(ws && (ws.readyState == WebSocket.CONNECTING || ws.readyState == WebSocket.OPEN)) {
console.log("ws ok", ws)
return;
}
let protocol = "ws://";
if (location.protocol === "https") {
protocol = "wss://"
}
ws = new WebSocket(protocol+location.host+"/ws");
ws.addEventListener("error", reconnect)
ws.addEventListener("close", reconnect)
ws.addEventListener("message", ({data}) => {
handle_ws_message(JSON.parse(data))
})
ws.addEventListener("open", () => {
display("server connected. waiting for a name to be assigned for you...")
ws.send(JSON.stringify({
message: {
type: MessageBootstrap,
},
room,
sender,
}))
})
console.log("connecting...")
}
let handle_ws_message = (wsMessage) => {
let recreateAndSetupPeer = async (peerName) => {
if (!peerMap.has(peerName)) {
let peer = new RTCPeerConnection({
iceServers: [{ urls: ["stun:nhz.jeffthecoder.xyz:3478", "stun:nhz.jeffthecoder.xyz:3479", "stun:nhz.jeffthecoder.xyz:13478"] }]
})
peer.addEventListener("signalingstatechange", (ev) => {
console.log("signaling state changed: ", peer.signalingState)
})
peer.addEventListener("connectionstatechange", (ev) => {
console.log("peer connection state changed: ", peer.connectionState)
switch (peer.connectionState) {
case "closed":
break;
case "disconnected":
case "failed":
peer.restartIce()
break
}
})
peer.addEventListener("icecandidate", (ev) => {
if (!ev.candidate) {
console.log("gather end")
return
}
let candidate = ev.candidate.toJSON()
ws.send(JSON.stringify({
message: {
type: MessageICECandidate,
candidate: JSON.stringify(candidate),
sender,
kind: 3,
},
room,
sender,
receiver: wsMessage.sender,
}))
})
peer.addEventListener("icegatheringstatechange", ev => {
console.log("gather", peer.iceGatheringState)
})
peer.addEventListener("datachannel", ({channel}) => {
channelMap.set(peerName, channel);
channel.addEventListener("open", (ev) => {
display("connected in event")
addPeer(peerName)
})
channel.addEventListener("message", (ev) => {
display(`${peerName} -> You: ${ev.data}`)
})
channel.addEventListener("close", () => {
removePeer(peerName)
})
})
peerMap.set(peerName, peer);
}
let peer = peerMap.get(peerName);
let resultSdp = null, resultMessageType;
if(wsMessage.message.type === MessageDiscoverResponse) {
let channel = peer.createDataChannel("chat");
channel.addEventListener("open", (ev) => {
display("connected in event")
addPeer(peerName)
})
channel.addEventListener("message", (ev) => {
display(`${peerName} -> You: ${ev.data}`)
})
channel.addEventListener("close", () => {
removePeer(peerName)
})
channelMap.set(peerName, channel);
resultSdp = await peer.createOffer();
resultMessageType = MessageSessionOffer;
peer.setLocalDescription(resultSdp)
console.log("set local offer")
} else {
peer.setRemoteDescription(JSON.parse(wsMessage.message.sdp))
console.log("set remote offer")
resultSdp = await peer.createAnswer();
resultMessageType = MessageSessionAnswer;
peer.setLocalDescription(resultSdp)
console.log("set local answer")
}
ws.send(JSON.stringify({
message: {
type: resultMessageType,
sdp: JSON.stringify(resultSdp),
sender,
kind: 3,
},
room,
sender,
receiver: wsMessage.sender,
}))
}
let peer = peerMap.get(wsMessage.sender)
switch (wsMessage.message.type) {
case MessageBootstrap:
sender = wsMessage.sender;
ws.send(JSON.stringify({
message: {
type: MessageDiscoverRequest,
},
room,
sender: wsMessage.sender,
}))
display(`You are ${sender}. Searching for peers...`)
break;
case MessageDiscoverRequest:
display("connecting to peer " + wsMessage.sender)
ws.send(JSON.stringify({
message: {
type: MessageDiscoverResponse,
},
room,
sender,
receiver: wsMessage.sender,
}))
break
case MessageDiscoverResponse:
recreateAndSetupPeer(wsMessage.sender)
break;
case MessageSessionOffer:
recreateAndSetupPeer(wsMessage.sender)
break
case MessageSessionAnswer:
display("receiving connection to peer: " + wsMessage.sender)
console.log("set remote answer")
peer.setRemoteDescription(JSON.parse(wsMessage.message.sdp))
peer.restartIce()
break
case MessageICECandidate:
let candidate = JSON.parse(wsMessage.message.candidate);
if(!peer) {
console.warn("candidate dropped", candidate)
return
}
peer.addIceCandidate(candidate)
}
}
reconnect()
console.log("document loaded")
})()
</script>
{% endblock %}