Split SUBSCRIBE_NAMESPACE and SUBSCRIBE_TRACKS. Remove SubscribeNamespaceOption, maintain SUBSCRIBE_NAMESPACE functionality, and provide stubs and parser/framer support for SUBSCRIBE_TRACKS.

Part of draft-18 migration.

PiperOrigin-RevId: 949714141
diff --git a/quiche/quic/moqt/moqt_framer.cc b/quiche/quic/moqt/moqt_framer.cc
index d446967..81bd37a 100644
--- a/quiche/quic/moqt/moqt_framer.cc
+++ b/quiche/quic/moqt/moqt_framer.cc
@@ -612,7 +612,14 @@
   return SerializeControlMessage(
       MoqtMessageType::kSubscribeNamespace, WireMoqVarInt(message.request_id),
       WireTrackNamespace(message.track_namespace_prefix),
-      WireMoqVarInt(message.subscribe_options),
+      WireKeyValuePairList(message.parameters.ToKeyValuePairList()));
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeSubscribeTracks(
+    const MoqtSubscribeTracks& message) {
+  return SerializeControlMessage(
+      MoqtMessageType::kSubscribeTracks, WireMoqVarInt(message.request_id),
+      WireTrackNamespace(message.track_namespace_prefix),
       WireKeyValuePairList(message.parameters.ToKeyValuePairList()));
 }
 
diff --git a/quiche/quic/moqt/moqt_framer.h b/quiche/quic/moqt/moqt_framer.h
index 0d8df94..e449986 100644
--- a/quiche/quic/moqt/moqt_framer.h
+++ b/quiche/quic/moqt/moqt_framer.h
@@ -68,6 +68,8 @@
   quiche::QuicheBuffer SerializeGoAway(const MoqtGoAway& message);
   quiche::QuicheBuffer SerializeSubscribeNamespace(
       const MoqtSubscribeNamespace& message);
+  quiche::QuicheBuffer SerializeSubscribeTracks(
+      const MoqtSubscribeTracks& message);
   quiche::QuicheBuffer SerializeMaxRequestId(const MoqtMaxRequestId& message);
   quiche::QuicheBuffer SerializeFetch(const MoqtFetch& message);
   quiche::QuicheBuffer SerializeFetchCancel(const MoqtFetchCancel& message);
diff --git a/quiche/quic/moqt/moqt_framer_test.cc b/quiche/quic/moqt/moqt_framer_test.cc
index 2000e69..3a6992a 100644
--- a/quiche/quic/moqt/moqt_framer_test.cc
+++ b/quiche/quic/moqt/moqt_framer_test.cc
@@ -55,6 +55,7 @@
       MoqtMessageType::kTrackStatus,
       MoqtMessageType::kGoAway,
       MoqtMessageType::kSubscribeNamespace,
+      MoqtMessageType::kSubscribeTracks,
       MoqtMessageType::kMaxRequestId,
       MoqtMessageType::kFetch,
       MoqtMessageType::kFetchCancel,
@@ -187,6 +188,10 @@
         auto data = std::get<MoqtSubscribeNamespace>(structured_data);
         return framer_.SerializeSubscribeNamespace(data);
       }
+      case moqt::MoqtMessageType::kSubscribeTracks: {
+        auto data = std::get<MoqtSubscribeTracks>(structured_data);
+        return framer_.SerializeSubscribeTracks(data);
+      }
       case moqt::MoqtMessageType::kMaxRequestId: {
         auto data = std::get<MoqtMaxRequestId>(structured_data);
         return framer_.SerializeMaxRequestId(data);
diff --git a/quiche/quic/moqt/moqt_messages.cc b/quiche/quic/moqt/moqt_messages.cc
index 3512ecb..d018263 100644
--- a/quiche/quic/moqt/moqt_messages.cc
+++ b/quiche/quic/moqt/moqt_messages.cc
@@ -103,6 +103,8 @@
       return "GOAWAY";
     case MoqtMessageType::kSubscribeNamespace:
       return "SUBSCRIBE_NAMESPACE";
+    case MoqtMessageType::kSubscribeTracks:
+      return "SUBSCRIBE_TRACKS";
     case MoqtMessageType::kMaxRequestId:
       return "MAX_REQUEST_ID";
     case MoqtMessageType::kPublish:
diff --git a/quiche/quic/moqt/moqt_messages.h b/quiche/quic/moqt/moqt_messages.h
index b05a392..98fa82e 100644
--- a/quiche/quic/moqt/moqt_messages.h
+++ b/quiche/quic/moqt/moqt_messages.h
@@ -213,13 +213,14 @@
   kTrackStatus = 0x0d,
   kNamespaceDone = 0x0e,
   kGoAway = 0x10,
-  kSubscribeNamespace = 0x11,
   kMaxRequestId = 0x15,
   kFetch = 0x16,
   kFetchCancel = 0x17,
   kFetchOk = 0x18,
   kRequestsBlocked = 0x1a,
   kPublish = 0x1d,
+  kSubscribeNamespace = 0x50,
+  kSubscribeTracks = 0x51,
   kSetup = 0x2f00,
 
   // QUICHE-specific extensions.
@@ -432,7 +433,12 @@
 struct QUICHE_EXPORT MoqtSubscribeNamespace {
   uint64_t request_id;
   TrackNamespace track_namespace_prefix;
-  SubscribeNamespaceOption subscribe_options;
+  MessageParameters parameters;
+};
+
+struct QUICHE_EXPORT MoqtSubscribeTracks {
+  uint64_t request_id;
+  TrackNamespace track_namespace_prefix;
   MessageParameters parameters;
 };
 
diff --git a/quiche/quic/moqt/moqt_namespace_stream.cc b/quiche/quic/moqt/moqt_namespace_stream.cc
index 4d32218..c561373 100644
--- a/quiche/quic/moqt/moqt_namespace_stream.cc
+++ b/quiche/quic/moqt/moqt_namespace_stream.cc
@@ -277,9 +277,8 @@
   }
   add_callback_ = nullptr;
   QUICHE_DCHECK(task_ == nullptr);
-  task_ =
-      application_(message.track_namespace_prefix, message.subscribe_options,
-                   message.parameters, ResponseCallback(request_id_));
+  task_ = application_(message.track_namespace_prefix, message.parameters,
+                       ResponseCallback(request_id_));
   if (task_ != nullptr) {
     task_->SetObjectsAvailableCallback([this]() { ProcessNamespaces(); });
   }
diff --git a/quiche/quic/moqt/moqt_namespace_stream_test.cc b/quiche/quic/moqt/moqt_namespace_stream_test.cc
index c38258f..fe14190 100644
--- a/quiche/quic/moqt/moqt_namespace_stream_test.cc
+++ b/quiche/quic/moqt/moqt_namespace_stream_test.cc
@@ -318,8 +318,7 @@
   testing::MockFunction<bool(const TrackNamespace&)> add_callback_;
   testing::MockFunction<void(const TrackNamespace&)> remove_callback_;
   testing::MockFunction<std::unique_ptr<MoqtNamespaceTask>(
-      const TrackNamespace&, SubscribeNamespaceOption, const MessageParameters&,
-      MoqtResponseCallback)>
+      const TrackNamespace&, const MessageParameters&, MoqtResponseCallback)>
       mock_application_;
   MoqtIncomingSubscribeNamespaceCallback application_callback_;
   MoqtSubscribeNamespaceResponseStream stream_;
@@ -329,7 +328,6 @@
   MoqtSubscribeNamespace message = {
       kRequestId,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   ObjectsAvailableCallback callback;
@@ -338,8 +336,7 @@
   ok.parameters.expires = quic::QuicTimeDelta::FromSeconds(60);
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(true));
   EXPECT_CALL(mock_application_, Call)
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(ok.parameters);
         auto task =
@@ -400,7 +397,6 @@
   MoqtSubscribeNamespace message = {
       kRequestId,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   ObjectsAvailableCallback callback;
@@ -409,8 +405,7 @@
   ok.parameters.expires = quic::QuicTimeDelta::FromSeconds(60);
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(true));
   EXPECT_CALL(mock_application_, Call)
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(ok.parameters);
         auto task =
