diff --git a/bfrontend/src/API/Gateway/GatewayConnection.js b/bfrontend/src/API/Gateway/GatewayConnection.js index 853bca7..38fcc4b 100644 --- a/bfrontend/src/API/Gateway/GatewayConnection.js +++ b/bfrontend/src/API/Gateway/GatewayConnection.js @@ -1,13 +1,20 @@ import logger from "../../Util/Logger"; const { log: logGateway } = logger([ "Gateway" ]); +const { log: logRtc } = logger([ "Gateway", "RTC" ]); + const opcodes = { 0: { name: "HELLO", data: "JSON" }, 1: { name: "YOO", data: "JSON" }, 2: { name: "YOO_ACK", data: "JSON" }, 3: { name: "ACTION_CREATE_MESSAGE", data: "JSON" }, - 4: { name: "EVENT_CREATE_MESSAGE", data: "JSON" } + 4: { name: "EVENT_CREATE_MESSAGE", data: "JSON" }, + 21: { name: "ACTION_VOICE_REQUEST_SESSION", data: "JSON" }, + 22: { name: "EVENT_VOICE_ASSIGN_SERVER", data: "JSON" }, + 23: { name: "ACTION_VOICE_CONNECTION_REQUEST", data: "JSON" }, + 24: { name: "EVENT_VOICE_CONNECTION_ANSWER", data: "JSON" }, + 25: { name: "EVENT_RENEGOTIATE_REQUIRED", data: "JSON" } }; const opcodeSeparator = "@"; @@ -55,6 +62,7 @@ GatewayConnection.prototype.connect = function(token) { this.handshakeCompleted = false; this.sessionInformation = null; + this.token = token; this.ws.onopen = () => logGateway("Open"); this.ws.onclose = (e) => { @@ -62,7 +70,7 @@ GatewayConnection.prototype.connect = function(token) { logGateway(`Close: ${e.code}:${e.reason}`); this.fire("onclose", e); }; - this.ws.onmessage = (message) => { + this.ws.onmessage = async (message) => { try { const packet = parseMessage(message.data); if (!packet) return console.error("gateway: invalid packet from server"); @@ -86,10 +94,33 @@ GatewayConnection.prototype.connect = function(token) { } case "EVENT_CREATE_MESSAGE": { // New message - // console.log("gateway: got new message", packet.data); this.fire("onmessage", packet.data); break; } + case "EVENT_VOICE_CONNECTION_ANSWER": { + if (!this.webrtcConnection) throw new Error("rtc: got remote answer without local offer"); + if (this.webrtcConnection.signalingState === "stable") { + logRtc("Server sent answer, but we were in stable state"); + return; + } + + const answer = packet.data.answer; + + logRtc("Got remote answer", answer); + + await this.webrtcConnection.setRemoteDescription(answer); + + break; + } + case "EVENT_RENEGOTIATE_REQUIRED": { + if (!this.webrtcConnection) throw new Error("rtc: got remote EVENT_RENEGOTIATE_REQUIRED without local offer"); + + logRtc("Server requested renegotiation"); + + this.negotiateVoiceSession(); + + break; + } default: { logGateway("Got unknown packet", message.data); break; @@ -112,6 +143,74 @@ GatewayConnection.prototype.sendMessage = function(content, channelId) { })); }; +GatewayConnection.prototype.negotiateVoiceSession = async function() { + if (this.webrtcConnection.connectionState === "connected" || this.webrtcConnection.connectionState === "connecting") return; + if (this.webrtcConnection.signalingState !== "stable") return; + + logRtc("Negotiating voice session..."); + this.webrtcConnection.ontrack = (e) => { + logRtc("Got remote track", e); + const audio = document.createElement("audio"); + audio.srcObject = e.streams[0]; + audio.controls = true; + audio.play(); + document.body.appendChild(audio); + }; + + this.webrtcConnection.onnegotiationneeded = () => this.negotiateVoiceSession(); + + this.webrtcConnection.onicecandidate = (event) => { + if (event.candidate || this.rtcGotCandidates) return; + this.rtcGotCandidates = true; + logRtc("End of candidates; we can send the offer to the server", this.webrtcConnection.localDescription); + this.ws.send(this.packet("ACTION_VOICE_CONNECTION_REQUEST", { + token: this.token, + channel: { + _id: this.currentVoiceChannel + }, + offer: this.webrtcConnection.localDescription + })); + }; + + this.webrtcConnection.onsignalingstatechange = () => { + logRtc(`onsignalingstatechange -> ${this.webrtcConnection.signalingState}`); + }; + + this.webrtcConnection.onconnectionstatechange = () => { + logRtc(`onsignalingstatechange -> ${this.webrtcConnection.connectionState}`); + }; + + + const offer = await this.webrtcConnection.createOffer(); + await this.webrtcConnection.setLocalDescription(offer); + if (this.rtcGotCandidates) { + logRtc("Already have candidates; we can send the offer to the server", this.webrtcConnection.localDescription); + this.ws.send(this.packet("ACTION_VOICE_CONNECTION_REQUEST", { + token: this.token, + channel: { + _id: this.currentVoiceChannel + }, + offer: this.webrtcConnection.localDescription + })); + } +}; + +GatewayConnection.prototype.beginVoiceSession = async function(channelId) { + logRtc("beginVoiceSession"); + this.currentVoiceChannel = channelId; + this.webrtcConnection = new RTCPeerConnection({ + iceServers: [ + { + urls: "stun://stun.l.google.com:19302" + } + ] + }); + + this.webrtcConnection.addTrack((await navigator.mediaDevices.getUserMedia({ + audio: true + })).getTracks()[0]); + await this.negotiateVoiceSession(); +}; GatewayConnection.prototype.packet = function(op, data) { if (typeof op === "string") op = getOpcodeByName(op); diff --git a/bfrontend/src/API/Gateway/globalGatewayConnection.js b/bfrontend/src/API/Gateway/globalGatewayConnection.js index 48fe236..8dcf674 100644 --- a/bfrontend/src/API/Gateway/globalGatewayConnection.js +++ b/bfrontend/src/API/Gateway/globalGatewayConnection.js @@ -7,7 +7,8 @@ const globalGatewayConnection = new GatewayConnection(config.gatewayUrl); globalGatewayConnection.onopen = (sessionData) => { store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: true } }); store.dispatch({ type: 'authenticator/updatelocaluserobject', user: sessionData.user }); - store.dispatch({ type: 'channels/updatechannellist', channels: sessionData.channels }) + store.dispatch({ type: 'channels/updatechannellist', channels: sessionData.channels }); + store.dispatch({ type: 'application/updateexperiments', user: sessionData.__global_experiments || [] }); }; globalGatewayConnection.onmessage = (message) => { diff --git a/bfrontend/src/Components/Channels/ChannelView.js b/bfrontend/src/Components/Channels/ChannelView.js index cb54307..0761761 100644 --- a/bfrontend/src/Components/Channels/ChannelView.js +++ b/bfrontend/src/Components/Channels/ChannelView.js @@ -9,7 +9,7 @@ import { connect, useDispatch } from 'react-redux'; import { useState, useRef, useEffect } from 'react'; -const ChannelView = ({ channels, messages, channel, selectedChannelId }) => { +const ChannelView = ({ channels, messages, channel, selectedChannelId, experiments }) => { const { id } = useParams(); const [ textInput, setTextInput ] = useState(''); @@ -52,13 +52,16 @@ const ChannelView = ({ channels, messages, channel, selectedChannelId }) => { } return ( -