From 94c246aa7b6353005d86cfe8d41fd24efdeb8efd Mon Sep 17 00:00:00 2001 From: amass Date: Mon, 13 Jan 2025 23:56:55 +0800 Subject: [PATCH] add ftxui. --- CMakeLists.txt | 6 + UnitTest/WebRTCClient/CMakeLists.txt | 7 +- UnitTest/WebRTCClient/Client.cpp | 187 +++++++++++++++++++++ UnitTest/WebRTCClient/Client.h | 28 ++++ UnitTest/WebRTCClient/main.cpp | 232 ++++++++++----------------- WebApplication/WebRTCClientPage.cpp | 4 +- 6 files changed, 310 insertions(+), 154 deletions(-) create mode 100644 UnitTest/WebRTCClient/Client.cpp create mode 100644 UnitTest/WebRTCClient/Client.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d39104f..ef41f76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,12 @@ set(KYLIN_WITH_NNG ON) # add_subdirectory(/mnt/e/Projects/Kylin Kylin) FetchContent_MakeAvailable(Kylin) +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui + GIT_TAG main # Important: Specify a version or a commit hash here. +) +FetchContent_MakeAvailable(ftxui) + add_subdirectory(Database) add_subdirectory(MediaServer) add_subdirectory(ToolKit) diff --git a/UnitTest/WebRTCClient/CMakeLists.txt b/UnitTest/WebRTCClient/CMakeLists.txt index 37d3450..1f3d7ad 100644 --- a/UnitTest/WebRTCClient/CMakeLists.txt +++ b/UnitTest/WebRTCClient/CMakeLists.txt @@ -4,11 +4,16 @@ find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) find_package(Boost COMPONENTS json REQUIRED) -add_executable(WebRTCClient main.cpp) +add_executable(WebRTCClient main.cpp + Client.h Client.cpp +) target_link_libraries(WebRTCClient PRIVATE LibDataChannel::LibDataChannel PRIVATE Threads::Threads PRIVATE Boost::json + PRIVATE ftxui::screen + PRIVATE ftxui::dom + PRIVATE ftxui::component PRIVATE Universal ) \ No newline at end of file diff --git a/UnitTest/WebRTCClient/Client.cpp b/UnitTest/WebRTCClient/Client.cpp new file mode 100644 index 0000000..1732ad4 --- /dev/null +++ b/UnitTest/WebRTCClient/Client.cpp @@ -0,0 +1,187 @@ +#include "Client.h" + +#include "BoostLog.h" +#include +#include +#include +#include +#include + +class ClientPrivate { +public: + Client *p = nullptr; + rtc::Configuration config; + std::shared_ptr ws; + std::unordered_map> peerConnectionMap; + std::unordered_map> dataChannelMap; + std::shared_ptr m_sender; + ClientPrivate(Client *p) : p(p) { + } + 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([this, id](std::shared_ptr dc) { + LOG(info) << "DataChannel from " << id << " received with label \"" << dc->label() << "\""; + + dc->onOpen([this, wdc = std::weak_ptr(dc)]() { + if (auto dc = wdc.lock()) dc->send("Hello from " + p->m_id); + }); + + 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; + } +}; + +bool Client::sendMessage(const std::string &message) { + bool ret = false; + if (m_d->m_sender) { + ret = m_d->m_sender->send(message); + } + return ret; +} + +bool Client::setRemoteId(const std::string &id) { + bool ret = false; + if (id.empty() || !m_d->ws->isOpen()) return ret; + if (id == m_id) { + LOG(error) << "Invalid remote ID (This is the local ID)"; + log("Invalid remote ID (This is the local ID)"); + return ret; + } + LOG(info) << "Offering to " << id; + log(std::format("Offering to {}", id)); + auto pc = m_d->createPeerConnection(m_d->config, m_d->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 << "\""; + log(std::format("Creating DataChannel with label \"{}\"", label)); + m_d->m_sender = pc->createDataChannel(label); + m_d->m_sender->onOpen([this, id, wdc = std::weak_ptr(m_d->m_sender)]() { + LOG(info) << "DataChannel from " << id << " open"; + if (auto dc = wdc.lock()) dc->send("Hello from " + m_id); + }); + m_d->m_sender->onClosed([id]() { LOG(info) << "DataChannel from " << id << " closed"; }); + m_d->m_sender->onMessage([id, wdc = std::weak_ptr(m_d->m_sender)](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(); + }); + m_d->dataChannelMap.emplace(id, m_d->m_sender); + return ret; +} + +std::string Client::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; +} + +Client::Client() : m_d{new ClientPrivate(this)} { + rtc::InitLogger(rtc::LogLevel::Info); + m_id = randomId(4); + LOG(info) << "The local ID is " << m_id; + log(std::format("The local ID is {}", m_id)); + + m_d->ws = std::make_shared(); + + m_d->ws->onOpen([this]() { + LOG(info) << "WebSocket connected, signaling ready"; + log("WebSocket connected, signaling ready"); + }); + m_d->ws->onError([this](std::string s) { + LOG(error) << "WebSocket error: " << s; + log(std::format("WebSocket error: {}", s)); + }); + m_d->ws->onClosed([]() { LOG(info) << "WebSocket closed"; }); + m_d->ws->onMessage([this](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 = m_d->peerConnectionMap.find(id); jt != m_d->peerConnectionMap.end()) { + pc = jt->second; + } else if (type == "offer") { + std::cout << "Answering to " + id << std::endl; + pc = m_d->createPeerConnection(m_d->config, m_d->ws, 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/{}", m_id); + LOG(info) << "WebSocket URL is " << url; + log(std::format("WebSocket URL is {}", url)); + m_d->ws->open(url); + + LOG(info) << "Waiting for signaling to be connected..."; + log("Waiting for signaling to be connected..."); +} + +Client::~Client() { + LOG(info) << "Cleaning up..."; + if (m_d != nullptr) { + delete m_d; + m_d = nullptr; + } +} + +std::string Client::id() const { + return m_id; +} + +void Client::log(const std::string &message) { + m_oss << message << std::endl; +} + +std::string Client::log() const { + return m_oss.str(); +} diff --git a/UnitTest/WebRTCClient/Client.h b/UnitTest/WebRTCClient/Client.h new file mode 100644 index 0000000..ccb4eba --- /dev/null +++ b/UnitTest/WebRTCClient/Client.h @@ -0,0 +1,28 @@ +#ifndef __CLIENT_H__ +#define __CLIENT_H__ + +#include +#include + +class ClientPrivate; + +class Client { + friend class ClientPrivate; + +public: + Client(); + ~Client(); + std::string id() const; + static std::string randomId(size_t length); + void log(const std::string &message); + std::string log() const; + bool setRemoteId(const std::string &id); + bool sendMessage(const std::string &message); + +private: + ClientPrivate *m_d = nullptr; + std::string m_id; + std::ostringstream m_oss; +}; + +#endif // __CLIENT_H__ \ No newline at end of file diff --git a/UnitTest/WebRTCClient/main.cpp b/UnitTest/WebRTCClient/main.cpp index e6ca2d7..b069613 100644 --- a/UnitTest/WebRTCClient/main.cpp +++ b/UnitTest/WebRTCClient/main.cpp @@ -1,162 +1,92 @@ #include "BoostLog.h" -#include -#include -#include -#include -#include +#include "Client.h" +#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; +ftxui::ButtonOption Style() { + using namespace ftxui; + auto option = ButtonOption::Animated(); + option.transform = [](const EntryState &s) { + auto element = text(s.label); + if (s.focused) { + element |= bold; } - auto id = static_cast(json.at("id").as_string()); - auto type = static_cast(json.at("type").as_string()); + return element | center | borderEmpty | flex; + }; + return option; +} - 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)); - } +int main(int argc, char const *argv[]) { + Client client; + std::string remoteId; + std::string message; + auto screen = ftxui::ScreenInteractive::Fullscreen(); + int depth = 0; + std::string dialogMessga = "请不要不要"; + ftxui::InputOption option; + option.multiline = false; + auto offerInput = ftxui::Input(remoteId, "remote id", option); + auto offerButton = ftxui::Button( + "Offer", + [&]() { + if (!client.setRemoteId(remoteId)) { + depth = 1; + dialogMessga = "请先连接信令服务器!"; + } + }, + ftxui::ButtonOption::Border()); + auto offerLayout = ftxui::Container::Horizontal({ + offerInput, + offerButton, }); - 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(); + auto messageInput = ftxui::Input(message, "请输入要发送的消息..."); + auto messageButton = ftxui::Button("发送", [&client, &message]() { client.sendMessage(message); }, ftxui::ButtonOption()); + auto messageLayout = ftxui::Container::Horizontal({ + messageInput, + messageButton, + }); - 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; + auto layout = ftxui::Container::Vertical({ + offerLayout, + messageLayout, + }); + + auto okButton = ftxui::Button("好的", [&depth]() { depth = 0; }); + auto dialog = ftxui::Renderer(okButton, [&]() { + return ftxui::vbox({ + ftxui::paragraph(dialogMessga), + ftxui::separator(), + ftxui::hbox({ftxui::filler(), okButton->Render()}), + }) | + ftxui::border; + }); + + auto mainPage = ftxui::Renderer(layout, [&] { + return ftxui::window(ftxui::text("WebRTC 测试"), + ftxui::vbox({ + ftxui::hbox({ftxui::text("ID: "), ftxui::text(client.id())}), + ftxui::separator(), + ftxui::paragraph(client.log()) | ftxui::flex, // logger + ftxui::separator(), + ftxui::text("通过信令服务器发送 offer") | ftxui::align_right, + ftxui::hbox({offerInput->Render(), offerButton->Render()}), + ftxui::separator(), + ftxui::text("通过 DataChannel 发送消息") | ftxui::align_right, + ftxui::hbox({messageInput->Render() | ftxui::yframe, messageButton->Render()}), + })); + }); + + auto component = ftxui::Container::Tab({mainPage, dialog}, &depth); + component = ftxui::Renderer(component, [&]() { + ftxui::Element document = mainPage->Render(); + + if (depth == 1) { + document = ftxui::dbox({document, dialog->Render() | ftxui::clear_under | ftxui::center}); } - LOG(info) << "Offering to " + id; - auto pc = createPeerConnection(config, ws, id); + return document; + }); - // 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(); + screen.Loop(component); 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/WebRTCClientPage.cpp b/WebApplication/WebRTCClientPage.cpp index d3903ab..71f74e1 100644 --- a/WebApplication/WebRTCClientPage.cpp +++ b/WebApplication/WebRTCClientPage.cpp @@ -39,8 +39,8 @@ WebRTCClientPage::WebRTCClientPage() : Wt::WTemplate(tr("Wt.WebRTC.Home")) { sendBtn->setText("发送"); sendBtn->setDisabled(true); - std::string url = "ws://127.0.0.1:8081/api/v1/webrtc/signal"; - + // std::string url = "ws://127.0.0.1:8081/api/v1/webrtc/signal"; + std::string url = std::format("ws://{}/api/v1/webrtc/signal", app->environment().hostName()); setJavaScriptMember(" WebRTCClient", std::format("new {}.WebRTCClient({},{},{},{},{},{},{}, '{}', '{}');", WT_CLASS, WT_CLASS, jsRef(), offerId->jsRef(), offerBtn->jsRef(), sendMsg->jsRef(), sendBtn->jsRef(), textBrowser->jsRef(), localId, url));