add support for broken voice backend and add support for serverside experiment toggling

This commit is contained in:
hippoz 2021-05-21 01:33:14 +03:00
parent 26a867b3b0
commit d2f1ca9e02
No known key found for this signature in database
GPG key ID: 7C52899193467641
8 changed files with 155 additions and 40 deletions

View file

@ -1,13 +1,20 @@
import logger from "../../Util/Logger"; import logger from "../../Util/Logger";
const { log: logGateway } = logger([ "Gateway" ]); const { log: logGateway } = logger([ "Gateway" ]);
const { log: logRtc } = logger([ "Gateway", "RTC" ]);
const opcodes = { const opcodes = {
0: { name: "HELLO", data: "JSON" }, 0: { name: "HELLO", data: "JSON" },
1: { name: "YOO", data: "JSON" }, 1: { name: "YOO", data: "JSON" },
2: { name: "YOO_ACK", data: "JSON" }, 2: { name: "YOO_ACK", data: "JSON" },
3: { name: "ACTION_CREATE_MESSAGE", 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 = "@"; const opcodeSeparator = "@";
@ -55,6 +62,7 @@ GatewayConnection.prototype.connect = function(token) {
this.handshakeCompleted = false; this.handshakeCompleted = false;
this.sessionInformation = null; this.sessionInformation = null;
this.token = token;
this.ws.onopen = () => logGateway("Open"); this.ws.onopen = () => logGateway("Open");
this.ws.onclose = (e) => { this.ws.onclose = (e) => {
@ -62,7 +70,7 @@ GatewayConnection.prototype.connect = function(token) {
logGateway(`Close: ${e.code}:${e.reason}`); logGateway(`Close: ${e.code}:${e.reason}`);
this.fire("onclose", e); this.fire("onclose", e);
}; };
this.ws.onmessage = (message) => { this.ws.onmessage = async (message) => {
try { try {
const packet = parseMessage(message.data); const packet = parseMessage(message.data);
if (!packet) return console.error("gateway: invalid packet from server"); if (!packet) return console.error("gateway: invalid packet from server");
@ -86,10 +94,33 @@ GatewayConnection.prototype.connect = function(token) {
} }
case "EVENT_CREATE_MESSAGE": { case "EVENT_CREATE_MESSAGE": {
// New message // New message
// console.log("gateway: got new message", packet.data);
this.fire("onmessage", packet.data); this.fire("onmessage", packet.data);
break; 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: { default: {
logGateway("Got unknown packet", message.data); logGateway("Got unknown packet", message.data);
break; 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) { GatewayConnection.prototype.packet = function(op, data) {
if (typeof op === "string") op = getOpcodeByName(op); if (typeof op === "string") op = getOpcodeByName(op);

View file

@ -7,7 +7,8 @@ const globalGatewayConnection = new GatewayConnection(config.gatewayUrl);
globalGatewayConnection.onopen = (sessionData) => { globalGatewayConnection.onopen = (sessionData) => {
store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: true } }); store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: true } });
store.dispatch({ type: 'authenticator/updatelocaluserobject', user: sessionData.user }); 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) => { globalGatewayConnection.onmessage = (message) => {

View file

@ -9,7 +9,7 @@ import { connect, useDispatch } from 'react-redux';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
const ChannelView = ({ channels, messages, channel, selectedChannelId }) => { const ChannelView = ({ channels, messages, channel, selectedChannelId, experiments }) => {
const { id } = useParams(); const { id } = useParams();
const [ textInput, setTextInput ] = useState(''); const [ textInput, setTextInput ] = useState('');
@ -52,13 +52,16 @@ const ChannelView = ({ channels, messages, channel, selectedChannelId }) => {
} }
return ( return (
<div className="col-flex-card"> <div className="col-flex-card hidden-overflow">
<div className="bar-card-accent"> <div className="bar-card-accent">
<ChannelProfile channel={ channel } size="24" /> <ChannelProfile channel={ channel } size="24" />
{ (experiments.voiceSFUTesting) && <button className="button" onClick={ () => gatewayConnection.beginVoiceSession(channel._id) }>
Join voice
</button>}
</div> </div>
<div className="main-card row-flex-card"> <div className="main-card row-flex-card hidden-overflow">
<div className="col-flex-card channel-view"> <div className="col-flex-card channel-message-panel hidden-overflow">
<div className="message-list-card"> <div className="message-list-card">
{ messagesView } { messagesView }
<div ref={ invisibleBottomMessageRef }></div> <div ref={ invisibleBottomMessageRef }></div>
@ -68,7 +71,7 @@ const ChannelView = ({ channels, messages, channel, selectedChannelId }) => {
<input className="text-input message-input" type="text" placeholder="Go on, type something interesting!" ref={ textInputRef } onKeyDown={ handleTextboxKeydown } onChange={ ({ target }) => setTextInput(target.value) }></input> <input className="text-input message-input" type="text" placeholder="Go on, type something interesting!" ref={ textInputRef } onKeyDown={ handleTextboxKeydown } onChange={ ({ target }) => setTextInput(target.value) }></input>
</div> </div>
</div> </div>
<ChannelUserList /> { (experiments.userListTest) && <ChannelUserList /> }
</div> </div>
</div> </div>
) )
@ -90,13 +93,14 @@ const ChannelView = ({ channels, messages, channel, selectedChannelId }) => {
}; };
const stateToProps = (state, ownProps) => { const stateToProps = (state, ownProps) => {
const channelId = ownProps.match.params.id; // NOTE(hippoz): kind of a hack, but it works and idk if theres any other solution const channelId = ownProps.match.params.id; // NOTE: kind of a hack, but it works and idk if theres any other solution
return { return {
channels: state?.channels, channels: state?.channels,
channel: state?.channels?.find(x => x._id === channelId), channel: state?.channels?.find(x => x._id === channelId),
messages: state?.messages[channelId] || [], messages: state?.messages[channelId] || [],
selectedChannelId: state?.selectedChannelId, selectedChannelId: state?.selectedChannelId,
experiments: state?.experiments || {}
}; };
}; };

View file

@ -30,7 +30,6 @@ function App({ user }) {
if (!hasError) { if (!hasError) {
return ( return (
<div id="main-container">
<div id="root-container" className="main-display"> <div id="root-container" className="main-display">
<BrowserRouter> <BrowserRouter>
<Switch> <Switch>
@ -53,11 +52,10 @@ function App({ user }) {
</Switch> </Switch>
</BrowserRouter> </BrowserRouter>
</div> </div>
</div>
); );
} else { } else {
return ( return (
<div id="main-container"> <div id="root-container" className="main-display">
<Notification text={notificationText}/> <Notification text={notificationText}/>
</div> </div>
); );

View file

@ -42,8 +42,8 @@ const reducer = (state = intitialState, payload) => {
...state, ...state,
messages: { messages: {
...state.messages, ...state.messages,
[payload.message.channel_id]: [ [payload.message.channel._id]: [
...state.messages[payload.message.channel_id] || [], ...state.messages[payload.message.channel._id] || [],
payload.message payload.message
] ]
} }
@ -67,6 +67,13 @@ const reducer = (state = intitialState, payload) => {
}; };
} }
case 'application/updateexperiments': {
return {
...state,
experiments: payload.experiments
}
}
default: { default: {
return state; return state;
} }

View file

@ -23,7 +23,8 @@ body {
color: var(--default-text-color); color: var(--default-text-color);
background-color: var(--background-color); background-color: var(--background-color);
font-family: Noto Sans,-apple-system,BlinkMacSystemFont,sans-serif; font-family: Noto Sans,-apple-system,BlinkMacSystemFont,sans-serif;
min-height: 100vh;
max-height: 100vh;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }

View file

@ -41,9 +41,8 @@
background-color: var(--channel-message-list-background-color); background-color: var(--channel-message-list-background-color);
box-shadow: none; box-shadow: none;
flex-basis: 100%;
border-radius: 0px; border-radius: 0px;
flex-grow: 1;
overflow: auto; overflow: auto;
} }

View file

@ -37,6 +37,12 @@
z-index: 100; z-index: 100;
} }
.channel-view { .channel-message-panel {
display: flex;
flex-direction: column;
background-color: var(--channel-view-container-color); background-color: var(--channel-view-container-color);
} }
.hidden-overflow {
overflow: hidden;
}