BareRTC/web/static/js/BareRTC.js

423 lines
14 KiB
JavaScript

console.log("BareRTC!");
// WebRTC configuration.
const configuration = {
iceServers: [{
urls: 'stun:stun.l.google.com:19302'
}]
};
const app = Vue.createApp({
delimiters: ['[[', ']]'],
data() {
return {
busy: false,
channel: "lobby",
username: "", //"test",
message: "",
// WebSocket connection.
ws: {
conn: null,
connected: false,
},
// Who List for the room.
whoList: [],
// My video feed.
webcam: {
busy: false,
active: false,
elem: null, // <video id="localVideo"> element
stream: null, // MediaStream object
},
// WebRTC sessions with other users.
WebRTC: {
// Streams per username.
streams: {},
// RTCPeerConnections per username.
pc: {},
},
// Chat history.
history: [],
historyScrollbox: null,
DMs: {},
loginModal: {
visible: false,
},
}
},
mounted() {
this.webcam.elem = document.querySelector("#localVideo");
this.historyScrollbox = document.querySelector("#chatHistory");
this.ChatServer("Welcome to BareRTC!")
if (!this.username) {
this.loginModal.visible = true;
} else {
this.signIn();
}
},
methods: {
signIn() {
this.loginModal.visible = false;
this.dial();
},
/**
* Chat API Methods (WebSocket packets sent/received)
*/
sendMessage() {
if (!this.message) {
return;
}
if (!this.ws.connected) {
this.ChatClient("You are not connected to the server.");
return;
}
console.debug("Send message: %s", this.message);
this.ws.conn.send(JSON.stringify({
action: "message",
message: this.message,
}));
this.message = "";
},
// 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}.`);
this.username = msg.username;
}
this.ChatClient(`User sync from backend: ${JSON.stringify(msg)}`);
},
// 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);
},
// Handle messages sent in chat.
onMessage(msg) {
this.pushHistory({
username: msg.username,
message: msg.message,
});
},
// Dial the WebSocket connection.
dial() {
console.log("Dialing WebSocket...");
const conn = new WebSocket(`ws://${location.host}/ws`);
conn.addEventListener("close", ev => {
this.ws.connected = false;
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
if (ev.code !== 1001) {
this.ChatClient("Reconnecting in 1s");
setTimeout(this.dial, 1000);
}
});
conn.addEventListener("open", ev => {
this.ws.connected = true;
this.ChatClient("Websocket connected!");
// 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);
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;
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,
});
default:
console.error("Unexpected action: %s", JSON.stringify(msg));
}
});
this.ws.conn = conn;
},
/**
* 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.
*/
// 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;
})
},
// 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);
},
// 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}) {
this.history.push({
action: action,
username: username,
message: message,
isChatServer,
isChatClient,
});
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,
});
},
}
});
app.mount("#BareRTC-App");