Update existing messages for draft-06. This is mostly turning track_namespace into a tuple.  Also generalizes Subscribe parameters.

The substantive stuff is in moqt_messages, moqt_framer, and moqt_parser. The rest is just fixing compiler problems, since FullTrackName is visible to applications.

New draft-06 messages will follow in another CL, although the codepoints are here.

PiperOrigin-RevId: 680678558
diff --git a/quiche/quic/moqt/moqt_framer.cc b/quiche/quic/moqt/moqt_framer.cc
index c4fe1e1..6974ca6 100644
--- a/quiche/quic/moqt/moqt_framer.cc
+++ b/quiche/quic/moqt/moqt_framer.cc
@@ -112,25 +112,133 @@
   const IntParameter& parameter_;
 };
 
+class WireSubscribeParameterList {
+ public:
+  explicit WireSubscribeParameterList(const MoqtSubscribeParameters& list)
+      : list_(list) {}
+
+  size_t GetLengthOnWire() {
+    uint64_t num_params = 0;
+    size_t length = 0;
+    if (list_.authorization_info.has_value()) {
+      ++num_params;
+      length +=
+          WireStringParameter(
+              StringParameter(MoqtTrackRequestParameter::kAuthorizationInfo,
+                              *list_.authorization_info))
+              .GetLengthOnWire();
+    }
+    if (list_.delivery_timeout.has_value()) {
+      ++num_params;
+      length += WireIntParameter(
+                    IntParameter(MoqtTrackRequestParameter::kDeliveryTimeout,
+                                 static_cast<uint64_t>(
+                                     list_.delivery_timeout->ToMilliseconds())))
+                    .GetLengthOnWire();
+    }
+    if (list_.max_cache_duration.has_value()) {
+      ++num_params;
+      length +=
+          WireIntParameter(
+              IntParameter(MoqtTrackRequestParameter::kMaxCacheDuration,
+                           static_cast<uint64_t>(
+                               list_.max_cache_duration->ToMilliseconds())))
+              .GetLengthOnWire();
+    }
+    if (list_.object_ack_window.has_value()) {
+      ++num_params;
+      length +=
+          WireIntParameter(
+              IntParameter(MoqtTrackRequestParameter::kOackWindowSize,
+                           static_cast<uint64_t>(
+                               list_.object_ack_window->ToMicroseconds())))
+              .GetLengthOnWire();
+    }
+    length += WireVarInt62(num_params).GetLengthOnWire();
+    return length;
+  }
+  absl::Status SerializeIntoWriter(quiche::QuicheDataWriter& writer) {
+    uint64_t num_params = (list_.authorization_info.has_value() ? 1 : 0) +
+                          (list_.delivery_timeout.has_value() ? 1 : 0) +
+                          (list_.max_cache_duration.has_value() ? 1 : 0) +
+                          (list_.object_ack_window.has_value() ? 1 : 0);
+    if (!writer.WriteVarInt62(num_params)) {
+      return absl::InternalError("Failed to serialize the length prefix");
+    }
+    if (list_.authorization_info.has_value() &&
+        WireStringParameter(
+            StringParameter(MoqtTrackRequestParameter::kAuthorizationInfo,
+                            *list_.authorization_info))
+                .SerializeIntoWriter(writer) != absl::OkStatus()) {
+      return absl::InternalError("Failed to serialize the authorization info");
+    }
+    if (list_.delivery_timeout.has_value() &&
+        WireIntParameter(
+            IntParameter(MoqtTrackRequestParameter::kDeliveryTimeout,
+                         static_cast<uint64_t>(
+                             list_.delivery_timeout->ToMilliseconds())))
+                .SerializeIntoWriter(writer) != absl::OkStatus()) {
+      return absl::InternalError("Failed to serialize the delivery timeout");
+    }
+    if (list_.max_cache_duration.has_value() &&
+        WireIntParameter(
+            IntParameter(MoqtTrackRequestParameter::kMaxCacheDuration,
+                         static_cast<uint64_t>(
+                             list_.max_cache_duration->ToMilliseconds())))
+                .SerializeIntoWriter(writer) != absl::OkStatus()) {
+      return absl::InternalError("Failed to serialize the max cache duration");
+    }
+    if (list_.object_ack_window.has_value() &&
+        WireIntParameter(
+            IntParameter(MoqtTrackRequestParameter::kOackWindowSize,
+                         static_cast<uint64_t>(
+                             list_.object_ack_window->ToMicroseconds())))
+                .SerializeIntoWriter(writer) != absl::OkStatus()) {
+      return absl::InternalError("Failed to serialize the oack window size");
+    }
+    return absl::OkStatus();
+  }
+
+ private:
+  const MoqtSubscribeParameters& list_;
+};
+
 class WireFullTrackName {
  public:
   using DataType = FullTrackName;
 
-  explicit WireFullTrackName(const FullTrackName& name) : name_(name) {}
+  // If |includes_name| is true, the last element in the tuple is the track
+  // name and is therefore not counted in the prefix of the namespace tuple.
+  WireFullTrackName(const FullTrackName& name, bool includes_name)
+      : name_(name), includes_name_(includes_name) {}
 
   size_t GetLengthOnWire() {
-    return quiche::ComputeLengthOnWire(
-        WireStringWithVarInt62Length(name_.track_namespace()),
-        WireStringWithVarInt62Length(name_.track_name()));
+    const auto tuple = name_.tuple();
+    size_t num_elements = includes_name_ ? (tuple.size() - 1) : tuple.size();
+    size_t length = WireVarInt62(num_elements).GetLengthOnWire();
+    for (const auto& element : tuple) {
+      length += WireStringWithVarInt62Length(element).GetLengthOnWire();
+    }
+    return length;
   }
   absl::Status SerializeIntoWriter(quiche::QuicheDataWriter& writer) {
-    return quiche::SerializeIntoWriter(
-        writer, WireStringWithVarInt62Length(name_.track_namespace()),
-        WireStringWithVarInt62Length(name_.track_name()));
+    const auto tuple = name_.tuple();
+    size_t num_elements = includes_name_ ? (tuple.size() - 1) : tuple.size();
+    if (!writer.WriteVarInt62(num_elements)) {
+      return absl::InternalError("Failed to serialize the length prefix");
+    }
+    for (const auto& element : tuple) {
+      if (WireStringWithVarInt62Length(element).SerializeIntoWriter(writer) !=
+          absl::OkStatus()) {
+        return absl::InternalError("Failed to serialize the element");
+      }
+    }
+    return absl::OkStatus();
   }
 
  private:
   const FullTrackName& name_;
+  const bool includes_name_;
 };
 
 // Serializes data into buffer using the default allocator.  Invokes QUICHE_BUG
@@ -333,58 +441,38 @@
     QUICHE_BUG(MoqtFramer_invalid_subscribe) << "Invalid object range";
     return quiche::QuicheBuffer();
   }
-  absl::InlinedVector<StringParameter, 1> string_params;
-  if (message.parameters.authorization_info.has_value()) {
-    string_params.push_back(
-        StringParameter(MoqtTrackRequestParameter::kAuthorizationInfo,
-                        *message.parameters.authorization_info));
-  }
-  absl::InlinedVector<IntParameter, 1> int_params;
-  if (message.parameters.object_ack_window.has_value()) {
-    QUICHE_DCHECK(message.parameters.object_ack_window->ToMicroseconds() >= 0);
-    int_params.push_back(IntParameter(
-        MoqtTrackRequestParameter::kOackWindowSize,
-        static_cast<uint64_t>(
-            message.parameters.object_ack_window->ToMicroseconds())));
-  }
   switch (filter_type) {
     case MoqtFilterType::kLatestGroup:
     case MoqtFilterType::kLatestObject:
       return Serialize(
           WireVarInt62(MoqtMessageType::kSubscribe),
           WireVarInt62(message.subscribe_id), WireVarInt62(message.track_alias),
-          WireFullTrackName(message.full_track_name),
+          WireFullTrackName(message.full_track_name, true),
           WireUint8(message.subscriber_priority),
           WireDeliveryOrder(message.group_order), WireVarInt62(filter_type),
-          WireVarInt62(string_params.size() + int_params.size()),
-          WireSpan<WireStringParameter>(string_params),
-          WireSpan<WireIntParameter>(int_params));
+          WireSubscribeParameterList(message.parameters));
     case MoqtFilterType::kAbsoluteStart:
       return Serialize(
           WireVarInt62(MoqtMessageType::kSubscribe),
           WireVarInt62(message.subscribe_id), WireVarInt62(message.track_alias),
-          WireFullTrackName(message.full_track_name),
+          WireFullTrackName(message.full_track_name, true),
           WireUint8(message.subscriber_priority),
           WireDeliveryOrder(message.group_order), WireVarInt62(filter_type),
           WireVarInt62(*message.start_group),
           WireVarInt62(*message.start_object),
-          WireVarInt62(string_params.size() + int_params.size()),
-          WireSpan<WireStringParameter>(string_params),
-          WireSpan<WireIntParameter>(int_params));
+          WireSubscribeParameterList(message.parameters));
     case MoqtFilterType::kAbsoluteRange:
       return Serialize(
           WireVarInt62(MoqtMessageType::kSubscribe),
           WireVarInt62(message.subscribe_id), WireVarInt62(message.track_alias),
-          WireFullTrackName(message.full_track_name),
+          WireFullTrackName(message.full_track_name, true),
           WireUint8(message.subscriber_priority),
           WireDeliveryOrder(message.group_order), WireVarInt62(filter_type),
           WireVarInt62(*message.start_group),
           WireVarInt62(*message.start_object), WireVarInt62(*message.end_group),
           WireVarInt62(message.end_object.has_value() ? *message.end_object + 1
                                                       : 0),
-          WireVarInt62(string_params.size() + int_params.size()),
-          WireSpan<WireStringParameter>(string_params),
-          WireSpan<WireIntParameter>(int_params));
+          WireSubscribeParameterList(message.parameters));
     default:
       QUICHE_BUG(MoqtFramer_end_group_missing) << "Subscribe framing error.";
       return quiche::QuicheBuffer();
