add support for broken voice backend and add support for serverside experiment toggling
This commit is contained in:
parent
26a867b3b0
commit
d2f1ca9e02
8 changed files with 155 additions and 40 deletions
|
@ -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);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 || {}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -30,34 +30,32 @@ 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>
|
<Route path="/login" component={ Login } />
|
||||||
<Route path="/login" component={ Login } />
|
<Route path="/channels/:id"
|
||||||
<Route path="/channels/:id"
|
render={(props) => {
|
||||||
render={(props) => {
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
<Sidebar />
|
||||||
<Sidebar />
|
<ChannelView match={ props.match } />
|
||||||
<ChannelView match={ props.match } />
|
</>
|
||||||
</>
|
);
|
||||||
);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
|
<Route path="/">
|
||||||
<Route path="/">
|
{ user && <Sidebar /> }
|
||||||
{ user && <Sidebar /> }
|
<Root user={user} />
|
||||||
<Root user={user} />
|
</Route>
|
||||||
</Route>
|
</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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
Loading…
Reference in a new issue