add webrtc test.
Some checks failed
Deploy / Build (push) Failing after 53s

This commit is contained in:
amass 2025-01-12 00:46:14 +08:00
parent ddef462307
commit dc5b750056
19 changed files with 456 additions and 41 deletions

View File

@ -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)

View File

@ -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();
} }

View File

@ -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__

View File

@ -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

View File

@ -1,8 +0,0 @@
add_library(WebRTC
WebSocketSignalSession.h WebSocketSignalSession.cpp
)
target_link_libraries(WebRTC
PUBLIC Universal
)

View File

@ -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);
} }

View File

@ -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));

View File

@ -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__

View File

@ -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)

View 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
)

View 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;
}

View File

@ -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>();

View File

@ -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

View File

@ -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();

View 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;
}

View 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__

View 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);
}
});
});

View File

@ -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
View 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>