@@ -2759,6 +2759,7 @@ TransportStatus MoqtSession::publish_live(std::istream& input,
27592759
27602760 if (auto_forward_) {
27612761 // Forward mode: publish media as fragments arrive, independent of downstream subscriptions.
2762+ std::optional<std::chrono::steady_clock::time_point> eof_deadline;
27622763 while (true ) {
27632764 // Drain media continuously in auto-forward mode.
27642765 {
@@ -2774,13 +2775,6 @@ TransportStatus MoqtSession::publish_live(std::istream& input,
27742775 std::lock_guard<std::mutex> lock (queue->mutex );
27752776 is_eof = queue->eof && queue->fragments .empty ();
27762777 }
2777- if (is_eof) {
2778- break ;
2779- }
2780-
2781- // Brief sleep to avoid busy-waiting
2782- std::this_thread::sleep_for (std::chrono::milliseconds (1 ));
2783-
27842778 // Read and process control messages (SUBSCRIBE, SUBSCRIBE_NAMESPACE)
27852779 std::vector<std::uint8_t > chunk;
27862780 bool fin = false ;
@@ -2794,6 +2788,21 @@ TransportStatus MoqtSession::publish_live(std::istream& input,
27942788 stdin_thread.join ();
27952789 return ctrl_status;
27962790 }
2791+
2792+ // Keep polling control briefly after EOF so downstream can still request
2793+ // one-shot tracks (catalog) before we tear down the session.
2794+ if (is_eof) {
2795+ if (!eof_deadline.has_value ()) {
2796+ eof_deadline = std::chrono::steady_clock::now () + subscriber_timeout_;
2797+ } else if (std::chrono::steady_clock::now () >= *eof_deadline) {
2798+ break ;
2799+ }
2800+ } else {
2801+ eof_deadline.reset ();
2802+ }
2803+
2804+ // Brief sleep to avoid busy-waiting
2805+ std::this_thread::sleep_for (std::chrono::milliseconds (1 ));
27972806 }
27982807 } else {
27992808 // Await-subscribe mode: wait for subscriptions, then stream
0 commit comments