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
Signed by: hippoz
GPG key ID: 7C52899193467641
8 changed files with 155 additions and 40 deletions

View file

@ -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);

View file

@ -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) => {

View file

@ -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 (
<div className="col-flex-card">
<div className="col-flex-card hidden-overflow">
<div className="bar-card-accent">
<ChannelProfile channel={ channel } size="24" />
{ (experiments.voiceSFUTesting) && <button className="button" onClick={ () => gatewayConnection.beginVoiceSession(channel._id) }>
Join voice
</button>}
</div>
<div className="main-card row-flex-card">
<div className="col-flex-card channel-view">
<div className="main-card row-flex-card hidden-overflow">
<div className="col-flex-card channel-message-panel hidden-overflow">
<div className="message-list-card">
{ messagesView }
<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>
</div>
</div>
<ChannelUserList />
{ (experiments.userListTest) && <ChannelUserList /> }
</div>
</div>
)
@ -90,13 +93,14 @@ const ChannelView = ({ channels, messages, channel, selectedChannelId }) => {
};
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 {
channels: state?.channels,
channel: state?.channels?.find(x => x._id === channelId),
messages: state?.messages[channelId] || [],
selectedChannelId: state?.selectedChannelId,
experiments: state?.experiments || {}
};
};

View file

@ -30,34 +30,32 @@ function App({ user }) {
if (!hasError) {
return (
<div id="main-container">
<div id="root-container" className="main-display">
<BrowserRouter>
<Switch>
<Route path="/login" component={ Login } />
<Route path="/channels/:id"
render={(props) => {
return (
<>
<Sidebar />
<ChannelView match={ props.match } />
</>
);
}}
/>
<Route path="/">
{ user && <Sidebar /> }
<Root user={user} />
</Route>
</Switch>
</BrowserRouter>
</div>
<div id="root-container" className="main-display">
<BrowserRouter>
<Switch>
<Route path="/login" component={ Login } />
<Route path="/channels/:id"
render={(props) => {
return (
<>
<Sidebar />
<ChannelView match={ props.match } />
</>
);
}}
/>
<Route path="/">
{ user && <Sidebar /> }
<Root user={user} />
</Route>
</Switch>
</BrowserRouter>
</div>
);
} else {
return (
<div id="main-container">
<div id="root-container" className="main-display">
<Notification text={notificationText}/>
</div>
);

View file

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

View file

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

View file

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

View file

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