@@ -2615,6 +2615,95 @@ int main() {
26152615 " expected live publish to finish with PUBLISH_NAMESPACE_DONE" );
26162616 }
26172617
2618+ {
2619+ // Regression for the stack owner's required behavior: once publish_live()
2620+ // has sent PUBLISH_NAMESPACE, a relay that never acknowledges it must NOT
2621+ // cause the session to tear down. moqxr should still publish its tracks
2622+ // and enter the await-subscriptions loop, exiting only via the normal
2623+ // subscriber-timeout idle path (or when the RTMP/stdin source ends), not
2624+ // because the ack never arrived. Before the fix, collect_control_
2625+ // acknowledgements()'s read_stream timeout ("timed out waiting for
2626+ // stream data") was propagated straight out of publish_live() as fatal.
2627+ MockTransport no_ack_transport;
2628+ no_ack_transport.reads [0 ].push_back (encode_server_setup_message ({
2629+ .draft = DraftVersion::kDraft16 ,
2630+ .max_request_id = 8 ,
2631+ }));
2632+ // Deliberately no PUBLISH_NAMESPACE_OK/REQUEST_OK queued for stream 0:
2633+ // once the setup message is consumed, every further read on the control
2634+ // stream reports the same failure a real transport reports for a
2635+ // genuine per-read timeout with a healthy connection.
2636+ no_ack_transport.missing_read_error = " timed out waiting for stream data" ;
2637+
2638+ MoqtSession no_ack_session (
2639+ no_ack_transport, std::string (kTestTrackNamespace ), false , false , false , std::chrono::seconds (1 ));
2640+ status = no_ack_session.connect (endpoint, tls);
2641+ ok &= expect (status.ok , " expected no-ack session connect to succeed" );
2642+
2643+ const auto no_ack_bytes = make_live_init_mp4 ();
2644+ std::string no_ack_input_bytes (no_ack_bytes.begin (), no_ack_bytes.end ());
2645+ std::istringstream no_ack_input (no_ack_input_bytes);
2646+ status = no_ack_session.publish_live (no_ack_input, DraftVersion::kDraft16 , false );
2647+ ok &= expect (status.ok ,
2648+ " expected publish_live to tolerate a missing namespace acknowledgement "
2649+ " rather than tearing the session down" );
2650+ ok &= expect (control_message_count (no_ack_transport, 0x1d ) == 1 ,
2651+ " expected publish_live to still preannounce media tracks after a missing ack" );
2652+ ok &= expect (!no_ack_transport.writes .empty () &&
2653+ message_type (no_ack_transport.writes .back ().bytes ) == 0x09 ,
2654+ " expected publish_live to still reach a clean PUBLISH_NAMESPACE_DONE exit "
2655+ " via the normal idle await-subscribe timeout, not an error return" );
2656+ }
2657+
2658+ {
2659+ // Companion regression: an acknowledgement that arrives late (after
2660+ // collect_control_acknowledgements() has already given up and
2661+ // publish_live() has moved on) must be consumed harmlessly by the main
2662+ // control-message loop rather than being lost or mis-parsed as a
2663+ // protocol violation. PUBLISH_NAMESPACE_OK/REQUEST_OK (type 0x07) is not
2664+ // one of the message types process_control_messages() acts on, so it
2665+ // should simply be drained off pending_control_bytes_ once it shows up.
2666+ MockTransport late_ack_transport;
2667+ late_ack_transport.reads [0 ].push_back (encode_server_setup_message ({
2668+ .draft = DraftVersion::kDraft16 ,
2669+ .max_request_id = 8 ,
2670+ }));
2671+ // No ack queued yet -- the first collect_control_acknowledgements() read
2672+ // times out (same mechanism as the no-ack case above). on_read injects
2673+ // the ack just before the *second* read of stream 0, modeling a relay
2674+ // that answers slightly later than the initial short wait -- this is
2675+ // read by the main await-subscribe loop, not by
2676+ // collect_control_acknowledgements() (which has already returned).
2677+ std::size_t stream0_reads = 0 ;
2678+ late_ack_transport.on_read = [&stream0_reads](MockTransport& transport, std::uint64_t stream_id) {
2679+ if (stream_id != 0 ) {
2680+ return ;
2681+ }
2682+ ++stream0_reads;
2683+ if (stream0_reads == 2 ) {
2684+ transport.reads [0 ].push_back (encode_publish_namespace_ok_message (DraftVersion::kDraft16 , 0 ));
2685+ }
2686+ };
2687+
2688+ MoqtSession late_ack_session (
2689+ late_ack_transport, std::string (kTestTrackNamespace ), false , false , false , std::chrono::seconds (1 ));
2690+ status = late_ack_session.connect (endpoint, tls);
2691+ ok &= expect (status.ok , " expected late-ack session connect to succeed" );
2692+
2693+ const auto late_ack_bytes = make_live_init_mp4 ();
2694+ std::string late_ack_input_bytes (late_ack_bytes.begin (), late_ack_bytes.end ());
2695+ std::istringstream late_ack_input (late_ack_input_bytes);
2696+ status = late_ack_session.publish_live (late_ack_input, DraftVersion::kDraft16 , false );
2697+ ok &= expect (status.ok ,
2698+ " expected a late-arriving namespace acknowledgement to be consumed without "
2699+ " surfacing as a protocol violation" );
2700+ ok &= expect (stream0_reads >= 2 ,
2701+ " expected the main loop to perform a second read that picks up the late ack" );
2702+ ok &= expect (!late_ack_transport.writes .empty () &&
2703+ message_type (late_ack_transport.writes .back ().bytes ) == 0x09 ,
2704+ " expected the late-ack session to still reach a clean PUBLISH_NAMESPACE_DONE exit" );
2705+ }
2706+
26182707 {
26192708 // Phase 5 regression: CMSF 4.1.2 makes an absent contentProtectionRefIDs
26202709 // mean "not protected", so a CENC-protected track (encv/sinf/schm/schi/
0 commit comments