mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:37:08 +00:00
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:
194
lib/www/client/source/src/lib/rtc.js
Normal file
194
lib/www/client/source/src/lib/rtc.js
Normal 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
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
if (msg.rtc === true) {
|
||||||
|
// Handle WebRTC message
|
||||||
|
} else {
|
||||||
this.setServerEvent(msg);
|
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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user