From dc5b7500566ff5c96e4c482b73ddaf3413e5d9d4 Mon Sep 17 00:00:00 2001 From: amass Date: Sun, 12 Jan 2025 00:46:14 +0800 Subject: [PATCH] add webrtc test. --- CMakeLists.txt | 1 + Server/Application.cpp | 3 + Server/Application.h | 2 + Server/CMakeLists.txt | 6 +- Server/WebRTC/CMakeLists.txt | 8 -- Server/WebRTC/SignalServer.cpp | 8 +- Server/WebRTC/WebSocketSignalSession.cpp | 36 ++--- Server/WebRTC/WebSocketSignalSession.h | 11 +- UnitTest/CMakeLists.txt | 2 + UnitTest/WebRTCClient/CMakeLists.txt | 14 ++ UnitTest/WebRTCClient/main.cpp | 162 +++++++++++++++++++++++ WebApplication/Application.cpp | 6 + WebApplication/CMakeLists.txt | 1 + WebApplication/HomePage.cpp | 4 + WebApplication/WebRTCClientPage.cpp | 52 ++++++++ WebApplication/WebRTCClientPage.h | 13 ++ WebApplication/js/WebRTCClient.js | 139 +++++++++++++++++++ resources/app.css | 7 +- resources/webrtc.xml | 22 +++ 19 files changed, 456 insertions(+), 41 deletions(-) delete mode 100644 Server/WebRTC/CMakeLists.txt create mode 100644 UnitTest/WebRTCClient/CMakeLists.txt create mode 100644 UnitTest/WebRTCClient/main.cpp create mode 100644 WebApplication/WebRTCClientPage.cpp create mode 100644 WebApplication/WebRTCClientPage.h create mode 100644 WebApplication/js/WebRTCClient.js create mode 100644 resources/webrtc.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 77c2b41..c5058a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ set(Libraries_ROOT /opt/Libraries) 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(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) diff --git a/Server/Application.cpp b/Server/Application.cpp index a02d6c9..481d5f4 100644 --- a/Server/Application.cpp +++ b/Server/Application.cpp @@ -8,6 +8,7 @@ #include "ServiceManager.h" #include "SystemUsage.h" #include "WeChatContext/CorporationContext.h" +#include "WebRTC/SignalServer.h" #include #include #include @@ -325,6 +326,8 @@ Application::Application(const std::string &path) : ApplicationSettings(path), m m_systemUsage = std::make_shared(*m_ioContext->ioContext(), getHomeAssistantAccessToken()); m_systemUsage->start(); + m_signalServer = std::make_shared(*this); + alarmTask(); } diff --git a/Server/Application.h b/Server/Application.h index 8a9165c..f43e247 100644 --- a/Server/Application.h +++ b/Server/Application.h @@ -11,6 +11,7 @@ class HttpSession; class SystemUsage; class IoContext; +class SignalServer; namespace Nng { namespace Asio { @@ -57,6 +58,7 @@ private: std::shared_ptr m_timer; std::shared_ptr m_systemUsage; std::shared_ptr m_replyer; + std::shared_ptr m_signalServer; }; #endif // __SETTINGS_H__ \ No newline at end of file diff --git a/Server/CMakeLists.txt b/Server/CMakeLists.txt index d56b8bb..05af036 100644 --- a/Server/CMakeLists.txt +++ b/Server/CMakeLists.txt @@ -1,7 +1,5 @@ find_package(Boost COMPONENTS program_options json process REQUIRED) -add_subdirectory(WebRTC) - add_executable(Server main.cpp Application.h Application.cpp HttpSession.h HttpSession.cpp @@ -10,6 +8,10 @@ add_executable(Server main.cpp ServiceManager.h SystemUsage.h SystemUsage.cpp Live2dBackend.h Live2dBackend.cpp + + WebRTC/SignalServer.h WebRTC/SignalServer.cpp + WebRTC/WebSocketSignalSession.h WebRTC/WebSocketSignalSession.cpp + WeChatContext/CorporationContext.h WeChatContext/CorporationContext.cpp WeChatContext/WeChatContext.h WeChatContext/WeChatContext.cpp WeChatContext/WeChatSession.h WeChatContext/WeChatSession.cpp diff --git a/Server/WebRTC/CMakeLists.txt b/Server/WebRTC/CMakeLists.txt deleted file mode 100644 index c6b041f..0000000 --- a/Server/WebRTC/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ - -add_library(WebRTC - WebSocketSignalSession.h WebSocketSignalSession.cpp -) - -target_link_libraries(WebRTC - PUBLIC Universal -) \ No newline at end of file diff --git a/Server/WebRTC/SignalServer.cpp b/Server/WebRTC/SignalServer.cpp index f4075a6..cc22892 100644 --- a/Server/WebRTC/SignalServer.cpp +++ b/Server/WebRTC/SignalServer.cpp @@ -1,5 +1,8 @@ #include "SignalServer.h" #include "../Application.h" +#include "../HttpSession.h" +#include "BoostLog.h" +#include "WebSocketSignalSession.h" #include 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) { auto id = matches.at("id"); if (boost::beast::websocket::is_upgrade(request)) { - auto ws = std::make_shared(); + auto ws = std::make_shared(session.releaseSocket(), *this, id); + ws->run(request); } }); // clang-format on @@ -25,7 +29,7 @@ void SignalServer::leave(const std::string &id) { } WebSocketSignalSession *SignalServer::client(const std::string &id) { - WebSocketSignalSession *ret; + WebSocketSignalSession *ret = nullptr; if (m_clients.contains(id)) { ret = m_clients.at(id); } diff --git a/Server/WebRTC/WebSocketSignalSession.cpp b/Server/WebRTC/WebSocketSignalSession.cpp index 4acc2ba..2ee22df 100644 --- a/Server/WebRTC/WebSocketSignalSession.cpp +++ b/Server/WebRTC/WebSocketSignalSession.cpp @@ -4,12 +4,13 @@ #include #include -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() { - auto server = Amass::Singleton::instance(); - server->leave(m_id); + m_server.leave(m_id); } 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"; return; } - LOG(info) << "accept websocket target: " << m_target; - if (m_target.find("/webrtc") == 0) { - auto splits = Amass::StringUtility::split(m_target, "/"); - if (!splits.empty()) m_id = splits.back(); - } - - auto server = Amass::Singleton::instance(); - server->join(m_id, this); + LOG(info) << "accept websocket target: " << m_target << ", id: " << m_id; // 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) { - // Handle the error, if any - if (ec) { - // Don't report these - if (ec == boost::asio::error::operation_aborted || ec == boost::beast::websocket::error::closed) return; - LOG(error) << "read: " << ec.message(); +void WebSocketSignalSession::onRead(const boost::beast::error_code &error, std::size_t bytesTransferred) { + if (error) { + if (error == boost::asio::error::operation_aborted || error == boost::beast::websocket::error::closed) return; + LOG(error) << error << ": " << error.message(); return; } 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); return; } - auto server = Amass::Singleton::instance(); auto destinationId = std::string(root["id"].as_string()); - auto destination = server->client(destinationId); + auto destination = m_server.client(destinationId); if (destination == nullptr) { LOG(info) << "client " << destinationId << " not found."; } 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_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 const &ss) { - // Post our work to the strand, this ensures - // that the members of `this` will not be - // accessed concurrently. m_ws.text(); boost::asio::post(m_ws.get_executor(), boost::beast::bind_front_handler(&WebSocketSignalSession::onSend, shared_from_this(), ss)); diff --git a/Server/WebRTC/WebSocketSignalSession.h b/Server/WebRTC/WebSocketSignalSession.h index 179fcfd..5dd8ee3 100644 --- a/Server/WebRTC/WebSocketSignalSession.h +++ b/Server/WebRTC/WebSocketSignalSession.h @@ -8,12 +8,14 @@ #include #include +class SignalServer; + /** * @brief Represents an active WebSocket connection to the server */ class WebSocketSignalSession : public std::enable_shared_from_this { public: - WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket); + WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket, SignalServer &server, const std::string &id); ~WebSocketSignalSession(); @@ -38,16 +40,17 @@ public: protected: 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 onSend(std::shared_ptr const &ss); private: + boost::beast::websocket::stream m_ws; + SignalServer &m_server; + std::string m_id; std::string m_target; boost::beast::flat_buffer m_buffer; - boost::beast::websocket::stream m_ws; std::vector> m_queue; - std::string m_id; }; #endif // __WEBSOCKETSIGNALSESSION_H__ \ No newline at end of file diff --git a/UnitTest/CMakeLists.txt b/UnitTest/CMakeLists.txt index bc418f2..9c7c308 100644 --- a/UnitTest/CMakeLists.txt +++ b/UnitTest/CMakeLists.txt @@ -18,3 +18,5 @@ target_link_libraries(UnitTest PRIVATE Boost::unit_test_framework PRIVATE Database ) + +add_subdirectory(WebRTCClient) diff --git a/UnitTest/WebRTCClient/CMakeLists.txt b/UnitTest/WebRTCClient/CMakeLists.txt new file mode 100644 index 0000000..37d3450 --- /dev/null +++ b/UnitTest/WebRTCClient/CMakeLists.txt @@ -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 +) \ No newline at end of file diff --git a/UnitTest/WebRTCClient/main.cpp b/UnitTest/WebRTCClient/main.cpp new file mode 100644 index 0000000..e6ca2d7 --- /dev/null +++ b/UnitTest/WebRTCClient/main.cpp @@ -0,0 +1,162 @@ +#include "BoostLog.h" +#include +#include +#include +#include +#include + +std::string localId; +std::unordered_map> peerConnectionMap; +std::unordered_map> dataChannelMap; + +std::shared_ptr createPeerConnection(const rtc::Configuration &config, std::weak_ptr 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(); + std::promise 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(data)) return; + auto jsonValue = boost::json::parse(std::get(data)); + auto &json = jsonValue.as_object(); + if (!json.contains("id") || !json.contains("type")) { + return; + } + auto id = static_cast(json.at("id").as_string()); + auto type = static_cast(json.at("type").as_string()); + + std::shared_ptr 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(json.at("description").as_string()); + pc->setRemoteDescription(rtc::Description(sdp, type)); + } else if (type == "candidate") { + auto sdp = static_cast(json.at("candidate").as_string()); + auto mid = static_cast(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(data)) + LOG(info) << "Message from " << id << " received: " << std::get(data); + else + LOG(info) << "Binary message from " << id << " received, size=" << std::get(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 createPeerConnection(const rtc::Configuration &config, std::weak_ptr wws, + std::string id) { + auto pc = std::make_shared(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 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(data)) + LOG(info) << "Message from " << id << " received: " << std::get(data); + else + LOG(info) << "Binary message from " << id << " received, size=" << std::get(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(high_resolution_clock::now().time_since_epoch().count())); + static const std::string characters("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + std::string id(length, '0'); + std::uniform_int_distribution uniform(0, int(characters.size() - 1)); + std::generate(id.begin(), id.end(), [&]() { return characters.at(uniform(rng)); }); + return id; +} \ No newline at end of file diff --git a/WebApplication/Application.cpp b/WebApplication/Application.cpp index 8ee5083..26e717f 100644 --- a/WebApplication/Application.cpp +++ b/WebApplication/Application.cpp @@ -8,6 +8,7 @@ #include "RedirectPage.h" #include "Restful.h" #include "VisitorRecordsPage.h" +#include "WebRTCClientPage.h" #include "model/AuthModel.h" #include #include @@ -37,6 +38,7 @@ Application::Application(const Wt::WEnvironment &env, bool embedded) setTheme(std::make_shared("bulma", !embedded)); std::string externalPath; if (!embedded) { + root()->addStyleClass("bulma-is-flex bulma-is-flex-direction-column"); m_navigationBar = root()->addNew(); m_navigationBar->registerClicked.connect([this]() { if (m_loginPage) { @@ -46,6 +48,7 @@ Application::Application(const Wt::WEnvironment &env, bool embedded) } }); m_root = root()->addNew(); + m_root->addStyleClass("bulma-container bulma-is-flex-grow-1 bulma-is-flex"); } else { std::unique_ptr topPtr = std::make_unique(); m_root = topPtr.get(); @@ -202,6 +205,9 @@ void Application::handlePathChange(const std::string &path) { auto p = m_root->addNew(); p->setMessage("这是一个跳转测试...."); p->setRedirect("https://amass.fun"); + } else if (path.starts_with("/wt/webrtc")) { + m_root->clear(); + auto p = m_root->addNew(); } else { m_root->clear(); m_root->addNew(); diff --git a/WebApplication/CMakeLists.txt b/WebApplication/CMakeLists.txt index e2b1dce..a9fbc84 100644 --- a/WebApplication/CMakeLists.txt +++ b/WebApplication/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(WebApplication NavigationBar.h NavigationBar.cpp RedirectPage.h RedirectPage.cpp VisitorRecordsPage.h VisitorRecordsPage.cpp + WebRTCClientPage.h WebRTCClientPage.cpp Restful.h Restful.cpp Dialog.h Dialog.cpp model/AuthModel.h model/AuthModel.cpp diff --git a/WebApplication/HomePage.cpp b/WebApplication/HomePage.cpp index c13def8..1c79c87 100644 --- a/WebApplication/HomePage.cpp +++ b/WebApplication/HomePage.cpp @@ -19,6 +19,10 @@ HomePage::HomePage() { li->setHtmlTagName("li"); li->addNew(Wt::WLink(Wt::LinkType::InternalPath, "/wt/visitor/analysis"), "访客数据"); + li = ul->addNew(); + li->setHtmlTagName("li"); + li->addNew(Wt::WLink(Wt::LinkType::InternalPath, "/wt/webrtc"), "WebRTC测试"); + addWidget(std::make_unique("Your name, please ? ")); m_nameEdit = addWidget(std::make_unique()); m_nameEdit->setFocus(); diff --git a/WebApplication/WebRTCClientPage.cpp b/WebApplication/WebRTCClientPage.cpp new file mode 100644 index 0000000..82df47a --- /dev/null +++ b/WebApplication/WebRTCClientPage.cpp @@ -0,0 +1,52 @@ +#include "WebRTCClientPage.h" +#include "BoostLog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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("localId")->setText(localId); + auto offerId = bindNew("offerId"); + + auto textBrowser = bindNew("textBrowser"); + textBrowser->setReadOnly(true); + + auto offerBtn = bindNew("offerBtn"); + offerBtn->setText("Offer"); + + auto sendMsg = bindNew("sendMsg"); + + auto sendBtn = bindNew("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(high_resolution_clock::now().time_since_epoch().count())); + static const std::string characters("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + std::string id(length, '0'); + std::uniform_int_distribution uniform(0, int(characters.size() - 1)); + std::generate(id.begin(), id.end(), [&]() { return characters.at(uniform(rng)); }); + return id; +} diff --git a/WebApplication/WebRTCClientPage.h b/WebApplication/WebRTCClientPage.h new file mode 100644 index 0000000..26c037f --- /dev/null +++ b/WebApplication/WebRTCClientPage.h @@ -0,0 +1,13 @@ +#ifndef __WEBRTCCLIENTPAGE_H__ +#define __WEBRTCCLIENTPAGE_H__ + +#include + +class WebRTCClientPage : public Wt::WTemplate { +public: + WebRTCClientPage(); + + static std::string randomId(size_t length); +}; + +#endif // __WEBRTCCLIENTPAGE_H__ \ No newline at end of file diff --git a/WebApplication/js/WebRTCClient.js b/WebApplication/js/WebRTCClient.js new file mode 100644 index 0000000..e9bba25 --- /dev/null +++ b/WebApplication/js/WebRTCClient.js @@ -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); + } + }); +}); \ No newline at end of file diff --git a/resources/app.css b/resources/app.css index 3522e11..e252402 100644 --- a/resources/app.css +++ b/resources/app.css @@ -1,3 +1,8 @@ +html, +body { + height: 100%; +} + .Wt-itemview .Wt-headerdiv { overflow: hidden; -webkit-user-select: none; @@ -101,5 +106,5 @@ } .is-pointer-cursor { - cursor: pointer !important; + cursor: pointer !important; } \ No newline at end of file diff --git a/resources/webrtc.xml b/resources/webrtc.xml new file mode 100644 index 0000000..75e0b58 --- /dev/null +++ b/resources/webrtc.xml @@ -0,0 +1,22 @@ + + + +

WebRTC 测试

+ ID: ${localId} +
+ ${textBrowser} +

通过信令服务器发送 offer

+ ${offerId type="text" placeholder="remote ID" disabled="true" } + ${offerBtn disabled="true"} +
+

通过 DataChannel 发送消息

+
+
+ ${sendMsg class="bulma-input" type="text" placeholder="message" disabled="true" } +
+
+ ${sendBtn class="bulma-button bulma-is-info" disabled="true"} +
+
+
+
\ No newline at end of file