@@ -393,18 +481,24 @@
 
 quiche::QuicheBuffer MoqtFramer::SerializeSubscribeOk(
     const MoqtSubscribeOk& message) {
+  if (message.parameters.authorization_info.has_value()) {
+    QUICHE_BUG(MoqtFramer_invalid_subscribe_ok)
+        << "SUBSCRIBE_OK with delivery timeout";
+  }
   if (message.largest_id.has_value()) {
     return Serialize(WireVarInt62(MoqtMessageType::kSubscribeOk),
                      WireVarInt62(message.subscribe_id),
                      WireVarInt62(message.expires.ToMilliseconds()),
                      WireDeliveryOrder(message.group_order), WireUint8(1),
                      WireVarInt62(message.largest_id->group),
-                     WireVarInt62(message.largest_id->object));
+                     WireVarInt62(message.largest_id->object),
+                     WireSubscribeParameterList(message.parameters));
   }
   return Serialize(WireVarInt62(MoqtMessageType::kSubscribeOk),
                    WireVarInt62(message.subscribe_id),
                    WireVarInt62(message.expires.ToMilliseconds()),
-                   WireDeliveryOrder(message.group_order), WireUint8(0));
+                   WireDeliveryOrder(message.group_order), WireUint8(0),
+                   WireSubscribeParameterList(message.parameters));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeSubscribeError(
@@ -440,6 +534,10 @@
 
 quiche::QuicheBuffer MoqtFramer::SerializeSubscribeUpdate(
     const MoqtSubscribeUpdate& message) {
+  if (message.parameters.authorization_info.has_value()) {
+    QUICHE_BUG(MoqtFramer_invalid_subscribe_update)
+        << "SUBSCRIBE_UPDATE with authorization info";
+  }
   uint64_t end_group =
       message.end_group.has_value() ? *message.end_group + 1 : 0;
   uint64_t end_object =
@@ -448,45 +546,35 @@
     QUICHE_BUG(MoqtFramer_invalid_subscribe_update) << "Invalid object range";
     return quiche::QuicheBuffer();
   }
-  absl::InlinedVector<StringParameter, 1> string_params;
-  if (message.authorization_info.has_value()) {
-    string_params.push_back(
-        StringParameter(MoqtTrackRequestParameter::kAuthorizationInfo,
-                        *message.authorization_info));
-  }
   return Serialize(
       WireVarInt62(MoqtMessageType::kSubscribeUpdate),
       WireVarInt62(message.subscribe_id), WireVarInt62(message.start_group),
       WireVarInt62(message.start_object), WireVarInt62(end_group),
       WireVarInt62(end_object), WireUint8(message.subscriber_priority),
-      WireSpan<WireStringParameter>(string_params));
+      WireSubscribeParameterList(message.parameters));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeAnnounce(
     const MoqtAnnounce& message) {
-  absl::InlinedVector<StringParameter, 1> string_params;
-  if (message.authorization_info.has_value()) {
-    string_params.push_back(
-        StringParameter(MoqtTrackRequestParameter::kAuthorizationInfo,
-                        *message.authorization_info));
+  if (message.parameters.delivery_timeout.has_value()) {
+    QUICHE_BUG(MoqtFramer_invalid_announce) << "ANNOUNCE with delivery timeout";
   }
   return Serialize(
       WireVarInt62(static_cast<uint64_t>(MoqtMessageType::kAnnounce)),
-      WireStringWithVarInt62Length(message.track_namespace),
-      WireVarInt62(string_params.size()),
-      WireSpan<WireStringParameter>(string_params));
+      WireFullTrackName(message.track_namespace, false),
+      WireSubscribeParameterList(message.parameters));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeAnnounceOk(
     const MoqtAnnounceOk& message) {
   return Serialize(WireVarInt62(MoqtMessageType::kAnnounceOk),
-                   WireStringWithVarInt62Length(message.track_namespace));
+                   WireFullTrackName(message.track_namespace, false));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeAnnounceError(
     const MoqtAnnounceError& message) {
   return Serialize(WireVarInt62(MoqtMessageType::kAnnounceError),
-                   WireStringWithVarInt62Length(message.track_namespace),
+                   WireFullTrackName(message.track_namespace, false),
                    WireVarInt62(message.error_code),
                    WireStringWithVarInt62Length(message.reason_phrase));
 }
@@ -494,25 +582,27 @@
 quiche::QuicheBuffer MoqtFramer::SerializeAnnounceCancel(
     const MoqtAnnounceCancel& message) {
   return Serialize(WireVarInt62(MoqtMessageType::kAnnounceCancel),
-                   WireStringWithVarInt62Length(message.track_namespace));
+                   WireFullTrackName(message.track_namespace, false),
+                   WireVarInt62(message.error_code),
+                   WireStringWithVarInt62Length(message.reason_phrase));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeTrackStatusRequest(
     const MoqtTrackStatusRequest& message) {
   return Serialize(WireVarInt62(MoqtMessageType::kTrackStatusRequest),
-                   WireFullTrackName(message.full_track_name));
+                   WireFullTrackName(message.full_track_name, true));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeUnannounce(
     const MoqtUnannounce& message) {
   return Serialize(WireVarInt62(MoqtMessageType::kUnannounce),
-                   WireStringWithVarInt62Length(message.track_namespace));
+                   WireFullTrackName(message.track_namespace, false));
 }
 
 quiche::QuicheBuffer MoqtFramer::SerializeTrackStatus(
     const MoqtTrackStatus& message) {
   return Serialize(WireVarInt62(MoqtMessageType::kTrackStatus),
-                   WireFullTrackName(message.full_track_name),
+                   WireFullTrackName(message.full_track_name, true),
                    WireVarInt62(message.status_code),
                    WireVarInt62(message.last_group),
                    WireVarInt62(message.last_object));
diff --git a/quiche/quic/moqt/moqt_framer_test.cc b/quiche/quic/moqt/moqt_framer_test.cc
index 55f6944..510dd17 100644
--- a/quiche/quic/moqt/moqt_framer_test.cc
+++ b/quiche/quic/moqt/moqt_framer_test.cc
@@ -309,7 +309,8 @@
               start_object,
               end_group,
               end_object,
-              MoqtSubscribeParameters{"bar"},
+              MoqtSubscribeParameters{"bar", std::nullopt, std::nullopt,
+                                      std::nullopt},
           };
           quiche::QuicheBuffer buffer;
           MoqtFilterType expected_filter_type = MoqtFilterType::kNone;
@@ -335,7 +336,7 @@
           }
           buffer = framer_.SerializeSubscribe(subscribe);
           // Go to the filter type.
-          const uint8_t* read = BufferAtOffset(buffer, 14);
+          const uint8_t* read = BufferAtOffset(buffer, 15);
           EXPECT_EQ(static_cast<MoqtFilterType>(*read), expected_filter_type);
           EXPECT_GT(buffer.size(), 0);
           if (expected_filter_type == MoqtFilterType::kAbsoluteRange &&
@@ -360,7 +361,7 @@
       /*start_object=*/std::optional<uint64_t>(3),
       /*end_group=*/std::optional<uint64_t>(3),
       /*end_object=*/std::nullopt,
-      MoqtSubscribeParameters{"bar"},
+      MoqtSubscribeParameters{"bar", std::nullopt, std::nullopt, std::nullopt},
   };
   quiche::QuicheBuffer buffer;
   EXPECT_QUIC_BUG(buffer = framer_.SerializeSubscribe(subscribe),
@@ -384,7 +385,7 @@
       /*start_object=*/std::optional<uint64_t>(3),
       /*end_group=*/std::nullopt,
       /*end_object=*/std::nullopt,
-      MoqtSubscribeParameters{"bar"},
+      MoqtSubscribeParameters{"bar", std::nullopt, std::nullopt, std::nullopt},
   };
   quiche::QuicheBuffer buffer;
   EXPECT_QUIC_BUG(buffer = framer_.SerializeSubscribe(subscribe),
@@ -400,7 +401,8 @@
       /*end_group=*/4,
       /*end_object=*/std::nullopt,
       /*subscriber_priority=*/0xaa,
-      /*authorization_info=*/"bar",
+      MoqtSubscribeParameters{std::nullopt, std::nullopt, std::nullopt,
+                              std::nullopt},
   };
   quiche::QuicheBuffer buffer;
   buffer = framer_.SerializeSubscribeUpdate(subscribe_update);
@@ -419,7 +421,8 @@
       /*end_group=*/4,
       /*end_object=*/6,
       /*subscriber_priority=*/0xaa,
-      /*authorization_info=*/"bar",
+      MoqtSubscribeParameters{std::nullopt, std::nullopt, std::nullopt,
+                              std::nullopt},
   };
   quiche::QuicheBuffer buffer;
   buffer = framer_.SerializeSubscribeUpdate(subscribe_update);
@@ -438,7 +441,8 @@
       /*end_group=*/std::nullopt,
       /*end_object=*/6,
       /*subscriber_priority=*/0xaa,
