Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/domain/networking/NodeList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ class NodeList extends LimitedNodeList {
#startNodeHolePunch = (node: Node): void => {
// C++ void startNodeHolePunch(const Node* node);
// While we don't need to do hole punching per se because WebRTC handles this, we initiate opening the WebRTC data
// channel and adopt the native client's use of pings and replys to coordinate setting up communications with the
// channel and adopt the native client's use of pings and replies to coordinate setting up communications with the
// assignment client.

// WebRTC: Initiate opening the WebRTC data channel.
Expand Down
2 changes: 1 addition & 1 deletion src/domain/networking/udt/Socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Socket {

static readonly #WEBRTCSOCKET_TO_SOCKET_STATES = [
Socket.UNCONNECTED,
Socket.UNCONNECTED,
Socket.CONNECTING,
Socket.CONNECTING,
Socket.CONNECTED
];
Expand Down
131 changes: 86 additions & 45 deletions src/domain/networking/webrtc/WebRTCDataChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import NodeType, { NodeTypeValue } from "../NodeType";
import WebRTCSignalingChannel, { SignalingMessage } from "./WebRTCSignalingChannel";
import assert from "../../shared/assert";


type OnOpenCallback = () => void;
Expand Down Expand Up @@ -102,14 +103,26 @@ class WebRTCDataChannel {

static readonly #CONFIGURATION = {
// WEBRTC TODO: Make configurable in the API.
iceServers: [{ urls: "stun:ice.vircadia.com:7337" }]
// FIXME: stun:ice.vircadia.com:7337 doesn't work for WebRTC.
// Firefox warns: "WebRTC: Using more than two STUN/TURN servers slows down discovery"
iceServers: [
{
urls: [
"stun:stun1.l.google.com:19302",
"stun:stun.schlund.de"
]
}
]
};


#_nodeType = NodeType.Unassigned;
#_nodeTypeName = "";
#_signalingChannel: WebRTCSignalingChannel | null = null;

#_peerConnection: RTCPeerConnection | null = null;
#_offer: RTCSessionDescriptionInit | null = null;
#_haveSetRemoteDescription = false;
#_dataChannel: RTCDataChannel | null = null;
#_dataChannelID = 0;
#_readyState = WebRTCDataChannel.CLOSED;
Expand All @@ -119,9 +132,12 @@ class WebRTCDataChannel {
#_oncloseCallback: OnCloseCallback | null = null;
#_onerrorCallback: OnErrorCallback | null = null;

#_DEBUG = false;


constructor(nodeType: NodeTypeValue, signalingChannel: WebRTCSignalingChannel) {
this.#_nodeType = nodeType;
this.#_nodeTypeName = NodeType.getNodeTypeName(nodeType);
this.#_signalingChannel = signalingChannel;
this.#_readyState = WebRTCDataChannel.CONNECTING;
setTimeout(() => {
Expand Down Expand Up @@ -223,46 +239,33 @@ class WebRTCDataChannel {


// Starts making a WebRTC connection.
#start(): void {
async #start(): Promise<void> {

assert(this.#_signalingChannel !== null);

// Create new peer connection object.
this.#_peerConnection = new RTCPeerConnection(WebRTCDataChannel.#CONFIGURATION);

// Send ICE candidates to the domain server.
this.#_peerConnection.onicecandidate = ({ candidate }) => {
if (candidate // The candidate is sometimes null for unknown reasons; don't send this.
&& this.#_signalingChannel && this.#_signalingChannel.readyState === WebRTCSignalingChannel.OPEN) {
this.#_signalingChannel.send({ to: this.#_nodeType, data: candidate });
}
};

// Generate an offer.
this.#_peerConnection.onnegotiationneeded = async () => {
if (!this.#_peerConnection || !this.#_signalingChannel
|| this.#_signalingChannel.readyState !== WebRTCSignalingChannel.OPEN) {
return;
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Obtained ICE candidate.`);
}
try {
// Create offer.
const offer = await this.#_peerConnection.createOffer();
await this.#_peerConnection.setLocalDescription(offer);

// Send offer to domain server.
this.#_signalingChannel.send({
to: this.#_nodeType,
data: { description: this.#_peerConnection.localDescription }
});
} catch (err) {
const errorMessage = "WebRTCDataChannel: Error during offer negotiation: " + <string>err;
console.error(errorMessage);
if (this.#_onerrorCallback) {
this.#_onerrorCallback(errorMessage);
if (candidate // The candidate is sometimes null; don't send this but do send empty string.
&& this.#_signalingChannel && this.#_signalingChannel.readyState === WebRTCSignalingChannel.OPEN) {
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Send ICE candidate.`);
}
this.#_signalingChannel.send({ to: this.#_nodeType, data: { candidate } });
}
};

// Observe connection state changes.
this.#_peerConnection.onconnectionstatechange = () => {
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Connection state changed:`,
this.#_peerConnection?.connectionState);
}
let errorMessage = "";
switch (this.#_peerConnection ? this.#_peerConnection.connectionState : "") {
case "new":
Expand All @@ -272,7 +275,7 @@ class WebRTCDataChannel {
break;
case "connected":
// The connection has become fully connected.
// However, _readyState isn't set to OPEN until the data channel has been connected.
// However, #_readyState isn't set to OPEN until the data channel has been connected.
break;
case "disconnected":
case "failed":
Expand Down Expand Up @@ -329,6 +332,28 @@ class WebRTCDataChannel {
}
};

// Create offer.
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Create offer.`);
}
const rtcOfferOptions = {
offerToReceiveAudio: false,
offerToReceiveVideo: false
};
this.#_offer = await this.#_peerConnection.createOffer(rtcOfferOptions);
// Don't set the local description until we have the remote answer because setting the local description triggers ICE
// candidate gathering and the remote isn't ready to handle them yet.
this.#_haveSetRemoteDescription = false;

// Send offer to domain server.
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Send offer.`);
}
this.#_signalingChannel.send({
to: this.#_nodeType,
data: { description: this.#_offer }
});

} // start

// Instigates the WebRTC connection process.
Expand Down Expand Up @@ -356,13 +381,12 @@ class WebRTCDataChannel {
return;
}

// Start a new peer connection if necessary.
if (!this.#_peerConnection && (description || candidate)) {
this.#start();
}

try {
if (description) {
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Received description.`);
}

