From 6c3762846808c891d77413b172ba93d52a075603 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Mon, 31 May 2021 12:29:48 +1200
Subject: [PATCH] Add WebRTC data channel

---
 domain-server/src/DomainServer.cpp            |  17 +-
 domain-server/src/DomainServer.h              |   7 +-
 libraries/networking/CMakeLists.txt           |   3 +-
 libraries/networking/src/NodeType.h           |   6 +-
 .../src/webrtc/WebRTCDataChannels.cpp         | 475 ++++++++++++++++++
 .../src/webrtc/WebRTCDataChannels.h           | 278 ++++++++++
 .../src/webrtc/WebRTCSignalingServer.cpp      |   4 +-
 .../src/webrtc/WebRTCSignalingServer.h        |  12 +-
 libraries/shared/src/shared/WebRTC.h          |   4 +-
 9 files changed, 791 insertions(+), 15 deletions(-)
 create mode 100644 libraries/networking/src/webrtc/WebRTCDataChannels.cpp
 create mode 100644 libraries/networking/src/webrtc/WebRTCDataChannels.h

diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp
index 02fd810b0e..c7a5700de2 100644
--- a/domain-server/src/DomainServer.cpp
+++ b/domain-server/src/DomainServer.cpp
@@ -165,8 +165,9 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection,
 DomainServer::DomainServer(int argc, char* argv[]) :
     QCoreApplication(argc, argv),
     _gatekeeper(this),
-#ifdef WEBRTC_DATA_CHANNEL
+#ifdef WEBRTC_DATA_CHANNELS
     _webrtcSignalingServer(QHostAddress::AnyIPv4, DEFAULT_DOMAIN_SERVER_WS_PORT, this),
+    _webrtcDataChannels(NodeType::DomainServer, this),
 #endif
     _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT,
         QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this)
@@ -251,6 +252,8 @@ DomainServer::DomainServer(int argc, char* argv[]) :
     updateDownstreamNodes();
     updateUpstreamNodes();
 