@@ -431,13 +426,11 @@
   MoqtSubscribeNamespace message = {
       kRequestId,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(true));
   EXPECT_CALL(mock_application_, Call)
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(MoqtRequestErrorInfo{
             RequestErrorCode::kInternalError,
@@ -453,14 +446,12 @@
   MoqtSubscribeNamespace message = {
       kRequestId,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   MockNamespaceTask* task_ptr = nullptr;
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(true));
   EXPECT_CALL(mock_application_, Call)
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(MessageParameters());
         auto task =
@@ -495,14 +486,12 @@
   MoqtSubscribeNamespace message = {
       kRequestId,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   MockNamespaceTask* task_ptr = nullptr;
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(true));
   EXPECT_CALL(mock_application_, Call)
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(MessageParameters());
         auto task =
@@ -538,7 +527,6 @@
   MoqtSubscribeNamespace message = {
       kRequestId,
       TrackNamespace({"foo", "bar", "baz"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   // The namespace tree already has a subscriber for a prefix of "foo".
@@ -553,15 +541,13 @@
   MoqtSubscribeNamespace message = {
       kRequestId,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(true));
   MoqtRequestOk ok(kRequestId);
   EXPECT_CALL(mock_stream_, Writev(SerializedControlMessage(ok), _));
   EXPECT_CALL(mock_application_, Call)
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(MessageParameters());
         return std::make_unique<MockNamespaceTask>(
@@ -574,7 +560,6 @@
   MoqtSubscribeNamespace message2 = {
       kRequestId + 2,
       TrackNamespace({"bar"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   ReceiveControlMessage(message2);
@@ -585,15 +570,13 @@
   MoqtSubscribeNamespace message1 = {
       kRequestId,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(true));
   MoqtRequestOk ok1(kRequestId);
   EXPECT_CALL(mock_stream_, Writev(SerializedControlMessage(ok1), _));
   EXPECT_CALL(mock_application_, Call)
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(MessageParameters());
         return std::make_unique<MockNamespaceTask>(
@@ -613,7 +596,6 @@
   MoqtSubscribeNamespace message2 = {
       kRequestId + 2,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   EXPECT_CALL(add_callback_, Call).WillOnce(Return(false));
@@ -626,7 +608,6 @@
   MoqtSubscribeNamespace message3 = {
       kRequestId + 4,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kNamespace,
       MessageParameters(),
   };
   stream2.CheckStatus(stream2.OnControlMessage(message3));
diff --git a/quiche/quic/moqt/moqt_parser.cc b/quiche/quic/moqt/moqt_parser.cc
index 5a297a6..3eec6fe 100644
--- a/quiche/quic/moqt/moqt_parser.cc
+++ b/quiche/quic/moqt/moqt_parser.cc
@@ -814,26 +814,32 @@
     absl::string_view data) const {
   quic::QuicDataReader reader(data);
   MoqtSubscribeNamespace subscribe_namespace;
-  uint64_t raw_option;
   if (!reader.ReadMoqVarInt(&subscribe_namespace.request_id)) {
     return absl::InvalidArgumentError("Request ID missing");
   }
   QUICHE_RETURN_IF_ERROR(
       ReadTrackNamespace(reader, subscribe_namespace.track_namespace_prefix));
-  if (!reader.ReadMoqVarInt(&raw_option)) {
-    return absl::InvalidArgumentError("SUBSCRIBE_NAMESPACE option missing");
-  }
-  if (raw_option > kMaxSubscribeOption) {
-    return absl::InvalidArgumentError("Invalid SUBSCRIBE_NAMESPACE option");
-  }
-  subscribe_namespace.subscribe_options =
-      static_cast<SubscribeNamespaceOption>(raw_option);
   QUICHE_RETURN_IF_ERROR(
       FillAndValidateMessageParameters(reader, subscribe_namespace.parameters));
   QUICHE_RETURN_IF_ERROR(CheckForTrailingData(reader));
   return subscribe_namespace;
 }
 
+absl::StatusOr<MoqtSubscribeTracks>
+MoqtControlMessageParser::ProcessSubscribeTracks(absl::string_view data) const {
+  quic::QuicDataReader reader(data);
+  MoqtSubscribeTracks subscribe_tracks;
+  if (!reader.ReadMoqVarInt(&subscribe_tracks.request_id)) {
+    return absl::InvalidArgumentError("Request ID missing");
+  }
+  QUICHE_RETURN_IF_ERROR(
+      ReadTrackNamespace(reader, subscribe_tracks.track_namespace_prefix));
+  QUICHE_RETURN_IF_ERROR(
+      FillAndValidateMessageParameters(reader, subscribe_tracks.parameters));
+  QUICHE_RETURN_IF_ERROR(CheckForTrailingData(reader));
+  return subscribe_tracks;
+}
+
 absl::StatusOr<MoqtMaxRequestId> MoqtControlMessageParser::ProcessMaxRequestId(
     absl::string_view data) const {
   quic::QuicDataReader reader(data);
diff --git a/quiche/quic/moqt/moqt_parser.h b/quiche/quic/moqt/moqt_parser.h
index eca79f4..f8f685b 100644
--- a/quiche/quic/moqt/moqt_parser.h
+++ b/quiche/quic/moqt/moqt_parser.h
@@ -145,6 +145,8 @@
   absl::StatusOr<MoqtGoAway> ProcessGoAway(absl::string_view data) const;
   absl::StatusOr<MoqtSubscribeNamespace> ProcessSubscribeNamespace(
       absl::string_view data) const;
+  absl::StatusOr<MoqtSubscribeTracks> ProcessSubscribeTracks(
+      absl::string_view data) const;
   absl::StatusOr<MoqtMaxRequestId> ProcessMaxRequestId(
       absl::string_view data) const;
   absl::StatusOr<MoqtFetch> ProcessFetch(absl::string_view data) const;
@@ -202,6 +204,8 @@
         return parse(&MoqtControlMessageParser::ProcessGoAway);
       case MoqtMessageType::kSubscribeNamespace:
         return parse(&MoqtControlMessageParser::ProcessSubscribeNamespace);
+      case MoqtMessageType::kSubscribeTracks:
+        return parse(&MoqtControlMessageParser::ProcessSubscribeTracks);
       case MoqtMessageType::kMaxRequestId:
         return parse(&MoqtControlMessageParser::ProcessMaxRequestId);
       case MoqtMessageType::kFetch:
diff --git a/quiche/quic/moqt/moqt_parser_test.cc b/quiche/quic/moqt/moqt_parser_test.cc
index 74fcd62..d598c3d 100644
--- a/quiche/quic/moqt/moqt_parser_test.cc
+++ b/quiche/quic/moqt/moqt_parser_test.cc
@@ -61,6 +61,7 @@
     MoqtMessageType::kPublishNamespaceCancel,
     MoqtMessageType::kGoAway,
     MoqtMessageType::kSubscribeNamespace,
+    MoqtMessageType::kSubscribeTracks,
     MoqtMessageType::kMaxRequestId,
     MoqtMessageType::kFetch,
     MoqtMessageType::kFetchCancel,
diff --git a/quiche/quic/moqt/moqt_relay_publisher.h b/quiche/quic/moqt/moqt_relay_publisher.h
index ee1fb71..1e1f89f 100644
--- a/quiche/quic/moqt/moqt_relay_publisher.h
+++ b/quiche/quic/moqt/moqt_relay_publisher.h
@@ -37,9 +37,16 @@
       const FullTrackName& track_name) override;
 
   std::unique_ptr<MoqtNamespaceTask> AddNamespaceSubscriber(
-      const TrackNamespace& track_namespace,
-      MoqtSessionInterface* absl_nullable session) {
-    return namespace_publishers_.AddSubscriber(track_namespace, session);
+      const TrackNamespace& track_namespace) {
+    return namespace_publishers_.AddNamespaceSubscriber(track_namespace);
+  }
+  bool AddTrackSubscriber(const TrackNamespace& track_namespace,
+                          MoqtSessionInterface* absl_nonnull session) {
+    return namespace_publishers_.AddTrackSubscriber(track_namespace, session);
+  }
+  void RemoveTrackSubscriber(const TrackNamespace& track_namespace,
+                             MoqtSessionInterface* absl_nonnull session) {
+    namespace_publishers_.RemoveTrackSubscriber(track_namespace, session);
   }
 
   // There is a new default upstream session. When there is no other namespace
diff --git a/quiche/quic/moqt/moqt_session.cc b/quiche/quic/moqt/moqt_session.cc
index d03dc55..c9b54f5 100644
--- a/quiche/quic/moqt/moqt_session.cc
+++ b/quiche/quic/moqt/moqt_session.cc
@@ -273,8 +273,7 @@
 }
 
 std::unique_ptr<MoqtNamespaceTask> MoqtSession::SubscribeNamespace(
-    TrackNamespace& prefix, SubscribeNamespaceOption option,
-    const MessageParameters& parameters,
+    TrackNamespace& prefix, const MessageParameters& parameters,
     MoqtResponseCallback response_callback) {
   if (received_goaway_ || sent_goaway_) {
     QUIC_DLOG(INFO) << ENDPOINT
@@ -295,18 +294,6 @@
                     << peer_max_request_id_;
     return nullptr;
   }
-  // Sanitize the option.
-  switch (option) {
-    case SubscribeNamespaceOption::kNamespace:
-      break;
-    case SubscribeNamespaceOption::kPublish:
-      // TODO(martinduke): Support PUBLISH.
-      return nullptr;
-    case SubscribeNamespaceOption::kBoth:
-      option = SubscribeNamespaceOption::kNamespace;
-      break;
-  }
-  QUICHE_DCHECK(option == SubscribeNamespaceOption::kNamespace);
   if (!outgoing_subscribe_namespace_.SubscribeNamespace(prefix)) {
     std::move(response_callback)(MoqtRequestErrorInfo{
         RequestErrorCode::kInternalError, std::nullopt,
@@ -339,10 +326,8 @@
     pending_bidi_streams_.push_back(std::move(state));
   }
   MoqtSubscribeNamespace message;
-  message.request_id = next_request_id_;
-  next_request_id_ += 2;
+  message.request_id = NextRequestId();
   message.track_namespace_prefix = prefix;
-  message.subscribe_options = SubscribeNamespaceOption::kNamespace;
   message.parameters = parameters;
   state_ptr->SendOrBufferMessageOrFatal(
       framer_.SerializeSubscribeNamespace(message));
@@ -351,6 +336,16 @@
   return state_ptr->CreateTask(prefix);
 }
 
+bool MoqtSession::SubscribeTracks(TrackNamespace& prefix,
+                                  const MessageParameters& parameters,
+                                  MoqtResponseCallback response_callback) {
+  return false;
+}
+
+void MoqtSession::UnsubscribeTracks(TrackNamespace& prefix) {
+  // Do nothing.
+}
+
 bool MoqtSession::PublishNamespace(
     const TrackNamespace& track_namespace, const MessageParameters& parameters,
     MoqtResponseCallback response_callback,
@@ -963,6 +958,21 @@
       temp_stream->OnCanRead();
       break;
     }
+    case MoqtMessageType::kSubscribeTracks: {
+      // TODO(martinduke): Implement this.
+      MoqtControlMessageQueue queue(stream_);
+      if (!queue
+               .SendOrBufferMessage(
+                   session_->framer_.SerializeRequestError(MoqtRequestError{
+                       /*request_id=*/0, RequestErrorCode::kNotSupported,
+                       std::nullopt, "SUBSCRIBE_TRACKS is not supported"}),
+                   /*fin=*/true)
+               .ok()) {
+        session_->Error(MoqtError::kInternalError, "Internal write error");
+        return;
+      }
+      break;
+    }
     case MoqtMessageType::kPublish: {
       auto publish_stream = std::make_unique<MoqtPublishResponseStream>(
           &session_->framer_, session_->ControlMessageParser(),
diff --git a/quiche/quic/moqt/moqt_session.h b/quiche/quic/moqt/moqt_session.h
index 66660be..b031dc2 100644
--- a/quiche/quic/moqt/moqt_session.h
+++ b/quiche/quic/moqt/moqt_session.h
@@ -132,9 +132,12 @@
   // MoqtNamespaceTask::SetObjectsAvailableCallback() to actually retrieve
   // namespaces.
   std::unique_ptr<MoqtNamespaceTask> SubscribeNamespace(
-      TrackNamespace& prefix, SubscribeNamespaceOption option,
-      const MessageParameters& parameters,
+      TrackNamespace& prefix, const MessageParameters& parameters,
       MoqtResponseCallback response_callback) override;
+  bool SubscribeTracks(TrackNamespace& prefix,
+                       const MessageParameters& parameters,
+                       MoqtResponseCallback response_callback) override;
+  void UnsubscribeTracks(TrackNamespace& prefix) override;
   quiche::QuicheWeakPtr<MoqtSessionInterface> GetWeakPtr() override {
     return weak_ptr_factory_.Create();
   }
diff --git a/quiche/quic/moqt/moqt_session_callbacks.h b/quiche/quic/moqt/moqt_session_callbacks.h
index 5b2070b..224ffac 100644
--- a/quiche/quic/moqt/moqt_session_callbacks.h
+++ b/quiche/quic/moqt/moqt_session_callbacks.h
@@ -104,9 +104,11 @@
 // tracks and namespaces, as appropriate, that are already present.
 using MoqtIncomingSubscribeNamespaceCallback =
     quiche::MultiUseCallback<std::unique_ptr<MoqtNamespaceTask>(
-        const TrackNamespace& prefix, SubscribeNamespaceOption option,
-        const MessageParameters& parameters,
+        const TrackNamespace& prefix, const MessageParameters& parameters,
         MoqtResponseCallback response_callback)>;
+using MoqtIncomingSubscribeTracksCallback = quiche::MultiUseCallback<void(
+    const TrackNamespace& prefix, const MessageParameters& parameters,
+    MoqtResponseCallback response_callback)>;
 
 inline void DefaultIncomingPublishNamespaceCallback(
     const TrackNamespace&, const std::optional<MessageParameters>&,
@@ -121,7 +123,7 @@
 
 inline std::unique_ptr<MoqtNamespaceTask>
 DefaultIncomingSubscribeNamespaceCallback(
-    const TrackNamespace&, SubscribeNamespaceOption, const MessageParameters&,
+    const TrackNamespace&, const MessageParameters&,
     MoqtResponseCallback response_callback) {
   std::move(response_callback)(
       MoqtRequestErrorInfo{RequestErrorCode::kNotSupported, std::nullopt,
@@ -129,6 +131,15 @@
   return nullptr;
 }
 
+// If |response_callback| is nullptr, it's removing a subscription.
+inline void DefaultIncomingSubscribeTracksCallback(
+    const TrackNamespace&, const MessageParameters&,
+    MoqtResponseCallback response_callback) {
+  std::move(response_callback)(
+      MoqtRequestErrorInfo{RequestErrorCode::kNotSupported, std::nullopt,
+                           "This endpoint cannot publish."});
+}
+
 inline SubscribeVisitor* DefaultIncomingPublishCallback(
     const FullTrackName&, const MessageParameters&, const TrackExtensions&,
     MoqtResponseCallback) {
@@ -148,6 +159,8 @@
       DefaultIncomingPublishNamespaceCallback;
   MoqtIncomingSubscribeNamespaceCallback incoming_subscribe_namespace_callback =
       DefaultIncomingSubscribeNamespaceCallback;
+  MoqtIncomingSubscribeTracksCallback incoming_subscribe_tracks_callback =
+      DefaultIncomingSubscribeTracksCallback;
   MoqtIncomingPublishCallback incoming_publish_callback =
       DefaultIncomingPublishCallback;
   const quic::QuicClock* clock = quic::QuicDefaultClock::Get();
diff --git a/quiche/quic/moqt/moqt_session_interface.h b/quiche/quic/moqt/moqt_session_interface.h
index 72c5164..c08cf50 100644
--- a/quiche/quic/moqt/moqt_session_interface.h
+++ b/quiche/quic/moqt/moqt_session_interface.h
@@ -177,9 +177,12 @@
   // Returns nullptr if the message cannot be sent.
   // To unsubscribe, simply destroy the returned MoqtNamespaceTask.
   virtual std::unique_ptr<MoqtNamespaceTask> SubscribeNamespace(
-      TrackNamespace& prefix, SubscribeNamespaceOption option,
-      const MessageParameters& parameters,
+      TrackNamespace& prefix, const MessageParameters& parameters,
       MoqtResponseCallback response_callback) = 0;
+  virtual bool SubscribeTracks(TrackNamespace& prefix,
+                               const MessageParameters& parameters,
+                               MoqtResponseCallback response_callback) = 0;
+  virtual void UnsubscribeTracks(TrackNamespace& prefix) = 0;
 
   // TODO(martinduke): Add an API for absolute joining fetch.
 
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index d4a6b97..0581f28 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -168,7 +168,7 @@
   // |wt_stream| is the underlying mock WebTransport stream; if nullptr, use
   // mock_bidi_stream_.
   static constexpr absl::string_view kSubscribeByte = "\x03";
-  static constexpr absl::string_view kSubscribeNamespaceByte = "\x11";
+  static constexpr absl::string_view kSubscribeNamespaceByte = "\x50";
   static constexpr absl::string_view kPublishByte = "\x1d";
   std::unique_ptr<MoqtBidiStreamBase> ResponseStream(
       absl::string_view first_byte,
@@ -1132,7 +1132,7 @@
       mock_bidi_stream_,
       Writev(ControlMessageOfType(MoqtMessageType::kSubscribeNamespace), _));
   std::unique_ptr<MoqtNamespaceTask> task = session_.SubscribeNamespace(
-      prefix, SubscribeNamespaceOption::kNamespace, MessageParameters(),
+      prefix, MessageParameters(),
       [&](std::variant<MessageParameters, MoqtRequestErrorInfo> response) {
         got_callback = true;
         EXPECT_TRUE(std::holds_alternative<MessageParameters>(response));
@@ -1151,7 +1151,7 @@
       mock_bidi_stream_,
       Writev(ControlMessageOfType(MoqtMessageType::kSubscribeNamespace), _));
   std::unique_ptr<MoqtNamespaceTask> task = session_.SubscribeNamespace(
-      prefix, SubscribeNamespaceOption::kNamespace, MessageParameters(),
+      prefix, MessageParameters(),
       [&](std::variant<MessageParameters, MoqtRequestErrorInfo> response) {
         got_callback = true;
         EXPECT_TRUE(std::holds_alternative<MoqtRequestErrorInfo>(response));
@@ -1167,26 +1167,6 @@
   EXPECT_TRUE(got_callback);
 }
 
-TEST_F(MoqtSessionTest, SubscribeNamespacePublishOnly) {
-  TrackNamespace prefix({"foo"});
-  // kPublish is not allowed.
-  EXPECT_EQ(session_.SubscribeNamespace(
-                prefix, SubscribeNamespaceOption::kPublish, MessageParameters(),
-                [&](std::variant<MessageParameters, MoqtRequestErrorInfo>) {}),
-            nullptr);
-  // kBoth is treated as kNamespace.
-  PrepareRequestStream(bidi_wrapper_);
-  EXPECT_CALL(mock_bidi_stream_,
-              Writev(SerializedControlMessage(MoqtSubscribeNamespace{
-                         0, prefix, SubscribeNamespaceOption::kNamespace,
-                         MessageParameters()}),
-                     _));
-  EXPECT_NE(session_.SubscribeNamespace(
-                prefix, SubscribeNamespaceOption::kBoth, MessageParameters(),
-                [&](std::variant<MessageParameters, MoqtRequestErrorInfo>) {}),
-            nullptr);
-}
-
 TEST_F(MoqtSessionTest, SubscribeOkWithBadTrackAlias) {
   PrepareRequestStream(bidi_wrapper_);
   session_.Subscribe(FullTrackName("foo", "bar"), &remote_track_visitor_,
@@ -1962,15 +1942,14 @@
   parameters.authorization_tokens.emplace_back(AuthTokenType::kOutOfBand,
                                                "foo");
 
-  MoqtSubscribeNamespace subscribe_namespace = {
-      /*request_id=*/1, prefix, SubscribeNamespaceOption::kBoth, parameters};
+  MoqtSubscribeNamespace subscribe_namespace = {/*request_id=*/1, prefix,
+                                                parameters};
   quiche::QuicheWeakPtr<MockNamespaceTask> task;
   MoqtRequestOk expected_ok(/*request_id=*/1);
   expected_ok.parameters.expires = quic::QuicTimeDelta::FromSeconds(60);
   EXPECT_CALL(session_callbacks_.incoming_subscribe_namespace_callback,
-              Call(prefix, SubscribeNamespaceOption::kBoth, parameters, _))
-      .WillOnce([&](const TrackNamespace& prefix, SubscribeNamespaceOption,
-                    const MessageParameters&,
+              Call(prefix, parameters, _))
+      .WillOnce([&](const TrackNamespace& prefix, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(expected_ok.parameters);
         auto task_ptr = std::make_unique<MockNamespaceTask>(prefix);
@@ -2013,12 +1992,11 @@
                                                "foo");
   bidi_wrapper_ = std::make_unique<MoqtBidiStreamTestWrapper>(
       ResponseStream(kSubscribeNamespaceByte));
-  MoqtSubscribeNamespace subscribe_namespace = {
-      /*request_id=*/1, prefix, SubscribeNamespaceOption::kBoth, parameters};
+  MoqtSubscribeNamespace subscribe_namespace = {/*request_id=*/1, prefix,
+                                                parameters};
   EXPECT_CALL(session_callbacks_.incoming_subscribe_namespace_callback,
-              Call(prefix, SubscribeNamespaceOption::kBoth, parameters, _))
-      .WillOnce([&](const TrackNamespace&, SubscribeNamespaceOption,
-                    const MessageParameters&,
+              Call(prefix, parameters, _))
+      .WillOnce([&](const TrackNamespace&, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(MoqtRequestErrorInfo{
             RequestErrorCode::kUnauthorized, std::nullopt, "foo"});
@@ -2041,12 +2019,11 @@
                                                "foo");
   bidi_wrapper_ = std::make_unique<MoqtBidiStreamTestWrapper>(
       ResponseStream(kSubscribeNamespaceByte));
-  MoqtSubscribeNamespace subscribe_namespace = {
-      /*request_id=*/1, foo, SubscribeNamespaceOption::kBoth, parameters};
+  MoqtSubscribeNamespace subscribe_namespace = {/*request_id=*/1, foo,
+                                                parameters};
   EXPECT_CALL(session_callbacks_.incoming_subscribe_namespace_callback,
-              Call(foo, SubscribeNamespaceOption::kBoth, parameters, _))
-      .WillOnce([&](const TrackNamespace& prefix, SubscribeNamespaceOption,
-                    const MessageParameters&,
+              Call(foo, parameters, _))
+      .WillOnce([&](const TrackNamespace& prefix, const MessageParameters&,
                     MoqtResponseCallback response_callback) {
         std::move(response_callback)(MessageParameters());
         auto task_ptr = std::make_unique<MockNamespaceTask>(prefix);
@@ -2328,11 +2305,10 @@
   EXPECT_FALSE(session_.Subscribe(FullTrackName("foo", "bar"),
                                   &remote_track_visitor_, parameters));
   TrackNamespace prefix({"foo"});
-  EXPECT_EQ(
-      session_.SubscribeNamespace(
-          prefix, SubscribeNamespaceOption::kNamespace, MessageParameters(),
-          +[](std::variant<MessageParameters, MoqtRequestErrorInfo>) {}),
-      nullptr);
+  EXPECT_EQ(session_.SubscribeNamespace(
+                prefix, MessageParameters(),
+                +[](std::variant<MessageParameters, MoqtRequestErrorInfo>) {}),
+            nullptr);
   session_.PublishNamespace(
       TrackNamespace{"foo"}, MessageParameters(),
       +[](std::variant<MessageParameters, MoqtRequestErrorInfo>) {},
@@ -2403,11 +2379,10 @@
   EXPECT_FALSE(session_.Subscribe(FullTrackName({"foo"}, "bar"),
                                   &remote_track_visitor_, parameters));
   TrackNamespace prefix({"foo"});
-  EXPECT_EQ(
-      session_.SubscribeNamespace(
-          prefix, SubscribeNamespaceOption::kNamespace, MessageParameters(),
-          +[](std::variant<MessageParameters, MoqtRequestErrorInfo>) {}),
-      nullptr);
+  EXPECT_EQ(session_.SubscribeNamespace(
+                prefix, MessageParameters(),
+                +[](std::variant<MessageParameters, MoqtRequestErrorInfo>) {}),
+            nullptr);
   session_.PublishNamespace(
       TrackNamespace{"foo"}, MessageParameters(),
       +[](std::variant<MessageParameters, MoqtRequestErrorInfo>) {},
@@ -2909,9 +2884,9 @@
         uint64_t type;
         uint16_t length;
         uint64_t request_id;
-        bool type_read = reader.ReadVarInt62(&type);
+        bool type_read = reader.ReadMoqVarInt(&type);
         bool length_read = reader.ReadUInt16(&length);
-        bool req_id_read = reader.ReadVarInt62(&request_id);
+        bool req_id_read = reader.ReadMoqVarInt(&request_id);
         EXPECT_TRUE(type_read) << "Failed to read type, written_data.size()="
                                << stream.write_buffer().length();
         EXPECT_TRUE(length_read);
@@ -2928,7 +2903,7 @@
       .WillOnce(Return(&sub_ns_stream));
   TrackNamespace namespace1({"namespace1"});
   std::unique_ptr<MoqtNamespaceTask> task1 = session_.SubscribeNamespace(
-      namespace1, SubscribeNamespaceOption::kNamespace, MessageParameters(),
+      namespace1, MessageParameters(),
       [](std::variant<MessageParameters, MoqtRequestErrorInfo>) {});
   ASSERT_NE(task1, nullptr);
   EXPECT_EQ(get_request_id(sub_ns_stream), next_request_id);
@@ -2996,7 +2971,7 @@
       .WillOnce(Return(&sub_ns_stream_2));
   TrackNamespace namespace_dup({"namespace_dup"});
   std::unique_ptr<MoqtNamespaceTask> task_dup = session_.SubscribeNamespace(
-      namespace_dup, SubscribeNamespaceOption::kNamespace, MessageParameters(),
+      namespace_dup, MessageParameters(),
       [](std::variant<MessageParameters, MoqtRequestErrorInfo>) {});
   ASSERT_NE(task_dup, nullptr);
   EXPECT_EQ(next_request_id, get_request_id(sub_ns_stream_2));
diff --git a/quiche/quic/moqt/moqt_types.h b/quiche/quic/moqt/moqt_types.h
index 32f38d6..873dc4f 100644
--- a/quiche/quic/moqt/moqt_types.h
+++ b/quiche/quic/moqt/moqt_types.h
@@ -79,13 +79,6 @@
   }
 };
 
-enum class QUICHE_EXPORT SubscribeNamespaceOption : uint64_t {
-  kPublish = 0x00,
-  kNamespace = 0x01,
-  kBoth = 0x02,
-};
-static constexpr uint64_t kMaxSubscribeOption = 0x02;
-
 }  // namespace moqt
 
 #endif  // QUICHE_QUIC_MOQT_MOQT_TYPES_H_
diff --git a/quiche/quic/moqt/relay_namespace_tree.cc b/quiche/quic/moqt/relay_namespace_tree.cc
index 8524f45..a21a344 100644
--- a/quiche/quic/moqt/relay_namespace_tree.cc
+++ b/quiche/quic/moqt/relay_namespace_tree.cc
@@ -27,7 +27,7 @@
 namespace moqt {
 
 RelayNamespaceTree::RelayNamespaceListener::~RelayNamespaceListener() {
-  tree_.RemoveSubscriber(prefix_, this);
+  tree_.RemoveNamespaceSubscriber(prefix_, this);
 }
 
 void RelayNamespaceTree::RelayNamespaceListener::SetObjectsAvailableCallback(
@@ -85,15 +85,6 @@
   callback_();
 }
 
-void RelayNamespaceTree::RelayNamespaceListener::Publish(TrackNamespace,
-                                                         absl::string_view) {
-  if (session_ == nullptr) {
-    return;  // Not interested in tracks.
-  }
-  // TODO(martinduke): Build a full track name from prefix_, suffix, and name,
-  // then call session_->Publish().
-}
-
 void RelayNamespaceTree::RelayNamespaceListener::DeclareEof() {
   if (eof_ || error_.has_value()) {
     return;
@@ -126,17 +117,16 @@
   }
 }
 
-std::unique_ptr<MoqtNamespaceTask> RelayNamespaceTree::AddSubscriber(
-    const TrackNamespace& prefix,
-    MoqtSessionInterface* absl_nullable track_listener) {
+std::unique_ptr<MoqtNamespaceTask> RelayNamespaceTree::AddNamespaceSubscriber(
+    const TrackNamespace& prefix) {
   Node* node = FindOrCreateNode(prefix);
-  auto task =
-      std::make_unique<RelayNamespaceListener>(*this, prefix, track_listener);
+  auto task = std::make_unique<RelayNamespaceListener>(*this, prefix);
   node->listeners[task.get()] = task->GetWeakPtr();
   return std::move(task);
 }
 
-void RelayNamespaceTree::RemoveSubscriber(
+// private
+void RelayNamespaceTree::RemoveNamespaceSubscriber(
     TrackNamespace prefix, MoqtNamespaceTask* absl_nonnull listener) {
   Node* node = FindNode(prefix);
   if (node == nullptr) {
@@ -204,9 +194,6 @@
   if (!node->publishers.empty()) {
     listener->AddPendingSuffix(suffix, TransactionType::kAdd);
   }
-  for (const std::string& track : node->published_tracks) {
-    listener->Publish(suffix, track);
-  }
   for (auto child = node->children.begin(); child != node->children.end();
        ++child) {
     if (std::optional<absl::string_view> element = (*child)->element) {
diff --git a/quiche/quic/moqt/relay_namespace_tree.h b/quiche/quic/moqt/relay_namespace_tree.h
index 2b58715..469a284 100644
--- a/quiche/quic/moqt/relay_namespace_tree.h
+++ b/quiche/quic/moqt/relay_namespace_tree.h
@@ -39,12 +39,8 @@
     // If |tracks| is nullptr, the listener is not interested in PUBLISH
     // messages.
     RelayNamespaceListener(RelayNamespaceTree& tree,
-                           const TrackNamespace& prefix,
-                           MoqtSessionInterface* absl_nullable tracks)
-        : prefix_(prefix),
-          tree_(tree),
-          session_(tracks),
-          weak_ptr_factory_(this) {}
+                           const TrackNamespace& prefix)
+        : prefix_(prefix), tree_(tree), weak_ptr_factory_(this) {}
     ~RelayNamespaceListener() override;
     // MoqtNamespaceTask methods.
     void SetObjectsAvailableCallback(ObjectsAvailableCallback
@@ -61,8 +57,6 @@
     // Queues a suffix corresponding to a NAMESPACE (if |type| is kAdd) or a
     // NAMESPACE_DONE (if |type| is kDelete).
     void AddPendingSuffix(TrackNamespace suffix, TransactionType type);
-    // Publishes a track in this namespace.
-    void Publish(TrackNamespace suffix, absl::string_view name);
     void DeclareEof();
     quiche::QuicheWeakPtr<RelayNamespaceListener> GetWeakPtr() {
       return weak_ptr_factory_.Create();
@@ -80,7 +74,6 @@
     RelayNamespaceTree& tree_;
     std::optional<webtransport::StreamErrorCode> error_;
     quiche::QuicheCircularDeque<PendingSuffix> pending_suffixes_;
-    MoqtSessionInterface* absl_nullable session_;
     ObjectsAvailableCallback absl_nullable callback_ = nullptr;
     bool eof_ = false;
     bool got_first_pull_ = false;
@@ -89,22 +82,35 @@
   };
 
  public:
-  // Adds a publisher to the namespace tree. The caller is responsible to call
-  // RemovePublisher if it goes away. |session| is stored as a WeakPtr.
+  // Adds a namespace publisher to the namespace tree. The caller is responsible
+  // to call RemovePublisher if it goes away. |session| is stored as a WeakPtr.
   void AddPublisher(TrackNamespace prefix,
                     MoqtSessionInterface* absl_nonnull session);
-
   void RemovePublisher(const TrackNamespace& prefix,
                        MoqtSessionInterface* absl_nonnull session);
 
-  // Called on incoming SUBSCRIBE_NAMESPACE messages. If track_subscriber is
-  // nullptr, it is not interested in PUBLISH messages. If callback is nullptr,
-  // it is not interested in NAMESPACE messages. If not interested in,
-  // namespaces, will return nullptr. Otherwise, will return a task to flow
-  // control published namespaces.
-  std::unique_ptr<MoqtNamespaceTask> AddSubscriber(
-      const TrackNamespace& prefix,
-      MoqtSessionInterface* absl_nullable track_listener);
+  // Called on incoming SUBSCRIBE_NAMESPACE messages. Returns nullptr if the
+  // relay is unable to support the request. The subscription can be removed by
+  // freeing the returned task.
+  std::unique_ptr<MoqtNamespaceTask> AddNamespaceSubscriber(
+      const TrackNamespace& prefix);
+  // Called on incoming SUBSCRIBE_TRACKS messages. Returns false if the relay is
+  // unable to support the request.
+  // TODO(martinduke): Implement this.
+  bool AddTrackSubscriber(
+      const TrackNamespace& /*prefix*/,
+      MoqtSessionInterface* absl_nonnull /*track_listener*/) {
+    return false;
+  }
+  void RemoveTrackSubscriber(
+      const TrackNamespace& /*prefix*/,
+      MoqtSessionInterface* absl_nonnull /*track_listener*/) {}
+
+  // As the relay learns about tracks, Add and Remove so that they can be
+  // PUBLISHEed on SUBSCRIBE_TRACKS. MoqtKnownTrackPublisher manages the
+  // existence of these tracks, so just store the track name.
+  void AddTrack(const FullTrackName& /*track_name*/) {}
+  void RemoveTrack(const FullTrackName& /*track_name*/) {}
 
   // Returns a raw pointer to the session that publishes the smallest namespace
   // that contains |track_namespace|. If a WeakPtr is found to be invalid,
@@ -135,6 +141,9 @@
     // go away.
     absl::flat_hash_map<void*, quiche::QuicheWeakPtr<RelayNamespaceListener>>
         listeners;
+    // Sessions listening for PUBLISH.
+    absl::flat_hash_map<void*, quiche::QuicheWeakPtr<MoqtSessionInterface>>
+        publish_subscribers;
     // Just store the track name. Additional information will be in the
     // TrackPublisher.
     absl::flat_hash_set<std::string> published_tracks;
@@ -145,10 +154,15 @@
 
   Node* FindOrCreateNode(TrackNamespace track_namespace);
 
-  // Recursive function to notify |listener| of all published namespaces and
-  // tracks in and below |node|.
+  // Recursive function to notify |listener| of all published namespaces in and
+  // below |node|.
   void NotifyOfAllChildren(Node* node, TrackNamespace& suffix,
                            RelayNamespaceListener* absl_nonnull listener);
+  // Recursive function to send PUBLISH to |session| for all tracks in or below
+  // |node|.
+  // TODO(martinduke): Implement this.
+  void NotifyOfAllChildren(Node* /*node*/, TrackNamespace& /*suffix*/,
+                           MoqtSessionInterface* absl_nonnull /*session*/) {}
 
   // If |adding| is true, sends NAMESPACE to all subscribers to a
   // parent namespace. If |adding| is false, sends NAMESPACE_DONE.
@@ -158,8 +172,9 @@
   // if the same applies to its parent.
   void MaybePrune(Node* node, TrackNamespace track_namespace);
 
-  void RemoveSubscriber(TrackNamespace prefix,
-                        MoqtNamespaceTask* absl_nonnull namespace_listener);
+  void RemoveNamespaceSubscriber(
+      TrackNamespace prefix,
+      MoqtNamespaceTask* absl_nonnull namespace_listener);
 
   // A map that allows quick access to any namespace without traversing the
   // tree. Use unique_ptr so that it's pointer stable.
diff --git a/quiche/quic/moqt/relay_namespace_tree_test.cc b/quiche/quic/moqt/relay_namespace_tree_test.cc
index 324e003..0ffc145 100644
--- a/quiche/quic/moqt/relay_namespace_tree_test.cc
+++ b/quiche/quic/moqt/relay_namespace_tree_test.cc
@@ -59,8 +59,7 @@
 TEST_F(RelayNamespaceTreeTest, AddGetRemoveListener) {
   // Add a listener to a namespace that has no publishers.
   EXPECT_EQ(tree_.NumNamespaces(), 0u);
-  std::unique_ptr<MoqtNamespaceTask> task =
-      tree_.AddSubscriber(ab_, session_.get());
+  std::unique_ptr<MoqtNamespaceTask> task = tree_.AddNamespaceSubscriber(ab_);
   task->SetObjectsAvailableCallback(std::move(callback_));
   EXPECT_EQ(tree_.NumNamespaces(), 3u);
   tree_.AddPublisher(a_, session_.get());
@@ -90,8 +89,7 @@
 }
 
 TEST_F(RelayNamespaceTreeTest, SessionDestroyed) {
-  std::unique_ptr<MoqtNamespaceTask> task =
-      tree_.AddSubscriber(ab_, session_.get());
+  std::unique_ptr<MoqtNamespaceTask> task = tree_.AddNamespaceSubscriber(ab_);
   task->SetObjectsAvailableCallback(std::move(callback_));
   tree_.AddPublisher(ab_, session_.get());
   EXPECT_EQ(objects_available_, 1);
@@ -107,8 +105,7 @@
   tree_.AddPublisher(a_, session_.get());
   tree_.AddPublisher(ab_, session_.get());
   tree_.AddPublisher(abc_, session_.get());
-  std::unique_ptr<MoqtNamespaceTask> task =
-      tree_.AddSubscriber(ab_, session_.get());
+  std::unique_ptr<MoqtNamespaceTask> task = tree_.AddNamespaceSubscriber(ab_);
   task->SetObjectsAvailableCallback(std::move(callback_));
   EXPECT_EQ(objects_available_, 2);
   CheckNextSuffix(task.get(), ab_);
@@ -116,8 +113,7 @@
 }
 
 TEST_F(RelayNamespaceTreeTest, MaxSizeNamespace) {
-  std::unique_ptr<MoqtNamespaceTask> task =
-      tree_.AddSubscriber(a_, session_.get());
+  std::unique_ptr<MoqtNamespaceTask> task = tree_.AddNamespaceSubscriber(a_);
   task->SetObjectsAvailableCallback(std::move(callback_));
   TrackNamespace big_namespace{"a", "b", "c", "d", "e", "f", "g", "h",
                                "i", "j", "k", "l", "m", "n", "o", "p",
diff --git a/quiche/quic/moqt/test_tools/mock_moqt_session.h b/quiche/quic/moqt/test_tools/mock_moqt_session.h
index 2cd1df2..135fe0b 100644
--- a/quiche/quic/moqt/test_tools/mock_moqt_session.h
+++ b/quiche/quic/moqt/test_tools/mock_moqt_session.h
@@ -119,9 +119,12 @@
                RequestErrorCode error_code, absl::string_view error_reason),
               (override));
   MOCK_METHOD(std::unique_ptr<MoqtNamespaceTask>, SubscribeNamespace,
-              (TrackNamespace&, SubscribeNamespaceOption,
-               const MessageParameters&, MoqtResponseCallback),
+              (TrackNamespace&, const MessageParameters&, MoqtResponseCallback),
               (override));
+  MOCK_METHOD(bool, SubscribeTracks,
+              (TrackNamespace&, const MessageParameters&, MoqtResponseCallback),
+              (override));
+  MOCK_METHOD(void, UnsubscribeTracks, (TrackNamespace&), (override));
 
   quiche::QuicheWeakPtr<MoqtSessionInterface> GetWeakPtr() override {
     return weak_factory_.Create();
diff --git a/quiche/quic/moqt/test_tools/moqt_framer_utils.cc b/quiche/quic/moqt/test_tools/moqt_framer_utils.cc
index a0e6d42..f35d15b 100644
--- a/quiche/quic/moqt/test_tools/moqt_framer_utils.cc
+++ b/quiche/quic/moqt/test_tools/moqt_framer_utils.cc
@@ -61,6 +61,9 @@
   quiche::QuicheBuffer operator()(const MoqtSubscribeNamespace& message) {
     return framer.SerializeSubscribeNamespace(message);
   }
+  quiche::QuicheBuffer operator()(const MoqtSubscribeTracks& message) {
+    return framer.SerializeSubscribeTracks(message);
+  }
   quiche::QuicheBuffer operator()(const MoqtMaxRequestId& message) {
     return framer.SerializeMaxRequestId(message);
   }
diff --git a/quiche/quic/moqt/test_tools/moqt_framer_utils.h b/quiche/quic/moqt/test_tools/moqt_framer_utils.h
index c5a5d4a..43a95f7 100644
--- a/quiche/quic/moqt/test_tools/moqt_framer_utils.h
+++ b/quiche/quic/moqt/test_tools/moqt_framer_utils.h
@@ -24,9 +24,9 @@
                  MoqtSubscribeOk, MoqtPublishDone, MoqtRequestUpdate,
                  MoqtPublishNamespace, MoqtPublishNamespaceDone,
                  MoqtPublishNamespaceCancel, MoqtTrackStatus, MoqtGoAway,
-                 MoqtSubscribeNamespace, MoqtMaxRequestId, MoqtFetch,
-                 MoqtFetchCancel, MoqtFetchOk, MoqtRequestsBlocked, MoqtPublish,
-                 MoqtNamespace, MoqtNamespaceDone, MoqtObjectAck>;
+                 MoqtSubscribeNamespace, MoqtSubscribeTracks, MoqtMaxRequestId,
+                 MoqtFetch, MoqtFetchCancel, MoqtFetchOk, MoqtRequestsBlocked,
+                 MoqtPublish, MoqtNamespace, MoqtNamespaceDone, MoqtObjectAck>;
 
 std::string SerializeGenericMessage(const AnyMoqtControlMessage& frame,
                                     bool use_webtrans = false);
diff --git a/quiche/quic/moqt/test_tools/moqt_mock_visitor.h b/quiche/quic/moqt/test_tools/moqt_mock_visitor.h
index 5b46d41..69d8269 100644
--- a/quiche/quic/moqt/test_tools/moqt_mock_visitor.h
+++ b/quiche/quic/moqt/test_tools/moqt_mock_visitor.h
@@ -47,9 +47,11 @@
                              MoqtResponseCallback)>
       incoming_publish_namespace_callback;
   testing::MockFunction<std::unique_ptr<MoqtNamespaceTask>(
-      const TrackNamespace&, SubscribeNamespaceOption, const MessageParameters&,
-      MoqtResponseCallback)>
+      const TrackNamespace&, const MessageParameters&, MoqtResponseCallback)>
       incoming_subscribe_namespace_callback;
+  testing::MockFunction<bool(const TrackNamespace&, const MessageParameters&,
+                             MoqtResponseCallback)>
+      incoming_subscribe_tracks_callback;
 
   MockSessionCallbacks() {
     ON_CALL(incoming_publish_namespace_callback, Call)
diff --git a/quiche/quic/moqt/test_tools/moqt_test_message.h b/quiche/quic/moqt/test_tools/moqt_test_message.h
index 4e1d394..76b5c67 100644
--- a/quiche/quic/moqt/test_tools/moqt_test_message.h
+++ b/quiche/quic/moqt/test_tools/moqt_test_message.h
@@ -116,8 +116,8 @@
       MoqtSetup, MoqtObject, MoqtRequestOk, MoqtRequestError, MoqtSubscribe,
       MoqtSubscribeOk, MoqtPublishDone, MoqtRequestUpdate, MoqtPublishNamespace,
       MoqtPublishNamespaceDone, MoqtPublishNamespaceCancel, MoqtTrackStatus,
-      MoqtGoAway, MoqtSubscribeNamespace, MoqtMaxRequestId, MoqtFetch,
-      MoqtFetchCancel, MoqtFetchOk, MoqtRequestsBlocked, MoqtPublish,
+      MoqtGoAway, MoqtSubscribeNamespace, MoqtSubscribeTracks, MoqtMaxRequestId,
+      MoqtFetch, MoqtFetchCancel, MoqtFetchOk, MoqtRequestsBlocked, MoqtPublish,
       MoqtNamespace, MoqtNamespaceDone, MoqtObjectAck>;
 
   // The total actual size of the message.
@@ -1267,10 +1267,6 @@
       QUIC_LOG(INFO) << "SUBSCRIBE_NAMESPACE track namespace mismatch";
       return false;
     }
-    if (cast.subscribe_options != subscribe_namespace_.subscribe_options) {
-      QUIC_LOG(INFO) << "SUBSCRIBE_NAMESPACE subscribe options mismatch";
-      return false;
-    }
     if (cast.parameters != subscribe_namespace_.parameters) {
       QUIC_LOG(INFO) << "SUBSCRIBE_NAMESPACE parameters mismatch";
       return false;
@@ -1278,17 +1274,16 @@
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vvv---vvv-----"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---vvv-----vv"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(subscribe_namespace_);
   }
 
  private:
-  uint8_t raw_packet_[20] = {
-      0x11, 0x00, 0x11, 0x01,                    // request_id = 1
+  uint8_t raw_packet_[19] = {
+      0x50, 0x00, 0x10, 0x01,                    // request_id = 1
       0x01, 0x03, 0x66, 0x6f, 0x6f,              // namespace = "foo"
-      0x02,                                      // subscribe_options = kBoth
       0x02,                                      // 2 parameters
       0x03, 0x05, 0x03, 0x00, 0x62, 0x61, 0x72,  // authorization_tag = "bar"
       0x0d, 0x01,                                // forward = true
@@ -1297,7 +1292,55 @@
   MoqtSubscribeNamespace subscribe_namespace_ = {
       /*request_id=*/1,
       TrackNamespace({"foo"}),
-      SubscribeNamespaceOption::kBoth,
+      MessageParameters(),  // set in constructor.
+  };
+};
+
+class QUICHE_NO_EXPORT SubscribeTracksMessage : public TestMessageBase {
+ public:
+  SubscribeTracksMessage() : TestMessageBase() {
+    subscribe_tracks_.parameters.authorization_tokens.push_back(
+        AuthToken(AuthTokenType::kOutOfBand, "bar"));
+    subscribe_tracks_.parameters.set_forward(true);
+    SetWireImage(raw_packet_, sizeof(raw_packet_));
+  }
+
+  bool EqualFieldValues(const MessageStructuredData& values) const override {
+    auto cast = std::get<MoqtSubscribeTracks>(values);
+    if (cast.request_id != subscribe_tracks_.request_id) {
+      QUIC_LOG(INFO) << "SUBSCRIBE_TRACKS request_id mismatch";
+      return false;
+    }
+    if (cast.track_namespace_prefix !=
+        subscribe_tracks_.track_namespace_prefix) {
+      QUIC_LOG(INFO) << "SUBSCRIBE_TRACKS track namespace mismatch";
+      return false;
+    }
+    if (cast.parameters != subscribe_tracks_.parameters) {
+      QUIC_LOG(INFO) << "SUBSCRIBE_TRACKS parameters mismatch";
+      return false;
+    }
+    return true;
+  }
+
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---vvv-----vv"); }
+
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(subscribe_tracks_);
+  }
+
+ private:
+  uint8_t raw_packet_[19] = {
+      0x51, 0x00, 0x10, 0x01,                    // request_id = 1
+      0x01, 0x03, 0x66, 0x6f, 0x6f,              // namespace = "foo"
+      0x02,                                      // 2 parameters
+      0x03, 0x05, 0x03, 0x00, 0x62, 0x61, 0x72,  // authorization_tag = "bar"
+      0x0d, 0x01,                                // forward = true
+  };
+
+  MoqtSubscribeTracks subscribe_tracks_ = {
+      /*request_id=*/1,
+      TrackNamespace({"foo"}),
       MessageParameters(),  // set in constructor.
   };
 };
@@ -1797,6 +1840,8 @@
       return std::make_unique<GoAwayMessage>();
     case MoqtMessageType::kSubscribeNamespace:
       return std::make_unique<SubscribeNamespaceMessage>();
+    case MoqtMessageType::kSubscribeTracks:
+      return std::make_unique<SubscribeTracksMessage>();
     case MoqtMessageType::kMaxRequestId:
       return std::make_unique<MaxRequestIdMessage>();
     case MoqtMessageType::kFetch:
diff --git a/quiche/quic/moqt/tools/chat_client.cc b/quiche/quic/moqt/tools/chat_client.cc
index 558470c..86b2750 100644
--- a/quiche/quic/moqt/tools/chat_client.cc
+++ b/quiche/quic/moqt/tools/chat_client.cc
@@ -324,9 +324,8 @@
   MessageParameters parameters;
   parameters.authorization_tokens.emplace_back(
       AuthTokenType::kOutOfBand, std::string(GetUsername(my_track_name_)));
-  namespace_task_ =
-      session_->SubscribeNamespace(prefix, SubscribeNamespaceOption::kNamespace,
-                                   parameters, std::move(response_callback));
+  namespace_task_ = session_->SubscribeNamespace(prefix, parameters,
+                                                 std::move(response_callback));
   if (namespace_task_ != nullptr) {
     namespace_task_->SetObjectsAvailableCallback(
         [this]() {
diff --git a/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc b/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc
index 75ffa99..7c2b46a 100644
--- a/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc
+++ b/quiche/quic/moqt/tools/moq_chat_end_to_end_test.cc
@@ -151,7 +151,7 @@
   TransactionType last_type;
   std::unique_ptr<MoqtNamespaceTask> namespace_probe =
       relay_.publisher()->AddNamespaceSubscriber(
-          TrackNamespace({moq_chat::kBasePath}), nullptr);
+          TrackNamespace({moq_chat::kBasePath}));
   namespace_probe->SetObjectsAvailableCallback([&]() {
     while (namespace_probe->GetNextSuffix(last_suffix, last_type) == kSuccess) {
       if (last_type == TransactionType::kAdd) {
diff --git a/quiche/quic/moqt/tools/moqt_relay.cc b/quiche/quic/moqt/tools/moqt_relay.cc
index 14b77bd..48d2a80 100644
--- a/quiche/quic/moqt/tools/moqt_relay.cc
+++ b/quiche/quic/moqt/tools/moqt_relay.cc
@@ -16,6 +16,7 @@
 #include "quiche/quic/core/crypto/proof_verifier.h"
 #include "quiche/quic/core/io/quic_event_loop.h"
 #include "quiche/quic/core/quic_server_id.h"
+#include "quiche/quic/moqt/moqt_error.h"
 #include "quiche/quic/moqt/moqt_fetch_task.h"
 #include "quiche/quic/moqt/moqt_key_value_pair.h"
 #include "quiche/quic/moqt/moqt_names.h"
@@ -127,27 +128,36 @@
         }
       };
   session->callbacks().incoming_subscribe_namespace_callback =
-      [this, session](const TrackNamespace& prefix,
-                      SubscribeNamespaceOption option,
-                      const MessageParameters& parameters,
-                      MoqtResponseCallback response_callback)
+      [this](const TrackNamespace& prefix, const MessageParameters& parameters,
+             MoqtResponseCallback response_callback)
       -> std::unique_ptr<MoqtNamespaceTask> {
     if (is_closing_) {
       return nullptr;
     }
-    std::unique_ptr<MoqtNamespaceTask> task;
-    switch (option) {
-      case SubscribeNamespaceOption::kNamespace:
-        task = publisher_.AddNamespaceSubscriber(prefix, nullptr);
-        break;
-      case SubscribeNamespaceOption::kBoth:
-      case SubscribeNamespaceOption::kPublish:
-        task = publisher_.AddNamespaceSubscriber(prefix, session);
-        break;
+    std::unique_ptr<MoqtNamespaceTask> task =
+        publisher_.AddNamespaceSubscriber(prefix);
+    if (task == nullptr) {
+      std::move(response_callback)(MoqtRequestErrorInfo{
+          RequestErrorCode::kInternalError, std::nullopt, ""});
+      return nullptr;
     }
     std::move(response_callback)(MessageParameters());
     return task;
   };
+  session->callbacks().incoming_subscribe_tracks_callback =
+      [this, session](const TrackNamespace& prefix,
+                      const MessageParameters& parameters,
+                      MoqtResponseCallback response_callback) {
+        if (is_closing_) {
+          return;
+        }
+        if (!publisher_.AddTrackSubscriber(prefix, session)) {
+          std::move(response_callback)(MoqtRequestErrorInfo{
+              RequestErrorCode::kInternalError, std::nullopt, ""});
+        } else {
+          std::move(response_callback)(MessageParameters());
+        }
+      };
 }
 
 absl::StatusOr<MoqtConfigureSessionCallback> MoqtRelay::IncomingSessionHandler(
diff --git a/quiche/quic/moqt/tools/moqt_relay_test.cc b/quiche/quic/moqt/tools/moqt_relay_test.cc
index 7c8ae77..2ea008e 100644
--- a/quiche/quic/moqt/tools/moqt_relay_test.cc
+++ b/quiche/quic/moqt/tools/moqt_relay_test.cc
@@ -23,7 +23,6 @@
 #include "quiche/quic/moqt/moqt_relay_publisher.h"
 #include "quiche/quic/moqt/moqt_session.h"
 #include "quiche/quic/moqt/moqt_session_interface.h"
-#include "quiche/quic/moqt/moqt_types.h"
 #include "quiche/quic/moqt/test_tools/moqt_mock_visitor.h"
 #include "quiche/quic/moqt/tools/moqt_client.h"
 #include "quiche/quic/moqt/tools/moqt_server.h"
@@ -161,7 +160,7 @@
   TrackNamespace suffix;
   TransactionType type;
   std::unique_ptr<MoqtNamespaceTask> relay_probe =
-      relay_.publisher()->AddNamespaceSubscriber(foo, nullptr);
+      relay_.publisher()->AddNamespaceSubscriber(foo);
   relay_probe->SetObjectsAvailableCallback([&]() {
     while (relay_probe->GetNextSuffix(suffix, type) == kSuccess) {
       if (type == TransactionType::kAdd) {
@@ -174,7 +173,7 @@
     }
   });
   std::unique_ptr<MoqtNamespaceTask> upstream_probe =
-      upstream_.publisher()->AddNamespaceSubscriber(foo, nullptr);
+      upstream_.publisher()->AddNamespaceSubscriber(foo);
   upstream_probe->SetObjectsAvailableCallback([&]() {
     while (upstream_probe->GetNextSuffix(suffix, type) == kSuccess) {
       if (type == TransactionType::kAdd) {
@@ -202,7 +201,7 @@
   // Upstream subscribes. Now it's notified and forwards it to the probe.
   std::unique_ptr<MoqtNamespaceTask> task =
       upstream_session->SubscribeNamespace(
-          foo, SubscribeNamespaceOption::kNamespace, MessageParameters(),
+          foo, MessageParameters(),
           [](std::variant<MessageParameters, MoqtRequestErrorInfo>) {});
   EXPECT_NE(task, nullptr);
   task->SetObjectsAvailableCallback([&]() {