2024-05-29 18:49:28 +08:00
|
|
|
{% 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" />
|
2024-05-30 02:07:37 +08:00
|
|
|
<input type="file" id="sendfile" disabled/>
|
2024-05-29 18:49:28 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block pagescripts %}
|
|
|
|
<script>
|
|
|
|
(() => {
|
|
|
|
let message_list = document.getElementById("message-list")
|
|
|
|
let peers = document.getElementById("peers")
|
|
|
|
let inputbox = document.getElementById("message-input")
|
2024-05-30 02:07:37 +08:00
|
|
|
let sendfile = document.getElementById("sendfile")
|
2024-05-29 18:49:28 +08:00
|
|
|
|
|
|
|
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";
|
|
|
|
|
2024-05-30 02:07:37 +08:00
|
|
|
let updateSendfile = () => {
|
|
|
|
if (peers.value != "") {
|
|
|
|
sendfile.removeAttribute("disabled")
|
|
|
|
} else {
|
|
|
|
sendfile.setAttribute("disabled", "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
peers.addEventListener("change", updateSendfile)
|
|
|
|
sendfile.addEventListener("change", async (ev) => {
|
|
|
|
let file = sendfile.files[0]
|
|
|
|
sendfile.value = ""
|
|
|
|
|
|
|
|
let peer = peerMap.get(peers.value)
|
|
|
|
if (!peer) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
display(`你给 ${peers.value} 传了一个文件: ${file.name}。传输中...`)
|
|
|
|
|
|
|
|
let reader = new FileReader()
|
|
|
|
let dc = peer.createDataChannel(`file:${file.name}`)
|
|
|
|
reader.addEventListener("load", (ev) => {
|
|
|
|
dc.send(reader.result)
|
|
|
|
dc.close()
|
|
|
|
})
|
|
|
|
dc.addEventListener("open", () => {
|
|
|
|
reader.readAsArrayBuffer(file)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
const createNode = (tag, ...children) => {
|
|
|
|
let node = document.createElement(tag)
|
|
|
|
children.forEach(child => {
|
|
|
|
if(typeof child === "string") {
|
|
|
|
let inner = document.createElement("span")
|
|
|
|
inner.innerHTML = child
|
|
|
|
node.appendChild(inner)
|
|
|
|
} else if (child instanceof HTMLElement) {
|
|
|
|
node.appendChild(child)
|
|
|
|
} else {
|
|
|
|
console.log("child is not a node: ", child, typeof child)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return node
|
|
|
|
}
|
|
|
|
|
|
|
|
const display = (...message) => {
|
|
|
|
let scrollToBottom = Math.abs(message_list.scrollHeight - message_list.scrollTop - message_list.clientHeight) < 1;
|
|
|
|
message_list.appendChild(createNode("div",
|
|
|
|
`${new Date().toTimeString()}: `,
|
|
|
|
...message
|
|
|
|
))
|
|
|
|
if(scrollToBottom) {
|
|
|
|
message_list.scrollTo({
|
|
|
|
top: message_list.scrollHeight
|
|
|
|
})
|
|
|
|
}
|
2024-05-29 18:49:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-05-30 02:07:37 +08:00
|
|
|
display(`你 -> ${peers.value}: ${inputbox.value}`)
|
2024-05-29 18:49:28 +08:00
|
|
|
channel.send(inputbox.value)
|
|
|
|
inputbox.value = ""
|
|
|
|
})
|
|
|
|
|
|
|
|
const addPeer = (peerName) => {
|
2024-05-30 02:07:37 +08:00
|
|
|
display(`连接上了 ${peerName}`)
|
|
|
|
|
2024-05-29 18:49:28 +08:00
|
|
|
let newNode = document.createElement("option")
|
|
|
|
newNode.id = peerName
|
|
|
|
newNode.setAttribute("value", peerName)
|
|
|
|
newNode.innerHTML = peerName;
|
|
|
|
peers.appendChild(newNode)
|
2024-05-30 02:07:37 +08:00
|
|
|
|
|
|
|
updateSendfile()
|
2024-05-29 18:49:28 +08:00
|
|
|
}
|
|
|
|
const removePeer = (peerName) => {
|
2024-05-30 02:07:37 +08:00
|
|
|
display(`${peerName} 走了...`)
|
2024-05-29 18:49:28 +08:00
|
|
|
let el = document.getElementById(peerName);
|
|
|
|
if(el) el.remove()
|
2024-05-30 02:07:37 +08:00
|
|
|
|
|
|
|
updateSendfile()
|
2024-05-29 18:49:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
let timeout;
|
|
|
|
|
|
|
|
let reconnect = () => {
|
|
|
|
if(ws && (ws.readyState == WebSocket.CONNECTING || ws.readyState == WebSocket.OPEN)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let protocol = "ws://";
|
2024-05-29 21:01:14 +08:00
|
|
|
if (location.protocol === "https:") {
|
2024-05-29 18:49:28 +08:00
|
|
|
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", () => {
|
2024-05-30 02:07:37 +08:00
|
|
|
display("服务器连上啦。你等等,给你起个名字...")
|
2024-05-29 18:49:28 +08:00
|
|
|
ws.send(JSON.stringify({
|
|
|
|
message: {
|
|
|
|
type: MessageBootstrap,
|
|
|
|
},
|
|
|
|
room,
|
|
|
|
sender,
|
|
|
|
}))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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}) => {
|
2024-05-30 02:07:37 +08:00
|
|
|
if(channel.label === "chat") {
|
|
|
|
channelMap.set(peerName, channel);
|
|
|
|
channel.addEventListener("open", (ev) => {
|
|
|
|
addPeer(peerName)
|
|
|
|
})
|
|
|
|
channel.addEventListener("message", (ev) => {
|
|
|
|
display(`${peerName} -> You: ${ev.data}`)
|
|
|
|
})
|
|
|
|
channel.addEventListener("close", () => {
|
|
|
|
removePeer(peerName)
|
|
|
|
})
|
|
|
|
} else if (channel.label.startsWith("file:")) {
|
|
|
|
let filename = channel.label.substr(5)
|
|
|
|
let buffers = [];
|
|
|
|
channel.addEventListener("open", ev => {
|
|
|
|
display(`${peerName} 给你发了一个文件: ${filename}。 接收中...`)
|
|
|
|
})
|
|
|
|
channel.addEventListener("message", ev => {
|
|
|
|
buffers.push(ev.data)
|
|
|
|
})
|
|
|
|
channel.addEventListener("close", () => {
|
|
|
|
var link = document.createElement('a');
|
|
|
|
link.href = window.URL.createObjectURL(new Blob(buffers));
|
|
|
|
link.download = filename;
|
|
|
|
link.innerHTML = filename
|
|
|
|
display(
|
|
|
|
"文件:",
|
|
|
|
link,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2024-05-29 18:49:28 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
addPeer(peerName)
|
|
|
|
})
|
|
|
|
channel.addEventListener("message", (ev) => {
|
2024-05-30 02:07:37 +08:00
|
|
|
display(`${peerName} -> 你: ${ev.data}`)
|
2024-05-29 18:49:28 +08:00
|
|
|
})
|
|
|
|
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,
|
|
|
|
}))
|
2024-05-30 02:07:37 +08:00
|
|
|
display(`决定了,就叫 ${sender}. 我们等等小伙伴吧...`)
|
2024-05-29 18:49:28 +08:00
|
|
|
break;
|
|
|
|
case MessageDiscoverRequest:
|
2024-05-30 02:07:37 +08:00
|
|
|
display(`正在尝试连接 ${wsMessage.sender}`)
|
2024-05-29 18:49:28 +08:00
|
|
|
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:
|
|
|
|
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()
|
|
|
|
})()
|
|
|
|
</script>
|
|
|
|
{% endblock %}
|