From 6afb8044ea02bbee35bcc74811ae8eebec354d25 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 20 May 2021 09:51:14 +1200 Subject: [PATCH] Add WebRTC signaling channel --- domain-server/src/DomainServer.cpp | 6 +- domain-server/src/DomainServer.h | 6 + libraries/networking/CMakeLists.txt | 6 +- libraries/networking/src/DomainHandler.h | 10 +- libraries/networking/src/NetworkLogging.cpp | 1 + libraries/networking/src/NetworkLogging.h | 1 + libraries/networking/src/NodeList.cpp | 1 - .../src/webrtc/WebRTCSignalingServer.cpp | 96 ++++++++++++++++ .../src/webrtc/WebRTCSignalingServer.h | 104 ++++++++++++++++++ 9 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 libraries/networking/src/webrtc/WebRTCSignalingServer.cpp create mode 100644 libraries/networking/src/webrtc/WebRTCSignalingServer.h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 3acd17f6af..02fd810b0e 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -165,7 +165,11 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, DomainServer::DomainServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _gatekeeper(this), - _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this) +#ifdef WEBRTC_DATA_CHANNEL + _webrtcSignalingServer(QHostAddress::AnyIPv4, DEFAULT_DOMAIN_SERVER_WS_PORT, this), +#endif + _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, + QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this) { if (_parentPID != -1) { watchParentProcess(_parentPID); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 11d04cb255..29142505a8 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include "AssetsBackupHandler.h" #include "DomainGatekeeper.h" @@ -311,6 +313,10 @@ private: std::unordered_map> _pendingContentFiles; QThread _assetClientThread; + +#ifdef WEBRTC_DATA_CHANNEL + WebRTCSignalingServer _webrtcSignalingServer; +#endif }; diff --git a/libraries/networking/CMakeLists.txt b/libraries/networking/CMakeLists.txt index 9f63f2cb00..50382cda99 100644 --- a/libraries/networking/CMakeLists.txt +++ b/libraries/networking/CMakeLists.txt @@ -1,9 +1,13 @@ set(TARGET_NAME networking) -setup_hifi_library(Network) +setup_hifi_library(Network WebSockets) link_hifi_libraries(shared platform) target_openssl() target_tbb() +if (WIN32) + # WEBRTC TODO: Add UNIX. + target_webrtc() +endif () if (WIN32) # we need ws2_32.lib on windows, but it's static so we don't bubble it up diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index a56d3162bd..a8c316572a 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -40,7 +40,15 @@ const unsigned short DEFAULT_DOMAIN_SERVER_PORT = ? QProcessEnvironment::systemEnvironment() .value("HIFI_DOMAIN_SERVER_PORT") .toUShort() - : 40102; + : 40102; // UDP + +const unsigned short DEFAULT_DOMAIN_SERVER_WS_PORT = + QProcessEnvironment::systemEnvironment() + .contains("HIFI_DOMAIN_SERVER_WS_PORT") + ? QProcessEnvironment::systemEnvironment() + .value("HIFI_DOMAIN_SERVER_WS_PORT") + .toUShort() + : 40102; // TCP const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = QProcessEnvironment::systemEnvironment() diff --git a/libraries/networking/src/NetworkLogging.cpp b/libraries/networking/src/NetworkLogging.cpp index 3d7c2fc5d5..daa8c8c416 100644 --- a/libraries/networking/src/NetworkLogging.cpp +++ b/libraries/networking/src/NetworkLogging.cpp @@ -16,3 +16,4 @@ Q_LOGGING_CATEGORY(networking_ice, "hifi.networking.ice") Q_LOGGING_CATEGORY(resourceLog, "hifi.networking.resource") Q_LOGGING_CATEGORY(asset_client, "hifi.networking.asset_client") Q_LOGGING_CATEGORY(messages_client, "hifi.networking.messages_client") +Q_LOGGING_CATEGORY(networking_webrtc, "hifi.networking.webrtc") diff --git a/libraries/networking/src/NetworkLogging.h b/libraries/networking/src/NetworkLogging.h index 8247c60096..906947b7c4 100644 --- a/libraries/networking/src/NetworkLogging.h +++ b/libraries/networking/src/NetworkLogging.h @@ -19,5 +19,6 @@ Q_DECLARE_LOGGING_CATEGORY(networking) Q_DECLARE_LOGGING_CATEGORY(networking_ice) Q_DECLARE_LOGGING_CATEGORY(asset_client) Q_DECLARE_LOGGING_CATEGORY(messages_client) +Q_DECLARE_LOGGING_CATEGORY(networking_webrtc) #endif // hifi_NetworkLogging_h diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index a975302699..31534f34f4 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -409,7 +409,6 @@ void NodeList::sendDomainServerCheckIn() { if (domainPacketType == PacketType::DomainConnectRequest) { #if (PR_BUILD || DEV_BUILD) - // ####### if (_shouldSendNewerVersion) { domainPacket->setVersion(versionForPacketType(domainPacketType) + 1); } diff --git a/libraries/networking/src/webrtc/WebRTCSignalingServer.cpp b/libraries/networking/src/webrtc/WebRTCSignalingServer.cpp new file mode 100644 index 0000000000..66b1d6616c --- /dev/null +++ b/libraries/networking/src/webrtc/WebRTCSignalingServer.cpp @@ -0,0 +1,96 @@ +// +// WebRTCSignalingServer.cpp +// libraries/networking/src/webrtc +// +// Created by David Rowe on 16 May 2021. +// Copyright 2021 Vircadia contributors. +// + +#include "WebRTCSignalingServer.h" + +#if defined(WEBRTC_DATA_CHANNEL) + +#include +#include + +#include "../NetworkLogging.h" +#include "../NodeType.h" + + +const int WEBRTC_SOCKET_CHECK_INTERVAL_IN_MS = 30000; + +WebRTCSignalingServer::WebRTCSignalingServer(const QHostAddress& address, quint16 port, QObject* parent) : + QObject(parent), + _address(address), + _port(port), + _webSocketServer(new QWebSocketServer(QStringLiteral("WebRTC Signaling Server"), QWebSocketServer::NonSecureMode, this)) +{ + connect(_webSocketServer, &QWebSocketServer::newConnection, this, &WebRTCSignalingServer::newWebSocketConnection); + + bindSocket(); + + // Automatically recover from network interruptions. + _isWebSocketServerListeningTimer = new QTimer(this); + connect(_isWebSocketServerListeningTimer, &QTimer::timeout, this, &WebRTCSignalingServer::checkWebSocketServerIsListening); + _isWebSocketServerListeningTimer->start(WEBRTC_SOCKET_CHECK_INTERVAL_IN_MS); +} + +void WebRTCSignalingServer::checkWebSocketServerIsListening() { + if (!_webSocketServer->isListening()) { + qCWarning(networking_webrtc) << "WebSocket on port " << QString::number(_port) << " is no longer listening"; + _webSockets.clear(); + bindSocket(); + } +} + +void WebRTCSignalingServer::bindSocket() { + if (!_webSocketServer->listen(_address, _port)) { + qCWarning(networking_webrtc) << "Failed to open WebSocket for WebRTC signaling."; + } +} + +void WebRTCSignalingServer::webSocketTextMessageReceived(const QString& message) { + auto source = qobject_cast(sender()); + if (source) { + QJsonObject json = QJsonDocument::fromJson(message.toUtf8()).object(); + // WEBRTC TODO: Move domain server echoing into domain server. + if (json.keys().contains("echo") && json.value("to").toString() == QString(QChar(NodeType::DomainServer))) { + // Domain server echo request - echo message back to sender. + json.remove("to"); + json.insert("from", QString(QChar(NodeType::DomainServer))); + QString echo = QJsonDocument(json).toJson(); + source->sendTextMessage(echo); + } else { + // WebRTC message or assignment client echo request. (Send both to target.) + json.insert("from", source->peerPort()); + emit messageReceived(json); + } + } else { + qCWarning(networking_webrtc) << "Failed to find WebSocket for incoming WebRTC signaling message."; + } +} + +void WebRTCSignalingServer::sendMessage(const QJsonObject& message) { + quint16 destinationPort = message.value("to").toInt(); + if (_webSockets.contains(destinationPort)) { + _webSockets.value(destinationPort)->sendTextMessage(QString(QJsonDocument(message).toJson())); + } else { + qCWarning(networking_webrtc) << "Failed to find WebSocket for outgoing WebRTC signaling message."; + } +} + +void WebRTCSignalingServer::webSocketDisconnected() { + auto source = qobject_cast(sender()); + if (source) { + _webSockets.remove(source->peerPort()); + } +} + +void WebRTCSignalingServer::newWebSocketConnection() { + auto webSocket = _webSocketServer->nextPendingConnection(); + connect(webSocket, &QWebSocket::textMessageReceived, this, &WebRTCSignalingServer::webSocketTextMessageReceived); + connect(webSocket, &QWebSocket::disconnected, this, &WebRTCSignalingServer::webSocketDisconnected); + _webSockets.insert(webSocket->peerPort(), webSocket); +} + +#endif // WEBRTC_DATA_CHANNEL diff --git a/libraries/networking/src/webrtc/WebRTCSignalingServer.h b/libraries/networking/src/webrtc/WebRTCSignalingServer.h new file mode 100644 index 0000000000..41d79dbd57 --- /dev/null +++ b/libraries/networking/src/webrtc/WebRTCSignalingServer.h @@ -0,0 +1,104 @@ +// +// 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 + +#include + +#if defined(WEBRTC_DATA_CHANNEL) + +#include +#include +#include + +#include "../HifiSockAddr.h" + +/// @brief WebRTC signaling server that Interface clients can use to initiate WebRTC connections to the domain server and +/// assignment clients. +/// +/// @details The signaling server is expected to be hosted in the domain server. It provides a WebSocket for Interface clients +/// to use in the WebRTC signaling handshake process to establish WebRTC data channel connections to each of the domain server +/// and the assignment clients (i.e., separate WebRTC data channels for each but only a single signaling WebSocket). The +/// assignment client signaling messages are expected to be relayed - by the domain server - via Vircadia protocol messages on +/// the UDP connections between the domain server and assignment clients. +/// +/// Additionally, for debugging purposes, instead of containing a WebRTC payload a signaling message may be an echo request. +/// This is bounced back to the client from the WebRTCSignalingServer if the domain server was the target, otherwise it is +/// expected to be bounced back upon receipt by the relevant assignment client. +/// +/// The signaling messages are sent and received as JSON objects, with `to` and `from` fields in addition to either the WebRTC +/// signaling `data` payload or an `echo` request: +/// +/// | Interface -> Server || +/// | -------- | -----------------------| +/// | `to` | NodeType | +/// | `from` | WebSocket port number* | +/// | [`data`] | WebRTC payload | +/// | [`echo`] | Echo request | +/// * The `from` field is filled in by the WebRTCSignalingServer. +/// +/// | Server -> Interface || +/// | -------- | --------------------- | +/// | `to` | WebSocket port number | +/// | `from` | NodeType | +/// | [`data`] | WebRTC payload | +/// | [`echo`] | Echo request | +/// +class WebRTCSignalingServer : public QObject { + Q_OBJECT + +public: + + /// @brief Constructs a new WebRTCSignalingServer. + /// @param address The IP address to use for the WebSocket. + /// @param port The port to use for the WebSocket. + /// @param parent Qt parent object. + WebRTCSignalingServer(const QHostAddress& address, quint16 port, QObject* parent = nullptr); + +public slots: + + /// @brief Send a WebRTC signaling channel message to an Interface client. + /// @param message The message to send to the Interface client. Includes details of the sender and the destination in + /// addition to the WebRTC signaling channel payload. + void sendMessage(const QJsonObject& message); + +signals: + + /// @brief A WebRTC signaling channel message was received from an Interface client. + /// @param message The message received from the Interface client. Includes details of the sender and the destination in + /// addition to the WebRTC signaling channel payload.\n + /// Not emitted if the message was an echo request for the domain server. + void messageReceived(const QJsonObject& message); + +private slots: + + void newWebSocketConnection(); + void webSocketTextMessageReceived(const QString& message); + void webSocketDisconnected(); + +private: + + void checkWebSocketServerIsListening(); + void bindSocket(); + + QWebSocketServer* _webSocketServer; + QHostAddress _address; + const quint16 _port; + + QHash _webSockets; // client WebSocket port, client WebSocket object + + QTimer* _isWebSocketServerListeningTimer; +}; + + +#endif // WEBRTC_DATA_CHANNEL + +#endif // vircadia_SignalingServer_h