This commit is contained in:
parent
ddef462307
commit
dc5b750056
@ -10,6 +10,7 @@ set(Libraries_ROOT /opt/Libraries)
|
|||||||
set(Wt_DIR ${Libraries_ROOT}/wt-4.11.1/lib/cmake/wt)
|
set(Wt_DIR ${Libraries_ROOT}/wt-4.11.1/lib/cmake/wt)
|
||||||
set(MbedTLS_DIR ${Libraries_ROOT}/mbedtls-3.6.2/lib/cmake/MbedTLS)
|
set(MbedTLS_DIR ${Libraries_ROOT}/mbedtls-3.6.2/lib/cmake/MbedTLS)
|
||||||
set(nng_DIR ${Libraries_ROOT}/nng-1.9.0/lib/cmake/nng)
|
set(nng_DIR ${Libraries_ROOT}/nng-1.9.0/lib/cmake/nng)
|
||||||
|
set(LibDataChannel_DIR ${Libraries_ROOT}/libdatachannel-0.22.3/lib/cmake/LibDataChannel)
|
||||||
|
|
||||||
set(OPENSSL_LIBRARIES ssl crypto)
|
set(OPENSSL_LIBRARIES ssl crypto)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include "ServiceManager.h"
|
#include "ServiceManager.h"
|
||||||
#include "SystemUsage.h"
|
#include "SystemUsage.h"
|
||||||
#include "WeChatContext/CorporationContext.h"
|
#include "WeChatContext/CorporationContext.h"
|
||||||
|
#include "WebRTC/SignalServer.h"
|
||||||
#include <Wt/Dbo/Json.h>
|
#include <Wt/Dbo/Json.h>
|
||||||
#include <boost/asio/strand.hpp>
|
#include <boost/asio/strand.hpp>
|
||||||
#include <boost/json/object.hpp>
|
#include <boost/json/object.hpp>
|
||||||
@ -325,6 +326,8 @@ Application::Application(const std::string &path) : ApplicationSettings(path), m
|
|||||||
m_systemUsage = std::make_shared<SystemUsage>(*m_ioContext->ioContext(), getHomeAssistantAccessToken());
|
m_systemUsage = std::make_shared<SystemUsage>(*m_ioContext->ioContext(), getHomeAssistantAccessToken());
|
||||||
m_systemUsage->start();
|
m_systemUsage->start();
|
||||||
|
|
||||||
|
m_signalServer = std::make_shared<SignalServer>(*this);
|
||||||
|
|
||||||
alarmTask();
|
alarmTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
class HttpSession;
|
class HttpSession;
|
||||||
class SystemUsage;
|
class SystemUsage;
|
||||||
class IoContext;
|
class IoContext;
|
||||||
|
class SignalServer;
|
||||||
|
|
||||||
namespace Nng {
|
namespace Nng {
|
||||||
namespace Asio {
|
namespace Asio {
|
||||||
@ -57,6 +58,7 @@ private:
|
|||||||
std::shared_ptr<boost::asio::system_timer> m_timer;
|
std::shared_ptr<boost::asio::system_timer> m_timer;
|
||||||
std::shared_ptr<SystemUsage> m_systemUsage;
|
std::shared_ptr<SystemUsage> m_systemUsage;
|
||||||
std::shared_ptr<Nng::Asio::Socket> m_replyer;
|
std::shared_ptr<Nng::Asio::Socket> m_replyer;
|
||||||
|
std::shared_ptr<SignalServer> m_signalServer;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // __SETTINGS_H__
|
#endif // __SETTINGS_H__
|
@ -1,7 +1,5 @@
|
|||||||
find_package(Boost COMPONENTS program_options json process REQUIRED)
|
find_package(Boost COMPONENTS program_options json process REQUIRED)
|
||||||
|
|
||||||
add_subdirectory(WebRTC)
|
|
||||||
|
|
||||||
add_executable(Server main.cpp
|
add_executable(Server main.cpp
|
||||||
Application.h Application.cpp
|
Application.h Application.cpp
|
||||||
HttpSession.h HttpSession.cpp
|
HttpSession.h HttpSession.cpp
|
||||||
@ -10,6 +8,10 @@ add_executable(Server main.cpp
|
|||||||
ServiceManager.h
|
ServiceManager.h
|
||||||
SystemUsage.h SystemUsage.cpp
|
SystemUsage.h SystemUsage.cpp
|
||||||
Live2dBackend.h Live2dBackend.cpp
|
Live2dBackend.h Live2dBackend.cpp
|
||||||
|
|
||||||
|
WebRTC/SignalServer.h WebRTC/SignalServer.cpp
|
||||||
|
WebRTC/WebSocketSignalSession.h WebRTC/WebSocketSignalSession.cpp
|
||||||
|
|
||||||
WeChatContext/CorporationContext.h WeChatContext/CorporationContext.cpp
|
WeChatContext/CorporationContext.h WeChatContext/CorporationContext.cpp
|
||||||
WeChatContext/WeChatContext.h WeChatContext/WeChatContext.cpp
|
WeChatContext/WeChatContext.h WeChatContext/WeChatContext.cpp
|
||||||
WeChatContext/WeChatSession.h WeChatContext/WeChatSession.cpp
|
WeChatContext/WeChatSession.h WeChatContext/WeChatSession.cpp
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
add_library(WebRTC
|
|
||||||
WebSocketSignalSession.h WebSocketSignalSession.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(WebRTC
|
|
||||||
PUBLIC Universal
|
|
||||||
)
|
|
@ -1,5 +1,8 @@
|
|||||||
#include "SignalServer.h"
|
#include "SignalServer.h"
|
||||||
#include "../Application.h"
|
#include "../Application.h"
|
||||||
|
#include "../HttpSession.h"
|
||||||
|
#include "BoostLog.h"
|
||||||
|
#include "WebSocketSignalSession.h"
|
||||||
#include <boost/beast/websocket/rfc6455.hpp>
|
#include <boost/beast/websocket/rfc6455.hpp>
|
||||||
|
|
||||||
SignalServer::SignalServer(Application &app) {
|
SignalServer::SignalServer(Application &app) {
|
||||||
@ -8,7 +11,8 @@ SignalServer::SignalServer(Application &app) {
|
|||||||
app.insertUrl("/api/v1/webrtc/signal/{id}", [this](HttpSession &session, const Application::Request &request, const matches &matches) {
|
app.insertUrl("/api/v1/webrtc/signal/{id}", [this](HttpSession &session, const Application::Request &request, const matches &matches) {
|
||||||
auto id = matches.at("id");
|
auto id = matches.at("id");
|
||||||
if (boost::beast::websocket::is_upgrade(request)) {
|
if (boost::beast::websocket::is_upgrade(request)) {
|
||||||
auto ws = std::make_shared<WebSocketSignalSession>();
|
auto ws = std::make_shared<WebSocketSignalSession>(session.releaseSocket(), *this, id);
|
||||||
|
ws->run(request);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// clang-format on
|
// clang-format on
|
||||||
@ -25,7 +29,7 @@ void SignalServer::leave(const std::string &id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WebSocketSignalSession *SignalServer::client(const std::string &id) {
|
WebSocketSignalSession *SignalServer::client(const std::string &id) {
|
||||||
WebSocketSignalSession *ret;
|
WebSocketSignalSession *ret = nullptr;
|
||||||
if (m_clients.contains(id)) {
|
if (m_clients.contains(id)) {
|
||||||
ret = m_clients.at(id);
|
ret = m_clients.at(id);
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,13 @@
|
|||||||
#include <boost/json/parse.hpp>
|
#include <boost/json/parse.hpp>
|
||||||
#include <boost/json/serialize.hpp>
|
#include <boost/json/serialize.hpp>
|
||||||
|
|
||||||
WebSocketSignalSession::WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket) : m_ws(std::move(socket)) {
|
WebSocketSignalSession::WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket, SignalServer &server, const std::string &id)
|
||||||
|
: m_ws(std::move(socket)), m_server(server), m_id(id) {
|
||||||
|
m_server.join(m_id, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
WebSocketSignalSession::~WebSocketSignalSession() {
|
WebSocketSignalSession::~WebSocketSignalSession() {
|
||||||
auto server = Amass::Singleton<SignalServer>::instance();
|
m_server.leave(m_id);
|
||||||
server->leave(m_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketSignalSession::onAccept(boost::beast::error_code ec) {
|
void WebSocketSignalSession::onAccept(boost::beast::error_code ec) {
|
||||||
@ -18,25 +19,16 @@ void WebSocketSignalSession::onAccept(boost::beast::error_code ec) {
|
|||||||
std::cerr << "accept: " << ec.message() << "\n";
|
std::cerr << "accept: " << ec.message() << "\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LOG(info) << "accept websocket target: " << m_target;
|
LOG(info) << "accept websocket target: " << m_target << ", id: " << m_id;
|
||||||
if (m_target.find("/webrtc") == 0) {
|
|
||||||
auto splits = Amass::StringUtility::split(m_target, "/");
|
|
||||||
if (!splits.empty()) m_id = splits.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto server = Amass::Singleton<SignalServer>::instance();
|
|
||||||
server->join(m_id, this);
|
|
||||||
|
|
||||||
// Read a message
|
// Read a message
|
||||||
m_ws.async_read(m_buffer, boost::beast::bind_front_handler(&WebSocketSignalSession::on_read, shared_from_this()));
|
m_ws.async_read(m_buffer, boost::beast::bind_front_handler(&WebSocketSignalSession::onRead, shared_from_this()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketSignalSession::on_read(boost::beast::error_code ec, std::size_t) {
|
void WebSocketSignalSession::onRead(const boost::beast::error_code &error, std::size_t bytesTransferred) {
|
||||||
// Handle the error, if any
|
if (error) {
|
||||||
if (ec) {
|
if (error == boost::asio::error::operation_aborted || error == boost::beast::websocket::error::closed) return;
|
||||||
// Don't report these
|
LOG(error) << error << ": " << error.message();
|
||||||
if (ec == boost::asio::error::operation_aborted || ec == boost::beast::websocket::error::closed) return;
|
|
||||||
LOG(error) << "read: " << ec.message();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto message = boost::beast::buffers_to_string(m_buffer.data());
|
auto message = boost::beast::buffers_to_string(m_buffer.data());
|
||||||
@ -49,9 +41,8 @@ void WebSocketSignalSession::on_read(boost::beast::error_code ec, std::size_t) {
|
|||||||
m_ws.close(boost::beast::websocket::close_code::normal);
|
m_ws.close(boost::beast::websocket::close_code::normal);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto server = Amass::Singleton<SignalServer>::instance();
|
|
||||||
auto destinationId = std::string(root["id"].as_string());
|
auto destinationId = std::string(root["id"].as_string());
|
||||||
auto destination = server->client(destinationId);
|
auto destination = m_server.client(destinationId);
|
||||||
if (destination == nullptr) {
|
if (destination == nullptr) {
|
||||||
LOG(info) << "client " << destinationId << " not found.";
|
LOG(info) << "client " << destinationId << " not found.";
|
||||||
} else {
|
} else {
|
||||||
@ -61,13 +52,10 @@ void WebSocketSignalSession::on_read(boost::beast::error_code ec, std::size_t) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_buffer.consume(m_buffer.size()); // Clear the buffer
|
m_buffer.consume(m_buffer.size()); // Clear the buffer
|
||||||
m_ws.async_read(m_buffer, boost::beast::bind_front_handler(&WebSocketSignalSession::on_read, shared_from_this()));
|
m_ws.async_read(m_buffer, boost::beast::bind_front_handler(&WebSocketSignalSession::onRead, shared_from_this()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketSignalSession::send(std::shared_ptr<std::string const> const &ss) {
|
void WebSocketSignalSession::send(std::shared_ptr<std::string const> const &ss) {
|
||||||
// Post our work to the strand, this ensures
|
|
||||||
// that the members of `this` will not be
|
|
||||||
// accessed concurrently.
|
|
||||||
m_ws.text();
|
m_ws.text();
|
||||||
boost::asio::post(m_ws.get_executor(),
|
boost::asio::post(m_ws.get_executor(),
|
||||||
boost::beast::bind_front_handler(&WebSocketSignalSession::onSend, shared_from_this(), ss));
|
boost::beast::bind_front_handler(&WebSocketSignalSession::onSend, shared_from_this(), ss));
|
||||||
|
@ -8,12 +8,14 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
class SignalServer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Represents an active WebSocket connection to the server
|
* @brief Represents an active WebSocket connection to the server
|
||||||
*/
|
*/
|
||||||
class WebSocketSignalSession : public std::enable_shared_from_this<WebSocketSignalSession> {
|
class WebSocketSignalSession : public std::enable_shared_from_this<WebSocketSignalSession> {
|
||||||
public:
|
public:
|
||||||
WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket);
|
WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket, SignalServer &server, const std::string &id);
|
||||||
|
|
||||||
~WebSocketSignalSession();
|
~WebSocketSignalSession();
|
||||||
|
|
||||||
@ -38,16 +40,17 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
void onAccept(boost::beast::error_code ec);
|
void onAccept(boost::beast::error_code ec);
|
||||||
void on_read(boost::beast::error_code ec, std::size_t bytes_transferred);
|
void onRead(const boost::beast::error_code &error, std::size_t bytesTransferred);
|
||||||
void on_write(boost::beast::error_code ec, std::size_t bytes_transferred);
|
void on_write(boost::beast::error_code ec, std::size_t bytes_transferred);
|
||||||
void onSend(std::shared_ptr<std::string const> const &ss);
|
void onSend(std::shared_ptr<std::string const> const &ss);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
boost::beast::websocket::stream<boost::beast::tcp_stream> m_ws;
|
||||||
|
SignalServer &m_server;
|
||||||
|
std::string m_id;
|
||||||
std::string m_target;
|
std::string m_target;
|
||||||
boost::beast::flat_buffer m_buffer;
|
boost::beast::flat_buffer m_buffer;
|
||||||
boost::beast::websocket::stream<boost::beast::tcp_stream> m_ws;
|
|
||||||
std::vector<std::shared_ptr<std::string const>> m_queue;
|
std::vector<std::shared_ptr<std::string const>> m_queue;
|
||||||
std::string m_id;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // __WEBSOCKETSIGNALSESSION_H__
|
#endif // __WEBSOCKETSIGNALSESSION_H__
|
@ -18,3 +18,5 @@ target_link_libraries(UnitTest
|
|||||||
PRIVATE Boost::unit_test_framework
|
PRIVATE Boost::unit_test_framework
|
||||||
PRIVATE Database
|
PRIVATE Database
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_subdirectory(WebRTCClient)
|
||||||
|
14
UnitTest/WebRTCClient/CMakeLists.txt
Normal file
14
UnitTest/WebRTCClient/CMakeLists.txt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
find_package(LibDataChannel REQUIRED)
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
find_package(Boost COMPONENTS json REQUIRED)
|
||||||
|
|
||||||
|
add_executable(WebRTCClient main.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(WebRTCClient
|
||||||
|
PRIVATE LibDataChannel::LibDataChannel
|
||||||
|
PRIVATE Threads::Threads
|
||||||
|
PRIVATE Boost::json
|
||||||
|
PRIVATE Universal
|
||||||
|
)
|
162
UnitTest/WebRTCClient/main.cpp
Normal file
162
UnitTest/WebRTCClient/main.cpp
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
#include "BoostLog.h"
|
||||||
|
#include <boost/json/object.hpp>
|
||||||
|
#include <boost/json/parse.hpp>
|
||||||
|
#include <boost/json/serialize.hpp>
|
||||||
|
#include <random>
|
||||||
|
#include <rtc/rtc.hpp>
|
||||||
|
|
||||||
|
std::string localId;
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<rtc::PeerConnection>> peerConnectionMap;
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<rtc::DataChannel>> dataChannelMap;
|
||||||
|
|
||||||
|
std::shared_ptr<rtc::PeerConnection> createPeerConnection(const rtc::Configuration &config, std::weak_ptr<rtc::WebSocket> wws,
|
||||||
|
std::string id);
|
||||||
|
std::string randomId(size_t length);
|
||||||
|
|
||||||
|
int main(int argc, char const *argv[]) try {
|
||||||
|
rtc::InitLogger(rtc::LogLevel::Info);
|
||||||
|
rtc::Configuration config;
|
||||||
|
|
||||||
|
localId = randomId(4);
|
||||||
|
LOG(info) << "The local ID is " << localId;
|
||||||
|
|
||||||
|
auto ws = std::make_shared<rtc::WebSocket>();
|
||||||
|
std::promise<void> wsPromise;
|
||||||
|
auto wsFuture = wsPromise.get_future();
|
||||||
|
|
||||||
|
ws->onOpen([&wsPromise]() {
|
||||||
|
LOG(info) << "WebSocket connected, signaling ready";
|
||||||
|
wsPromise.set_value();
|
||||||
|
});
|
||||||
|
ws->onError([&wsPromise](std::string s) {
|
||||||
|
LOG(error) << "WebSocket error";
|
||||||
|
wsPromise.set_exception(std::make_exception_ptr(std::runtime_error(s)));
|
||||||
|
});
|
||||||
|
ws->onClosed([]() { LOG(info) << "WebSocket closed"; });
|
||||||
|
ws->onMessage([&config, wws = std::weak_ptr(ws)](auto data) {
|
||||||
|
if (!std::holds_alternative<std::string>(data)) return;
|
||||||
|
auto jsonValue = boost::json::parse(std::get<std::string>(data));
|
||||||
|
auto &json = jsonValue.as_object();
|
||||||
|
if (!json.contains("id") || !json.contains("type")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto id = static_cast<std::string>(json.at("id").as_string());
|
||||||
|
auto type = static_cast<std::string>(json.at("type").as_string());
|
||||||
|
|
||||||
|
std::shared_ptr<rtc::PeerConnection> pc;
|
||||||
|
if (auto jt = peerConnectionMap.find(id); jt != peerConnectionMap.end()) {
|
||||||
|
pc = jt->second;
|
||||||
|
} else if (type == "offer") {
|
||||||
|
std::cout << "Answering to " + id << std::endl;
|
||||||
|
pc = createPeerConnection(config, wws, id);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type == "offer" || type == "answer") {
|
||||||
|
auto sdp = static_cast<std::string>(json.at("description").as_string());
|
||||||
|
pc->setRemoteDescription(rtc::Description(sdp, type));
|
||||||
|
} else if (type == "candidate") {
|
||||||
|
auto sdp = static_cast<std::string>(json.at("candidate").as_string());
|
||||||
|
auto mid = static_cast<std::string>(json.at("mid").as_string());
|
||||||
|
pc->addRemoteCandidate(rtc::Candidate(sdp, mid));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const std::string url = std::format("ws://127.0.0.1:8081/api/v1/webrtc/signal/{}", localId);
|
||||||
|
LOG(info) << "WebSocket URL is " << url;
|
||||||
|
ws->open(url);
|
||||||
|
LOG(info) << "Waiting for signaling to be connected...";
|
||||||
|
wsFuture.get();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
std::string id;
|
||||||
|
std::cout << "Enter a remote ID to send an offer:" << std::endl;
|
||||||
|
std::cin >> id;
|
||||||
|
std::cin.ignore();
|
||||||
|
if (id.empty()) break;
|
||||||
|
if (id == localId) {
|
||||||
|
LOG(error) << "Invalid remote ID (This is the local ID)" << std::endl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LOG(info) << "Offering to " + id;
|
||||||
|
auto pc = createPeerConnection(config, ws, id);
|
||||||
|
|
||||||
|
// We are the offerer, so create a data channel to initiate the process
|
||||||
|
const std::string label = "test";
|
||||||
|
LOG(info) << "Creating DataChannel with label \"" << label << "\"";
|
||||||
|
auto dc = pc->createDataChannel(label);
|
||||||
|
dc->onOpen([id, wdc = std::weak_ptr(dc)]() {
|
||||||
|
LOG(info) << "DataChannel from " << id << " open";
|
||||||
|
if (auto dc = wdc.lock()) dc->send("Hello from " + localId);
|
||||||
|
});
|
||||||
|
dc->onClosed([id]() { LOG(info) << "DataChannel from " << id << " closed"; });
|
||||||
|
dc->onMessage([id, wdc = std::weak_ptr(dc)](auto data) {
|
||||||
|
// data holds either std::string or rtc::binary
|
||||||
|
if (std::holds_alternative<std::string>(data))
|
||||||
|
LOG(info) << "Message from " << id << " received: " << std::get<std::string>(data);
|
||||||
|
else
|
||||||
|
LOG(info) << "Binary message from " << id << " received, size=" << std::get<rtc::binary>(data).size();
|
||||||
|
});
|
||||||
|
dataChannelMap.emplace(id, dc);
|
||||||
|
}
|
||||||
|
LOG(info) << "Cleaning up...";
|
||||||
|
dataChannelMap.clear();
|
||||||
|
peerConnectionMap.clear();
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
LOG(error) << "Error: " << e.what() << std::endl;
|
||||||
|
dataChannelMap.clear();
|
||||||
|
peerConnectionMap.clear();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<rtc::PeerConnection> createPeerConnection(const rtc::Configuration &config, std::weak_ptr<rtc::WebSocket> wws,
|
||||||
|
std::string id) {
|
||||||
|
auto pc = std::make_shared<rtc::PeerConnection>(config);
|
||||||
|
pc->onStateChange([](rtc::PeerConnection::State state) { LOG(info) << "State: " << state; });
|
||||||
|
pc->onGatheringStateChange([](rtc::PeerConnection::GatheringState state) { LOG(info) << "Gathering State: " << state; });
|
||||||
|
pc->onLocalDescription([wws, id](rtc::Description description) {
|
||||||
|
boost::json::object message;
|
||||||
|
message["id"] = id;
|
||||||
|
message["type"] = description.typeString();
|
||||||
|
message["description"] = std::string(description);
|
||||||
|
if (auto ws = wws.lock()) ws->send(boost::json::serialize(message));
|
||||||
|
});
|
||||||
|
pc->onLocalCandidate([wws, id](rtc::Candidate candidate) {
|
||||||
|
boost::json::object message;
|
||||||
|
message["id"] = id;
|
||||||
|
message["type"] = "candidate";
|
||||||
|
message["candidate"] = std::string(candidate);
|
||||||
|
message["mid"] = candidate.mid();
|
||||||
|
if (auto ws = wws.lock()) ws->send(boost::json::serialize(message));
|
||||||
|
});
|
||||||
|
pc->onDataChannel([id](std::shared_ptr<rtc::DataChannel> dc) {
|
||||||
|
LOG(info) << "DataChannel from " << id << " received with label \"" << dc->label() << "\"";
|
||||||
|
|
||||||
|
dc->onOpen([wdc = std::weak_ptr(dc)]() {
|
||||||
|
if (auto dc = wdc.lock()) dc->send("Hello from " + localId);
|
||||||
|
});
|
||||||
|
|
||||||
|
dc->onClosed([id]() { LOG(info) << "DataChannel from " << id << " closed"; });
|
||||||
|
|
||||||
|
dc->onMessage([id](auto data) {
|
||||||
|
// data holds either std::string or rtc::binary
|
||||||
|
if (std::holds_alternative<std::string>(data))
|
||||||
|
LOG(info) << "Message from " << id << " received: " << std::get<std::string>(data);
|
||||||
|
else
|
||||||
|
LOG(info) << "Binary message from " << id << " received, size=" << std::get<rtc::binary>(data).size();
|
||||||
|
});
|
||||||
|
dataChannelMap.emplace(id, dc);
|
||||||
|
});
|
||||||
|
peerConnectionMap.emplace(id, pc);
|
||||||
|
return pc;
|
||||||
|
}
|
||||||
|
std::string randomId(size_t length) {
|
||||||
|
using std::chrono::high_resolution_clock;
|
||||||
|
static thread_local std::mt19937 rng(static_cast<unsigned int>(high_resolution_clock::now().time_since_epoch().count()));
|
||||||
|
static const std::string characters("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
|
||||||
|
std::string id(length, '0');
|
||||||
|
std::uniform_int_distribution<int> uniform(0, int(characters.size() - 1));
|
||||||
|
std::generate(id.begin(), id.end(), [&]() { return characters.at(uniform(rng)); });
|
||||||
|
return id;
|
||||||
|
}
|
@ -8,6 +8,7 @@
|
|||||||
#include "RedirectPage.h"
|
#include "RedirectPage.h"
|
||||||
#include "Restful.h"
|
#include "Restful.h"
|
||||||
#include "VisitorRecordsPage.h"
|
#include "VisitorRecordsPage.h"
|
||||||
|
#include "WebRTCClientPage.h"
|
||||||
#include "model/AuthModel.h"
|
#include "model/AuthModel.h"
|
||||||
#include <Wt/Auth/AuthService.h>
|
#include <Wt/Auth/AuthService.h>
|
||||||
#include <Wt/Auth/HashFunction.h>
|
#include <Wt/Auth/HashFunction.h>
|
||||||
@ -37,6 +38,7 @@ Application::Application(const Wt::WEnvironment &env, bool embedded)
|
|||||||
setTheme(std::make_shared<BulmaTheme>("bulma", !embedded));
|
setTheme(std::make_shared<BulmaTheme>("bulma", !embedded));
|
||||||
std::string externalPath;
|
std::string externalPath;
|
||||||
if (!embedded) {
|
if (!embedded) {
|
||||||
|
root()->addStyleClass("bulma-is-flex bulma-is-flex-direction-column");
|
||||||
m_navigationBar = root()->addNew<NavigationBar>();
|
m_navigationBar = root()->addNew<NavigationBar>();
|
||||||
m_navigationBar->registerClicked.connect([this]() {
|
m_navigationBar->registerClicked.connect([this]() {
|
||||||
if (m_loginPage) {
|
if (m_loginPage) {
|
||||||
@ -46,6 +48,7 @@ Application::Application(const Wt::WEnvironment &env, bool embedded)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
m_root = root()->addNew<Wt::WContainerWidget>();
|
m_root = root()->addNew<Wt::WContainerWidget>();
|
||||||
|
m_root->addStyleClass("bulma-container bulma-is-flex-grow-1 bulma-is-flex");
|
||||||
} else {
|
} else {
|
||||||
std::unique_ptr<Wt::WContainerWidget> topPtr = std::make_unique<Wt::WContainerWidget>();
|
std::unique_ptr<Wt::WContainerWidget> topPtr = std::make_unique<Wt::WContainerWidget>();
|
||||||
m_root = topPtr.get();
|
m_root = topPtr.get();
|
||||||
@ -202,6 +205,9 @@ void Application::handlePathChange(const std::string &path) {
|
|||||||
auto p = m_root->addNew<RedirectPage>();
|
auto p = m_root->addNew<RedirectPage>();
|
||||||
p->setMessage("这是一个跳转测试....");
|
p->setMessage("这是一个跳转测试....");
|
||||||
p->setRedirect("https://amass.fun");
|
p->setRedirect("https://amass.fun");
|
||||||
|
} else if (path.starts_with("/wt/webrtc")) {
|
||||||
|
m_root->clear();
|
||||||
|
auto p = m_root->addNew<WebRTCClientPage>();
|
||||||
} else {
|
} else {
|
||||||
m_root->clear();
|
m_root->clear();
|
||||||
m_root->addNew<HomePage>();
|
m_root->addNew<HomePage>();
|
||||||
|
@ -9,6 +9,7 @@ add_library(WebApplication
|
|||||||
NavigationBar.h NavigationBar.cpp
|
NavigationBar.h NavigationBar.cpp
|
||||||
RedirectPage.h RedirectPage.cpp
|
RedirectPage.h RedirectPage.cpp
|
||||||
VisitorRecordsPage.h VisitorRecordsPage.cpp
|
VisitorRecordsPage.h VisitorRecordsPage.cpp
|
||||||
|
WebRTCClientPage.h WebRTCClientPage.cpp
|
||||||
Restful.h Restful.cpp
|
Restful.h Restful.cpp
|
||||||
Dialog.h Dialog.cpp
|
Dialog.h Dialog.cpp
|
||||||
model/AuthModel.h model/AuthModel.cpp
|
model/AuthModel.h model/AuthModel.cpp
|
||||||
|
@ -19,6 +19,10 @@ HomePage::HomePage() {
|
|||||||
li->setHtmlTagName("li");
|
li->setHtmlTagName("li");
|
||||||
li->addNew<Wt::WAnchor>(Wt::WLink(Wt::LinkType::InternalPath, "/wt/visitor/analysis"), "访客数据");
|
li->addNew<Wt::WAnchor>(Wt::WLink(Wt::LinkType::InternalPath, "/wt/visitor/analysis"), "访客数据");
|
||||||
|
|
||||||
|
li = ul->addNew<Wt::WContainerWidget>();
|
||||||
|
li->setHtmlTagName("li");
|
||||||
|
li->addNew<Wt::WAnchor>(Wt::WLink(Wt::LinkType::InternalPath, "/wt/webrtc"), "WebRTC测试");
|
||||||
|
|
||||||
addWidget(std::make_unique<Wt::WText>("Your name, please ? "));
|
addWidget(std::make_unique<Wt::WText>("Your name, please ? "));
|
||||||
m_nameEdit = addWidget(std::make_unique<Wt::WLineEdit>());
|
m_nameEdit = addWidget(std::make_unique<Wt::WLineEdit>());
|
||||||
m_nameEdit->setFocus();
|
m_nameEdit->setFocus();
|
||||||
|
52
WebApplication/WebRTCClientPage.cpp
Normal file
52
WebApplication/WebRTCClientPage.cpp
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#include "WebRTCClientPage.h"
|
||||||
|
#include "BoostLog.h"
|
||||||
|
#include <Wt/WApplication.h>
|
||||||
|
#include <Wt/WEnvironment.h>
|
||||||
|
#include <Wt/WLabel.h>
|
||||||
|
#include <Wt/WLineEdit.h>
|
||||||
|
#include <Wt/WPushButton.h>
|
||||||
|
#include <Wt/WServer.h>
|
||||||
|
#include <Wt/WTextArea.h>
|
||||||
|
#include <format>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
#include "js/WebRTCClient.js"
|
||||||
|
|
||||||
|
WebRTCClientPage::WebRTCClientPage() : Wt::WTemplate(tr("Wt.WebRTC.Home")) {
|
||||||
|
using namespace Wt;
|
||||||
|
WApplication *app = WApplication::instance();
|
||||||
|
app->messageResourceBundle().use(app->appRoot() + "webrtc");
|
||||||
|
LOAD_JAVASCRIPT(app, "js/WebRTCClient.js", "WebRTCClient", wtjs1);
|
||||||
|
|
||||||
|
addStyleClass("bulma-is-flex-grow-1 bulma-is-flex bulma-is-flex-direction-column");
|
||||||
|
auto localId = randomId(4);
|
||||||
|
bindNew<Wt::WLabel>("localId")->setText(localId);
|
||||||
|
auto offerId = bindNew<Wt::WLineEdit>("offerId");
|
||||||
|
|
||||||
|
auto textBrowser = bindNew<Wt::WTextArea>("textBrowser");
|
||||||
|
textBrowser->setReadOnly(true);
|
||||||
|
|
||||||
|
auto offerBtn = bindNew<Wt::WPushButton>("offerBtn");
|
||||||
|
offerBtn->setText("Offer");
|
||||||
|
|
||||||
|
auto sendMsg = bindNew<Wt::WLineEdit>("sendMsg");
|
||||||
|
|
||||||
|
auto sendBtn = bindNew<Wt::WPushButton>("sendBtn");
|
||||||
|
sendBtn->setText("发送");
|
||||||
|
|
||||||
|
std::string url = "ws://127.0.0.1:8081/api/v1/webrtc/signal";
|
||||||
|
|
||||||
|
setJavaScriptMember(" WebRTCClient", std::format("new {}.WebRTCClient({},{},{},{},{},{},{}, '{}', '{}');", WT_CLASS, WT_CLASS,
|
||||||
|
jsRef(), offerId->jsRef(), offerBtn->jsRef(), sendMsg->jsRef(),
|
||||||
|
sendBtn->jsRef(), textBrowser->jsRef(), localId, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string WebRTCClientPage::randomId(size_t length) {
|
||||||
|
using std::chrono::high_resolution_clock;
|
||||||
|
static thread_local std::mt19937 rng(static_cast<unsigned int>(high_resolution_clock::now().time_since_epoch().count()));
|
||||||
|
static const std::string characters("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
|
||||||
|
std::string id(length, '0');
|
||||||
|
std::uniform_int_distribution<int> uniform(0, int(characters.size() - 1));
|
||||||
|
std::generate(id.begin(), id.end(), [&]() { return characters.at(uniform(rng)); });
|
||||||
|
return id;
|
||||||
|
}
|
13
WebApplication/WebRTCClientPage.h
Normal file
13
WebApplication/WebRTCClientPage.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#ifndef __WEBRTCCLIENTPAGE_H__
|
||||||
|
#define __WEBRTCCLIENTPAGE_H__
|
||||||
|
|
||||||
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
|
class WebRTCClientPage : public Wt::WTemplate {
|
||||||
|
public:
|
||||||
|
WebRTCClientPage();
|
||||||
|
|
||||||
|
static std::string randomId(size_t length);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // __WEBRTCCLIENTPAGE_H__
|
139
WebApplication/js/WebRTCClient.js
Normal file
139
WebApplication/js/WebRTCClient.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
WT_DECLARE_WT_MEMBER(1, JavaScriptConstructor, "WebRTCClient", function (WT, client, offerId, offerBtn, sendMsg, sendBtn, textBrowser, localId, url) {
|
||||||
|
this.peerConnectionMap = {};
|
||||||
|
this.dataChannelMap = {};
|
||||||
|
this.config = {};
|
||||||
|
this.log = (text) => {
|
||||||
|
textBrowser.value = `${textBrowser.value}\n${text}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendLocalDescription(ws, id, pc, type) {
|
||||||
|
(type == 'offer' ? pc.createOffer() : pc.createAnswer())
|
||||||
|
.then((desc) => pc.setLocalDescription(desc))
|
||||||
|
.then(() => {
|
||||||
|
const { sdp, type } = pc.localDescription;
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
description: sdp,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendLocalCandidate(ws, id, cand) {
|
||||||
|
const { candidate, sdpMid } = cand;
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
id,
|
||||||
|
type: 'candidate',
|
||||||
|
candidate,
|
||||||
|
mid: sdpMid,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setupDataChannel = (dc, id) => {
|
||||||
|
dc.onopen = () => {
|
||||||
|
console.log(`DataChannel from ${id} open`);
|
||||||
|
sendMsg.disabled = false;
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.onclick = () => dc.send(sendMsg.value);
|
||||||
|
};
|
||||||
|
dc.onclose = () => { console.log(`DataChannel from ${id} closed`); };
|
||||||
|
dc.onmessage = (e) => {
|
||||||
|
if (typeof (e.data) != 'string')
|
||||||
|
return;
|
||||||
|
console.log(`Message from ${id} received: ${e.data}`);
|
||||||
|
this.log(`${id}: ${e.data}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dataChannelMap[id] = dc;
|
||||||
|
return dc;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.createPeerConnection = (ws, id) => {
|
||||||
|
const pc = new RTCPeerConnection(this.config);
|
||||||
|
pc.oniceconnectionstatechange = () => console.log(`Connection state: ${pc.iceConnectionState}`);
|
||||||
|
pc.onicegatheringstatechange = () => console.log(`Gathering state: ${pc.iceGatheringState}`);
|
||||||
|
pc.onicecandidate = (e) => {
|
||||||
|
if (e.candidate && e.candidate.candidate) {
|
||||||
|
sendLocalCandidate(ws, id, e.candidate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pc.ondatachannel = (e) => {
|
||||||
|
const dc = e.channel;
|
||||||
|
console.log(`DataChannel from ${id} received with label "${dc.label}"`);
|
||||||
|
this.setupDataChannel(dc, id);
|
||||||
|
|
||||||
|
dc.send(`Hello from ${localId}`);
|
||||||
|
|
||||||
|
sendMsg.disabled = false;
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.onclick = () => dc.send(sendMsg.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.peerConnectionMap[id] = pc;
|
||||||
|
return pc;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.offerPeerConnection = function (ws, id) {
|
||||||
|
console.log(`Offering to ${id}`);
|
||||||
|
pc = this.createPeerConnection(ws, id);
|
||||||
|
|
||||||
|
const label = "test";
|
||||||
|
console.log(`Creating DataChannel with label "${label}"`);
|
||||||
|
const dc = pc.createDataChannel(label);
|
||||||
|
this.setupDataChannel(dc, id);
|
||||||
|
|
||||||
|
sendLocalDescription(ws, id, pc, 'offer');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.openSignaling = function (url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
ws.onopen = () => resolve(ws);
|
||||||
|
ws.onerror = () => reject(new Error('WebSocket error'));
|
||||||
|
ws.onclose = () => console.error('WebSocket disconnected');
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
if (typeof (e.data) != 'string') return;
|
||||||
|
const message = JSON.parse(e.data);
|
||||||
|
console.log(message);
|
||||||
|
const { id, type } = message;
|
||||||
|
let pc = this.peerConnectionMap[id];
|
||||||
|
if (!pc) {
|
||||||
|
if (type != 'offer')
|
||||||
|
return;
|
||||||
|
console.log(`Answering to ${id}`);
|
||||||
|
pc = this.createPeerConnection(ws, id);
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'offer':
|
||||||
|
case 'answer':
|
||||||
|
pc.setRemoteDescription({
|
||||||
|
sdp: message.description,
|
||||||
|
type: message.type,
|
||||||
|
}).then(() => {
|
||||||
|
if (type == 'offer') {
|
||||||
|
sendLocalDescription(ws, id, pc, 'answer');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'candidate':
|
||||||
|
pc.addIceCandidate({
|
||||||
|
candidate: message.candidate,
|
||||||
|
sdpMid: message.mid,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
client.wtWebRTCClient = this;
|
||||||
|
this.openSignaling(`${url}/${localId}`).then(ws => {
|
||||||
|
this.log('WebSocket connected, signaling ready');
|
||||||
|
offerId.disabled = false;
|
||||||
|
offerBtn.disabled = false;
|
||||||
|
offerBtn.onclick = () => {
|
||||||
|
this.offerPeerConnection(ws, offerId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,8 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.Wt-itemview .Wt-headerdiv {
|
.Wt-itemview .Wt-headerdiv {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
22
resources/webrtc.xml
Normal file
22
resources/webrtc.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<messages>
|
||||||
|
<message id="Wt.WebRTC.Home">
|
||||||
|
<h1 class="bulma-subtitle bulma-has-text-centered">WebRTC 测试</h1>
|
||||||
|
ID: ${localId}
|
||||||
|
<br/>
|
||||||
|
${textBrowser}
|
||||||
|
<h2 class="bulma-subtitle bulma-is-6">通过信令服务器发送 offer</h2>
|
||||||
|
${offerId type="text" placeholder="remote ID" disabled="true" }
|
||||||
|
${offerBtn disabled="true"}
|
||||||
|
<br/>
|
||||||
|
<h2 class="bulma-subtitle bulma-is-6">通过 DataChannel 发送消息</h2>
|
||||||
|
<div class="bulma-field bulma-has-addons">
|
||||||
|
<div class="bulma-control bulma-is-expanded">
|
||||||
|
${sendMsg class="bulma-input" type="text" placeholder="message" disabled="true" }
|
||||||
|
</div>
|
||||||
|
<div class="bulma-control">
|
||||||
|
${sendBtn class="bulma-button bulma-is-info" disabled="true"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
</messages>
|
Loading…
Reference in New Issue
Block a user