if (!this.#_peerConnection) {
const errorMessage = "WebRTCDataChannel: Peer connection is closed!";
console.error(errorMessage);
Expand All @@ -372,20 +396,37 @@ class WebRTCDataChannel {
return;
}

// Add remote connection information to peer connection.
await this.#_peerConnection.setRemoteDescription(description);
// We got an answer.
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Description is ${description.type}.`);
}
if (description.type === "answer" && this.#_signalingChannel) {
assert(this.#_offer !== null);

// The server is ready to handle ICE candidates so set we can set the local description now.
await this.#_peerConnection.setLocalDescription(this.#_offer);

// We got an offer; reply with an answer.
if (description.type === "offer" && this.#_signalingChannel) {
await this.#_peerConnection.setLocalDescription(description);
this.#_signalingChannel.send({
description: this.#_peerConnection.localDescription
});
await this.#_peerConnection.setRemoteDescription(description);
this.#_haveSetRemoteDescription = true;
} else {
const errorMessage = `WebRTCDataChannel: Unexpected answer! ${description.type}`;
console.error(errorMessage);
if (this.#_onerrorCallback) {
this.#_onerrorCallback(errorMessage);
}
}
} else if (candidate) {
// Add ICE candidate to peer connection.
if (this.#_peerConnection) {
// Add ICE candidate to the peer connection.
// Don't set unless the remote description has been set, otherwise an error is generated. The first ICE
// candidate from the server may arrive before the remote description has been set because of the delay
// introduced by setting the local description just before setting the remote description.
if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Received ICE candidate.`);
}
if (this.#_peerConnection && this.#_haveSetRemoteDescription) {
await this.#_peerConnection.addIceCandidate(candidate);
} else if (this.#_DEBUG) {
console.debug(`[webrtc] [${this.#_nodeTypeName}] Skipped adding ICE candidate.`);
}
} else if (echo) {
// Ignore signaling channel "echo" messages.
Expand All @@ -409,7 +450,7 @@ class WebRTCDataChannel {
});

// Start the WebRTC connection process.
this.#start();
void this.#start();

} // #connect

Expand Down
3 changes: 1 addition & 2 deletions src/domain/networking/webrtc/WebRTCSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,9 @@ class WebRTCSocket {
// Fall through.
}
}
if (this.#_webrtcSignalingChannel !== null) {
if (nodeType === NodeTypeValue.DomainServer && this.#_webrtcSignalingChannel !== null) {
switch (this.#_webrtcSignalingChannel.readyState) {
case WebRTCSignalingChannel.OPEN:
return WebRTCSocket.SIGNALING;
case WebRTCSignalingChannel.CONNECTING:
return WebRTCSocket.SIGNALING;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("WebRTCSocket - integration tests", () => {
expect(webrtcSocket.state(TestConfig.SERVER_SIGNALING_SOCKET_URL, NodeType.DomainServer))
.toBe(WebRTCSocket.CONNECTED);
expect(webrtcSocket.state(TestConfig.SERVER_SIGNALING_SOCKET_URL, NodeType.AudioMixer))
.toBe(WebRTCSocket.SIGNALING);
.toBe(WebRTCSocket.UNCONNECTED);
expect(webrtcSocket.state(TestConfig.SERVER_SIGNALING_SOCKET_URL + "1", NodeType.DomainServer))
.toBe(WebRTCSocket.UNCONNECTED);
webrtcSocket.abort();
Expand Down