-      /*authorization_info=*/"bar",
+      MoqtSubscribeParameters{std::nullopt, std::nullopt, std::nullopt,
+                              std::nullopt},
   };
   quiche::QuicheBuffer buffer;
   EXPECT_QUIC_BUG(buffer = framer_.SerializeSubscribeUpdate(subscribe_update),
diff --git a/quiche/quic/moqt/moqt_integration_test.cc b/quiche/quic/moqt/moqt_integration_test.cc
index f7578cd..11f6be4 100644
--- a/quiche/quic/moqt/moqt_integration_test.cc
+++ b/quiche/quic/moqt/moqt_integration_test.cc
@@ -122,19 +122,21 @@
 
 TEST_F(MoqtIntegrationTest, AnnounceSuccess) {
   EstablishSession();
-  EXPECT_CALL(server_callbacks_.incoming_announce_callback, Call("foo"))
+  EXPECT_CALL(server_callbacks_.incoming_announce_callback,
+              Call(FullTrackName{"foo"}))
       .WillOnce(Return(std::nullopt));
   testing::MockFunction<void(
-      absl::string_view track_namespace,
+      FullTrackName track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_callback;
-  client_->session()->Announce("foo", announce_callback.AsStdFunction());
+  client_->session()->Announce(FullTrackName{"foo"},
+                               announce_callback.AsStdFunction());
   bool matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](absl::string_view track_namespace,
+      .WillOnce([&](FullTrackName track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         matches = true;
-        EXPECT_EQ(track_namespace, "foo");
+        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
         EXPECT_FALSE(error.has_value());
       });
   bool success =
@@ -144,22 +146,25 @@
 
 TEST_F(MoqtIntegrationTest, AnnounceSuccessSubscribeInResponse) {
   EstablishSession();
-  EXPECT_CALL(server_callbacks_.incoming_announce_callback, Call("foo"))
+  EXPECT_CALL(server_callbacks_.incoming_announce_callback,
+              Call(FullTrackName{"foo"}))
       .WillOnce(Return(std::nullopt));
   MockRemoteTrackVisitor server_visitor;
   testing::MockFunction<void(
-      absl::string_view track_namespace,
+      FullTrackName track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_callback;
-  client_->session()->Announce("foo", announce_callback.AsStdFunction());
+  client_->session()->Announce(FullTrackName{"foo"},
+                               announce_callback.AsStdFunction());
   bool matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](absl::string_view track_namespace,
+      .WillOnce([&](FullTrackName track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
-        EXPECT_EQ(track_namespace, "foo");
+        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
+        FullTrackName track_name = track_namespace;
+        track_name.AddElement("/catalog");
         EXPECT_FALSE(error.has_value());
-        server_->session()->SubscribeCurrentGroup(
-            FullTrackName(track_namespace, "/catalog"), &server_visitor);
+        server_->session()->SubscribeCurrentGroup(track_name, &server_visitor);
       });
   EXPECT_CALL(server_visitor, OnReply(_, _)).WillOnce([&]() {
     matches = true;
@@ -176,10 +181,11 @@
   // it receives.
   MockRemoteTrackVisitor server_visitor;
   EXPECT_CALL(server_callbacks_.incoming_announce_callback, Call(_))
-      .WillOnce([&](absl::string_view track_namespace) {
+      .WillOnce([&](FullTrackName track_namespace) {
+        FullTrackName track_name = track_namespace;
+        track_name.AddElement("data");
         server_->session()->SubscribeAbsolute(
-            FullTrackName(track_namespace, "data"), /*start_group=*/0,
-            /*start_object=*/0, &server_visitor);
+            track_name, /*start_group=*/0, /*start_object=*/0, &server_visitor);
         return std::optional<MoqtAnnounceErrorReason>();
       });
 
@@ -194,7 +200,8 @@
     received_subscribe_ok = true;
   });
   client_->session()->Announce(
-      "test", [](absl::string_view, std::optional<MoqtAnnounceErrorReason>) {});
+      FullTrackName{"test"},
+      [](FullTrackName, std::optional<MoqtAnnounceErrorReason>) {});
 
   bool received_object = false;
   EXPECT_CALL(server_visitor, OnObjectFragment(_, _, _, _, _, _, _, _))
@@ -335,16 +342,17 @@
 TEST_F(MoqtIntegrationTest, AnnounceFailure) {
   EstablishSession();
   testing::MockFunction<void(
-      absl::string_view track_namespace,
+      FullTrackName track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_callback;
-  client_->session()->Announce("foo", announce_callback.AsStdFunction());
+  client_->session()->Announce(FullTrackName{"foo"},
+                               announce_callback.AsStdFunction());
   bool matches = false;
   EXPECT_CALL(announce_callback, Call(_, _))
-      .WillOnce([&](absl::string_view track_namespace,
+      .WillOnce([&](FullTrackName track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         matches = true;
-        EXPECT_EQ(track_namespace, "foo");
+        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
         ASSERT_TRUE(error.has_value());
         EXPECT_EQ(error->error_code,
                   MoqtAnnounceErrorCode::kAnnounceNotSupported);
diff --git a/quiche/quic/moqt/moqt_messages.cc b/quiche/quic/moqt/moqt_messages.cc
index 26acbac..5c2e503 100644
--- a/quiche/quic/moqt/moqt_messages.cc
+++ b/quiche/quic/moqt/moqt_messages.cc
@@ -95,6 +95,14 @@
       return "UNANNOUNCE";
     case MoqtMessageType::kGoAway:
       return "GOAWAY";
+    case MoqtMessageType::kSubscribeNamespace:
+      return "SUBSCRIBE_NAMESPACE";
+    case MoqtMessageType::kSubscribeNamespaceOk:
+      return "SUBSCRIBE_NAMESPACE_OK";
+    case MoqtMessageType::kSubscribeNamespaceError:
+      return "SUBSCRIBE_NAMESPACE_ERROR";
+    case MoqtMessageType::kUnsubscribeNamespace:
+      return "UNSUBSCRIBE_NAMESPACE";
     case MoqtMessageType::kMaxSubscribeId:
       return "MAX_SUBSCRIBE_ID";
     case MoqtMessageType::kObjectAck:
@@ -190,16 +198,6 @@
   return absl::c_lexicographical_compare(tuple_, other.tuple_);
 }
 FullTrackName::FullTrackName(absl::Span<const absl::string_view> elements)
-    : tuple_(elements.begin(), elements.end()) {
-  if (tuple_.size() < 2) {
-    QUICHE_BUG(FullTrackName_too_short)
-        << "Full track name should be at least two elements long";
-    // Failsafe.
-    while (tuple_.size() < 2) {
-      tuple_.push_back("");
-    }
-    return;
-  }
-}
+    : tuple_(elements.begin(), elements.end()) {}
 
 }  // namespace moqt
diff --git a/quiche/quic/moqt/moqt_messages.h b/quiche/quic/moqt/moqt_messages.h
index c522f01..4a08832 100644
--- a/quiche/quic/moqt/moqt_messages.h
+++ b/quiche/quic/moqt/moqt_messages.h
@@ -89,6 +89,10 @@
   kTrackStatusRequest = 0x0d,
   kTrackStatus = 0x0e,
   kGoAway = 0x10,
+  kSubscribeNamespace = 0x11,
+  kSubscribeNamespaceOk = 0x12,
+  kSubscribeNamespaceError = 0x13,
+  kUnsubscribeNamespace = 0x14,
   kMaxSubscribeId = 0x15,
   kClientSetup = 0x40,
   kServerSetup = 0x41,
@@ -137,6 +141,8 @@
 
 enum class QUICHE_EXPORT MoqtTrackRequestParameter : uint64_t {
   kAuthorizationInfo = 0x2,
+  kDeliveryTimeout = 0x3,
+  kMaxCacheDuration = 0x4,
 
   // QUICHE-specific extensions.
   kOackWindowSize = 0xbbf1439,
@@ -154,10 +160,9 @@
   std::string reason_phrase;
 };
 
-// Full track name represents a tuple of the track namespace and the the track
-// name.  (TODO) After draft-06, multiple elements in track namespace will be
-// supported; if https://github.com/moq-wg/moq-transport/issues/508 goes
-// through, the distinction between different parts will disappear.
+// Full track name represents a tuple of name elements. All higher order
+// elements MUST be present, but lower-order ones (like the name) can be
+// omitted.
 class FullTrackName {
  public:
   explicit FullTrackName(absl::Span<const absl::string_view> elements);
@@ -167,15 +172,30 @@
             std::data(elements), std::size(elements))) {}
   explicit FullTrackName(absl::string_view ns, absl::string_view name)
       : FullTrackName({ns, name}) {}
-  FullTrackName() : FullTrackName({"", ""}) {}
+  FullTrackName() : FullTrackName({}) {}
 
   std::string ToString() const;
 
-  absl::string_view track_namespace() const {
-    // TODO: turn into a tuple for draft-06.
-    return tuple_[0];
+  void AddElement(absl::string_view element) {
+    tuple_.push_back(std::string(element));
   }
-  absl::string_view track_name() const { return tuple_[tuple_.size() - 1]; }
+  // Remove the last element to convert a name to a namespace.
+  void NameToNamespace() { tuple_.pop_back(); }
+  // returns true is |this| is a subdomain of |other|.
+  bool InNamespace(const FullTrackName& other) const {
+    if (tuple_.size() < other.tuple_.size()) {
+      return false;
+    }
+    for (int i = 0; i < other.tuple_.size(); ++i) {
+      if (tuple_[i] != other.tuple_[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+  absl::Span<const std::string> tuple() const {
+    return absl::MakeConstSpan(tuple_);
+  }
 
   bool operator==(const FullTrackName& other) const;
   bool operator<(const FullTrackName& other) const;
@@ -190,6 +210,8 @@
     sink.Append(track_name.ToString());
   }
 
+  bool empty() const { return tuple_.empty(); }
+
  private:
   absl::InlinedVector<std::string, 2> tuple_;
 };
@@ -288,12 +310,21 @@
 
 struct QUICHE_EXPORT MoqtSubscribeParameters {
   std::optional<std::string> authorization_info;
+  std::optional<quic::QuicTimeDelta> delivery_timeout;
+  std::optional<quic::QuicTimeDelta> max_cache_duration;
 
   // If present, indicates that OBJECT_ACK messages will be sent in response to
   // the objects on the stream. The actual value is informational, and it
   // communicates how many frames the subscriber is willing to buffer, in
   // microseconds.
   std::optional<quic::QuicTimeDelta> object_ack_window;
+
+  bool operator==(const MoqtSubscribeParameters& other) const {
+    return authorization_info == other.authorization_info &&
+           delivery_timeout == other.delivery_timeout &&
+           max_cache_duration == other.max_cache_duration &&
+           object_ack_window == other.object_ack_window;
+  }
 };
 
 struct QUICHE_EXPORT MoqtSubscribe {
@@ -331,12 +362,16 @@
   MoqtDeliveryOrder group_order;
   // If ContextExists on the wire is zero, largest_id has no value.
   std::optional<FullSequence> largest_id;
+  MoqtSubscribeParameters parameters;
 };
 
 enum class QUICHE_EXPORT SubscribeErrorCode : uint64_t {
   kInternalError = 0x0,
   kInvalidRange = 0x1,
   kRetryTrackAlias = 0x2,
+  kTrackDoesNotExist = 0x3,
+  kUnauthorized = 0x4,
+  kTimeout = 0x5,
 };
 
 struct QUICHE_EXPORT MoqtSubscribeError {
@@ -374,26 +409,26 @@
   std::optional<uint64_t> end_group;
   std::optional<uint64_t> end_object;
   MoqtPriority subscriber_priority;
-  std::optional<std::string> authorization_info;
+  MoqtSubscribeParameters parameters;
 };
 
 struct QUICHE_EXPORT MoqtAnnounce {
-  std::string track_namespace;
-  std::optional<std::string> authorization_info;
+  FullTrackName track_namespace;
+  MoqtSubscribeParameters parameters;
 };
 
 struct QUICHE_EXPORT MoqtAnnounceOk {
-  std::string track_namespace;
+  FullTrackName track_namespace;
 };
 
 struct QUICHE_EXPORT MoqtAnnounceError {
-  std::string track_namespace;
+  FullTrackName track_namespace;
   MoqtAnnounceErrorCode error_code;
   std::string reason_phrase;
 };
 
 struct QUICHE_EXPORT MoqtUnannounce {
-  std::string track_namespace;
+  FullTrackName track_namespace;
 };
 
 enum class QUICHE_EXPORT MoqtTrackStatusCode : uint64_t {
@@ -425,7 +460,10 @@
 };
 
 struct QUICHE_EXPORT MoqtAnnounceCancel {
-  std::string track_namespace;
+  FullTrackName track_namespace;
+  // TODO: What namespace is this error code in?
+  uint64_t error_code;
+  std::string reason_phrase;
 };
 
 struct QUICHE_EXPORT MoqtTrackStatusRequest {
diff --git a/quiche/quic/moqt/moqt_messages_test.cc b/quiche/quic/moqt/moqt_messages_test.cc
index bec3c4b..0b554f2 100644
--- a/quiche/quic/moqt/moqt_messages_test.cc
+++ b/quiche/quic/moqt/moqt_messages_test.cc
@@ -30,6 +30,16 @@
   EXPECT_LT(name1, name3);
 }
 
+TEST(MoqtMessagesTest, FullTrackNameInNamespace) {
+  FullTrackName name1({"a", "b"});
+  FullTrackName name2({"a", "b", "c"});
+  FullTrackName name3({"d", "b"});
+  EXPECT_TRUE(name2.InNamespace(name1));
+  EXPECT_FALSE(name1.InNamespace(name2));
+  EXPECT_TRUE(name1.InNamespace(name1));
+  EXPECT_FALSE(name2.InNamespace(name3));
+}
+
 TEST(MoqtMessagesTest, FullTrackNameToString) {
   FullTrackName name1({"a", "b"});
   EXPECT_EQ(name1.ToString(), R"({"a", "b"})");
diff --git a/quiche/quic/moqt/moqt_parser.cc b/quiche/quic/moqt/moqt_parser.cc
index a00416b..541c550 100644
--- a/quiche/quic/moqt/moqt_parser.cc
+++ b/quiche/quic/moqt/moqt_parser.cc
@@ -8,6 +8,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <initializer_list>
 #include <optional>
 #include <string>
 
@@ -25,16 +26,6 @@
 
 namespace {
 
-bool ReadFullTrackName(quic::QuicDataReader& reader, FullTrackName& out) {
-  absl::string_view track_namespace, track_name;
-  if (!reader.ReadStringPieceVarInt62(&track_namespace) ||
-      !reader.ReadStringPieceVarInt62(&track_name)) {
-    return false;
-  }
-  out = FullTrackName({track_namespace, track_name});
-  return true;
-}
-
 bool ParseDeliveryOrder(uint8_t raw_value,
                         std::optional<MoqtDeliveryOrder>& output) {
   switch (raw_value) {
@@ -238,8 +229,9 @@
       return ProcessMaxSubscribeId(reader);
     case moqt::MoqtMessageType::kObjectAck:
       return ProcessObjectAck(reader);
+    default:
+      ParseError("Unknown message type");
   }
-  ParseError("Unknown message type");
   return 0;
 }
 
@@ -408,13 +400,16 @@
   MoqtSubscribe subscribe_request;
   uint64_t filter, group, object;
   uint8_t group_order;
+  absl::string_view track_name;
   if (!reader.ReadVarInt62(&subscribe_request.subscribe_id) ||
       !reader.ReadVarInt62(&subscribe_request.track_alias) ||
-      !ReadFullTrackName(reader, subscribe_request.full_track_name) ||
+      !ReadTrackNamespace(reader, subscribe_request.full_track_name) ||
+      !reader.ReadStringPieceVarInt62(&track_name) ||
       !reader.ReadUInt8(&subscribe_request.subscriber_priority) ||
       !reader.ReadUInt8(&group_order) || !reader.ReadVarInt62(&filter)) {
     return 0;
   }
+  subscribe_request.full_track_name.AddElement(track_name);
   if (!ParseDeliveryOrder(group_order, subscribe_request.group_order)) {
     ParseError("Invalid group order value in SUBSCRIBE message");
     return 0;
@@ -459,46 +454,9 @@
       ParseError("Invalid filter type");
       return 0;
   }
-  uint64_t num_params;
-  if (!reader.ReadVarInt62(&num_params)) {
+  if (!ReadSubscribeParameters(reader, subscribe_request.parameters)) {
     return 0;
   }
-  for (uint64_t i = 0; i < num_params; ++i) {
-    uint64_t type;
-    absl::string_view value;
-    if (!ReadParameter(reader, type, value)) {
-      return 0;
-    }
-    auto key = static_cast<MoqtTrackRequestParameter>(type);
-    switch (key) {
-      case MoqtTrackRequestParameter::kAuthorizationInfo:
-        if (subscribe_request.parameters.authorization_info.has_value()) {
-          ParseError(
-              "AUTHORIZATION_INFO parameter appears twice in "
-              "SUBSCRIBE");
-          return 0;
-        }
-        subscribe_request.parameters.authorization_info = value;
-        break;
-      case MoqtTrackRequestParameter::kOackWindowSize: {
-        if (subscribe_request.parameters.object_ack_window.has_value()) {
-          ParseError("OACK_WINDOW_SIZE parameter appears twice in SUBSCRIBE");
-          return 0;
-        }
-        uint64_t raw_value;
-        if (!StringViewToVarInt(value, raw_value)) {
-          ParseError("OACK_WINDOW_SIZE parameter is not a valid varint");
-          return 0;
-        }
-        subscribe_request.parameters.object_ack_window =
-            quic::QuicTimeDelta::FromMicroseconds(raw_value);
-        break;
-      }
-      default:
-        // Skip over the parameter.
-        break;
-    }
-  }
   visitor_.OnSubscribeMessage(subscribe_request);
   return reader.PreviouslyReadPayload().length();
 }
@@ -530,6 +488,13 @@
       return 0;
     }
   }
+  if (!ReadSubscribeParameters(reader, subscribe_ok.parameters)) {
+    return 0;
+  }
+  if (subscribe_ok.parameters.authorization_info.has_value()) {
+    ParseError("SUBSCRIBE_OK has authorization info");
+    return 0;
+  }
   visitor_.OnSubscribeOkMessage(subscribe_ok);
   return reader.PreviouslyReadPayload().length();
 }
@@ -585,13 +550,15 @@
 
 size_t MoqtControlParser::ProcessSubscribeUpdate(quic::QuicDataReader& reader) {
   MoqtSubscribeUpdate subscribe_update;
-  uint64_t end_group, end_object, num_params;
+  uint64_t end_group, end_object;
   if (!reader.ReadVarInt62(&subscribe_update.subscribe_id) ||
       !reader.ReadVarInt62(&subscribe_update.start_group) ||
       !reader.ReadVarInt62(&subscribe_update.start_object) ||
       !reader.ReadVarInt62(&end_group) || !reader.ReadVarInt62(&end_object) ||
-      !reader.ReadUInt8(&subscribe_update.subscriber_priority) ||
-      !reader.ReadVarInt62(&num_params)) {
+      !reader.ReadUInt8(&subscribe_update.subscriber_priority)) {
+    return 0;
+  }
+  if (!ReadSubscribeParameters(reader, subscribe_update.parameters)) {
     return 0;
   }
   if (end_group == 0) {
@@ -618,27 +585,9 @@
   } else {
     subscribe_update.end_object = std::nullopt;
   }
-  for (uint64_t i = 0; i < num_params; ++i) {
-    uint64_t type;
-    absl::string_view value;
-    if (!ReadParameter(reader, type, value)) {
-      return 0;
-    }
-    auto key = static_cast<MoqtTrackRequestParameter>(type);
-    switch (key) {
-      case MoqtTrackRequestParameter::kAuthorizationInfo:
-        if (subscribe_update.authorization_info.has_value()) {
-          ParseError(
-              "AUTHORIZATION_INFO parameter appears twice in "
-              "SUBSCRIBE_UPDATE");
-          return 0;
-        }
-        subscribe_update.authorization_info = value;
-        break;
-      default:
-        // Skip over the parameter.
-        break;
-    }
+  if (subscribe_update.parameters.authorization_info.has_value()) {
+    ParseError("SUBSCRIBE_UPDATE has authorization info");
+    return 0;
   }
   visitor_.OnSubscribeUpdateMessage(subscribe_update);
   return reader.PreviouslyReadPayload().length();
@@ -646,32 +595,15 @@
 
 size_t MoqtControlParser::ProcessAnnounce(quic::QuicDataReader& reader) {
   MoqtAnnounce announce;
-  if (!reader.ReadStringVarInt62(announce.track_namespace)) {
+  if (!ReadTrackNamespace(reader, announce.track_namespace)) {
     return 0;
   }
-  uint64_t num_params;
-  if (!reader.ReadVarInt62(&num_params)) {
+  if (!ReadSubscribeParameters(reader, announce.parameters)) {
     return 0;
   }
-  for (uint64_t i = 0; i < num_params; ++i) {
-    uint64_t type;
-    absl::string_view value;
-    if (!ReadParameter(reader, type, value)) {
-      return 0;
-    }
-    auto key = static_cast<MoqtTrackRequestParameter>(type);
-    switch (key) {
-      case MoqtTrackRequestParameter::kAuthorizationInfo:
-        if (announce.authorization_info.has_value()) {
-          ParseError("AUTHORIZATION_INFO parameter appears twice in ANNOUNCE");
-          return 0;
-        }
-        announce.authorization_info = value;
-        break;
-      default:
-        // Skip over the parameter.
-        break;
-    }
+  if (announce.parameters.delivery_timeout.has_value()) {
+    ParseError("ANNOUNCE has delivery timeout");
+    return 0;
   }
   visitor_.OnAnnounceMessage(announce);
   return reader.PreviouslyReadPayload().length();
@@ -679,7 +611,7 @@
 
 size_t MoqtControlParser::ProcessAnnounceOk(quic::QuicDataReader& reader) {
   MoqtAnnounceOk announce_ok;
-  if (!reader.ReadStringVarInt62(announce_ok.track_namespace)) {
+  if (!ReadTrackNamespace(reader, announce_ok.track_namespace)) {
     return 0;
   }
   visitor_.OnAnnounceOkMessage(announce_ok);
@@ -688,7 +620,7 @@
 
 size_t MoqtControlParser::ProcessAnnounceError(quic::QuicDataReader& reader) {
   MoqtAnnounceError announce_error;
-  if (!reader.ReadStringVarInt62(announce_error.track_namespace)) {
+  if (!ReadTrackNamespace(reader, announce_error.track_namespace)) {
     return 0;
   }
   uint64_t error_code;
@@ -705,7 +637,11 @@
 
 size_t MoqtControlParser::ProcessAnnounceCancel(quic::QuicDataReader& reader) {
   MoqtAnnounceCancel announce_cancel;
-  if (!reader.ReadStringVarInt62(announce_cancel.track_namespace)) {
+  if (!ReadTrackNamespace(reader, announce_cancel.track_namespace)) {
+    return 0;
+  }
+  if (!reader.ReadVarInt62(&announce_cancel.error_code) ||
+      !reader.ReadStringVarInt62(announce_cancel.reason_phrase)) {
     return 0;
   }
   visitor_.OnAnnounceCancelMessage(announce_cancel);
@@ -715,16 +651,21 @@
 size_t MoqtControlParser::ProcessTrackStatusRequest(
     quic::QuicDataReader& reader) {
   MoqtTrackStatusRequest track_status_request;
-  if (!ReadFullTrackName(reader, track_status_request.full_track_name)) {
+  if (!ReadTrackNamespace(reader, track_status_request.full_track_name)) {
     return 0;
   }
+  absl::string_view name;
+  if (!reader.ReadStringPieceVarInt62(&name)) {
+    return 0;
+  }
+  track_status_request.full_track_name.AddElement(name);
   visitor_.OnTrackStatusRequestMessage(track_status_request);
   return reader.PreviouslyReadPayload().length();
 }
 
 size_t MoqtControlParser::ProcessUnannounce(quic::QuicDataReader& reader) {
   MoqtUnannounce unannounce;
-  if (!reader.ReadStringVarInt62(unannounce.track_namespace)) {
+  if (!ReadTrackNamespace(reader, unannounce.track_namespace)) {
     return 0;
   }
   visitor_.OnUnannounceMessage(unannounce);
@@ -733,9 +674,16 @@
 
 size_t MoqtControlParser::ProcessTrackStatus(quic::QuicDataReader& reader) {
   MoqtTrackStatus track_status;
+  if (!ReadTrackNamespace(reader, track_status.full_track_name)) {
+    return 0;
+  }
+  absl::string_view name;
+  if (!reader.ReadStringPieceVarInt62(&name)) {
+    return 0;
+  }
+  track_status.full_track_name.AddElement(name);
   uint64_t value;
-  if (!ReadFullTrackName(reader, track_status.full_track_name) ||
-      !reader.ReadVarInt62(&value) ||
+  if (!reader.ReadVarInt62(&value) ||
       !reader.ReadVarInt62(&track_status.last_group) ||
       !reader.ReadVarInt62(&track_status.last_object)) {
     return 0;
@@ -818,6 +766,71 @@
   return reader.ReadStringPieceVarInt62(&value);
 }
 
+bool MoqtControlParser::ReadSubscribeParameters(
+    quic::QuicDataReader& reader, MoqtSubscribeParameters& params) {
+  uint64_t num_params;
+  if (!reader.ReadVarInt62(&num_params)) {
+    return false;
+  }
+  for (uint64_t i = 0; i < num_params; ++i) {
+    uint64_t type;
+    absl::string_view value;
+    if (!ReadParameter(reader, type, value)) {
+      return false;
+    }
+    uint64_t raw_value;
+    auto key = static_cast<MoqtTrackRequestParameter>(type);
+    switch (key) {
+      case MoqtTrackRequestParameter::kAuthorizationInfo:
+        if (params.authorization_info.has_value()) {
+          ParseError("AUTHORIZATION_INFO parameter appears twice");
+          return false;
+        }
+        params.authorization_info = value;
+        break;
+      case moqt::MoqtTrackRequestParameter::kDeliveryTimeout:
+        if (params.delivery_timeout.has_value()) {
+          ParseError("DELIVERY_TIMEOUT parameter appears twice");
+          return false;
+        }
+        if (!StringViewToVarInt(value, raw_value)) {
+          return false;
+        }
+        params.delivery_timeout =
+            quic::QuicTimeDelta::FromMilliseconds(raw_value);
+        break;
+      case moqt::MoqtTrackRequestParameter::kMaxCacheDuration:
+        if (params.max_cache_duration.has_value()) {
+          ParseError("MAX_CACHE_DURATION parameter appears twice");
+          return false;
+        }
+        if (!StringViewToVarInt(value, raw_value)) {
+          return false;
+        }
+        params.max_cache_duration =
+            quic::QuicTimeDelta::FromMilliseconds(raw_value);
+        break;
+      case MoqtTrackRequestParameter::kOackWindowSize: {
+        if (params.object_ack_window.has_value()) {
+          ParseError("OACK_WINDOW_SIZE parameter appears twice in SUBSCRIBE");
+          return false;
+        }
+        if (!StringViewToVarInt(value, raw_value)) {
+          ParseError("OACK_WINDOW_SIZE parameter is not a valid varint");
+          return false;
+        }
+        params.object_ack_window =
+            quic::QuicTimeDelta::FromMicroseconds(raw_value);
+        break;
+      }
+      default:
+        // Skip over the parameter.
+        break;
+    }
+  }
+  return true;
+}
+
 bool MoqtControlParser::StringViewToVarInt(absl::string_view& sv,
                                            uint64_t& vi) {
   quic::QuicDataReader reader(sv);
@@ -830,6 +843,23 @@
   return true;
 }
 
+bool MoqtControlParser::ReadTrackNamespace(quic::QuicDataReader& reader,
+                                           FullTrackName& full_track_name) {
+  QUICHE_DCHECK(full_track_name.empty());
+  uint64_t num_elements;
+  if (!reader.ReadVarInt62(&num_elements)) {
+    return 0;
+  }
+  for (uint64_t i = 0; i < num_elements; ++i) {
+    absl::string_view element;
+    if (!reader.ReadStringPieceVarInt62(&element)) {
+      return false;
+    }
+    full_track_name.AddElement(element);
+  }
+  return true;
+}
+
 void MoqtDataParser::ParseError(absl::string_view reason) {
   if (parsing_error_) {
     return;  // Don't send multiple parse errors.
diff --git a/quiche/quic/moqt/moqt_parser.h b/quiche/quic/moqt/moqt_parser.h
index f19a0a6..918573a 100644
--- a/quiche/quic/moqt/moqt_parser.h
+++ b/quiche/quic/moqt/moqt_parser.h
@@ -122,10 +122,23 @@
   // |reader| does not have enough data.
   bool ReadParameter(quic::QuicDataReader& reader, uint64_t& type,
                      absl::string_view& value);
+  // Reads MoqtSubscribeParameter from one of the message types that supports
+  // it. The cursor in |reader| should point to the "number of parameters"
+  // field in the message. The cursor will move to the end of the parameters.
+  // Returns false if it could not parse the full message, in which case the
+  // cursor in |reader| should not be used.
+  bool ReadSubscribeParameters(quic::QuicDataReader& reader,
+                               MoqtSubscribeParameters& params);
   // Convert a string view to a varint. Throws an error and returns false if the
   // string_view is not exactly the right length.
   bool StringViewToVarInt(absl::string_view& sv, uint64_t& vi);
 
+  // Parses a message that a track namespace but not name. The last element of
+  // |full_track_name| will be set to the empty string. Returns false if it
+  // could not parse the full namespace field.
+  bool ReadTrackNamespace(quic::QuicDataReader& reader,
+                          FullTrackName& full_track_name);
+
   MoqtControlParserVisitor& visitor_;
   bool uses_web_transport_;
   bool no_more_data_ = false;  // Fatal error or fin. No more parsing.
diff --git a/quiche/quic/moqt/moqt_parser_test.cc b/quiche/quic/moqt/moqt_parser_test.cc
index f69ef96..633f83f 100644
--- a/quiche/quic/moqt/moqt_parser_test.cc
+++ b/quiche/quic/moqt/moqt_parser_test.cc
@@ -677,51 +677,156 @@
 TEST_F(MoqtMessageSpecificTest, SubscribeAuthorizationInfoTwice) {
   MoqtControlParser parser(kWebTrans, visitor_);
   char subscribe[] = {
-      0x03, 0x01, 0x02, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
-      0x04, 0x61, 0x62, 0x63, 0x64,              // track_name = "abcd"
-      0x20, 0x02,                                // priority = 0x20 descending
-      0x02,                                      // filter_type = kLatestObject
-      0x02,                                      // two params
-      0x02, 0x03, 0x62, 0x61, 0x72,              // authorization_info = "bar"
-      0x02, 0x03, 0x62, 0x61, 0x72,              // authorization_info = "bar"
+      0x03, 0x01, 0x02, 0x01, 0x03,
+      0x66, 0x6f, 0x6f,              // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
+      0x20, 0x02,                    // priority = 0x20 descending
+      0x02,                          // filter_type = kLatestObject
+      0x02,                          // two params
+      0x02, 0x03, 0x62, 0x61, 0x72,  // authorization_info = "bar"
+      0x02, 0x03, 0x62, 0x61, 0x72,  // authorization_info = "bar"
   };
   parser.ProcessData(absl::string_view(subscribe, sizeof(subscribe)), false);
   EXPECT_EQ(visitor_.messages_received_, 0);
   EXPECT_EQ(visitor_.parsing_error_,
-            "AUTHORIZATION_INFO parameter appears twice in SUBSCRIBE");
+            "AUTHORIZATION_INFO parameter appears twice");
   EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kProtocolViolation);
 }
 
-TEST_F(MoqtMessageSpecificTest, SubscribeUpdateAuthorizationInfoTwice) {
+TEST_F(MoqtMessageSpecificTest, SubscribeDeliveryTimeoutTwice) {
+  MoqtControlParser parser(kRawQuic, visitor_);
+  char subscribe[] = {
+      0x03, 0x01, 0x02, 0x01, 0x03,
+      0x66, 0x6f, 0x6f,              // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
+      0x20, 0x02,                    // priority = 0x20 descending
+      0x02,                          // filter_type = kLatestObject
+      0x02,                          // two params
+      0x03, 0x02, 0x67, 0x10,        // delivery_timeout = 10000
+      0x03, 0x02, 0x67, 0x10,        // delivery_timeout = 10000
+  };
+  parser.ProcessData(absl::string_view(subscribe, sizeof(subscribe)), false);
+  EXPECT_EQ(visitor_.messages_received_, 0);
+  EXPECT_EQ(visitor_.parsing_error_,
+            "DELIVERY_TIMEOUT parameter appears twice");
+  EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kProtocolViolation);
+}
+
+TEST_F(MoqtMessageSpecificTest, SubscribeDeliveryTimeoutMalformed) {
+  MoqtControlParser parser(kRawQuic, visitor_);
+  char subscribe[] = {
+      0x03, 0x01, 0x02, 0x01, 0x03,
+      0x66, 0x6f, 0x6f,              // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
+      0x20, 0x02,                    // priority = 0x20 descending
+      0x02,                          // filter_type = kLatestObject
+      0x01,                          // one param
+      0x03, 0x01, 0x67, 0x10,        // delivery_timeout = 10000
+  };
+  parser.ProcessData(absl::string_view(subscribe, sizeof(subscribe)), false);
+  EXPECT_EQ(visitor_.messages_received_, 0);
+  EXPECT_EQ(visitor_.parsing_error_,
+            "Parameter length does not match varint encoding");
+  EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kParameterLengthMismatch);
+}
+
+TEST_F(MoqtMessageSpecificTest, SubscribeMaxCacheDurationTwice) {
+  MoqtControlParser parser(kRawQuic, visitor_);
+  char subscribe[] = {
+      0x03, 0x01, 0x02, 0x01, 0x03,
+      0x66, 0x6f, 0x6f,              // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
+      0x20, 0x02,                    // priority = 0x20 descending
+      0x02,                          // filter_type = kLatestObject
+      0x02,                          // two params
+      0x04, 0x02, 0x67, 0x10,        // max_cache_duration = 10000
+      0x04, 0x02, 0x67, 0x10,        // max_cache_duration = 10000
+  };
+  parser.ProcessData(absl::string_view(subscribe, sizeof(subscribe)), false);
+  EXPECT_EQ(visitor_.messages_received_, 0);
+  EXPECT_EQ(visitor_.parsing_error_,
+            "MAX_CACHE_DURATION parameter appears twice");
+  EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kProtocolViolation);
+}
+
+TEST_F(MoqtMessageSpecificTest, SubscribeMaxCacheDurationMalformed) {
+  MoqtControlParser parser(kRawQuic, visitor_);
+  char subscribe[] = {
+      0x03, 0x01, 0x02, 0x01, 0x03,
+      0x66, 0x6f, 0x6f,              // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
+      0x20, 0x02,                    // priority = 0x20 descending
+      0x02,                          // filter_type = kLatestObject
+      0x01,                          // one param
+      0x04, 0x01, 0x67, 0x10,        // max_cache_duration = 10000
+  };
+  parser.ProcessData(absl::string_view(subscribe, sizeof(subscribe)), false);
+  EXPECT_EQ(visitor_.messages_received_, 0);
+  EXPECT_EQ(visitor_.parsing_error_,
+            "Parameter length does not match varint encoding");
+  EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kParameterLengthMismatch);
+}
+
+TEST_F(MoqtMessageSpecificTest, SubscribeOkHasAuthorizationInfo) {
+  MoqtControlParser parser(kWebTrans, visitor_);
+  char subscribe_ok[] = {
+      0x04, 0x01, 0x03,        // subscribe_id = 1, expires = 3
+      0x02, 0x01,              // group_order = 2, content exists
+      0x0c, 0x14,              // largest_group_id = 12, largest_object_id = 20,
+      0x02,                    // 2 parameters
+      0x03, 0x02, 0x67, 0x10,  // delivery_timeout = 10000
+      0x02, 0x03, 0x62, 0x61, 0x72,  // authorization_info = "bar"
+  };
+  parser.ProcessData(absl::string_view(subscribe_ok, sizeof(subscribe_ok)),
+                     false);
+  EXPECT_EQ(visitor_.messages_received_, 0);
+  EXPECT_EQ(visitor_.parsing_error_, "SUBSCRIBE_OK has authorization info");
+  EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kProtocolViolation);
+}
+
+TEST_F(MoqtMessageSpecificTest, SubscribeUpdateHasAuthorizationInfo) {
   MoqtControlParser parser(kWebTrans, visitor_);
   char subscribe_update[] = {
       0x02, 0x02, 0x03, 0x01, 0x05, 0x06,  // start and end sequences
       0xaa,                                // priority = 0xaa
-      0x02,                                // 2 parameters
-      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
+      0x01,                                // 1 parameter
       0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
   };
   parser.ProcessData(
       absl::string_view(subscribe_update, sizeof(subscribe_update)), false);
   EXPECT_EQ(visitor_.messages_received_, 0);
-  EXPECT_EQ(visitor_.parsing_error_,
-            "AUTHORIZATION_INFO parameter appears twice in SUBSCRIBE_UPDATE");
+  EXPECT_EQ(visitor_.parsing_error_, "SUBSCRIBE_UPDATE has authorization info");
   EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kProtocolViolation);
 }
 
 TEST_F(MoqtMessageSpecificTest, AnnounceAuthorizationInfoTwice) {
   MoqtControlParser parser(kWebTrans, visitor_);
   char announce[] = {
-      0x06, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
-      0x02,                          // 2 params
-      0x02, 0x03, 0x62, 0x61, 0x72,  // authorization_info = "bar"
-      0x02, 0x03, 0x62, 0x61, 0x72,  // authorization_info = "bar"
+      0x06, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x02,                                // 2 params
+      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
+      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
   };
   parser.ProcessData(absl::string_view(announce, sizeof(announce)), false);
   EXPECT_EQ(visitor_.messages_received_, 0);
   EXPECT_TRUE(visitor_.parsing_error_.has_value());
   EXPECT_EQ(*visitor_.parsing_error_,
-            "AUTHORIZATION_INFO parameter appears twice in ANNOUNCE");
+            "AUTHORIZATION_INFO parameter appears twice");
+  EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kProtocolViolation);
+}
+
+TEST_F(MoqtMessageSpecificTest, AnnounceHasDeliveryTimeout) {
+  MoqtControlParser parser(kWebTrans, visitor_);
+  char announce[] = {
+      0x06, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x02,                                // 2 params
+      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
+      0x03, 0x02, 0x67, 0x10,              // delivery_timeout = 10000
+  };
+  parser.ProcessData(absl::string_view(announce, sizeof(announce)), false);
+  EXPECT_EQ(visitor_.messages_received_, 0);
+  EXPECT_TRUE(visitor_.parsing_error_.has_value());
+  EXPECT_EQ(*visitor_.parsing_error_, "ANNOUNCE has delivery timeout");
   EXPECT_EQ(visitor_.parsing_error_code_, MoqtError::kProtocolViolation);
 }
 
@@ -817,7 +922,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x01,                          // filter_type = kLatestGroup
@@ -839,7 +944,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x02,                          // filter_type = kLatestObject
@@ -861,7 +966,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x08,                    // priority = 0x20 ???
       0x01,                          // filter_type = kLatestGroup
@@ -877,7 +982,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x03,                          // filter_type = kAbsoluteStart
@@ -901,7 +1006,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x04,                          // filter_type = kAbsoluteStart
@@ -927,7 +1032,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x04,                          // filter_type = kAbsoluteRange
@@ -953,7 +1058,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x04,                          // filter_type = kAbsoluteRange
@@ -974,7 +1079,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x04,                          // filter_type = kAbsoluteRange
@@ -1019,7 +1124,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe[] = {
       0x03, 0x01, 0x02,              // id and alias
-      0x03, 0x66, 0x6f, 0x6f,        // track_namespace = "foo"
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
       0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
       0x20, 0x02,                    // priority = 0x20 descending
       0x04,                          // filter_type = kAbsoluteRange
@@ -1040,8 +1145,7 @@
   MoqtControlParser parser(kRawQuic, visitor_);
   char subscribe_update[] = {
       0x02, 0x02, 0x03, 0x02, 0x04, 0x01,  // start and end sequences
-      0x01,                                // 1 parameter
-      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
+      0xf0, 0x00,                          // priority, no parameter
   };
   parser.ProcessData(
       absl::string_view(subscribe_update, sizeof(subscribe_update)), false);
@@ -1055,8 +1159,7 @@
   char subscribe_update[] = {
       0x02, 0x02, 0x03, 0x02, 0x00, 0x01,  // start and end sequences
       0x20,                                // priority
-      0x01,                                // 1 parameter
-      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
+      0x00,                                // No parameter
   };
   parser.ProcessData(
       absl::string_view(subscribe_update, sizeof(subscribe_update)), false);
diff --git a/quiche/quic/moqt/moqt_session.cc b/quiche/quic/moqt/moqt_session.cc
index a943ba5..723162c 100644
--- a/quiche/quic/moqt/moqt_session.cc
+++ b/quiche/quic/moqt/moqt_session.cc
@@ -225,7 +225,7 @@
 
 // TODO: Create state that allows ANNOUNCE_OK/ERROR on spurious namespaces to
 // trigger session errors.
-void MoqtSession::Announce(absl::string_view track_namespace,
+void MoqtSession::Announce(FullTrackName track_namespace,
                            MoqtOutgoingAnnounceCallback announce_callback) {
   if (peer_role_ == MoqtRole::kPublisher) {
     std::move(announce_callback)(
@@ -505,7 +505,7 @@
     auto subscribe_it = active_subscribes_.find(message.subscribe_id);
     if (subscribe_it == active_subscribes_.end()) {
       return std::pair<FullTrackName, RemoteTrack::Visitor*>(
-          {FullTrackName{"", ""}, nullptr});
+          {FullTrackName{}, nullptr});
     }
     ActiveSubscribe& subscribe = subscribe_it->second;
     visitor = subscribe.visitor;
@@ -515,7 +515,7 @@
         Error(MoqtError::kProtocolViolation,
               "Forwarding preference changes mid-track");
         return std::pair<FullTrackName, RemoteTrack::Visitor*>(
-            {FullTrackName{"", ""}, nullptr});
+            {FullTrackName{}, nullptr});
       }
     } else {
       subscribe.forwarding_preference = message.forwarding_preference;
@@ -528,7 +528,7 @@
     Error(MoqtError::kProtocolViolation,
           "Forwarding preference changes mid-track");
     return std::pair<FullTrackName, RemoteTrack::Visitor*>(
-        {FullTrackName{"", ""}, nullptr});
+        {FullTrackName{}, nullptr});
   }
   return std::make_pair(track.full_track_name(), track.visitor());
 }
@@ -669,7 +669,7 @@
     QUIC_DLOG(INFO) << ENDPOINT << "SUBSCRIBE for " << track_name
                     << " rejected by the application: "
                     << track_publisher.status();
-    SendSubscribeError(message, SubscribeErrorCode::kInternalError,
+    SendSubscribeError(message, SubscribeErrorCode::kTrackDoesNotExist,
                        track_publisher.status().message(), message.track_alias);
     return;
   }
diff --git a/quiche/quic/moqt/moqt_session.h b/quiche/quic/moqt/moqt_session.h
index b3124a9..ecfe8d0 100644
--- a/quiche/quic/moqt/moqt_session.h
+++ b/quiche/quic/moqt/moqt_session.h
@@ -43,14 +43,14 @@
 using MoqtSessionDeletedCallback = quiche::SingleUseCallback<void()>;
 // If |error_message| is nullopt, the ANNOUNCE was successful.
 using MoqtOutgoingAnnounceCallback = quiche::SingleUseCallback<void(
-    absl::string_view track_namespace,
+    FullTrackName track_namespace,
     std::optional<MoqtAnnounceErrorReason> error)>;
 using MoqtIncomingAnnounceCallback =
     quiche::MultiUseCallback<std::optional<MoqtAnnounceErrorReason>(
-        absl::string_view track_namespace)>;
+        FullTrackName track_namespace)>;
 
 inline std::optional<MoqtAnnounceErrorReason> DefaultIncomingAnnounceCallback(
-    absl::string_view /*track_namespace*/) {
+    FullTrackName /*track_namespace*/) {
   return std::optional(MoqtAnnounceErrorReason{
       MoqtAnnounceErrorCode::kAnnounceNotSupported,
       "This endpoint does not accept incoming ANNOUNCE messages"});
@@ -108,7 +108,7 @@
   // Send an ANNOUNCE message for |track_namespace|, and call
   // |announce_callback| when the response arrives. Will fail immediately if
   // there is already an unresolved ANNOUNCE for that namespace.
-  void Announce(absl::string_view track_namespace,
+  void Announce(FullTrackName track_namespace,
                 MoqtOutgoingAnnounceCallback announce_callback);
 
   // Returns true if SUBSCRIBE was sent. If there is already a subscription to
@@ -437,7 +437,7 @@
                                        FullSequence first_object);
 
   // Get FullTrackName and visitor for a subscribe_id and track_alias. Returns
-  // nullptr if not present.
+  // an empty FullTrackName tuple and nullptr if not present.
   std::pair<FullTrackName, RemoteTrack::Visitor*> TrackPropertiesFromAlias(
       const MoqtObject& message);
 
@@ -511,7 +511,7 @@
       monitoring_interfaces_for_published_tracks_;
 
   // Indexed by track namespace.
-  absl::flat_hash_map<std::string, MoqtOutgoingAnnounceCallback>
+  absl::flat_hash_map<FullTrackName, MoqtOutgoingAnnounceCallback>
       pending_outgoing_announces_;
 
   // The role the peer advertised in its SETUP message. Initialize it to avoid
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index f1d572f..994e85a 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -358,7 +358,7 @@
 
 TEST_F(MoqtSessionTest, AnnounceWithOk) {
   testing::MockFunction<void(
-      absl::string_view track_namespace,
+      FullTrackName track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_resolved_callback;
   webtransport::test::MockStream mock_stream;
@@ -373,18 +373,19 @@
         EXPECT_EQ(*ExtractMessageType(data[0]), MoqtMessageType::kAnnounce);
         return absl::OkStatus();
       });
-  session_.Announce("foo", announce_resolved_callback.AsStdFunction());
+  session_.Announce(FullTrackName{"foo"},
+                    announce_resolved_callback.AsStdFunction());
   EXPECT_TRUE(correct_message);
 
   MoqtAnnounceOk ok = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
   };
   correct_message = false;
   EXPECT_CALL(announce_resolved_callback, Call(_, _))
-      .WillOnce([&](absl::string_view track_namespace,
+      .WillOnce([&](FullTrackName track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         correct_message = true;
-        EXPECT_EQ(track_namespace, "foo");
+        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
         EXPECT_FALSE(error.has_value());
       });
   stream_input->OnAnnounceOkMessage(ok);
@@ -393,7 +394,7 @@
 
 TEST_F(MoqtSessionTest, AnnounceWithError) {
   testing::MockFunction<void(
-      absl::string_view track_namespace,
+      FullTrackName track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_resolved_callback;
   webtransport::test::MockStream mock_stream;
@@ -408,20 +409,21 @@
         EXPECT_EQ(*ExtractMessageType(data[0]), MoqtMessageType::kAnnounce);
         return absl::OkStatus();
       });
-  session_.Announce("foo", announce_resolved_callback.AsStdFunction());
+  session_.Announce(FullTrackName{"foo"},
+                    announce_resolved_callback.AsStdFunction());
   EXPECT_TRUE(correct_message);
 
   MoqtAnnounceError error = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
       /*error_code=*/MoqtAnnounceErrorCode::kInternalError,
       /*reason_phrase=*/"Test error",
   };
   correct_message = false;
   EXPECT_CALL(announce_resolved_callback, Call(_, _))
-      .WillOnce([&](absl::string_view track_namespace,
+      .WillOnce([&](FullTrackName track_namespace,
                     std::optional<MoqtAnnounceErrorReason> error) {
         correct_message = true;
-        EXPECT_EQ(track_namespace, "foo");
+        EXPECT_EQ(track_namespace, FullTrackName{"foo"});
         ASSERT_TRUE(error.has_value());
         EXPECT_EQ(error->error_code, MoqtAnnounceErrorCode::kInternalError);
         EXPECT_EQ(error->reason_phrase, "Test error");
@@ -686,10 +688,11 @@
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
       MoqtSessionPeer::CreateControlStream(&session_, &mock_stream);
   MoqtAnnounce announce = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
   };
   bool correct_message = false;
-  EXPECT_CALL(session_callbacks_.incoming_announce_callback, Call("foo"))
+  EXPECT_CALL(session_callbacks_.incoming_announce_callback,
+              Call(FullTrackName{"foo"}))
       .WillOnce(Return(std::nullopt));
   EXPECT_CALL(mock_stream, Writev(_, _))
       .WillOnce([&](absl::Span<const absl::string_view> data,
@@ -1355,11 +1358,12 @@
 TEST_F(MoqtSessionTest, AnnounceToPublisher) {
   MoqtSessionPeer::set_peer_role(&session_, MoqtRole::kPublisher);
   testing::MockFunction<void(
-      absl::string_view track_namespace,
+      FullTrackName track_namespace,
       std::optional<MoqtAnnounceErrorReason> error_message)>
       announce_resolved_callback;
   EXPECT_CALL(announce_resolved_callback, Call(_, _)).Times(1);
-  session_.Announce("foo", announce_resolved_callback.AsStdFunction());
+  session_.Announce(FullTrackName{"foo"},
+                    announce_resolved_callback.AsStdFunction());
 }
 
 TEST_F(MoqtSessionTest, SubscribeFromPublisher) {
@@ -1394,7 +1398,7 @@
   std::unique_ptr<MoqtControlParserVisitor> stream_input =
       MoqtSessionPeer::CreateControlStream(&session_, &mock_stream);
   MoqtAnnounce announce = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
   };
   EXPECT_CALL(mock_session_,
               CloseSession(static_cast<uint64_t>(MoqtError::kProtocolViolation),
diff --git a/quiche/quic/moqt/test_tools/moqt_test_message.h b/quiche/quic/moqt/test_tools/moqt_test_message.h
index 1be7b08..7e4fafe 100644
--- a/quiche/quic/moqt/test_tools/moqt_test_message.h
+++ b/quiche/quic/moqt/test_tools/moqt_test_message.h
@@ -439,16 +439,15 @@
       QUIC_LOG(INFO) << "SUBSCRIBE end object mismatch";
       return false;
     }
-    if (cast.parameters.authorization_info !=
-        subscribe_.parameters.authorization_info) {
-      QUIC_LOG(INFO) << "SUBSCRIBE authorization info mismatch";
+    if (cast.parameters != subscribe_.parameters) {
+      QUIC_LOG(INFO) << "SUBSCRIBE parameter mismatch";
       return false;
     }
     return true;
   }
 
   void ExpandVarints() override {
-    ExpandVarintsImpl("vvvv---v------vvvvvv---");
+    ExpandVarintsImpl("vvvvv---v------vvvvvv---vv--vv--");
   }
 
   MessageStructuredData structured_data() const override {
@@ -456,22 +455,20 @@
   }
 
  private:
-  uint8_t raw_packet_[23] = {
-      0x03, 0x01,
-      0x02,  // id and alias
-      0x03, 0x66, 0x6f,
-      0x6f,  // track_namespace = "foo"
-      0x04, 0x61, 0x62, 0x63,
-      0x64,  // track_name = "abcd"
-      0x20,  // subscriber priority = 0x20
-      0x02,  // group order = descending
-      0x03,  // Filter type: Absolute Start
-      0x04,  // start_group = 4 (relative previous)
-      0x01,  // start_object = 1 (absolute)
+  uint8_t raw_packet_[32] = {
+      0x03, 0x01, 0x02,              // id and alias
+      0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
+      0x20,                          // subscriber priority = 0x20
+      0x02,                          // group order = descending
+      0x03,                          // Filter type: Absolute Start
+      0x04,                          // start_group = 4 (relative previous)
+      0x01,                          // start_object = 1 (absolute)
       // No EndGroup or EndObject
-      0x01,  // 1 parameter
-      0x02, 0x03, 0x62, 0x61,
-      0x72,  // authorization_info = "bar"
+      0x03,                          // 3 parameters
+      0x02, 0x03, 0x62, 0x61, 0x72,  // authorization_info = "bar"
+      0x03, 0x02, 0x67, 0x10,        // delivery_timeout = 10000 ms
+      0x04, 0x02, 0x67, 0x10,        // max_cache_duration = 10000 ms
   };
 
   MoqtSubscribe subscribe_ = {
@@ -484,7 +481,10 @@
       /*start_object=*/1,
       /*end_group=*/std::nullopt,
       /*end_object=*/std::nullopt,
-      /*authorization_info=*/MoqtSubscribeParameters{"bar", std::nullopt},
+      /*parameters=*/
+      MoqtSubscribeParameters{
+          "bar", quic::QuicTimeDelta::FromMilliseconds(10000),
+          quic::QuicTimeDelta::FromMilliseconds(10000), std::nullopt},
   };
 };
 
@@ -512,10 +512,14 @@
       QUIC_LOG(INFO) << "SUBSCRIBE OK largest ID mismatch";
       return false;
     }
+    if (cast.parameters != subscribe_ok_.parameters) {
+      QUIC_LOG(INFO) << "SUBSCRIBE OK parameter mismatch";
+      return false;
+    }
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vvv--vv"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv--vvvvv--vv--"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(subscribe_ok_);
@@ -532,10 +536,13 @@
   }
 
  private:
-  uint8_t raw_packet_[7] = {
-      0x04, 0x01, 0x03,  // subscribe_id = 1, expires = 3
-      0x02,              // delivery_order = 2,
-      0x01, 0x0c, 0x14,  // largest_group_id = 12, largest_object_id = 20,
+  uint8_t raw_packet_[16] = {
+      0x04, 0x01, 0x03,        // subscribe_id = 1, expires = 3
+      0x02, 0x01,              // group_order = 2, content exists
+      0x0c, 0x14,              // largest_group_id = 12, largest_object_id = 20,
+      0x02,                    // 2 parameters
+      0x03, 0x02, 0x67, 0x10,  // delivery_timeout = 10000
+      0x04, 0x02, 0x67, 0x10,  // max_cache_duration = 10000
   };
 
   MoqtSubscribeOk subscribe_ok_ = {
@@ -543,6 +550,10 @@
       /*expires=*/quic::QuicTimeDelta::FromMilliseconds(3),
       /*group_order=*/MoqtDeliveryOrder::kDescending,
       /*largest_id=*/FullSequence(12, 20),
+      /*parameters=*/
+      MoqtSubscribeParameters{
+          std::nullopt, quic::QuicTimeDelta::FromMilliseconds(10000),
+          quic::QuicTimeDelta::FromMilliseconds(10000), std::nullopt},
   };
 };
 
@@ -711,8 +722,8 @@
       QUIC_LOG(INFO) << "SUBSCRIBE_UPDATE subscriber priority mismatch";
       return false;
     }
-    if (cast.authorization_info != subscribe_update_.authorization_info) {
-      QUIC_LOG(INFO) << "SUBSCRIBE_UPDATE authorization info mismatch";
+    if (cast.parameters != subscribe_update_.parameters) {
+      QUIC_LOG(INFO) << "SUBSCRIBE_UPDATE parameter mismatch";
       return false;
     }
     return true;
@@ -725,11 +736,12 @@
   }
 
  private:
-  uint8_t raw_packet_[13] = {
+  uint8_t raw_packet_[16] = {
       0x02, 0x02, 0x03, 0x01, 0x05, 0x06,  // start and end sequences
       0xaa,                                // subscriber_priority
-      0x01,                                // 1 parameter
-      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
+      0x02,                                // 1 parameter
+      0x03, 0x02, 0x67, 0x10,              // delivery_timeout = 10000
+      0x04, 0x02, 0x67, 0x10,              // max_cache_duration = 10000
   };
 
   MoqtSubscribeUpdate subscribe_update_ = {
@@ -739,7 +751,10 @@
       /*end_group=*/4,
       /*end_object=*/5,
       /*subscriber_priority=*/0xaa,
-      /*authorization_info=*/"bar",
+      /*parameters=*/
+      MoqtSubscribeParameters{
+          std::nullopt, quic::QuicTimeDelta::FromMilliseconds(10000),
+          quic::QuicTimeDelta::FromMilliseconds(10000), std::nullopt},
   };
 };
 
@@ -755,29 +770,33 @@
       QUIC_LOG(INFO) << "ANNOUNCE MESSAGE track namespace mismatch";
       return false;
     }
-    if (cast.authorization_info != announce_.authorization_info) {
+    if (cast.parameters != announce_.parameters) {
       QUIC_LOG(INFO) << "ANNOUNCE MESSAGE authorization info mismatch";
       return false;
     }
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vv---vvv---"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---vvv---vv--"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(announce_);
   }
 
  private:
-  uint8_t raw_packet_[11] = {
-      0x06, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
-      0x01,                          // 1 parameter
-      0x02, 0x03, 0x62, 0x61, 0x72,  // authorization_info = "bar"
+  uint8_t raw_packet_[16] = {
+      0x06, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x02,                                // 2 parameters
+      0x02, 0x03, 0x62, 0x61, 0x72,        // authorization_info = "bar"
+      0x04, 0x02, 0x67, 0x10,              // max_cache_duration = 10000ms
   };
 
   MoqtAnnounce announce_ = {
-      /*track_namespace=*/"foo",
-      /*authorization_info=*/"bar",
+      /*track_namespace=*/FullTrackName{"foo"},
+      /*parameters=*/
+      MoqtSubscribeParameters{"bar", std::nullopt,
+                              quic::QuicTimeDelta::FromMilliseconds(10000),
+                              std::nullopt},
   };
 };
 
@@ -796,19 +815,19 @@
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vv---"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(announce_ok_);
   }
 
  private:
-  uint8_t raw_packet_[5] = {
-      0x07, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+  uint8_t raw_packet_[6] = {
+      0x07, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
   };
 
   MoqtAnnounceOk announce_ok_ = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
   };
 };
 
@@ -835,21 +854,21 @@
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vv---vv---"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---vv---"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(announce_error_);
   }
 
  private:
-  uint8_t raw_packet_[10] = {
-      0x08, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
-      0x01,                          // error_code = 1
-      0x03, 0x62, 0x61, 0x72,        // reason_phrase = "bar"
+  uint8_t raw_packet_[11] = {
+      0x08, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x01,                                // error_code = 1
+      0x03, 0x62, 0x61, 0x72,              // reason_phrase = "bar"
   };
 
   MoqtAnnounceError announce_error_ = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
       /*error_code=*/MoqtAnnounceErrorCode::kAnnounceNotSupported,
       /*reason_phrase=*/"bar",
   };
@@ -867,22 +886,34 @@
       QUIC_LOG(INFO) << "ANNOUNCE CANCEL track namespace mismatch";
       return false;
     }
+    if (cast.error_code != announce_cancel_.error_code) {
+      QUIC_LOG(INFO) << "ANNOUNCE CANCEL error code mismatch";
+      return false;
+    }
+    if (cast.reason_phrase != announce_cancel_.reason_phrase) {
+      QUIC_LOG(INFO) << "ANNOUNCE CANCEL reason phrase mismatch";
+      return false;
+    }
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vv---"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---vv---"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(announce_cancel_);
   }
 
  private:
-  uint8_t raw_packet_[5] = {
-      0x0c, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+  uint8_t raw_packet_[11] = {
+      0x0c, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x01,                                // error_code = 1
+      0x03, 0x62, 0x61, 0x72,              // reason_phrase = "bar"
   };
 
   MoqtAnnounceCancel announce_cancel_ = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
+      /*error_code=*/1,
+      /*reason_phrase=*/"bar",
   };
 };
 
@@ -901,16 +932,16 @@
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vv---v----"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---v----"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(track_status_request_);
   }
 
  private:
-  uint8_t raw_packet_[10] = {
-      0x0d, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
-      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
+  uint8_t raw_packet_[11] = {
+      0x0d, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,        // track_name = "abcd"
   };
 
   MoqtTrackStatusRequest track_status_request_ = {
@@ -933,19 +964,19 @@
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vv---"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(unannounce_);
   }
 
  private:
-  uint8_t raw_packet_[5] = {
-      0x09, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace
+  uint8_t raw_packet_[6] = {
+      0x09, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace
   };
 
   MoqtUnannounce unannounce_ = {
-      /*track_namespace=*/"foo",
+      /*track_namespace=*/FullTrackName{"foo"},
   };
 };
 
@@ -976,17 +1007,17 @@
     return true;
   }
 
-  void ExpandVarints() override { ExpandVarintsImpl("vv---v----vvv"); }
+  void ExpandVarints() override { ExpandVarintsImpl("vvv---v----vvv"); }
 
   MessageStructuredData structured_data() const override {
     return TestMessageBase::MessageStructuredData(track_status_);
   }
 
  private:
-  uint8_t raw_packet_[13] = {
-      0x0e, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
-      0x04, 0x61, 0x62, 0x63, 0x64,  // track_name = "abcd"
-      0x00, 0x0c, 0x14,              // status, last_group, last_object
+  uint8_t raw_packet_[14] = {
+      0x0e, 0x01, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
+      0x04, 0x61, 0x62, 0x63, 0x64,        // track_name = "abcd"
+      0x00, 0x0c, 0x14,                    // status, last_group, last_object
   };
 
   MoqtTrackStatus track_status_ = {
diff --git a/quiche/quic/moqt/tools/chat_client.cc b/quiche/quic/moqt/tools/chat_client.cc
index 9e43485..b706469 100644
--- a/quiche/quic/moqt/tools/chat_client.cc
+++ b/quiche/quic/moqt/tools/chat_client.cc
@@ -114,8 +114,7 @@
   if (full_track_name == client_->chat_strings_->GetCatalogName()) {
     std::cout << "Subscription to catalog ";
   } else {
-    std::cout << "Subscription to user " << full_track_name.track_namespace()
-              << " ";
+    std::cout << "Subscription to user " << full_track_name.ToString() << " ";
   }
   if (reason_phrase.has_value()) {
     std::cout << "REJECTED, reason = " << *reason_phrase << "\n";
@@ -142,8 +141,8 @@
     client_->ProcessCatalog(object, this, group_sequence, object_sequence);
     return;
   }
-  std::string username(full_track_name.track_namespace());
-  username = username.substr(username.find_last_of('/') + 1);
+  std::string username(
+      client_->chat_strings_->GetUsernameFromFullTrackName(full_track_name));
   if (!client_->other_users_.contains(username)) {
     std::cout << "Username " << username << "doesn't exist\n";
     return;
@@ -170,7 +169,7 @@
     publisher_.Add(queue_);
     session_->set_publisher(&publisher_);
     MoqtOutgoingAnnounceCallback announce_callback =
-        [this](absl::string_view track_namespace,
+        [this](FullTrackName track_namespace,
                std::optional<MoqtAnnounceErrorReason> reason) {
           if (reason.has_value()) {
             std::cout << "ANNOUNCE rejected, " << reason->reason_phrase << "\n";
@@ -178,18 +177,21 @@
                             "Local ANNOUNCE rejected");
             return;
           }
-          std::cout << "ANNOUNCE for " << track_namespace << " accepted\n";
+          std::cout << "ANNOUNCE for " << track_namespace.ToString()
+                    << " accepted\n";
           return;
         };
-    std::cout << "Announcing " << my_track_name.track_namespace() << "\n";
-    session_->Announce(my_track_name.track_namespace(),
-                       std::move(announce_callback));
+    FullTrackName my_track_namespace = my_track_name;
+    my_track_namespace.NameToNamespace();
+    std::cout << "Announcing " << my_track_namespace.ToString() << "\n";
+    session_->Announce(my_track_namespace, std::move(announce_callback));
   }
   remote_track_visitor_ = std::make_unique<RemoteTrackVisitor>(this);
   FullTrackName catalog_name = chat_strings_->GetCatalogName();
   if (!session_->SubscribeCurrentGroup(
           catalog_name, remote_track_visitor_.get(),
-          MoqtSubscribeParameters{username_, std::nullopt})) {
+          MoqtSubscribeParameters{username_, std::nullopt, std::nullopt,
+                                  std::nullopt})) {
     std::cout << "Failed to get catalog\n";
     return false;
   }
diff --git a/quiche/quic/moqt/tools/chat_server.cc b/quiche/quic/moqt/tools/chat_server.cc
index c7c6ce5..29d0842 100644
--- a/quiche/quic/moqt/tools/chat_server.cc
+++ b/quiche/quic/moqt/tools/chat_server.cc
@@ -33,15 +33,17 @@
     MoqtSession* session, ChatServer* server)
     : session_(session), server_(server) {
   session_->callbacks().incoming_announce_callback =
-      [&](absl::string_view track_namespace) {
-        std::cout << "Received ANNOUNCE for " << track_namespace << "\n";
-        username_ =
-            server_->strings().GetUsernameFromTrackNamespace(track_namespace);
+      [&](FullTrackName track_namespace) {
+        FullTrackName track_name = track_namespace;
+        track_name.AddElement("");
+        std::cout << "Received ANNOUNCE for " << track_namespace.ToString()
+                  << "\n";
+        username_ = server_->strings().GetUsernameFromFullTrackName(track_name);
         if (username_->empty()) {
           std::cout << "Malformed ANNOUNCE namespace\n";
           return std::nullopt;
         }
-        session_->SubscribeCurrentGroup(FullTrackName({track_namespace, ""}),
+        session_->SubscribeCurrentGroup(track_name,
                                         server_->remote_track_visitor());
         server_->AddUser(*username_);
         return std::nullopt;
@@ -69,7 +71,8 @@
 void ChatServer::RemoteTrackVisitor::OnReply(
     const moqt::FullTrackName& full_track_name,
     std::optional<absl::string_view> reason_phrase) {
-  std::cout << "Subscription to user " << full_track_name.track_namespace()
+  std::cout << "Subscription to user "
+            << server_->strings().GetUsernameFromFullTrackName(full_track_name)
             << " ";
   if (reason_phrase.has_value()) {
     std::cout << "REJECTED, reason = " << *reason_phrase << "\n";
diff --git a/quiche/quic/moqt/tools/moq_chat.h b/quiche/quic/moqt/tools/moq_chat.h
index 1e6ac66..6789f82 100644
--- a/quiche/quic/moqt/tools/moq_chat.h
+++ b/quiche/quic/moqt/tools/moq_chat.h
@@ -32,10 +32,16 @@
   }
 
   // Returns "" if the track namespace is not a participant track.
-  std::string GetUsernameFromTrackNamespace(
-      absl::string_view track_namespace) const {
+  std::string GetUsernameFromFullTrackName(
+      FullTrackName full_track_name) const {
+    if (full_track_name.tuple().size() != 2) {
+      return "";
+    }
+    if (!full_track_name.tuple()[1].empty()) {
+      return "";
+    }
     std::vector<absl::string_view> elements =
-        absl::StrSplit(track_namespace, '/');
+        absl::StrSplit(full_track_name.tuple()[0], '/');
     if (elements.size() != 4 || elements[0] != kBasePath ||
         elements[1] != chat_id_ || elements[2] != kParticipantPath) {
       return "";
@@ -43,16 +49,6 @@
     return std::string(elements[3]);
   }
 
-  // Returns "" if the full track name is not a participant track.
-  std::string GetUsernameFromFullTrackName(
-      FullTrackName full_track_name) const {
-    // Check the full path
-    if (!full_track_name.track_name().empty()) {
-      return "";
-    }
-    return GetUsernameFromTrackNamespace(full_track_name.track_namespace());
-  }
-
   FullTrackName GetFullTrackNameFromUsername(absl::string_view username) const {
     return FullTrackName{absl::StrCat(kBasePath, "/", chat_id_, "/",
                                       kParticipantPath, "/", username),
diff --git a/quiche/quic/moqt/tools/moq_chat_test.cc b/quiche/quic/moqt/tools/moq_chat_test.cc
index 1c93d17..f2c8d09 100644
--- a/quiche/quic/moqt/tools/moq_chat_test.cc
+++ b/quiche/quic/moqt/tools/moq_chat_test.cc
@@ -23,38 +23,42 @@
   EXPECT_FALSE(strings_.IsValidPath("/moq-chat/"));
 }
 
-TEST_F(MoqChatStringsTest, GetUsernameFromTrackNamespace) {
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace(
-                "moq-chat/chat-id/participant/user"),
-            "user");
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace(
-                "/moq-chat/chat-id/participant/user"),
-            "");
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace(
-                "moq-chat/chat-id/participant/user/"),
-            "");
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace(
-                "moq-cha/chat-id/participant/user"),
-            "");
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace(
-                "moq-chat/chat-i/participant/user"),
-            "");
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace(
-                "moq-chat/chat-id/participan/user"),
-            "");
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace("moq-chat/chat-id/user"),
-            "");
-  EXPECT_EQ(strings_.GetUsernameFromTrackNamespace(
-                "moq-chat/chat-id/participant/foo/user"),
-            "");
-}
-
 TEST_F(MoqChatStringsTest, GetUsernameFromFullTrackName) {
   EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
-                FullTrackName("moq-chat/chat-id/participant/user", "")),
+                FullTrackName{"moq-chat/chat-id/participant/user", ""}),
             "user");
+}
+
+TEST_F(MoqChatStringsTest, GetUsernameFromFullTrackNameInvalidInput) {
   EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
-                FullTrackName("moq-chat/chat-id/participant/user", "foo")),
+                FullTrackName{"/moq-chat/chat-id/participant/user", ""}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-chat/chat-id/participant/user/", ""}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-cha/chat-id/participant/user", ""}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-chat/chat-i/participant/user", ""}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-chat/chat-id/participan/user", ""}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-chat/chat-id/user", ""}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-chat/chat-id/participant/foo/user", ""}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-chat/chat-id/participant/user", "foo"}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"moq-chat/chat-id/participant/user"}),
+            "");
+  EXPECT_EQ(strings_.GetUsernameFromFullTrackName(
+                FullTrackName{"foo", "moq-chat/chat-id/participant/user", ""}),
             "");
 }
 
diff --git a/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc b/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
index 4e4ac10..bd0f72f 100644
--- a/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
+++ b/quiche/quic/moqt/tools/moqt_ingestion_server_bin.cc
@@ -82,16 +82,25 @@
   return absl::ascii_isalnum(c) || c == '-' || c == '_';
 }
 
-bool IsValidTrackNamespace(absl::string_view track_namespace) {
-  return absl::c_all_of(track_namespace, IsValidTrackNamespaceChar);
+bool IsValidTrackNamespace(FullTrackName track_namespace) {
+  for (const auto& element : track_namespace.tuple()) {
+    if (!absl::c_all_of(element, IsValidTrackNamespaceChar)) {
+      return false;
+    }
+  }
+  return true;
 }
 
-std::string CleanUpTrackNamespace(absl::string_view track_namespace) {
-  std::string output(track_namespace);
-  for (char& c : output) {
-    if (!IsValidTrackNamespaceChar(c)) {
-      c = '_';
+FullTrackName CleanUpTrackNamespace(FullTrackName track_namespace) {
+  FullTrackName output;
+  for (auto& it : track_namespace.tuple()) {
+    std::string element = it;
+    for (char& c : element) {
+      if (!IsValidTrackNamespaceChar(c)) {
+        c = '_';
+      }
     }
+    output.AddElement(element);
   }
   return output;
 }
@@ -107,7 +116,7 @@
   }
 
   std::optional<MoqtAnnounceErrorReason> OnAnnounceReceived(
-      absl::string_view track_namespace) {
+      FullTrackName track_namespace) {
     if (!IsValidTrackNamespace(track_namespace) &&
         !quiche::GetQuicheCommandLineFlag(
             FLAGS_allow_invalid_track_namespaces)) {
@@ -142,8 +151,9 @@
     std::vector<absl::string_view> tracks_to_subscribe =
         absl::StrSplit(track_list, ',', absl::AllowEmpty());
     for (absl::string_view track : tracks_to_subscribe) {
-      session_->SubscribeCurrentGroup(FullTrackName({track_namespace, track}),
-                                      &it->second);
+      FullTrackName full_track_name = track_namespace;
+      full_track_name.AddElement(track);
+      session_->SubscribeCurrentGroup(full_track_name, &it->second);
     }
 
     return std::nullopt;
@@ -174,7 +184,7 @@
                           absl::string_view object,
                           bool /*end_of_message*/) override {
       std::string file_name = absl::StrCat(group_sequence, "-", object_sequence,
-                                           ".", full_track_name.track_name());
+                                           ".", full_track_name.tuple().back());
       std::string file_path = quiche::JoinPath(directory_, file_name);
       std::ofstream output(file_path, std::ios::binary | std::ios::ate);
       output.write(object.data(), object.size());
@@ -187,7 +197,7 @@
 
   MoqtSession* session_;  // Not owned.
   std::string output_root_;
-  absl::node_hash_map<std::string, NamespaceHandler> subscribed_namespaces_;
+  absl::node_hash_map<FullTrackName, NamespaceHandler> subscribed_namespaces_;
 };
 
 absl::StatusOr<MoqtConfigureSessionCallback> IncomingSessionHandler(
diff --git a/quiche/quic/moqt/tools/moqt_mock_visitor.h b/quiche/quic/moqt/tools/moqt_mock_visitor.h
index 1e4aebf..371a0c9 100644
--- a/quiche/quic/moqt/tools/moqt_mock_visitor.h
+++ b/quiche/quic/moqt/tools/moqt_mock_visitor.h
@@ -26,8 +26,7 @@
   testing::MockFunction<void()> session_established_callback;
   testing::MockFunction<void(absl::string_view)> session_terminated_callback;
   testing::MockFunction<void()> session_deleted_callback;
-  testing::MockFunction<std::optional<MoqtAnnounceErrorReason>(
-      absl::string_view)>
+  testing::MockFunction<std::optional<MoqtAnnounceErrorReason>(FullTrackName)>
       incoming_announce_callback;
 
   MockSessionCallbacks() {