Skip to content

Commit 4e46ccc

Browse files
authored
Merge pull request #20 from mondain/fix/publish-live-namespace-ack-nonfatal
publish_live: don't tear down the session on a missing or late namespace ack
2 parents 6083ab4 + 096840e commit 4e46ccc

2 files changed

Lines changed: 126 additions & 6 deletions

File tree

src/transport/moqt_session.cpp

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,14 @@ TransportStatus collect_control_acknowledgements(PublisherTransport& transport,
839839
std::vector<std::uint8_t> chunk;
840840
const TransportStatus status = transport.read_stream(control_stream_id, chunk, fin, std::chrono::seconds(2));
841841
if (!status.ok) {
842+
// Preserve whatever bytes we already parsed-but-deferred (or received
843+
// but hadn't parsed yet) before bailing out. A caller that treats this
844+
// failure as non-fatal -- e.g. publish_live() tolerating a missing
845+
// namespace acknowledgement -- still needs those bytes available to
846+
// its own control-message loop, so a late-arriving ack is not
847+
// silently dropped here.
848+
deferred_messages.insert(deferred_messages.end(), buffer.begin(), buffer.end());
849+
pending_control_bytes = std::move(deferred_messages);
842850
return status;
843851
}
844852
if (trace_enabled()) {
@@ -3766,15 +3774,38 @@ TransportStatus MoqtSession::publish_live(std::istream& input,
37663774
if (status.ok) {
37673775
namespace_stream_open_ = true;
37683776
}
3777+
if (!status.ok) {
3778+
return status;
3779+
}
37693780
} else {
37703781
status = write_frame(control_stream_id_, encode_namespace_message(namespace_message), false);
3771-
if (status.ok) {
3772-
status = collect_control_acknowledgements(
3773-
transport_, control_stream_id_, draft_version, 1, 0, pending_control_bytes_);
3782+
if (!status.ok) {
3783+
// The write itself failed -- the transport is broken. Always fatal.
3784+
return status;
3785+
}
3786+
status = collect_control_acknowledgements(
3787+
transport_, control_stream_id_, draft_version, 1, 0, pending_control_bytes_);
3788+
if (!status.ok) {
3789+
if (status.message != "timed out waiting for stream data") {
3790+
// A real transport failure (connection closed/reset, malformed
3791+
// response, peer rejection, ...) -- still fatal.
3792+
return status;
3793+
}
3794+
// Only the per-read wait (collect_control_acknowledgements's 2s
3795+
// read_stream timeout) elapsed with no PUBLISH_NAMESPACE_OK/
3796+
// REQUEST_OK; the connection itself is healthy. Per the live-publish
3797+
// contract, a missing/late namespace acknowledgement must not tear
3798+
// the session down: moqxr stays ready, publishes its tracks, and
3799+
// waits for subscriptions until the RTMP source itself ends. Any
3800+
// ack that does arrive later is still sitting in
3801+
// pending_control_bytes_ (collect_control_acknowledgements
3802+
// preserves it even on this early return) and will be parsed
3803+
// harmlessly -- and silently, since it isn't one of the message
3804+
// types process_control_messages() acts on -- once the
3805+
// await-subscriptions loop below starts draining control bytes.
3806+
std::cerr << "[moqt-session] live: no namespace acknowledgement within timeout; "
3807+
"continuing without waiting further (relay may ack late or not at all)\n";
37743808
}
3775-
}
3776-
if (!status.ok) {
3777-
return status;
37783809
}
37793810

37803811
// Build track alias map (self-assigned, matching legacy serve_subscriptions behavior)

tests/moqt_session_test.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)