+    setUpWebRTC();
+
     if (_type != NonMetaverse) {
         // if we have a metaverse domain, we'll use an access token for API calls
         resetAccountManagerAccessToken();
@@ -3136,6 +3139,18 @@ void DomainServer::updateUpstreamNodes() {
     updateReplicationNodes(Upstream);
 }
 
+void DomainServer::setUpWebRTC() {
+
+    // Inbound WebRTC signaling messages received from a client.
+    connect(&_webrtcSignalingServer, &WebRTCSignalingServer::messageReceived,
+        &_webrtcDataChannels, &WebRTCDataChannels::onSignalingMessage);
+
+    // Outbound WebRTC signaling messages being sent to a client.
+    connect(&_webrtcDataChannels, &WebRTCDataChannels::signalingMessage,
+        &_webrtcSignalingServer, &WebRTCSignalingServer::sendMessage);
+
+}
+
 void DomainServer::initializeExporter() {
     static const QString ENABLE_EXPORTER = "monitoring.enable_prometheus_exporter";
     static const QString EXPORTER_PORT = "monitoring.prometheus_exporter_port";
diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h
index 29142505a8..23e1299374 100644
--- a/domain-server/src/DomainServer.h
+++ b/domain-server/src/DomainServer.h
@@ -28,6 +28,7 @@
 #include <HTTPSConnection.h>
 #include <LimitedNodeList.h>
 #include <shared/WebRTC.h>
+#include <webrtc/WebRTCDataChannels.h>
 #include <webrtc/WebRTCSignalingServer.h>
 
 #include "AssetsBackupHandler.h"
@@ -143,6 +144,9 @@ private slots:
     void updateReplicatedNodes();
     void updateDownstreamNodes();
     void updateUpstreamNodes();
+
+    void setUpWebRTC();
+
     void initializeExporter();
     void initializeMetadataExporter();
 
@@ -314,8 +318,9 @@ private:
 
     QThread _assetClientThread;
 
-#ifdef WEBRTC_DATA_CHANNEL
+#ifdef WEBRTC_DATA_CHANNELS
     WebRTCSignalingServer _webrtcSignalingServer;
+    WebRTCDataChannels _webrtcDataChannels;
 #endif
 };
 
diff --git a/libraries/networking/CMakeLists.txt b/libraries/networking/CMakeLists.txt
index 50382cda99..1835c7a6cd 100644
--- a/libraries/networking/CMakeLists.txt
+++ b/libraries/networking/CMakeLists.txt
@@ -11,7 +11,8 @@ endif ()
 
 if (WIN32)
     # we need ws2_32.lib on windows, but it's static so we don't bubble it up
-    target_link_libraries(${TARGET_NAME} ws2_32.lib)
+    # Libraries needed for WebRTC: security.log winmm.lib
+    target_link_libraries(${TARGET_NAME} ws2_32.lib security.lib winmm.lib)
 elseif(APPLE)
     # IOKit is needed for getting machine fingerprint
     find_library(FRAMEWORK_IOKIT IOKit)
diff --git a/libraries/networking/src/NodeType.h b/libraries/networking/src/NodeType.h
index 2b2cc4e011..8539ce8fb3 100644
--- a/libraries/networking/src/NodeType.h
+++ b/libraries/networking/src/NodeType.h
@@ -4,6 +4,7 @@
 //
 //  Created by Stephen Birarda on 05/29/15.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2021 Vircadia contributors.
 //
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@@ -14,6 +15,10 @@
 
 #pragma once
 
+/// @file
+/// @brief NodeType
+
+/// @brief An 8-bit value identifying the type of a node - domain server, audio mixer, etc.
 typedef quint8 NodeType_t;
 
 namespace NodeType {
@@ -37,7 +42,6 @@ namespace NodeType {
     NodeType_t upstreamType(NodeType_t primaryType);
     NodeType_t downstreamType(NodeType_t primaryType);
 
-
     NodeType_t fromString(QString type);
 }
 
diff --git a/libraries/networking/src/webrtc/WebRTCDataChannels.cpp b/libraries/networking/src/webrtc/WebRTCDataChannels.cpp
new file mode 100644
index 0000000000..321b844eb0
--- /dev/null
+++ b/libraries/networking/src/webrtc/WebRTCDataChannels.cpp
@@ -0,0 +1,475 @@
+//
+//  WebRTCDataChannels.cpp
+//  libraries/networking/src/webrtc
+//
+//  Created by David Rowe on 21 May 2021.
+//  Copyright 2021 Vircadia contributors.
+//
+
+#include "WebRTCDataChannels.h"
+
+#if defined(WEBRTC_DATA_CHANNELS)
+
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include "../NetworkLogging.h"
+
+
+// References:
+// - https://webrtc.github.io/webrtc-org/native-code/native-apis/
+// - https://webrtc.googlesource.com/src/+/master/api/peer_connection_interface.h
+
+const std::string ICE_SERVER_URI = "stun://ice.vircadia.com:7337";
+
+#define WEBRTC_DEBUG
+
+
+void WDCSetSessionDescriptionObserver::OnSuccess() {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCSetSessionDescriptionObserver::OnSuccess()";
+#endif
+}
+
+void WDCSetSessionDescriptionObserver::OnFailure(RTCError error) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCSetSessionDescriptionObserver::OnFailure() :" << error.message();
+#endif
+}
+
+
+WDCCreateSessionDescriptionObserver::WDCCreateSessionDescriptionObserver(WDCConnection* parent) :
+    _parent(parent)
+{ }
+
+void WDCCreateSessionDescriptionObserver::OnSuccess(SessionDescriptionInterface* description) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCCreateSessionDescriptionObserver::OnSuccess()";
+#endif
+    _parent->sendAnswer(description);
+    _parent->setLocalDescription(description);
+}
+
+void WDCCreateSessionDescriptionObserver::OnFailure(RTCError error) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCCreateSessionDescriptionObserver::OnFailure() :" << error.message();
+#endif
+}
+
+
+WDCPeerConnectionObserver::WDCPeerConnectionObserver(WDCConnection* parent) :
+    _parent(parent)
+{ }
+
+void WDCPeerConnectionObserver::OnSignalingChange(PeerConnectionInterface::SignalingState newState) {
+#ifdef WEBRTC_DEBUG
+    QStringList states{
+        "Stable",
+        "HaveLocalOffer",
+        "HaveLocalPrAnswer",
+        "HaveRemoteOffer",
+        "HaveRemotePrAnswer",
+        "Closed"
+    };
+    qCDebug(networking_webrtc) << "WDCPeerConnectionObserver::OnSignalingChange()" << newState << states[newState];
+#endif
+}
+
+void WDCPeerConnectionObserver::OnRenegotiationNeeded() {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCPeerConnectionObserver::OnRenegotiationNeeded()";
+#endif
+}
+
+void WDCPeerConnectionObserver::OnIceGatheringChange(PeerConnectionInterface::IceGatheringState newState) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCPeerConnectionObserver::OnIceGatheringChange()" << newState;
+#endif
+}
+
+void WDCPeerConnectionObserver::OnIceCandidate(const IceCandidateInterface* candidate) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCPeerConnectionObserver::OnIceCandidate()";
+#endif
+    _parent->sendIceCandidate(candidate);
+}
+
+void WDCPeerConnectionObserver::OnDataChannel(rtc::scoped_refptr<DataChannelInterface> dataChannel) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCPeerConnectionObserver::OnDataChannel()";
+#endif
+    _parent->onDataChannelOpened(dataChannel);
+}
+
+void WDCPeerConnectionObserver::OnConnectionChange(PeerConnectionInterface::PeerConnectionState newState) {
+}
+
+
+WDCDataChannelObserver::WDCDataChannelObserver(WDCConnection* parent) :
+    _parent(parent)
+{ }
+
+void WDCDataChannelObserver::OnStateChange() {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCDataChannelObserver::OnStateChange()";
+#endif
+    _parent->onDataChannelStateChanged();
+}
+
+void WDCDataChannelObserver::OnMessage(const DataBuffer& buffer) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCDataChannelObserver::OnMessage()";
+#endif
+    _parent->onDataChannelMessageReceived(buffer);
+}
+
+
+WDCConnection::WDCConnection(quint16 webSocketID, WebRTCDataChannels* parent) :
+    _webSocketID(webSocketID),
+    _parent(parent)
+{
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::WebRTCDataChannels()";
+#endif
+
+    // Create observers.
+    _setSessionDescriptionObserver = new rtc::RefCountedObject<WDCSetSessionDescriptionObserver>();
+    _createSessionDescriptionObserver = new rtc::RefCountedObject<WDCCreateSessionDescriptionObserver>(this);
+    _dataChannelObserver = std::make_shared<WDCDataChannelObserver>(this);
+    _peerConnectionObserver = std::make_shared<WDCPeerConnectionObserver>(this);
+
+    // Create new peer connection.
+    _peerConnection = _parent->createPeerConnection(_peerConnectionObserver);
+};
+
+void WDCConnection::setRemoteDescription(QJsonObject& description) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::setRemoteDescription() :" << description;
+#endif
+
+    SdpParseError sdpParseError;
+    auto sessionDescription = CreateSessionDescription(
+        description.value("type").toString().toStdString(),
+        description.value("sdp").toString().toStdString(),
+        &sdpParseError);
+    if (!sessionDescription) {
+        qCWarning(networking_webrtc) << "Error creating WebRTC remote description:"
+            << QString::fromStdString(sdpParseError.description);
+        return;
+    }
+
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "3. Set remote description:" << sessionDescription;
+#endif
+    _peerConnection->SetRemoteDescription(_setSessionDescriptionObserver, sessionDescription);
+}
+
+void WDCConnection::createAnswer() {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::createAnswer()";
+    qCDebug(networking_webrtc) << "4.a Create answer";
+#endif
+    _peerConnection->CreateAnswer(_createSessionDescriptionObserver, PeerConnectionInterface::RTCOfferAnswerOptions());
+}
+
+void WDCConnection::sendAnswer(SessionDescriptionInterface* description) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::sendAnswer()";
+    qCDebug(networking_webrtc) << "4.b Send answer to the remote peer";
+#endif
+
+    QJsonObject jsonDescription;
+    std::string descriptionString;
+    description->ToString(&descriptionString);
+    jsonDescription.insert("sdp", QString::fromStdString(descriptionString));
+    jsonDescription.insert("type", "answer");
+
+    QJsonObject jsonWebRTCPayload;
+    jsonWebRTCPayload.insert("description", jsonDescription);
+
+    QJsonObject jsonObject;
+    jsonObject.insert("from", QString(_parent->getNodeType()));
+    jsonObject.insert("to", _webSocketID);
+    jsonObject.insert("data", jsonWebRTCPayload);
+
+    _parent->sendSignalingMessage(jsonObject);
+}
+
+void WDCConnection::setLocalDescription(SessionDescriptionInterface* description) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::setLocalDescription()";
+    qCDebug(networking_webrtc) << "5. Set local description";
+#endif
+    _peerConnection->SetLocalDescription(_setSessionDescriptionObserver, description);
+}
+
+void WDCConnection::addIceCandidate(QJsonObject& data) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::addIceCandidate()";
+#endif
+
+    SdpParseError sdpParseError;
+    auto iceCandidate = CreateIceCandidate(
+        data.value("sdpMid").toString().toStdString(),
+        data.value("sdpMLineIndex").toInt(),
+        data.value("candidate").toString().toStdString(),
+        &sdpParseError);
+    if (!iceCandidate) {
+        qCWarning(networking_webrtc) << "Error adding WebRTC ICE candidate:"
+            << QString::fromStdString(sdpParseError.description);
+        return;
+    }
+
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "6. Add ICE candidate";
+#endif
+    _peerConnection->AddIceCandidate(iceCandidate);
+}
+
+void WDCConnection::sendIceCandidate(const IceCandidateInterface* candidate) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::sendIceCandidate()";
+#endif
+
+    std::string candidateString;
+    candidate->ToString(&candidateString);
+    QJsonObject jsonCandidate;
+    jsonCandidate.insert("candidate", QString::fromStdString(candidateString));
+    jsonCandidate.insert("sdpMid", QString::fromStdString(candidate->sdp_mid()));
+    jsonCandidate.insert("sdpMLineIndex", candidate->sdp_mline_index());
+
+    QJsonObject jsonWebRTCData;
+    jsonWebRTCData.insert("candidate", jsonCandidate);
+
+    QJsonObject jsonObject;
+    jsonObject.insert("from", QString(_parent->getNodeType()));
+    jsonObject.insert("to", _webSocketID);
+    jsonObject.insert("data", jsonWebRTCData);
+    QJsonDocument jsonDocument = QJsonDocument(jsonObject);
+
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "7. Send ICE candidate to the remote peer";
+#endif
+    _parent->sendSignalingMessage(jsonObject);
+}
+
+void WDCConnection::onDataChannelOpened(rtc::scoped_refptr<DataChannelInterface> dataChannel) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::onDataChannelOpened() :"
+        << dataChannel->id()
+        << QString::fromStdString(dataChannel->label())
+        << QString::fromStdString(dataChannel->protocol())
+        << dataChannel->negotiated()
+        << dataChannel->maxRetransmitTime()
+        << dataChannel->maxRetransmits()
+        << dataChannel->maxPacketLifeTime().value_or(-1)
+        << dataChannel->maxRetransmitsOpt().value_or(-1);
+#endif
+
+    _dataChannel = dataChannel;
+    _dataChannelID = dataChannel->id();
+    _dataChannel->RegisterObserver(_dataChannelObserver.get());
+
+    _parent->onDataChannelOpened(this, _dataChannelID);
+}
+
+void WDCConnection::onDataChannelStateChanged() {
+    auto state = _dataChannel->state();
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::dataChannelStateChanged() :" << (int)state
+        << DataChannelInterface::DataStateString(state);
+#endif
+    if (state == DataChannelInterface::kClosed) {
+        _parent->onDataChannelClosed(this, _dataChannelID);
+    }
+}
+
+void WDCConnection::onDataChannelMessageReceived(const DataBuffer& buffer) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::onDataChannelMessageReceived()";
+#endif
+
+    auto byteArray = QByteArray(buffer.data.data<char>(), (int)buffer.data.size());
+
+    // Echo message back to sender.
+    if (byteArray.startsWith("echo:")) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "Echo message back";
+#endif
+        _parent->sendDataMessage(_dataChannelID, byteArray);  // Use parent method to exercise the code stack.
+        return;
+    }
+
+    _parent->emitDataMessage(_dataChannelID, byteArray);
+}
+
+bool WDCConnection::sendDataMessage(const DataBuffer& buffer) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WDCConnection::sendDataMessage()";
+#endif
+    const int MAX_WEBRTC_BUFFER_SIZE = 16 * 1024 * 1024;  // 16MB
+    if (_dataChannel->buffered_amount() + buffer.size() > MAX_WEBRTC_BUFFER_SIZE) {
+        // Don't send, otherwise the data channel will be closed.
+        return false;
+    }
+    return _dataChannel->Send(buffer);
+}
+
+
+WebRTCDataChannels::WebRTCDataChannels(NodeType_t nodeType, QObject* parent) :
+    _nodeType(nodeType),
+    _parent(parent)
+{
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::WebRTCDataChannels()";
+#endif
+
+    // Create a peer connection factory.
+#ifdef WEBRTC_DEBUG
+    // Numbers are per WebRTC's peer_connection_interface.h.
+    qCDebug(networking_webrtc) << "1. Create a new PeerConnectionFactoryInterface";
+#endif
+    _rtcNetworkThread = rtc::Thread::CreateWithSocketServer();
+    _rtcNetworkThread->Start();
+    _rtcWorkerThread = rtc::Thread::Create();
+    _rtcWorkerThread->Start();
+    _rtcSignalingThread = rtc::Thread::Create();
+    _rtcSignalingThread->Start();
+    PeerConnectionFactoryDependencies dependencies;
+    dependencies.network_thread = _rtcNetworkThread.get();
+    dependencies.worker_thread = _rtcWorkerThread.get();
+    dependencies.signaling_thread = _rtcSignalingThread.get();
+    _peerConnectionFactory = CreateModularPeerConnectionFactory(std::move(dependencies));
+    if (!_peerConnectionFactory) {
+        qCWarning(networking_webrtc) << "Failed to create WebRTC peer connection factory";
+    }
+}
+
+WebRTCDataChannels::~WebRTCDataChannels() {
+    QHashIterator<int, WDCConnection*> i(_connectionsByDataChannel);
+    while (i.hasNext()) {
+        i.next();
+        delete i.value();
+    }
+    _connectionsByWebSocket.clear();
+    _connectionsByDataChannel.clear();
+
+    _peerConnectionFactory = nullptr;
+    _rtcSignalingThread->Stop();
+    _rtcSignalingThread = nullptr;
+    _rtcWorkerThread->Stop();
+    _rtcWorkerThread = nullptr;
+    _rtcNetworkThread->Stop();
+    _rtcNetworkThread = nullptr;
+}
+
+void WebRTCDataChannels::onDataChannelOpened(WDCConnection* connection, int dataChannelID) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::onDataChannelOpened() :" << dataChannelID;
+#endif
+    _connectionsByDataChannel.insert(dataChannelID, connection);
+}
+
+void WebRTCDataChannels::onDataChannelClosed(WDCConnection* connection, int dataChannelID) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::onDataChannelClosed() :" << dataChannelID;
+#endif
+
+    // Delete WDCConnection.
+    _connectionsByWebSocket.remove(connection->getWebSocketID());
+    _connectionsByDataChannel.remove(dataChannelID);
+    delete connection;
+}
+
+void WebRTCDataChannels::onSignalingMessage(const QJsonObject& message) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannel::onSignalingMessage()" << message;
+#endif
+
+    // Validate message.
+    const int MAX_DEBUG_DETAIL_LENGTH = 64;
+    auto data = message.value("data").isObject() ? message.value("data").toObject() : QJsonObject();
+    int from = message.value("from").isDouble() ? (quint16)(message.value("from").toInt()) : 0;
+    if (from <= 0 || from > MAXUINT16 || !data.contains("description") && !data.contains("candidate")) {
+        qCWarning(networking_webrtc) << "Unexpected signaling message:"
+            << QJsonDocument(message).toJson(QJsonDocument::Compact).left(MAX_DEBUG_DETAIL_LENGTH);
+        return;
+    }
+
+    // Find or create a connection.
+    WDCConnection* connection;
+    if (_connectionsByWebSocket.contains(from)) {
+        connection = _connectionsByWebSocket.value(from);
+    } else {
+        connection = new WDCConnection(from, this);
+        _connectionsByWebSocket.insert(from, connection);
+    }
+
+    // Set the remote description and reply with an answer.
+    if (data.contains("description")) {
+        auto description = data.value("description").toObject();
+        if (description.value("type").toString() == "offer") {
+            connection->setRemoteDescription(description);
+            connection->createAnswer();
+        } else {
+            qCWarning(networking_webrtc) << "Unexpected signaling description:"
+                << QJsonDocument(description).toJson(QJsonDocument::Compact).left(MAX_DEBUG_DETAIL_LENGTH);
+        }
+    }
+
+    // Add a remote ICE candidate.
+    if (data.contains("candidate")) {
+        connection->addIceCandidate(data);
+    }
+
+}
+
+void WebRTCDataChannels::sendSignalingMessage(const QJsonObject& message) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::sendSignalingMessage() :" << QJsonDocument(message).toJson(QJsonDocument::Compact);
+#endif
+    emit signalingMessage(message);
+}
+
+void WebRTCDataChannels::emitDataMessage(int dataChannelID, const QByteArray& byteArray) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::emitDataMessage() :" << dataChannelID;
+#endif
+    emit dataMessage(dataChannelID, byteArray);
+}
+
+bool WebRTCDataChannels::sendDataMessage(int dataChannelID, const QByteArray& byteArray) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::sendDataMessage() :" << dataChannelID;
+#endif
+
+    // Find connection.
+    if (!_connectionsByDataChannel.contains(dataChannelID)) {
+        qCWarning(networking_webrtc) << "Could not find data channel to send message on!";
+        return false;
+    }
+
+    auto connection = _connectionsByDataChannel.value(dataChannelID);
+    DataBuffer buffer(byteArray.toStdString(), true);
+    return connection->sendDataMessage(buffer);
+}
+
+rtc::scoped_refptr<PeerConnectionInterface> WebRTCDataChannels::createPeerConnection(
+        const std::shared_ptr<WDCPeerConnectionObserver> peerConnectionObserver) {
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "WebRTCDataChannels::createPeerConnection()";
+#endif
+
+    PeerConnectionInterface::RTCConfiguration configuration;
+    PeerConnectionInterface::IceServer iceServer;
+    iceServer.uri = ICE_SERVER_URI;
+    configuration.servers.push_back(iceServer);
+
+#ifdef WEBRTC_DEBUG
+    qCDebug(networking_webrtc) << "2. Create a new PeerConnection";
+#endif
+    return _peerConnectionFactory->CreatePeerConnection(configuration, nullptr, nullptr, peerConnectionObserver.get());
+}
+
+
+#endif // WEBRTC_DATA_CHANNELS
diff --git a/libraries/networking/src/webrtc/WebRTCDataChannels.h b/libraries/networking/src/webrtc/WebRTCDataChannels.h
new file mode 100644
index 0000000000..9ea75bc5ff
--- /dev/null
+++ b/libraries/networking/src/webrtc/WebRTCDataChannels.h
@@ -0,0 +1,278 @@
+//
+//  WebRTCDataChannels.h
+//  libraries/networking/src/webrtc
+//
+//  Created by David Rowe on 21 May 2021.
+//  Copyright 2021 Vircadia contributors.
+//
+
+#ifndef vircadia_WebRTCDataChannels_h
+#define vircadia_WebRTCDataChannels_h
+
+#include <shared/WebRTC.h>
+
+#if defined(WEBRTC_DATA_CHANNELS)
+
+
+#include <QObject>
+#include <QHash>
+
+#undef emit  // Avoid conflict between Qt signals/slots and the WebRTC library's.
+#include <api/peer_connection_interface.h>
+#define emit
+
+#include "../NodeType.h"
+
+using namespace webrtc;
+
+class WebRTCDataChannels;
+class WDCConnection;
+
+
+// A WebRTC data channel session description observer.
+class WDCSetSessionDescriptionObserver : public SetSessionDescriptionObserver {
+public:
+    // The call to SetLocalDescription or SetRemoteDescription succeeded.
+    void OnSuccess() override;
+
+    // The call to SetLocalDescription or SetRemoteDescription failed.
+    void OnFailure(RTCError error) override;
+};
+
+
+// A WebRTC data channel create session description observer.
+class WDCCreateSessionDescriptionObserver : public CreateSessionDescriptionObserver {
+public:
+    WDCCreateSessionDescriptionObserver(WDCConnection* parent);
+
+    // The call to CreateAnswer succeeded.
+    void OnSuccess(SessionDescriptionInterface* desc) override;
+
+    // The call to CreateAnswer failed.
+    void OnFailure(RTCError error) override;
+
+private:
+    WDCConnection* _parent;
+};
+
+
+// A WebRTC data channel peer connection observer.
+class WDCPeerConnectionObserver : public PeerConnectionObserver {
+public:
+    WDCPeerConnectionObserver(WDCConnection* parent);
+
+    // Triggered when the SignalingState changed.
+    void OnSignalingChange(PeerConnectionInterface::SignalingState newState) override;
+
+    // Triggered when renegotiation is needed. For example, an ICE restart has begun.
+    void OnRenegotiationNeeded() override;
+
+    // Called any time the IceGatheringState changes.
+    void OnIceGatheringChange(PeerConnectionInterface::IceGatheringState newState) override;
+
+    // A new ICE candidate has been gathered.
+    void OnIceCandidate(const IceCandidateInterface* candidate) override;
+
+    // Triggered when a remote peer opens a data channel.
+    void OnDataChannel(rtc::scoped_refptr<DataChannelInterface> dataChannel) override;
+
+    // Called any time the PeerConnectionState changes.
+    void OnConnectionChange(PeerConnectionInterface::PeerConnectionState newState) override;
+
+private:
+    WDCConnection* _parent;
+};
+
+
+// A WebRTC data channel observer.
+class WDCDataChannelObserver : public DataChannelObserver {
+public:
+    WDCDataChannelObserver(WDCConnection* parent);
+
+    // The data channel state changed.
+    void OnStateChange() override;
+
+    //  A data buffer was successfully received.
+    void OnMessage(const DataBuffer& buffer) override;
+
+private:
+    WDCConnection* _parent;
+};
+
+
+/// @brief A WebRTC data channel connection.
+/// @details Opens and manages a WebRTC data channel connection.
+class WDCConnection {
+
+public:
+    /// @brief Constructs a new WDCConnection and opens a WebRTC data connection.
+    /// @param webSocketID The signaling channel that initiated the opening of the WebRTC data channel.
+    /// @param parent The parent WebRTCDataChannels object.
+    WDCConnection(quint16 webSocketID, WebRTCDataChannels* parent);
+
+    /// @brief Gets the WebSocket ID.
+    /// @return The ID of the WebSocket.
+    quint16 getWebSocketID() { return _webSocketID; }
+
+    /// @brief Gets the WebRTC data channel ID.
+    /// @return The WebRTC data channel ID. `-1` if not open yet.
+    int getDataChannelID() { return _dataChannelID; }
+
+
+    /// @brief Sets the remote session description received from the remote client via the signaling channel.
+    /// @param description The remote session description.
+    void setRemoteDescription(QJsonObject& description);
+
+    /// @brief Creates an answer to an offer received from the remote client via the signaling channel.
+    void createAnswer();
+
+    /// @brief Sends an answer to the remote client via the signaling channel.
+    /// @param description The answer.
+    void sendAnswer(SessionDescriptionInterface* description);
+    
+    /// @brief Sets the local session description on the WebRTC data channel being connected.
+    /// @param description The local session description.
+    void setLocalDescription(SessionDescriptionInterface* description);
+    
+    /// @brief Adds an ICE candidate received from the remote client via the signaling channel.
+    /// @param data The ICE candidate.
+    void addIceCandidate(QJsonObject& data);
+
+    /// @brief Sends an ICE candidate to the remote vlient via the signaling channel.
+    /// @param candidate The ICE candidate.
+    void sendIceCandidate(const IceCandidateInterface* candidate);
+
+    /// @brief Handles the WebRTC data channel being opened.
+    /// @param dataChannel The WebRTC data channel.
+    void onDataChannelOpened(rtc::scoped_refptr<DataChannelInterface> dataChannel);
+
+    /// @brief Handles a change in the state of the WebRTC data channel.
+    void onDataChannelStateChanged();
+
+
+    /// @brief Handles a message being received on the WebRTC data channel.
+    /// @param buffer The message received.
+    void onDataChannelMessageReceived(const DataBuffer& buffer);
+
+    /// @brief Sends a message on the WebRTC data channel.
+    /// @param buffer The message to send.
+    /// @return `true` if the message was sent, otherwise `false`.
+    bool sendDataMessage(const DataBuffer& buffer);
+    
+private:
+    WebRTCDataChannels* _parent;
+    quint16 _webSocketID { 0 };
+    int _dataChannelID { -1 };
+
+    rtc::scoped_refptr<WDCSetSessionDescriptionObserver> _setSessionDescriptionObserver { nullptr };
+    rtc::scoped_refptr<WDCCreateSessionDescriptionObserver> _createSessionDescriptionObserver { nullptr };
+
+    std::shared_ptr<WDCDataChannelObserver> _dataChannelObserver { nullptr };
+    rtc::scoped_refptr<DataChannelInterface> _dataChannel { nullptr };
+
+    std::shared_ptr<WDCPeerConnectionObserver> _peerConnectionObserver { nullptr };
+    rtc::scoped_refptr<PeerConnectionInterface> _peerConnection { nullptr };
+};
+
+
+/// @brief Manages WebRTC data channels on the domain server or an assignment clients that Interface clients can connect to.
+///
+/// @details Presents multiple individual WebRTC data channels as a single one-to-many WebRTCDataChannels object. Interface
+/// clients may use WebRTC data channels for Vircadia protocol network communications instead of UDP.
+/// A WebRTCSignalingServer is used in the process of setting up a WebRTC data channel between an Interface client and the
+/// domain server or assignment client.
+/// The Interface client initiates the connection - including initiating the data channel - and the domain server or assignment
+/// client responds.
+///
+/// Additionally, for debugging purposes, instead of containing a Vircadia protocol payload, a WebRTC message may be an echo
+/// request. This is bounced back to the client.
+/// 
+class WebRTCDataChannels : public QObject {
+    Q_OBJECT
+
+public:
+
+    /// @brief Constructs a new WebRTCDataChannels object.
+    /// @paramm nodeType The type of node that the WebRTCDataChannels object is being used in.
+    /// @param parent The parent Qt object.
+    WebRTCDataChannels(NodeType_t nodeType, QObject* parent);
+
+    /// @brief Destroys a WebRTCDataChannels object.
+    ~WebRTCDataChannels();
+
+    /// @brief Returns the type of node that the WebRTCDataChannels object is being used in.
+    /// @return The type of node.
+    NodeType_t getNodeType() {
+        return _nodeType;
+    }
+
+    /// @brief Handles a WebRTC data channel opening.
+    /// @param connection The WebRTC data channel connection.
+    /// @param dataChannelID The WebRTC data channel ID.
+    void onDataChannelOpened(WDCConnection* connection, int dataChannelID);
+
+    /// @brief Handles a WebRTC data channel closing.
+    /// @param connection The WebRTC data channel connection.
+    /// @param dataChannelID The WebRTC data channel ID.
+    void onDataChannelClosed(WDCConnection* connection, int dataChannelID);
+
+    /// @brief Emits a signalingMessage received for the Interface client.
+    /// @param message The WebRTC signaling message to send.
+    void sendSignalingMessage(const QJsonObject& message);
+
+    /// @brief Emits a dataMessage received from the Interface client.
+    /// @param dataChannelID The WebRTC data channel the message was received on.
+    /// @param byteArray The data message received.
+    void emitDataMessage(int dataChannelID, const QByteArray& byteArray);
+
+    /// @brief Sends a data message to an Interface client.
+    /// @param dataChannelID The WebRTC channel ID of the Interface client.
+    /// @param message The data message to send.
+    /// @return `true` if the data message was sent, otherwise `false`.
+    bool sendDataMessage(int dataChannelID, const QByteArray& message);
+
+    /// @brief Creates a new WebRTC peer connection for connecting to an Interface client.
+    /// @param peerConnectionObserver An observer to monitor the WebRTC peer connection.
+    /// @return The new WebRTC peer connection.
+    rtc::scoped_refptr<PeerConnectionInterface> createPeerConnection(
+        const std::shared_ptr<WDCPeerConnectionObserver> peerConnectionObserver);
+
+public slots:
+
+    /// @brief Handles a WebRTC signaling message received from the Interface client.
+    /// @param message The WebRTC signaling message.
+    void onSignalingMessage(const QJsonObject& message);
+
+signals:
+
+    /// @brief A WebRTC signaling message to be sent to the Interface client.
+    /// @description This message is for the WebRTCSignalingServer to send.
+    /// @param message The WebRTC signaling message to send.
+    void signalingMessage(const QJsonObject& message);
+
+    /// @brief A WebRTC data message received from the Interface client.
+    /// @description This message is for handling at a higher level in the Vircadia protocol.
+    /// @param dataChannelID The WebRTC data channel ID.
+    /// @param byteArray The Vircadia protocol message.
+    void dataMessage(int dataChannelID, const QByteArray& byteArray);
+
+private:
+
+    QObject* _parent;
+
+    NodeType_t _nodeType;
+
+    std::unique_ptr<rtc::Thread> _rtcNetworkThread { nullptr };
+    std::unique_ptr<rtc::Thread> _rtcWorkerThread { nullptr };
+    std::unique_ptr<rtc::Thread> _rtcSignalingThread { nullptr };
+
+    rtc::scoped_refptr<PeerConnectionFactoryInterface> _peerConnectionFactory { nullptr };
+
+    QHash<quint16, WDCConnection*> _connectionsByWebSocket;
+    QHash<int, WDCConnection*> _connectionsByDataChannel;
+};
+
+
+#endif // WEBRTC_DATA_CHANNELS
+
+#endif // vircadia_WebRTCDataChannels_h
diff --git a/libraries/networking/src/webrtc/WebRTCSignalingServer.cpp b/libraries/networking/src/webrtc/WebRTCSignalingServer.cpp
index 66b1d6616c..8c661bbf8c 100644
--- a/libraries/networking/src/webrtc/WebRTCSignalingServer.cpp
+++ b/libraries/networking/src/webrtc/WebRTCSignalingServer.cpp
@@ -8,7 +8,7 @@
 
 #include "WebRTCSignalingServer.h"
 
