massive rewrite of frontend
This commit is contained in:
parent
683462cc10
commit
3acfe8a9ea
@ -12,330 +12,530 @@
|
|||||||
<select id="peers">
|
<select id="peers">
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
<input id="message-input" />
|
<input id="message-input" disabled />
|
||||||
<input type="file" id="sendfile" disabled/>
|
<input type="file" id="sendfile" disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block pagescripts %}
|
{% block pagescripts %}
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
let message_list = document.getElementById("message-list")
|
const MessageBootstrap = "Bootstrap";
|
||||||
let peers = document.getElementById("peers")
|
const MessageDiscoverRequest = "DiscoverRequest";
|
||||||
let inputbox = document.getElementById("message-input")
|
const MessageDiscoverResponse = "DiscoverResponse";
|
||||||
let sendfile = document.getElementById("sendfile")
|
const MessageSessionOffer = "SessionOffer";
|
||||||
|
const MessageSessionAnswer = "SessionAnswer";
|
||||||
|
const MessageICECandidate = "IceCandidate";
|
||||||
|
|
||||||
let search = new URLSearchParams(location.search);
|
function ChatRoom(room, name, ui, options) {
|
||||||
let sender = search.get("name") || "";
|
options = options || {};
|
||||||
|
let { iceServers } = options;
|
||||||
let room = search.get("room") || "public";
|
|
||||||
|
|
||||||
let peerMap = new Map();
|
this.room = room;
|
||||||
let channelMap = new Map();
|
this.name = name;
|
||||||
|
this.iceServers = iceServers;
|
||||||
|
|
||||||
let ws;
|
this.peers = new Map();
|
||||||
let wsReconnectInterval = 5000;
|
this.channels = new Map();
|
||||||
|
this.i_am_polite = new Map();
|
||||||
|
this.i_am_offering = new Map();
|
||||||
|
|
||||||
const MessageBootstrap = "Bootstrap";
|
this.ws = null;
|
||||||
const MessageDiscoverRequest = "DiscoverRequest";
|
|
||||||
const MessageDiscoverResponse = "DiscoverResponse";
|
|
||||||
const MessageSessionOffer = "SessionOffer";
|
|
||||||
const MessageSessionAnswer = "SessionAnswer";
|
|
||||||
const MessageICECandidate = "IceCandidate";
|
|
||||||
|
|
||||||
let updateSendfile = () => {
|
this.ui = ui;
|
||||||
if (peers.value != "") {
|
|
||||||
sendfile.removeAttribute("disabled")
|
this.event = new EventTarget();
|
||||||
} 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)
|
let nativeEvent = window.Event;
|
||||||
if (!peer) {
|
function Event(type, data) {
|
||||||
return
|
let event = new nativeEvent(type);
|
||||||
}
|
|
||||||
display(`你给 ${peers.value} 传了一个文件: ${file.name}。传输中...`)
|
|
||||||
|
|
||||||
let reader = new FileReader()
|
if (data) {
|
||||||
let dc = peer.createDataChannel(`file:${file.name}`)
|
for (let [k, v] of Object.entries(data)) {
|
||||||
reader.addEventListener("load", (ev) => {
|
event[k] = v
|
||||||
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) => {
|
return event;
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
display(`你 -> ${peers.value}: ${inputbox.value}`)
|
// ---------------- Chat Room Methods ------------
|
||||||
channel.send(inputbox.value)
|
|
||||||
inputbox.value = ""
|
|
||||||
})
|
|
||||||
|
|
||||||
const addPeer = (peerName) => {
|
|
||||||
display(`连接上了 ${peerName}`)
|
|
||||||
|
|
||||||
let newNode = document.createElement("option")
|
// ---------------- Utilities --------------------
|
||||||
newNode.id = peerName
|
|
||||||
newNode.setAttribute("value", peerName)
|
|
||||||
newNode.innerHTML = peerName;
|
|
||||||
peers.appendChild(newNode)
|
|
||||||
|
|
||||||
updateSendfile()
|
ChatRoom.prototype.kickoff = function () {
|
||||||
}
|
this.event.dispatchEvent(new Event("kickoff"))
|
||||||
const removePeer = (peerName) => {
|
this.send_message({
|
||||||
display(`${peerName} 走了...`)
|
|
||||||
let el = document.getElementById(peerName);
|
|
||||||
if(el) el.remove()
|
|
||||||
|
|
||||||
updateSendfile()
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout;
|
|
||||||
|
|
||||||
let reconnect = () => {
|
|
||||||
if(ws && (ws.readyState == WebSocket.CONNECTING || ws.readyState == WebSocket.OPEN)) {
|
|
||||||
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("服务器连上啦。你等等,给你起个名字...")
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
message: {
|
message: {
|
||||||
type: MessageBootstrap,
|
type: MessageBootstrap,
|
||||||
},
|
},
|
||||||
room,
|
})
|
||||||
sender,
|
}
|
||||||
}))
|
ChatRoom.prototype.setup_peerconnection = function (sender) {
|
||||||
})
|
peer = this.peers.get(sender);
|
||||||
}
|
if (peer == null) {
|
||||||
|
peer = new RTCPeerConnection({
|
||||||
let handle_ws_message = (wsMessage) => {
|
iceServers: [{ urls: this.iceServers }]
|
||||||
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) => {
|
peer.addEventListener("iceconnectionstatechange", () => {
|
||||||
console.log("signaling state changed: ", peer.signalingState)
|
if (peer.iceConnectionState === "failed") {
|
||||||
})
|
peer.restartIce();
|
||||||
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("negotiationneeded", async (ev) => {
|
||||||
|
this.on_negotiation_needed(sender)
|
||||||
|
})
|
||||||
peer.addEventListener("icecandidate", (ev) => {
|
peer.addEventListener("icecandidate", (ev) => {
|
||||||
if (!ev.candidate) {
|
if (!ev.candidate) {
|
||||||
console.log("gather end")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidate = ev.candidate.toJSON()
|
let candidate = ev.candidate.toJSON()
|
||||||
ws.send(JSON.stringify({
|
this.on_local_ice_candidate(sender, candidate)
|
||||||
message: {
|
|
||||||
type: MessageICECandidate,
|
|
||||||
candidate: JSON.stringify(candidate),
|
|
||||||
sender,
|
|
||||||
kind: 3,
|
|
||||||
},
|
|
||||||
room,
|
|
||||||
sender,
|
|
||||||
receiver: wsMessage.sender,
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
peer.addEventListener("icegatheringstatechange", ev => {
|
peer.addEventListener("datachannel", ({ channel }) => {
|
||||||
console.log("gather", peer.iceGatheringState)
|
this.on_datachannel(sender, channel)
|
||||||
})
|
})
|
||||||
peer.addEventListener("datachannel", ({channel}) => {
|
this.peers.set(sender, peer);
|
||||||
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,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
display(`${peerName} -> 你: ${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({
|
return {
|
||||||
message: {
|
peer,
|
||||||
type: resultMessageType,
|
}
|
||||||
sdp: JSON.stringify(resultSdp),
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.setup_chat_datachannel = function (sender, channel) {
|
||||||
|
console.log("setup chat datachannel", { sender, channel })
|
||||||
|
channel.addEventListener("open", (ev) => {
|
||||||
|
this.event.dispatchEvent(new Event("chat-channel-open", {
|
||||||
sender,
|
sender,
|
||||||
kind: 3,
|
channel
|
||||||
},
|
}))
|
||||||
room,
|
})
|
||||||
sender,
|
channel.addEventListener("message", (ev) => {
|
||||||
receiver: wsMessage.sender,
|
console.log(ev)
|
||||||
|
this.on_chat_message(sender, ev.data)
|
||||||
|
})
|
||||||
|
channel.addEventListener("close", () => {
|
||||||
|
this.event.dispatchEvent(new Event("chat-channel-close", {
|
||||||
|
sender,
|
||||||
|
channel
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ChatRoom.prototype.setup_file_datachannel = function (sender, channel) {
|
||||||
|
let filename = channel.label.substr(5)
|
||||||
|
let buffers = [];
|
||||||
|
channel.addEventListener("open", ev => {
|
||||||
|
this.event.dispatchEvent(new Event("file_receiving", {
|
||||||
|
filename,
|
||||||
|
}))
|
||||||
|
// this.display(`${sender} 给你发了一个文件: ${filename}。 接收中...`)
|
||||||
|
})
|
||||||
|
channel.addEventListener("message", ev => {
|
||||||
|
buffers.push(ev.data)
|
||||||
|
})
|
||||||
|
channel.addEventListener("close", () => {
|
||||||
|
this.event.dispatchEvent(new Event("file_received", {
|
||||||
|
buffer: new Blob(buffers),
|
||||||
|
filename
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.reconnect = function () {
|
||||||
|
this.event.dispatchEvent(new Event("ws_reconnect", { ws: this.ws }))
|
||||||
|
if (this.ws && (this.ws.readyState == WebSocket.CONNECTING || this.ws.readyState == WebSocket.OPEN)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let protocol = "ws://";
|
||||||
|
if (location.protocol === "https:") {
|
||||||
|
protocol = "wss://"
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws = new WebSocket(protocol + location.host + "/ws");
|
||||||
|
ws.addEventListener("error", (ev) => {
|
||||||
|
console.error("ws error", ev)
|
||||||
|
this.reconnect()
|
||||||
|
})
|
||||||
|
ws.addEventListener("close", () => {
|
||||||
|
console.warn("ws closed")
|
||||||
|
this.reconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.addEventListener("message", ({ data }) => {
|
||||||
|
this.on_ws_message(JSON.parse(data))
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.addEventListener("open", () => {
|
||||||
|
this.kickoff()
|
||||||
|
})
|
||||||
|
this.ws = ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.send_message = function (message) {
|
||||||
|
this.event.dispatchEvent(new Event("send_wsmessage", { message }))
|
||||||
|
console.trace("sending message", message)
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
room: this.room,
|
||||||
|
sender: this.name,
|
||||||
|
...message
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
let peer = peerMap.get(wsMessage.sender)
|
// ----------------- Message from websocket --------------
|
||||||
|
|
||||||
switch (wsMessage.message.type) {
|
|
||||||
case MessageBootstrap:
|
|
||||||
sender = wsMessage.sender;
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
message: {
|
|
||||||
type: MessageDiscoverRequest,
|
|
||||||
},
|
|
||||||
room,
|
|
||||||
sender: wsMessage.sender,
|
|
||||||
}))
|
|
||||||
display(`决定了,就叫 ${sender}. 我们等等小伙伴吧...`)
|
|
||||||
break;
|
|
||||||
case MessageDiscoverRequest:
|
|
||||||
display(`正在尝试连接 ${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:
|
|
||||||
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()
|
|
||||||
})()
|
|
||||||
|
ChatRoom.prototype.on_ws_message = function (wsMessage) {
|
||||||
|
this.event.dispatchEvent(new Event("recv_wsmessage", { wsMessage }))
|
||||||
|
console.trace("receiving message", wsMessage)
|
||||||
|
switch (wsMessage.message.type) {
|
||||||
|
case MessageBootstrap:
|
||||||
|
this.on_bootstrap(wsMessage);
|
||||||
|
break;
|
||||||
|
case MessageDiscoverRequest:
|
||||||
|
this.on_discover_request(wsMessage)
|
||||||
|
break
|
||||||
|
case MessageDiscoverResponse:
|
||||||
|
this.on_discover_response(wsMessage)
|
||||||
|
break;
|
||||||
|
case MessageSessionOffer:
|
||||||
|
this.on_session_message(wsMessage)
|
||||||
|
break
|
||||||
|
case MessageSessionAnswer:
|
||||||
|
this.on_session_message(wsMessage)
|
||||||
|
break
|
||||||
|
case MessageICECandidate:
|
||||||
|
this.on_remote_ice_candidate(wsMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_bootstrap = function ({ sender, iceServers }) {
|
||||||
|
this.event.dispatchEvent(new Event("bootstrap", { sender }))
|
||||||
|
this.name = sender;
|
||||||
|
this.iceServers = iceServers || this.iceServers || ["stun:nhz.jeffthecoder.xyz:3478", "stun:nhz.jeffthecoder.xyz:3479", "stun:nhz.jeffthecoder.xyz:13478"];
|
||||||
|
|
||||||
|
this.send_message({
|
||||||
|
message: {
|
||||||
|
type: MessageDiscoverRequest,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_discover_request = function ({ sender }) {
|
||||||
|
this.event.dispatchEvent(new Event("discover_request", { sender }))
|
||||||
|
|
||||||
|
this.i_am_polite.set(sender, false)
|
||||||
|
this.setup_peerconnection(sender)
|
||||||
|
|
||||||
|
this.send_message({
|
||||||
|
message: {
|
||||||
|
type: MessageDiscoverResponse,
|
||||||
|
},
|
||||||
|
receiver: sender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_discover_response = async function ({ sender }) {
|
||||||
|
this.i_am_polite.set(sender, true)
|
||||||
|
let { peer } = this.setup_peerconnection(sender);
|
||||||
|
|
||||||
|
let channel = peer.createDataChannel("chat");
|
||||||
|
this.setup_chat_datachannel(sender, channel)
|
||||||
|
this.channels.set(sender, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_session_message = async function ({ sender, message }) {
|
||||||
|
let description = JSON.parse(message.sdp)
|
||||||
|
let polite = this.i_am_polite.get(sender);
|
||||||
|
let makingOffer = this.i_am_offering.get(sender) || false;
|
||||||
|
let peer = this.peers.get(sender)
|
||||||
|
|
||||||
|
console.log("on session message", { message, description, polite, makingOffer })
|
||||||
|
|
||||||
|
const offerCollision =
|
||||||
|
description.type === "offer" &&
|
||||||
|
(makingOffer || peer.signalingState !== "stable");
|
||||||
|
|
||||||
|
ignoreOffer = !polite && offerCollision;
|
||||||
|
if (ignoreOffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await peer.setRemoteDescription(description);
|
||||||
|
if (description.type === "offer") {
|
||||||
|
await peer.setLocalDescription();
|
||||||
|
this.event.dispatchEvent(new Event("offer", { sender, description }))
|
||||||
|
|
||||||
|
this.send_message({
|
||||||
|
message: {
|
||||||
|
type: MessageSessionAnswer,
|
||||||
|
sdp: JSON.stringify(peer.localDescription),
|
||||||
|
sender: this.name,
|
||||||
|
kind: 3,
|
||||||
|
},
|
||||||
|
receiver: sender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_remote_ice_candidate = function ({ sender, message }) {
|
||||||
|
let candidate = JSON.parse(message.candidate);
|
||||||
|
this.event.dispatchEvent(new Event("candidate", { sender, candidate }))
|
||||||
|
|
||||||
|
let peer = this.peers.get(sender)
|
||||||
|
console.log("remote_candidate", sender, message)
|
||||||
|
peer.addIceCandidate(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------- webrtc state & events -----------------
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_negotiation_needed = async function (sender) {
|
||||||
|
let polite = this.i_am_polite.get(sender);
|
||||||
|
console.log("negotiationneeded", { sender, polite })
|
||||||
|
|
||||||
|
let peer = this.peers.get(sender);
|
||||||
|
this.i_am_offering.set(sender, true)
|
||||||
|
try {
|
||||||
|
await peer.setLocalDescription();
|
||||||
|
this.send_message({
|
||||||
|
message: {
|
||||||
|
type: MessageSessionOffer,
|
||||||
|
sdp: JSON.stringify(peer.localDescription),
|
||||||
|
sender: this.name,
|
||||||
|
kind: 3,
|
||||||
|
},
|
||||||
|
receiver: sender,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.i_am_offering.delete(sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_local_ice_candidate = function (sender, candidate) {
|
||||||
|
this.event.dispatchEvent(new Event("candidate", { candidate }))
|
||||||
|
this.send_message({
|
||||||
|
message: {
|
||||||
|
type: MessageICECandidate,
|
||||||
|
candidate: JSON.stringify(candidate),
|
||||||
|
sender: this.name,
|
||||||
|
kind: 3,
|
||||||
|
},
|
||||||
|
receiver: sender,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_chat_message = function (sender, data) {
|
||||||
|
this.event.dispatchEvent(new Event("chat_message", { sender, data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_channel_open = function (sender, channel) {
|
||||||
|
this.event.dispatchEvent(new Event("channel_open", { data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_channel_close = function (sender, channel) {
|
||||||
|
this.event.dispatchEvent(new Event("channel_close", { data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatRoom.prototype.on_datachannel = function (sender, channel) {
|
||||||
|
console.trace("on datachannel", sender, channel)
|
||||||
|
if (channel.label === "chat") {
|
||||||
|
this.channels.set(sender, channel);
|
||||||
|
this.setup_chat_datachannel(sender, channel)
|
||||||
|
} else if (channel.label.startsWith("file:")) {
|
||||||
|
this.setup_file_datachannel(sender, channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
|
||||||
|
function ChatUI(messages, peerOptions, inputbox, sendfile) {
|
||||||
|
this.messages = messages;
|
||||||
|
this.peerOptions = peerOptions;
|
||||||
|
this.inputbox = inputbox;
|
||||||
|
this.sendfile = sendfile;
|
||||||
|
|
||||||
|
this.receiver = null;
|
||||||
|
|
||||||
|
this.event = new EventTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ChatUI.prototype.setup = function () {
|
||||||
|
this.sendfile.addEventListener("change", e => this.on_sendfile_changed(e))
|
||||||
|
this.peerOptions.addEventListener("change", e => this.on_peer_list_changed(e))
|
||||||
|
this.inputbox.addEventListener("keyup", e => this.on_key_up(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUI.prototype.on_peer_list_changed = function () {
|
||||||
|
this.receiver = this.peerOptions.value;
|
||||||
|
if (this.peerOptions.value != "") {
|
||||||
|
this.sendfile.removeAttribute("disabled")
|
||||||
|
this.inputbox.removeAttribute("disabled")
|
||||||
|
} else {
|
||||||
|
this.sendfile.setAttribute("disabled", "")
|
||||||
|
this.inputbox.setAttribute("disabled", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUI.prototype.on_key_up = function (e) {
|
||||||
|
if (!(e.key === 'Enter' || e.keyCode === 13)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.inputbox.value || this.inputbox.value.trim().length == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let value = this.inputbox.value;
|
||||||
|
let receiver = this.receiver;
|
||||||
|
|
||||||
|
this.event.dispatchEvent(new Event("send_message", {
|
||||||
|
value,
|
||||||
|
receiver,
|
||||||
|
}))
|
||||||
|
this.inputbox.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUI.prototype.on_sendfile_changed = function () {
|
||||||
|
let file = sendfile.files[0]
|
||||||
|
let receiver = this.receiver;
|
||||||
|
sendfile.value = ""
|
||||||
|
|
||||||
|
this.display(`你给 ${this.receiver} 传了一个文件: ${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)
|
||||||
|
// })
|
||||||
|
this.event.dispatchEvent(new Event("send_file", {
|
||||||
|
file,
|
||||||
|
receiver,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUI.prototype.add_peer = function (peerName) {
|
||||||
|
let newNode = document.createElement("option")
|
||||||
|
newNode.id = `peer-${peerName}`
|
||||||
|
newNode.setAttribute("value", peerName)
|
||||||
|
newNode.innerHTML = peerName;
|
||||||
|
this.peerOptions.appendChild(newNode)
|
||||||
|
|
||||||
|
this.on_peer_list_changed()
|
||||||
|
}
|
||||||
|
ChatUI.prototype.remove_peer = function (peerName) {
|
||||||
|
let el = document.getElementById(`peer-${peerName}`);
|
||||||
|
if (el) el.remove()
|
||||||
|
|
||||||
|
this.on_peer_list_changed()
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUI.prototype.display = function (...message) {
|
||||||
|
let message_list = this.messages;
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_list = document.getElementById("message-list")
|
||||||
|
let peers = document.getElementById("peers")
|
||||||
|
let inputbox = document.getElementById("message-input")
|
||||||
|
let sendfile = document.getElementById("sendfile")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let search = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
let room = new ChatRoom(search.get("room") || "public", search.get("name") || "", undefined, {})
|
||||||
|
let ui = new ChatUI(message_list, peers, inputbox, sendfile);
|
||||||
|
ui.setup()
|
||||||
|
|
||||||
|
room.event.addEventListener("kickoff", () => {
|
||||||
|
ui.display("服务器连上啦。你等等,给你起个名字...")
|
||||||
|
})
|
||||||
|
room.event.addEventListener("chat-channel-open", ({ channel, sender }) => {
|
||||||
|
ui.display(`${sender} 连上了`)
|
||||||
|
ui.add_peer(sender)
|
||||||
|
})
|
||||||
|
room.event.addEventListener("chat-channel-closed", ({ channel, sender }) => {
|
||||||
|
ui.display(`${sender} 断开了`)
|
||||||
|
ui.remove_peer(sender)
|
||||||
|
})
|
||||||
|
|
||||||
|
room.event.addEventListener("chat_message", ({ sender, data }) => {
|
||||||
|
ui.display(`${sender} -> 你: ${data}`)
|
||||||
|
})
|
||||||
|
room.event.addEventListener("file_received", ({ filename, buffer }) => {
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.href = window.URL.createObjectURL(buffer);
|
||||||
|
link.download = filename;
|
||||||
|
link.innerHTML = filename;
|
||||||
|
ui.display(
|
||||||
|
"文件:",
|
||||||
|
link,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.event.addEventListener("send_message", ({ receiver, value }) => {
|
||||||
|
let channel = room.channels.get(receiver)
|
||||||
|
if (!channel) {
|
||||||
|
|
||||||
|
ui.display(`${receiver} 没准备好,这条消息发不出去: ${value}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.display(`你 -> ${receiver}: ${value}`)
|
||||||
|
channel.send(value)
|
||||||
|
})
|
||||||
|
ui.event.addEventListener("send_file", ({file,receiver}) => {
|
||||||
|
let peer = room.peers.get(receiver);
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let display = ui.display.bind(ui);
|
||||||
|
|
||||||
|
room.reconnect()
|
||||||
|
})()
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user