Add client-side RTC logic.

It tries to grab the microphone and open a
connection to every other user as soon as the
websocket connection is made. It shows audio
controls for every connection at the bottom
of the page for debugging purposes.
This commit is contained in:
D. Berge
2020-10-10 12:07:31 +02:00
parent b72b55c6dd
commit 1f8157cc5a
2 changed files with 203 additions and 1 deletions

View File

@@ -0,0 +1,194 @@
let ws = null;
let peerId = null;
let peers = {};
let stream = null;
function init (socket) {
ws = socket;
ws.addEventListener("message", (ev) => {
try {
const payload = JSON.parse(ev.data);
if (payload.rtc === true) {
// Handle this message
handle (payload);
}
} catch (err) {
console.error("Invalid message", ev, err);
}
});
}
async function talk () {
try {
if (!stream) {
const constraints = { audio: true, video: false };
stream = await navigator.mediaDevices.getUserMedia(constraints);
console.log("Grabbed stream", stream);
}
if (peerId && Object.keys(peers).length) {
for (const track of stream.getTracks()) {
for (const peer in peers) {
peers[peer].addTrack(track, stream);
}
}
}
} catch (err) {
console.error("talk() error", err);
}
}
class PeerConnection {
constructor (otherPeerId) {
this.otherPeerId = otherPeerId;
this.makingOffer = false;
this.polite = this.otherPeerId > peerId;
this.start();
}
send (message) {
message.from = peerId;
message.to = this.otherPeerId;
send(message);
}
async handle (message) {
try {
let ignoreOffer = false;
if ("description" in message) {
const offerCollision = (message.description.type == "offer") &&
(this.makingOffer || this.conn.signalingState != "stable");
ignoreOffer = !this.polite && offerCollision;
if (ignoreOffer) {
return;
}
await this.conn.setRemoteDescription(message.description);
if (message.description.type == "offer") {
await this.conn.setLocalDescription();
this.send({ description: this.conn.localDescription });
}
}
if ("candidate" in message) {
try {
await this.conn.addIceCandidate(message.candidate);
} catch (err) {
if (!ignoreOffer) {
throw err;
}
}
}
} catch (err) {
console.error(err);
}
}
start () {
const config = { iceServers: [] };
this.conn = new RTCPeerConnection(config);
console.log("Have peer connection", this.conn);
this.conn.ontrack = ({track, streams}) => {
// FIXME Need to remove these elements when done
if (!this.remoteAudio) {
this.remoteAudio = document.createElement("audio");
this.remoteAudio.setAttribute("autoplay", "true");
this.remoteAudio.controls = true;
document.getElementsByTagName("footer")[0].appendChild(this.remoteAudio);
console.log("Added <audio> element", this.remoteAudio);
}
track.onunmute = () => {
if (this.remoteAudio.srcObject) {
return;
}
this.remoteAudio.srcObject = streams[0];
console.log("unmuted");
};
};
this.conn.onnegotiationneeded = async () => {
console.log("negotiation needed");
try {
this.makingOffer = true;
await this.conn.setLocalDescription();
this.send({ description: this.conn.localDescription });
} catch (err) {
console.error(err);
} finally {
this.makingOffer = false;
}
};
this.conn.oniceconnectionstatechange = () => {
console.log("state change");
if (this.conn.iceConnectionState == "failed") {
this.conn.restartIce();
}
}
this.conn.onicecandidate = async ({candidate}) => {
console.log("send candidate", candidate);
this.send({candidate});
};
if (stream) {
for (const track of stream.getAudioTracks()) {
this.addTrack(track, stream);
}
}
}
addTrack (track, stream) {
console.log("add track to connection", this.conn, track, stream);
if (this.conn) {
this.conn.addTrack(track, stream);
}
}
};
function send (message) {
console.log("Send message", message, "via", ws);
if (ws) {
message.rtc = true;
ws.send(JSON.stringify(message));
}
}
function handle (message) {
console.log("Handle message", message);
if ("peerId" in message) {
peerId = message.peerId;
}
if ("otherPeers" in message) {
for (const peer of message.otherPeers) {
if (peer in peers) continue;
peers[peer] = new PeerConnection(peer);
}
}
if ("newPeer" in message) {
peers[message.newPeer] = new PeerConnection(message.newPeer);
}
if ("to" in message && "from" in message && message.to == peerId) {
peers[message.from].handle(message);
}
}
export default {
init, talk
};

View File

@@ -6,6 +6,8 @@ import vuetify from './plugins/vuetify'
import vueDebounce from 'vue-debounce' import vueDebounce from 'vue-debounce'
import { mapMutations } from 'vuex'; import { mapMutations } from 'vuex';
import rtc from './lib/rtc';
Vue.config.productionTip = false Vue.config.productionTip = false
@@ -51,12 +53,18 @@ new Vue({
this.ws.addEventListener("message", (ev) => { this.ws.addEventListener("message", (ev) => {
const msg = JSON.parse(ev.data); const msg = JSON.parse(ev.data);
this.setServerEvent(msg); if (msg.rtc === true) {
// Handle WebRTC message
} else {
this.setServerEvent(msg);
}
}); });
this.ws.addEventListener("open", (ev) => { this.ws.addEventListener("open", (ev) => {
console.log("WebSocket connection open", ev); console.log("WebSocket connection open", ev);
this.setServerConnectionState(true); this.setServerConnectionState(true);
rtc.init(this.ws);
rtc.talk();
}); });
this.ws.addEventListener("close", (ev) => { this.ws.addEventListener("close", (ev) => {