-#if defined(WEBRTC_DATA_CHANNEL)
+#if defined(WEBRTC_DATA_CHANNELS)
 
 #include <QtCore>
 #include <QWebSocket>
@@ -93,4 +93,4 @@ void WebRTCSignalingServer::newWebSocketConnection() {
     _webSockets.insert(webSocket->peerPort(), webSocket);
 }
 
-#endif // WEBRTC_DATA_CHANNEL
+#endif // WEBRTC_DATA_CHANNELS
diff --git a/libraries/networking/src/webrtc/WebRTCSignalingServer.h b/libraries/networking/src/webrtc/WebRTCSignalingServer.h
index 41d79dbd57..9f4214d3c7 100644
--- a/libraries/networking/src/webrtc/WebRTCSignalingServer.h
+++ b/libraries/networking/src/webrtc/WebRTCSignalingServer.h
@@ -2,18 +2,16 @@
 //  WebRTCSignalingServer.h
 //  libraries/networking/src/webrtc
 //
-//  Provides a signaling channel for setting up WebRTC connections between the Web app and the domain servers and mixers.
-//
 //  Created by David Rowe on 16 May 2021.
 //  Copyright 2021 Vircadia contributors.
 //
 
-#ifndef vircadia_SignalingServer_h
-#define vircadia_SignalingServer_h
+#ifndef vircadia_WebRTCSignalingServer_h
+#define vircadia_WebRTCSignalingServer_h
 
 #include <shared/WebRTC.h>
 
