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";
|
||||
|
||||
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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 || {}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue