55#include < map>
66#include < mutex>
77#include < string>
8+ #include < thread>
89#include < vector>
910
1011#include " roqr/gateway/bridge.hpp"
1112#include " roqr/gateway/gap.hpp"
13+ #include " roqr/gateway/player_queue.hpp"
1214#include " roqr/gateway/rtmp_commands.hpp"
1315#include " roqr/quic/client.hpp"
1416#include " roqr/rtmp/classify.hpp"
@@ -39,10 +41,34 @@ struct EgressGateway::Impl {
3941
4042 roqr::gateway::GapTracker gaps; // draft s8, defined in gap.hpp
4143
44+ // Bounded queue + dedicated writer thread (draft s11): on_frame (QUIC
45+ // network thread) and on_player_play (session thread) only enqueue;
46+ // the writer thread does the blocking player->send, so a stalled RTMP
47+ // player never wedges the QUIC loop. Must be declared before `client`
48+ // (see below) so they're destroyed after it re: construction order,
49+ // but they're joined/closed explicitly in stop() well before that.
50+ static constexpr size_t kMaxQueuedMessages = 512 ;
51+ PlayerQueue queue{kMaxQueuedMessages };
52+ std::thread writer;
53+ bool writer_started = false ;
54+
4255 // Declared last: ~Client joins the network thread before the members
4356 // its handlers touch are destroyed.
4457 roqr::quic::Client client;
4558
59+ void writer_loop () {
60+ for (;;) {
61+ auto msg = queue.pop ();
62+ if (!msg) return ; // closed and drained
63+ roqr::rtmp::ServerSession* p = nullptr ;
64+ {
65+ std::lock_guard lock (player_mutex);
66+ if (player_ready) p = player;
67+ }
68+ if (p != nullptr ) p->send (*msg);
69+ }
70+ }
71+
4672 static bool is_init (const roqr::rtmp::RtmpMessage& msg) {
4773 if (msg.type == roqr::rtmp::kTypeDataAmf0 || msg.type == 15 /* AMF3 data */ )
4874 return true ;
@@ -67,23 +93,32 @@ struct EgressGateway::Impl {
6793 if (msg.type == roqr::rtmp::kTypeCommandAmf0 ) return ; // relay replies
6894 if (msg.type == 9 && !accept_video (msg)) return ;
6995
70- std::lock_guard lock (player_mutex);
71- if (is_init (msg)) {
72- if (init_cache.find (msg.type ) == init_cache.end ()) {
73- init_types.push_back (msg.type );
96+ const bool init = is_init (msg);
97+ {
98+ std::lock_guard lock (player_mutex);
99+ if (init) {
100+ if (init_cache.find (msg.type ) == init_cache.end ()) {
101+ init_types.push_back (msg.type );
102+ }
103+ init_cache[msg.type ] = msg;
74104 }
75- init_cache[msg. type ] = msg;
105+ if (!player_ready) return ; // pre-play: cache only, don't enqueue
76106 }
77- if (player != nullptr && player_ready) player->send (msg);
107+ queue.push (std::move (msg),
108+ init ? PlayerQueue::Kind::Init : PlayerQueue::Kind::Coded);
78109 }
79110
80111 // Called on the session thread when the player issues play (after the
81- // RTMP handshake). Primes it with the cached init frames, then opens
82- // live delivery.
112+ // RTMP handshake). Enqueues the cached init frames (so the writer
113+ // thread delivers them, not the session thread) and sets player_ready
114+ // under the lock before releasing, so on_frame cannot interleave a live
115+ // coded frame ahead of the init frames.
83116 void on_player_play () {
84117 std::lock_guard lock (player_mutex);
85118 if (player == nullptr ) return ;
86- for (uint8_t type : init_types) player->send (init_cache.at (type));
119+ for (uint8_t type : init_types) {
120+ queue.push (init_cache.at (type), PlayerQueue::Kind::Init);
121+ }
87122 player_ready = true ;
88123 }
89124
@@ -113,12 +148,24 @@ EgressGateway::~EgressGateway() { stop(); }
113148bool EgressGateway::start (const EgressOptions& options) {
114149 impl_->options = options;
115150 Impl* impl = impl_.get ();
151+ if (!impl->writer_started ) {
152+ impl->writer = std::thread ([impl] { impl->writer_loop (); });
153+ impl->writer_started = true ;
154+ }
116155 impl->begin_play (); // connect + play before accepting the player
117156 return impl->listener .start (
118157 options.rtmp_port ,
119158 [impl](roqr::rtmp::ServerSession& s) {
120159 {
121160 std::lock_guard lock (impl->player_mutex );
161+ // A previous player may still be alive (e.g. a stalled one
162+ // that never disconnected): shut its fd down so any writer
163+ // blocked in its send() unblocks, then drop its queued
164+ // frames so the new player isn't fed stale, prior-era media
165+ // ahead of its own init frames.
166+ if (impl->player != nullptr && impl->player != &s)
167+ impl->player ->close ();
168+ impl->queue .clear ();
122169 impl->player = &s;
123170 impl->player_ready = false ;
124171 }
@@ -139,9 +186,26 @@ bool EgressGateway::start(const EgressOptions& options) {
139186}
140187
141188void EgressGateway::stop () {
142- impl_-> listener . stop ();
189+ // 1. Stop the QUIC network thread so on_frame stops enqueuing.
143190 impl_->client .close ();
144191 impl_->client .wait_closed (std::chrono::seconds (2 ));
192+ // 2. Unblock a writer that may be stuck in a blocking send to a stalled
193+ // player: shut down the player's fd so the send returns.
194+ {
195+ std::lock_guard lock (impl_->player_mutex );
196+ if (impl_->player != nullptr ) impl_->player ->close ();
197+ }
198+ // 3. Close the queue and join the writer (it drains, sends fail fast on
199+ // the shut-down fd, then exits).
200+ impl_->queue .close ();
201+ if (impl_->writer .joinable ()) impl_->writer .join ();
202+ impl_->writer_started = false ;
203+ // 4. Now no thread touches the player: safe to destroy the sessions.
204+ impl_->listener .stop ();
205+ }
206+
207+ uint64_t EgressGateway::frames_dropped () const {
208+ return impl_->queue .dropped ();
145209}
146210
147211bool EgressGateway::wait_playing (std::chrono::milliseconds timeout) {
0 commit comments