-#if defined(WEBRTC_DATA_CHANNEL)
+#if defined(WEBRTC_DATA_CHANNELS)
 
 #include <QObject>
 #include <QtCore/QTimer>
@@ -99,6 +97,6 @@ private:
 };
 
 
-#endif // WEBRTC_DATA_CHANNEL
+#endif // WEBRTC_DATA_CHANNELS
 
-#endif // vircadia_SignalingServer_h
+#endif // vircadia_WebRTCSignalingServer_h
diff --git a/libraries/shared/src/shared/WebRTC.h b/libraries/shared/src/shared/WebRTC.h
index f2b64d7a2a..888877eadb 100644
--- a/libraries/shared/src/shared/WebRTC.h
+++ b/libraries/shared/src/shared/WebRTC.h
@@ -17,7 +17,7 @@
 #endif
 
 // WEBRTC_AUDIO: WebRTC audio features, e.g., echo canceling.
-// WEBRTC_DATA_CHANNEL: WebRTC client-server connections in parallel with UDP.
+// WEBRTC_DATA_CHANNELS: WebRTC client-server connections in parallel with UDP.
 
 #if defined(Q_OS_MAC)
 #  define WEBRTC_AUDIO 1
@@ -25,7 +25,7 @@
 #  define WEBRTC_LEGACY 1
 #elif defined(Q_OS_WIN)
 #  define WEBRTC_AUDIO 1
-#  define WEBRTC_DATA_CHANNEL 1
+#  define WEBRTC_DATA_CHANNELS 1
 #  define WEBRTC_WIN 1
 #  define NOMINMAX 1
 #  define WIN32_LEAN_AND_MEAN 1