RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
PeerLink.cpp
Go to the documentation of this file.
1#include "PeerLink.h"
2#include "NetworkManager.h"
3#include "Message.h"
4#include "Logger.h"
5#include "NetworkUtilities.h"
6
7PeerLink::PeerLink(const std::string& id, std::weak_ptr<NetworkManager> parent) :
8 peerId(id), network_manager(parent)
9{
10 if (auto nm = network_manager.lock())
11 {
12 auto config = nm->getRTCConfig();
13 config.iceServers.push_back({"stun:stun.l.google.com:19302"}); // Google
14 config.iceServers.push_back({"stun:stun1.l.google.com:19302"}); // Google alt
15 config.iceServers.push_back({"stun:stun.stunprotocol.org:3478"}); // Community server
16 pc = std::make_shared<rtc::PeerConnection>(config);
18 }
19 else
20 {
21 throw std::runtime_error("NetworkManager expired");
22 }
23}
24
25// PeerLink.cpp
26void PeerLink::setDisplayName(std::string n)
27{
28 displayName_ = std::move(n);
29}
30
31const std::string& PeerLink::displayName() const
32{
33 return displayName_;
34}
35
36//void PeerLink::send(const std::string& msg) {
37// if (dc && dc->isOpen()) {
38// dc->send(msg);
39// }
40//}
41
42//void PeerLink::createDataChannel(const std::string& label) {
43// if (!pc) return; // ensure pc exists
44// if (dc && dc->isOpen()) return; // idempotent-ish: keep existing dc
45//
46// dc = pc->createDataChannel(label); // negotiated=false by default
47// attachChannelHandlers(dc);
48//}
49
51{
52 if (!pc)
53 return;
54
55 rtc::DataChannelInit init; // default reliable/ordered
56 rtc::DataChannelInit markerMoveInit; //
57 auto reliability = rtc::Reliability{};
58 reliability.unordered = true;
59 reliability.maxPacketLifeTime = std::chrono::milliseconds(500);
60 //reliability.maxRetransmits = 0;
61 markerMoveInit.reliability = reliability;
62
63 // Offerer creates channels; answerer gets them in pc->onDataChannel
64 auto dcGame = pc->createDataChannel(std::string(msg::dc::name::Game), init);
65 dcs_[std::string(msg::dc::name::Game)] = dcGame;
66 attachChannelHandlers(dcGame, std::string(msg::dc::name::Game));
67
68 auto dcChat = pc->createDataChannel(std::string(msg::dc::name::Chat), init);
69 dcs_[std::string(msg::dc::name::Chat)] = dcChat;
70 attachChannelHandlers(dcChat, std::string(msg::dc::name::Chat));
71
72 auto dcNotes = pc->createDataChannel(std::string(msg::dc::name::Notes), init);
73 dcs_[std::string(msg::dc::name::Notes)] = dcNotes;
74 attachChannelHandlers(dcNotes, std::string(msg::dc::name::Notes));
75
76 auto dcMarkerMove = pc->createDataChannel(std::string(msg::dc::name::MarkerMove), markerMoveInit);
77 dcs_[std::string(msg::dc::name::MarkerMove)] = dcMarkerMove;
78 attachChannelHandlers(dcMarkerMove, std::string(msg::dc::name::MarkerMove));
79}
80
81rtc::Description PeerLink::createOffer()
82{
83 auto offer = pc->createOffer();
84 rtc::LocalDescriptionInit init;
85 init.icePwd = offer.icePwd(); // string
86 init.iceUfrag = offer.iceUfrag(); // string
87 pc->setLocalDescription(offer.type(), init);
88 return offer;
89}
90
91rtc::Description PeerLink::createAnswer()
92{
93 auto answer = pc->createAnswer();
94 rtc::LocalDescriptionInit init;
95 init.icePwd = answer.icePwd();
96 init.iceUfrag = answer.iceUfrag();
97 pc->setLocalDescription(answer.type(), init);
98 return answer; // Send this via signaling
99}
100
101void PeerLink::setRemoteDescription(const rtc::Description& desc)
102{
103 pc->setRemoteDescription(desc);
104 {
105 std::lock_guard<std::mutex> lk(candMx_);
106 remoteDescSet_.store(true, std::memory_order_release);
107 for (auto& c : pendingRemoteCandidates_)
108 {
109 pc->addRemoteCandidate(c);
110 }
112 }
113}
114
115void PeerLink::addIceCandidate(const rtc::Candidate& candidate)
116{
117 std::lock_guard<std::mutex> lk(candMx_);
118 if (!remoteDescSet_.load(std::memory_order_acquire))
119 {
120 pendingRemoteCandidates_.push_back(candidate);
121 }
122 else
123 {
124 pc->addRemoteCandidate(candidate);
125 }
126}
127
129{
130 pc->onStateChange([this](rtc::PeerConnection::State s)
131 {
132 lastState_ = s;
133 lastStateAt_ = std::chrono::seconds().count();
134 std::cout << "[PeerLink] State(" << peerId << "): " << (int)s << "at " << lastStateAt_ << "\n";
135
136 if (s == rtc::PeerConnection::State::Closed || s == rtc::PeerConnection::State::Failed) {
137 if (auto nm = network_manager.lock()) {
139 nm->events_.push(std::move(ev));
140 }
141 }
142 if (s == rtc::PeerConnection::State::Connected) {
143 if (auto nm = network_manager.lock()) {
145 nm->events_.push(std::move(ev));
146 }
147 } });
148
149 pc->onLocalDescription([wk = network_manager, id = peerId](rtc::Description desc)
150 {
151 if (auto nm = wk.lock()) nm->onPeerLocalDescription(id, desc); });
152
153 pc->onLocalCandidate([wk = network_manager, id = peerId](rtc::Candidate cand)
154 {
155 if (auto nm = wk.lock()) nm->onPeerLocalCandidate(id, cand); });
156
157 pc->onDataChannel([this](std::shared_ptr<rtc::DataChannel> ch)
158 {
159 const std::string label = ch->label();
160 dcs_[label] = ch;
161 attachChannelHandlers(ch, label);
162 std::cout << "[PeerLink] Received DC \"" << label << "\" from " << peerId << "\n"; });
163}
164//
165//void PeerLink::sendOn(const std::string& label, const std::vector<std::byte>& bytes) {
166// auto it = dcs_.find(label);
167// if (it == dcs_.end() || !it->second || !it->second->isOpen()) return;
168// it->second->send(&bytes.front(), bytes.size());
169//}
170
171bool PeerLink::sendOn(const std::string& label, std::string_view text)
172{
173 auto it = dcs_.find(label);
174 if (it == dcs_.end() || !it->second)
175 return false;
176 auto& ch = it->second;
177 if (!ch->isOpen())
178 return false;
179
180 // optional backpressure guard
181 // if (ch->bufferedAmount() > kMaxBufferedBytes) return false;
182
183 ch->send(std::string(text)); // TEXT frame over DC
184 return true;
185}
186
187bool PeerLink::sendOn(const std::string& label, const std::vector<uint8_t>& bytes)
188{
189 auto it = dcs_.find(label);
190 if (it == dcs_.end() || !it->second)
191 return false;
192 auto& ch = it->second;
193 if (!ch->isOpen())
194 return false;
195
197 //if (ch->bufferedAmount() > kMaxBufferedBytes)
198 //{
199 // // You can queue locally instead of dropping, if you want
200 // return false;
201 //}
202 rtc::binary b;
203 b.resize(bytes.size());
204 if (!bytes.empty())
205 {
206 std::memcpy(b.data(), bytes.data(), bytes.size()); // std::byte is trivially copyable
207 }
208 ch->send(b); // libdatachannel handles SCTP fragmentation
209 return true;
210}
211bool PeerLink::sendGame(const std::vector<uint8_t>& bytes)
212{
213 return sendOn(std::string(msg::dc::name::Game), bytes);
214}
215
216bool PeerLink::sendChat(const std::vector<uint8_t>& bytes)
217{
218 return sendOn(std::string(msg::dc::name::Chat), bytes);
219}
220
221bool PeerLink::sendNote(const std::vector<uint8_t>& bytes)
222{
223 return sendOn(std::string(msg::dc::name::Notes), bytes);
224}
225
226bool PeerLink::sendMarkerMove(const std::vector<uint8_t>& bytes)
227{
228 return sendOn(std::string(msg::dc::name::MarkerMove), bytes);
229}
230void PeerLink::sendChatJson(const std::string& jsonText)
231{
232 sendOn(msg::dc::name::Chat, jsonText);
233}
234
236{
237 // minimal: only require the channel used for snapshots
238 auto it = dcOpen_.find(msg::dc::name::Game);
239 return it != dcOpen_.end() && it->second;
240}
241
242void PeerLink::attachChannelHandlers(const std::shared_ptr<rtc::DataChannel>& dc, const std::string& label)
243{
244 if (!dc)
245 return;
246
247 dc->onOpen([this, id = peerId, label]()
248 {
249 std::cout << "[PeerLink] DC open \"" << label << "\" to " << id << "\n";
250 if (auto nm = network_manager.lock())
251 {
252 msg::NetEvent ev{msg::NetEvent::Type::DcOpen, id, label};
253 nm->events_.push(std::move(ev)); // or nm->notifyDcOpen(id, label);
254 }
255 dcOpen_[label] = true; });
256
257 dc->onClosed([this, id = peerId, label]()
258 {
259 std::cout << "[PeerLink] DC closed \"" << label << "\" to " << id << "\n";
260 dcOpen_[label] = false;
261 bootstrapSent_ = false;
262 if (auto nm = network_manager.lock()) {
264 nm->events_.push(std::move(ev));
265 } });
266
267 dc->onMessage([this, id = peerId, label](rtc::message_variant m)
268 {
269 Logger::instance().log("localtunnel", Logger::Level::Info, "MESSAGE RECEIVED!! FROM: "+label);
270 if (auto nm = network_manager.lock())
271 {
272 if (std::holds_alternative<std::string>(m))
273 {
274 const auto& s = std::get<std::string>(m);
275 std::vector<uint8_t> bytes(s.begin(), s.end());
276 nm->inboundRaw_.push(msg::InboundRaw{peerId, label, std::move(bytes)});
277 }
278 else
279 {
280 const auto& bin = std::get<rtc::binary>(m);
281 std::vector<uint8_t> bytes(bin.size());
282 std::memcpy(bytes.data(), bin.data(), bin.size());
283 nm->inboundRaw_.push(msg::InboundRaw{peerId, label, std::move(bytes)});
284 }
285 } });
286}
287
289{
290 // “usable” = PC connected AND at least Game channel open
291 auto it = dcs_.find(std::string(msg::dc::name::Game));
292 return isPcConnectedOnly() && it != dcs_.end() && it->second && it->second->isOpen();
293}
294
296{
297 for (auto& [label, ch] : dcs_)
298 {
299 if (ch && ch->isOpen())
300 return true;
301 }
302 return false;
303}
304
306{
307 return pc && pc->state() == rtc::PeerConnection::State::Connected;
308}
309
310/*void PeerLink::close()
311{
312 // Close datachannel first
313 for (auto& [label, ch] : dcs_)
314 {
315 if (ch)
316 {
317 try
318 {
319 ch->close();
320 }
321 catch (...)
322 {
323 }
324 }
325 }
326 dcs_.clear();
327
328 if (pc)
329 {
330 try
331 {
332 pc->close();
333 }
334 catch (...)
335 {
336 }
337 }
338 pc.reset();
339 if (auto nm = network_manager.lock())
340 nm->removePeer(peerId);
341}*/
343{
344 static std::atomic<uint64_t> guardSeq{0};
345 const uint64_t seq = ++guardSeq;
346
347 if (closing_.exchange(true))
348 {
349 Logger::instance().log("main", Logger::Level::Debug, "PeerLink::close() re-entry ignored");
350 return;
351 }
352
353 Logger::instance().log("main", Logger::Level::Debug, "PeerLink::close() begin #" + std::to_string(seq));
354
355 try
356 {
357 for (auto& [label, ch] : dcs_)
358 {
359 if (!ch)
360 continue;
361 ch->onOpen(nullptr);
362 ch->onMessage(nullptr);
363 ch->onBufferedAmountLow(nullptr);
364 ch->onClosed(nullptr);
365 ch->onError(nullptr);
366 }
367 if (pc)
368 {
369 pc->onStateChange(nullptr);
370 pc->onGatheringStateChange(nullptr);
371 pc->onLocalDescription(nullptr);
372 pc->onDataChannel(nullptr);
373 pc->onTrack(nullptr);
374 }
375 }
376 catch (...)
377 {
378 // ignore
379 }
380
381 std::unordered_map<std::string, std::shared_ptr<rtc::DataChannel>> movedDcs;
382 movedDcs.swap(dcs_);
383
384 for (auto& kv : movedDcs)
385 {
387 }
388 movedDcs.clear();
389
391
392 Logger::instance().log("main", Logger::Level::Debug, "PeerLink::close() end #" + std::to_string(seq));
393}
394
395rtc::PeerConnection::State PeerLink::pcState() const
396{
397 if (!pc)
398 return rtc::PeerConnection::State::Closed;
399 return pc->state();
400}
401
403{
404 if (!pc)
405 return true;
406 auto s = pc->state();
407 return s == rtc::PeerConnection::State::Closed ||
408 s == rtc::PeerConnection::State::Failed;
409}
410
411const char* PeerLink::pcStateString() const
412{
413 auto s = pcState();
414 using S = rtc::PeerConnection::State;
415 switch (s)
416 {
417 case S::New:
418 return "New";
419 case S::Connecting:
420 return "Connecting";
421 case S::Connected:
422 return "Connected";
423 case S::Disconnected:
424 return "Disconnected";
425 case S::Failed:
426 return "Failed";
427 case S::Closed:
428 return "Closed";
429 }
430 return "Unknown";
431}
432
433//if (std::holds_alternative<std::string>(m)) {
434// const auto& s = std::get<std::string>(m);
435// // Route text by label if you wish; for now, just log & (optionally) notify NM later
436// if (label == msg::dc::name::Chat) {
437// // TODO: add NM hook: nm->onPeerChatText(peerId, s);
438// std::cout << "[PeerLink] CHAT(" << peerId << "): " << s << "\n";
439// }
440// else {
441// std::cout << "[PeerLink] TEXT(" << label << " from " << peerId << "): " << s << "\n";
442// }
443// return;
444//}
445
446//toBytes(msg) → bytes
447// inboxRaw_.push({peerId, label, std::move(bytes)})
449//const auto& bin = std::get<rtc::binary>(m);
450//std::vector<uint8_t> bytes(bin.size());
451//std::memcpy(bytes.data(), bin.data(), bin.size());
452
453//if (auto nm = network_manager.lock()) {
454// if (label == msg::dc::name::Game) {
455// // Your existing binary entrypoint:
456// nm->onDcGameBinary(peerId, bytes);
457// }
458// else if (label == msg::dc::name::Chat) {
459// // Optional: add nm->onPeerChatBinary(peerId, bytes) later
460// std::cout << "[PeerLink] CHAT/BIN " << bytes.size() << "B from " << peerId << "\n";
461// }
462// else if (label == msg::dc::name::Notes) {
463// // Optional: nm->onPeerNotesBinary(peerId, bytes)
464// std::cout << "[PeerLink] NOTES/BIN " << bytes.size() << "B from " << peerId << "\n";
465// }
466// else {
467// // Unknown label, forward to generic handler if you add one later
468// std::cout << "[PeerLink] DC(" << label << ") BIN " << bytes.size() << "B from " << peerId << "\n";
469// }
470//}
static Logger & instance()
Definition Logger.h:39
static void safeCloseDataChannel(std::shared_ptr< rtc::DataChannel > &dc)
static void safeClosePeerConnection(std::shared_ptr< rtc::PeerConnection > &pc)
constexpr std::string Chat
Definition Message.h:298
constexpr std::string Game
Definition Message.h:297
constexpr std::string Notes
Definition Message.h:299
constexpr std::string MarkerMove
Definition Message.h:300