2023-01-11 06:38:48 +00:00
|
|
|
console.log("BareRTC!");
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// WebRTC configuration.
|
|
|
|
const configuration = {
|
|
|
|
iceServers: [{
|
|
|
|
urls: 'stun:stun.l.google.com:19302'
|
|
|
|
}]
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2023-01-11 06:38:48 +00:00
|
|
|
const app = Vue.createApp({
|
|
|
|
delimiters: ['[[', ']]'],
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
busy: false,
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
channel: "lobby",
|
|
|
|
username: "", //"test",
|
2023-01-11 06:38:48 +00:00
|
|
|
message: "",
|
|
|
|
|
|
|
|
// WebSocket connection.
|
|
|
|
ws: {
|
|
|
|
conn: null,
|
|
|
|
connected: false,
|
|
|
|
},
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Who List for the room.
|
|
|
|
whoList: [],
|
|
|
|
|
|
|
|
// My video feed.
|
|
|
|
webcam: {
|
|
|
|
busy: false,
|
|
|
|
active: false,
|
|
|
|
elem: null, // <video id="localVideo"> element
|
|
|
|
stream: null, // MediaStream object
|
|
|
|
},
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// WebRTC sessions with other users.
|
|
|
|
WebRTC: {
|
|
|
|
// Streams per username.
|
|
|
|
streams: {},
|
|
|
|
|
|
|
|
// RTCPeerConnections per username.
|
|
|
|
pc: {},
|
|
|
|
},
|
|
|
|
|
2023-01-11 06:38:48 +00:00
|
|
|
// Chat history.
|
|
|
|
history: [],
|
2023-01-27 04:34:58 +00:00
|
|
|
historyScrollbox: null,
|
2023-01-11 06:38:48 +00:00
|
|
|
DMs: {},
|
|
|
|
|
|
|
|
loginModal: {
|
|
|
|
visible: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
mounted() {
|
2023-01-27 04:34:58 +00:00
|
|
|
this.webcam.elem = document.querySelector("#localVideo");
|
|
|
|
this.historyScrollbox = document.querySelector("#chatHistory");
|
|
|
|
|
|
|
|
this.ChatServer("Welcome to BareRTC!")
|
2023-01-11 06:38:48 +00:00
|
|
|
|
|
|
|
if (!this.username) {
|
|
|
|
this.loginModal.visible = true;
|
2023-01-27 04:34:58 +00:00
|
|
|
} else {
|
|
|
|
this.signIn();
|
2023-01-11 06:38:48 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
signIn() {
|
|
|
|
this.loginModal.visible = false;
|
|
|
|
this.dial();
|
|
|
|
},
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
/**
|
|
|
|
* Chat API Methods (WebSocket packets sent/received)
|
|
|
|
*/
|
|
|
|
|
2023-01-11 06:38:48 +00:00
|
|
|
sendMessage() {
|
|
|
|
if (!this.message) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.ws.connected) {
|
2023-01-27 04:34:58 +00:00
|
|
|
this.ChatClient("You are not connected to the server.");
|
2023-01-11 06:38:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.debug("Send message: %s", this.message);
|
|
|
|
this.ws.conn.send(JSON.stringify({
|
|
|
|
action: "message",
|
|
|
|
message: this.message,
|
|
|
|
}));
|
|
|
|
|
|
|
|
this.message = "";
|
|
|
|
},
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Sync the current user state (such as video broadcasting status) to
|
|
|
|
// the backend, which will reload everybody's Who List.
|
|
|
|
sendMe() {
|
|
|
|
this.ws.conn.send(JSON.stringify({
|
|
|
|
action: "me",
|
|
|
|
videoActive: this.webcam.active,
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
onMe(msg) {
|
|
|
|
// We have had settings pushed to us by the server, such as a change
|
|
|
|
// in our choice of username.
|
|
|
|
if (this.username != msg.username) {
|
|
|
|
this.ChatServer(`Your username has been changed to ${msg.username}.`);
|
2023-01-27 06:54:02 +00:00
|
|
|
this.username = msg.username;
|
2023-01-27 04:34:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.ChatClient(`User sync from backend: ${JSON.stringify(msg)}`);
|
|
|
|
},
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// Send a video request to access a user's camera.
|
|
|
|
sendOpen(username) {
|
|
|
|
this.ws.conn.send(JSON.stringify({
|
|
|
|
action: "open",
|
|
|
|
username: username,
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
onOpen(msg) {
|
|
|
|
// Response for the opener to begin WebRTC connection.
|
|
|
|
const secret = msg.openSecret;
|
|
|
|
console.log("OPEN: connect to %s with secret %s", msg.username, secret);
|
|
|
|
this.ChatClient(`Connecting to stream for ${msg.username}.`);
|
|
|
|
|
|
|
|
this.startWebRTC(msg.username, true);
|
|
|
|
},
|
|
|
|
onRing(msg) {
|
|
|
|
// Message for the receiver to begin WebRTC connection.
|
|
|
|
const secret = msg.openSecret;
|
|
|
|
console.log("RING: connection from %s with secret %s", msg.username, secret);
|
|
|
|
this.ChatServer(`${msg.username} has opened your camera.`);
|
|
|
|
|
|
|
|
this.startWebRTC(msg.username, false);
|
|
|
|
},
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Handle messages sent in chat.
|
|
|
|
onMessage(msg) {
|
|
|
|
this.pushHistory({
|
|
|
|
username: msg.username,
|
|
|
|
message: msg.message,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2023-01-11 06:38:48 +00:00
|
|
|
// Dial the WebSocket connection.
|
|
|
|
dial() {
|
2023-01-27 06:54:02 +00:00
|
|
|
console.log("Dialing WebSocket...");
|
2023-01-11 06:38:48 +00:00
|
|
|
const conn = new WebSocket(`ws://${location.host}/ws`);
|
|
|
|
|
|
|
|
conn.addEventListener("close", ev => {
|
|
|
|
this.ws.connected = false;
|
2023-01-27 04:34:58 +00:00
|
|
|
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
2023-01-11 06:38:48 +00:00
|
|
|
|
|
|
|
if (ev.code !== 1001) {
|
2023-01-27 04:34:58 +00:00
|
|
|
this.ChatClient("Reconnecting in 1s");
|
2023-01-11 06:38:48 +00:00
|
|
|
setTimeout(this.dial, 1000);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
conn.addEventListener("open", ev => {
|
|
|
|
this.ws.connected = true;
|
2023-01-27 04:34:58 +00:00
|
|
|
this.ChatClient("Websocket connected!");
|
2023-01-11 06:38:48 +00:00
|
|
|
|
|
|
|
// Tell the server our username.
|
|
|
|
this.ws.conn.send(JSON.stringify({
|
|
|
|
action: "login",
|
|
|
|
username: this.username,
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
|
|
conn.addEventListener("message", ev => {
|
|
|
|
console.log(ev);
|
|
|
|
if (typeof ev.data !== "string") {
|
|
|
|
console.error("unexpected message type", typeof ev.data);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let msg = JSON.parse(ev.data);
|
2023-01-27 04:34:58 +00:00
|
|
|
switch (msg.action) {
|
|
|
|
case "who":
|
|
|
|
console.log("Got the Who List: %s", msg);
|
|
|
|
this.whoList = msg.whoList;
|
|
|
|
break;
|
|
|
|
case "me":
|
|
|
|
console.log("Got a self-update: %s", msg);
|
|
|
|
this.onMe(msg);
|
|
|
|
break;
|
|
|
|
case "message":
|
|
|
|
this.onMessage(msg);
|
|
|
|
break;
|
|
|
|
case "presence":
|
|
|
|
this.pushHistory({
|
|
|
|
action: msg.action,
|
|
|
|
username: msg.username,
|
|
|
|
message: msg.message,
|
|
|
|
});
|
|
|
|
break;
|
2023-01-27 06:54:02 +00:00
|
|
|
case "ring":
|
|
|
|
this.onRing(msg);
|
|
|
|
break;
|
|
|
|
case "open":
|
|
|
|
this.onOpen(msg);
|
|
|
|
break;
|
|
|
|
case "candidate":
|
|
|
|
this.onCandidate(msg);
|
|
|
|
break;
|
|
|
|
case "sdp":
|
|
|
|
this.onSDP(msg);
|
|
|
|
break;
|
|
|
|
case "error":
|
|
|
|
this.pushHistory({
|
|
|
|
username: msg.username || 'Internal Server Error',
|
|
|
|
message: msg.message,
|
|
|
|
isChatServer: true,
|
|
|
|
});
|
2023-01-27 04:34:58 +00:00
|
|
|
default:
|
|
|
|
console.error("Unexpected action: %s", JSON.stringify(msg));
|
|
|
|
}
|
2023-01-11 06:38:48 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.ws.conn = conn;
|
|
|
|
},
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
/**
|
|
|
|
* WebRTC concerns.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Start WebRTC with the other username.
|
|
|
|
startWebRTC(username, isOfferer) {
|
|
|
|
this.ChatClient(`Begin WebRTC with ${username}.`);
|
|
|
|
let pc = new RTCPeerConnection(configuration);
|
|
|
|
this.WebRTC.pc[username] = pc;
|
|
|
|
|
|
|
|
// 'onicecandidate' notifies us whenever an ICE agent needs to deliver a
|
|
|
|
// message to the other peer through the signaling server.
|
|
|
|
pc.onicecandidate = event => {
|
|
|
|
this.ChatClient("On ICE Candidate called");
|
|
|
|
console.log(event);
|
|
|
|
console.log(event.candidate);
|
|
|
|
if (event.candidate) {
|
|
|
|
this.ChatClient(`Send ICE candidate: ${event.candidate}`);
|
|
|
|
this.ws.conn.send(JSON.stringify({
|
|
|
|
action: "candidate",
|
|
|
|
username: username,
|
|
|
|
candidate: event.candidate,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// If the user is offerer let the 'negotiationneeded' event create the offer.
|
|
|
|
if (isOfferer) {
|
|
|
|
this.ChatClient("Sending offer:");
|
|
|
|
pc.onnegotiationneeded = () => {
|
|
|
|
this.ChatClient("Negotiation Needed, creating WebRTC offer.");
|
|
|
|
pc.createOffer().then(this.localDescCreated(pc, username)).catch(this.ChatClient);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// When a remote stream arrives.
|
|
|
|
pc.ontrack = event => {
|
|
|
|
const stream = event.streams[0];
|
|
|
|
|
|
|
|
// Do we already have it?
|
|
|
|
this.ChatClient(`Received a video stream from ${username}.`);
|
|
|
|
if (this.WebRTC.streams[username] == undefined ||
|
|
|
|
this.WebRTC.streams[username].id !== stream.id) {
|
|
|
|
this.WebRTC.streams[username] = stream;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// If we were already broadcasting video, send our stream to
|
|
|
|
// the connecting user.
|
|
|
|
if (!isOfferer && this.webcam.active) {
|
|
|
|
this.ChatClient(`Sharing our video stream to ${username}.`);
|
|
|
|
this.webcam.stream.getTracks().forEach(track => pc.addTrack(track, this.webcam.stream));
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are the offerer, begin the connection.
|
|
|
|
if (isOfferer) {
|
|
|
|
pc.createOffer().then(this.localDescCreated(pc, username)).catch(this.ChatClient);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Common handler function for
|
|
|
|
localDescCreated(pc, username) {
|
|
|
|
return (desc) => {
|
|
|
|
this.ChatClient(`setLocalDescription ${JSON.stringify(desc)}`);
|
|
|
|
pc.setLocalDescription(
|
|
|
|
new RTCSessionDescription(desc),
|
|
|
|
() => {
|
|
|
|
this.ws.conn.send(JSON.stringify({
|
|
|
|
action: "sdp",
|
|
|
|
username: username,
|
|
|
|
description: JSON.stringify(pc.localDescription),
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
console.error,
|
|
|
|
)
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
// Handle inbound WebRTC signaling messages proxied by the websocket.
|
|
|
|
onCandidate(msg) {
|
|
|
|
if (this.WebRTC.pc[msg.username] == undefined) return;
|
|
|
|
let pc = this.WebRTC.pc[msg.username];
|
|
|
|
|
|
|
|
// Add the new ICE candidate.
|
|
|
|
console.log("Add ICE candidate: %s", msg.candidate);
|
|
|
|
this.ChatClient(`Received an ICE candidate from ${username}: ${msg.candidate}`);
|
|
|
|
pc.addIceCandidate(
|
|
|
|
new RTCIceCandidate(
|
|
|
|
msg.candidate,
|
|
|
|
() => {},
|
|
|
|
console.error,
|
|
|
|
)
|
|
|
|
);
|
|
|
|
},
|
|
|
|
onSDP(msg) {
|
|
|
|
if (this.WebRTC.pc[msg.username] == undefined) return;
|
|
|
|
let pc = this.WebRTC.pc[msg.username];
|
|
|
|
let description = JSON.parse(msg.description);
|
|
|
|
|
|
|
|
// Add the new ICE candidate.
|
|
|
|
console.log("Set description: %s", description);
|
|
|
|
this.ChatClient(`Received a Remote Description from ${msg.username}: ${msg.description}.`);
|
|
|
|
pc.setRemoteDescription(new RTCSessionDescription(description), () => {
|
|
|
|
// When receiving an offer let's answer it.
|
|
|
|
if (pc.remoteDescription.type === 'offer') {
|
|
|
|
pc.createAnswer().then(this.localDescCreated(pc, msg.username)).catch(this.ChatClient);
|
|
|
|
}
|
|
|
|
}, console.error);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Front-end web app concerns.
|
|
|
|
*/
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Start broadcasting my webcam.
|
|
|
|
startVideo() {
|
|
|
|
if (this.webcam.busy) return;
|
|
|
|
this.webcam.busy = true;
|
|
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia({
|
|
|
|
audio: true,
|
|
|
|
video: true,
|
|
|
|
}).then(stream => {
|
|
|
|
this.webcam.active = true;
|
|
|
|
this.webcam.elem.srcObject = stream;
|
|
|
|
this.webcam.stream = stream;
|
|
|
|
|
|
|
|
// Tell backend the camera is ready.
|
|
|
|
this.sendMe();
|
|
|
|
}).catch(err => {
|
|
|
|
this.ChatClient(`Webcam error: ${err}`);
|
|
|
|
}).finally(() => {
|
|
|
|
this.webcam.busy = false;
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2023-01-27 06:54:02 +00:00
|
|
|
// Begin connecting to someone else's webcam.
|
|
|
|
openVideo(user) {
|
|
|
|
if (user.username === this.username) {
|
|
|
|
this.ChatClient("You can already see your own webcam.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.sendOpen(user.username);
|
|
|
|
},
|
|
|
|
|
2023-01-27 04:34:58 +00:00
|
|
|
// Stop broadcasting.
|
|
|
|
stopVideo() {
|
|
|
|
this.webcam.elem.srcObject = null;
|
|
|
|
this.webcam.stream = null;
|
|
|
|
this.webcam.active = false;
|
|
|
|
|
|
|
|
// Tell backend our camera state.
|
|
|
|
this.sendMe();
|
|
|
|
},
|
|
|
|
|
|
|
|
pushHistory({username, message, action="message", isChatServer, isChatClient}) {
|
2023-01-11 06:38:48 +00:00
|
|
|
this.history.push({
|
2023-01-27 04:34:58 +00:00
|
|
|
action: action,
|
2023-01-11 06:38:48 +00:00
|
|
|
username: username,
|
|
|
|
message: message,
|
2023-01-27 04:34:58 +00:00
|
|
|
isChatServer,
|
|
|
|
isChatClient,
|
2023-01-11 06:38:48 +00:00
|
|
|
});
|
2023-01-27 04:34:58 +00:00
|
|
|
this.scrollHistory();
|
|
|
|
},
|
|
|
|
|
|
|
|
scrollHistory() {
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
this.historyScrollbox.scroll({
|
|
|
|
top: this.historyScrollbox.scrollHeight,
|
|
|
|
behavior: 'smooth',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
// Send a chat message as ChatServer
|
|
|
|
ChatServer(message) {
|
|
|
|
this.pushHistory({
|
|
|
|
username: "ChatServer",
|
|
|
|
message: message,
|
|
|
|
isChatServer: true,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
ChatClient(message) {
|
|
|
|
this.pushHistory({
|
|
|
|
username: "ChatClient",
|
|
|
|
message: message,
|
|
|
|
isChatClient: true,
|
|
|
|
});
|
|
|
|
},
|
2023-01-11 06:38:48 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
app.mount("#BareRTC-App");
|