Roll to MoQT draft-14, with core wire image changes.

Datagram header compression has been reworked.

Follow-on objects in Subgroup streams have an an object ID delta instead of an object ID.

TBD: new SETUP parameters, Malformed track definition, cosmetic renaming.
PiperOrigin-RevId: 803497308
diff --git a/quiche/quic/moqt/moqt_framer.cc b/quiche/quic/moqt/moqt_framer.cc
index c70164c..7ba6ab5 100644
--- a/quiche/quic/moqt/moqt_framer.cc
+++ b/quiche/quic/moqt/moqt_framer.cc
@@ -270,12 +270,13 @@
 
 quiche::QuicheBuffer MoqtFramer::SerializeObjectHeader(
     const MoqtObject& message, MoqtDataStreamType message_type,
-    bool is_first_in_stream) {
+    std::optional<uint64_t> previous_object_in_stream) {
   if (!ValidateObjectMetadata(message, /*is_datagram=*/false)) {
     QUICHE_BUG(QUICHE_BUG_serialize_object_header_01)
         << "Object metadata is invalid";
     return quiche::QuicheBuffer();
   }
+  bool is_first_in_stream = !previous_object_in_stream.has_value();
   // Not all fields will be written to the wire. Keep optional ones in
   // std::optional so that they can be excluded.
   // Three fields are always optional.
@@ -300,11 +301,23 @@
         WireVarInt62(message.payload_length),
         WireOptional<WireVarInt62>(object_status));
   }
+  if (previous_object_in_stream.has_value() &&
+      message.object_id <= *previous_object_in_stream) {
+    QUICHE_BUG(QUICHE_BUG_serialize_object_header_02)
+        << "Object ID is not increasing";
+    return quiche::QuicheBuffer();
+  }
   // Subgroup headers have more optional fields.
   QUICHE_CHECK(message_type.IsSubgroup());
   std::optional<uint64_t> group_id =
-      is_first_in_stream ? std::optional<uint64_t>(message.group_id)
-                         : std::nullopt;
+      previous_object_in_stream.has_value()
+          ? std::nullopt
+          : std::optional<uint64_t>(message.group_id);
+  uint64_t object_id = message.object_id;
+  if (!is_first_in_stream) {
+    // The value is actually an object ID delta, not the absolute object ID.
+    object_id -= (*previous_object_in_stream + 1);
+  }
   std::optional<uint64_t> subgroup_id =
       (is_first_in_stream && message_type.IsSubgroupPresent())
           ? std::optional<uint64_t>(message.subgroup_id)
@@ -321,8 +334,7 @@
       WireOptional<WireVarInt62>(track_alias),
       WireOptional<WireVarInt62>(group_id),
       WireOptional<WireVarInt62>(subgroup_id),
-      WireOptional<WireUint8>(publisher_priority),
-      WireVarInt62(message.object_id),
+      WireOptional<WireUint8>(publisher_priority), WireVarInt62(object_id),
       WireOptional<WireStringWithVarInt62Length>(extension_headers),
       WireVarInt62(message.payload_length),
       WireOptional<WireVarInt62>(object_status));
@@ -341,9 +353,12 @@
     return quiche::QuicheBuffer();
   }
   MoqtDatagramType datagram_type(
-      /*has_status=*/payload.empty(), !message.extension_headers.empty(),
-      !payload.empty() &&
-          message.object_status == MoqtObjectStatus::kEndOfGroup);
+      !payload.empty(), !message.extension_headers.empty(),
+      message.object_status == MoqtObjectStatus::kEndOfGroup,
+      message.object_id == 0);
+  std::optional<uint64_t> object_id =
+      datagram_type.has_object_id() ? std::optional<uint64_t>(message.object_id)
+                                    : std::nullopt;
   std::optional<absl::string_view> extensions =
       datagram_type.has_extension()
           ? std::optional<absl::string_view>(message.extension_headers)
@@ -357,7 +372,7 @@
                       : std::optional<absl::string_view>(payload);
   return Serialize(
       WireVarInt62(datagram_type.value()), WireVarInt62(message.track_alias),
-      WireVarInt62(message.group_id), WireVarInt62(message.object_id),
+      WireVarInt62(message.group_id), WireOptional<WireVarInt62>(object_id),
       WireUint8(message.publisher_priority),
       WireOptional<WireStringWithVarInt62Length>(extensions),
       WireOptional<WireVarInt62>(object_status),
diff --git a/quiche/quic/moqt/moqt_framer.h b/quiche/quic/moqt/moqt_framer.h
index 650d75e..d1a6294 100644
--- a/quiche/quic/moqt/moqt_framer.h
+++ b/quiche/quic/moqt/moqt_framer.h
@@ -5,6 +5,9 @@
 #ifndef QUICHE_QUIC_MOQT_MOQT_FRAMER_H_
 #define QUICHE_QUIC_MOQT_MOQT_FRAMER_H_
 
+#include <cstdint>
+#include <optional>
+
 #include "absl/strings/string_view.h"
 #include "quiche/quic/moqt/moqt_messages.h"
 #include "quiche/common/platform/api/quiche_export.h"
@@ -28,11 +31,12 @@
   // Serialize functions. Takes structured data and serializes it into a
   // QuicheBuffer for delivery to the stream.
 
-  // Serializes the header for an object, including the appropriate stream
-  // header if `is_first_in_stream` is set to true.
-  quiche::QuicheBuffer SerializeObjectHeader(const MoqtObject& message,
-                                             MoqtDataStreamType message_type,
-                                             bool is_first_in_stream);
+  // Serializes the header for an object, |previous_object_in_stream| is nullopt
+  // if this is the first object in the stream, the object ID of the previous
+  // one otherwise.
+  quiche::QuicheBuffer SerializeObjectHeader(
+      const MoqtObject& message, MoqtDataStreamType message_type,
+      std::optional<uint64_t> previous_object_in_stream);
   // Serializes both OBJECT and OBJECT_STATUS datagrams.
   quiche::QuicheBuffer SerializeObjectDatagram(const MoqtObject& message,
                                                absl::string_view payload);
diff --git a/quiche/quic/moqt/moqt_framer_test.cc b/quiche/quic/moqt/moqt_framer_test.cc
index 2da7139..5503c54 100644
--- a/quiche/quic/moqt/moqt_framer_test.cc
+++ b/quiche/quic/moqt/moqt_framer_test.cc
@@ -86,15 +86,20 @@
          (info.param.uses_web_transport ? "WebTransport" : "QUIC");
 }
 
+// If |change_in_object_id| is 0, it's the first object in the stream.
 quiche::QuicheBuffer SerializeObject(MoqtFramer& framer,
                                      const MoqtObject& message,
                                      absl::string_view payload,
                                      MoqtDataStreamType stream_type,
-                                     bool is_first_in_stream) {
+                                     uint64_t change_in_object_id) {
   MoqtObject adjusted_message = message;
   adjusted_message.payload_length = payload.size();
+  QUICHE_DCHECK(message.object_id > change_in_object_id);
   quiche::QuicheBuffer header = framer.SerializeObjectHeader(
-      adjusted_message, stream_type, is_first_in_stream);
+      adjusted_message, stream_type,
+      change_in_object_id == 0
+          ? std::nullopt
+          : std::optional<uint64_t>(message.object_id - change_in_object_id));
   if (header.empty()) {
     return quiche::QuicheBuffer();
   }
@@ -284,16 +289,15 @@
 TEST_F(MoqtFramerSimpleTest, GroupMiddler) {
   MoqtDataStreamType type = MoqtDataStreamType::Subgroup(1, 1, true);
   auto header = std::make_unique<StreamHeaderSubgroupMessage>(type);
-  auto buffer1 =
-      SerializeObject(framer_, std::get<MoqtObject>(header->structured_data()),
-                      "foo", type, true);
+  auto buffer1 = SerializeObject(
+      framer_, std::get<MoqtObject>(header->structured_data()), "foo", type, 0);
   EXPECT_EQ(buffer1.size(), header->total_message_size());
   EXPECT_EQ(buffer1.AsStringView(), header->PacketSample());
 
   auto middler = std::make_unique<StreamMiddlerSubgroupMessage>(type);
   auto buffer2 =
       SerializeObject(framer_, std::get<MoqtObject>(middler->structured_data()),
-                      "bar", type, false);
+                      "bar", type, /*change_in_object_id=*/3);
   EXPECT_EQ(buffer2.size(), middler->total_message_size());
   EXPECT_EQ(buffer2.AsStringView(), middler->PacketSample());
 }
@@ -302,14 +306,14 @@
   auto header = std::make_unique<StreamHeaderFetchMessage>();
   auto buffer1 =
       SerializeObject(framer_, std::get<MoqtObject>(header->structured_data()),
-                      "foo", MoqtDataStreamType::Fetch(), true);
+                      "foo", MoqtDataStreamType::Fetch(), 0);
   EXPECT_EQ(buffer1.size(), header->total_message_size());
   EXPECT_EQ(buffer1.AsStringView(), header->PacketSample());
 
   auto middler = std::make_unique<StreamMiddlerFetchMessage>();
   auto buffer2 =
       SerializeObject(framer_, std::get<MoqtObject>(middler->structured_data()),
-                      "bar", MoqtDataStreamType::Fetch(), false);
+                      "bar", MoqtDataStreamType::Fetch(), 3);
   EXPECT_EQ(buffer2.size(), middler->total_message_size());
   EXPECT_EQ(buffer2.AsStringView(), middler->PacketSample());
 }
@@ -368,65 +372,15 @@
   EXPECT_TRUE(buffer.empty());
 }
 
-TEST_F(MoqtFramerSimpleTest, Datagram) {
-  auto datagram = std::make_unique<ObjectDatagramMessage>(
-      MoqtDatagramType(/*has_status=*/false, /*has_extension=*/true,
-                       /*end_of_group=*/false));
-  MoqtObject object = {
-      /*track_alias=*/4,
-      /*group_id=*/5,
-      /*object_id=*/6,
-      /*publisher_priority=*/7,
-      std::string(kDefaultExtensionBlob),
-      /*object_status=*/MoqtObjectStatus::kNormal,
-      /*subgroup_id=*/6,
-      /*payload_length=*/3,
-  };
-  std::string payload = "foo";
-  quiche::QuicheBuffer buffer;
-  buffer = framer_.SerializeObjectDatagram(object, payload);
-  EXPECT_EQ(buffer.size(), datagram->total_message_size());
-  EXPECT_EQ(buffer.AsStringView(), datagram->PacketSample());
-}
-
-TEST_F(MoqtFramerSimpleTest, DatagramStatus) {
-  auto datagram = std::make_unique<ObjectDatagramMessage>(
-      MoqtDatagramType(true, true, false));
-  MoqtObject object = {
-      /*track_alias=*/4,
-      /*group_id=*/5,
-      /*object_id=*/6,
-      /*publisher_priority=*/7,
-      std::string(kDefaultExtensionBlob),
-      /*object_status=*/MoqtObjectStatus::kObjectDoesNotExist,
-      /*subgroup_id=*/6,
-      /*payload_length=*/0,
-  };
-  quiche::QuicheBuffer buffer;
-  buffer = framer_.SerializeObjectDatagram(object, "");
-  EXPECT_EQ(buffer.size(), datagram->total_message_size());
-  EXPECT_EQ(buffer.AsStringView(), datagram->PacketSample());
-}
-
-TEST_F(MoqtFramerSimpleTest, DatagramEndOfGroup) {
-  auto datagram = std::make_unique<ObjectDatagramMessage>(
-      MoqtDatagramType(/*has_status=*/false, /*has_extension=*/true,
-                       /*end_of_group=*/true));
-  MoqtObject object = {
-      /*track_alias=*/4,
-      /*group_id=*/5,
-      /*object_id=*/6,
-      /*publisher_priority=*/7,
-      std::string(kDefaultExtensionBlob),
-      /*object_status=*/MoqtObjectStatus::kEndOfGroup,
-      /*subgroup_id=*/6,
-      /*payload_length=*/3,
-  };
-  std::string payload = "foo";
-  quiche::QuicheBuffer buffer;
-  buffer = framer_.SerializeObjectDatagram(object, payload);
-  EXPECT_EQ(buffer.size(), datagram->total_message_size());
-  EXPECT_EQ(buffer.AsStringView(), datagram->PacketSample());
+TEST_F(MoqtFramerSimpleTest, AllDatagramTypes) {
+  for (MoqtDatagramType type : AllMoqtDatagramTypes()) {
+    ObjectDatagramMessage message(type);
+    MoqtObject object = std::get<MoqtObject>(message.structured_data());
+    quiche::QuicheBuffer buffer =
+        framer_.SerializeObjectDatagram(object, type.has_status() ? "" : "foo");
+    EXPECT_EQ(buffer.size(), message.total_message_size());
+    EXPECT_EQ(buffer.AsStringView(), message.PacketSample());
+  }
 }
 
 TEST_F(MoqtFramerSimpleTest, AllSubscribeInputs) {
diff --git a/quiche/quic/moqt/moqt_messages.h b/quiche/quic/moqt/moqt_messages.h
index c545c0a..3d94c44 100644
--- a/quiche/quic/moqt/moqt_messages.h
+++ b/quiche/quic/moqt/moqt_messages.h
@@ -41,11 +41,11 @@
 }
 
 enum class MoqtVersion : uint64_t {
-  kDraft13 = 0xff00000d,
+  kDraft14 = 0xff00000e,
   kUnrecognizedVersionForTests = 0xfe0000ff,
 };
 
-inline constexpr MoqtVersion kDefaultMoqtVersion = MoqtVersion::kDraft13;
+inline constexpr MoqtVersion kDefaultMoqtVersion = MoqtVersion::kDraft14;
 inline constexpr uint64_t kDefaultInitialMaxRequestId = 100;
 // TODO(martinduke): Implement an auth token cache.
 inline constexpr uint64_t kDefaultMaxAuthTokenCacheSize = 0;
@@ -193,32 +193,46 @@
 
 class QUICHE_EXPORT MoqtDatagramType {
  public:
-  MoqtDatagramType(bool has_status, bool has_extension, bool end_of_group)
+  // The arguments here are properties of the object. The constructor creates
+  // the appropriate type given those properties and the spec restrictions.
+  MoqtDatagramType(bool payload, bool extension, bool end_of_group,
+                   bool zero_object_id)
       : value_(0) {
-    if (has_extension) {
+    // Avoid illegal types. Status cannot coexist with the zero-object-id flag
+    // or the end-of-group flag.
+    if (!payload && !end_of_group) {
+      // The only way to express non-normal, non-end-of-group with no payload is
+      // with an explicit status, so we cannot utilize object ID compression.
+      zero_object_id = false;
+    } else if (zero_object_id) {
+      // zero-object-id saves a byte; no-payload does not.
+      payload = true;
+    } else if (!payload) {
+      // If it's an empty end-of-group object, use the explict status because
+      // it's more readable.
+      end_of_group = false;
+    }
+    if (extension) {
       value_ |= 0x01;
     }
     if (end_of_group) {
       value_ |= 0x02;
     }
-    if (has_status) {
+    if (zero_object_id) {
       value_ |= 0x04;
     }
-    if (value_ > 0x5) {
-      QUICHE_BUG(Moqt_invalid_datagram_type)
-          << "Invalid datagram type: " << value_;
-      // Clear the end of group bit.
-      value_ &= 0x5;
-      return;
+    if (!payload) {
+      value_ |= 0x20;
     }
   }
   static std::optional<MoqtDatagramType> FromValue(uint64_t value) {
-    if (value <= 5) {
+    if (value <= 7 || value == 0x20 || value == 0x21) {
       return MoqtDatagramType(value);
     }
     return std::nullopt;
   }
-  bool has_status() const { return value_ & 0x04; }
+  bool has_status() const { return value_ & 0x20; }
+  bool has_object_id() const { return !(value_ & 0x04); }
   bool end_of_group() const { return value_ & 0x02; }
   bool has_extension() const { return value_ & 0x01; }
   uint64_t value() const { return value_; }
diff --git a/quiche/quic/moqt/moqt_messages_test.cc b/quiche/quic/moqt/moqt_messages_test.cc
index c799137..beb131d 100644
--- a/quiche/quic/moqt/moqt_messages_test.cc
+++ b/quiche/quic/moqt/moqt_messages_test.cc
@@ -96,21 +96,24 @@
 }
 
 TEST(MoqtMessagesTest, MoqtDatagramType) {
-  for (bool has_status : {false, true}) {
-    for (bool has_extension : {false, true}) {
+  for (bool payload : {false, true}) {
+    for (bool extension : {false, true}) {
       for (bool end_of_group : {false, true}) {
-        if (has_status && end_of_group) {
-          EXPECT_QUICHE_BUG(
-              MoqtDatagramType(has_status, has_extension, end_of_group),
-              "Invalid datagram type");
-          continue;
+        for (bool zero_object_id : {false, true}) {
+          MoqtDatagramType type(payload, extension, end_of_group,
+                                zero_object_id);
+          EXPECT_EQ(type.has_status(),
+                    !payload && (!end_of_group || !zero_object_id));
+          EXPECT_EQ(type.has_extension(), extension);
+          EXPECT_EQ(type.end_of_group(),
+                    end_of_group && (payload || zero_object_id));
+          EXPECT_EQ(type.has_object_id(),
+                    !zero_object_id || (!payload && !end_of_group));
+          // The constructor should always produce a valid value.
+          std::optional<MoqtDatagramType> from_value =
+              MoqtDatagramType::FromValue(type.value());
+          EXPECT_TRUE(from_value.has_value() && type == *from_value);
         }
-        MoqtDatagramType type(has_status, has_extension, end_of_group);
-        EXPECT_EQ(type.has_status(), has_status);
-        EXPECT_EQ(type.has_extension(), has_extension);
-        std::optional<MoqtDatagramType> from_value =
-            MoqtDatagramType::FromValue(type.value());
-        EXPECT_TRUE(from_value.has_value() && type == *from_value);
       }
     }
   }
diff --git a/quiche/quic/moqt/moqt_parser.cc b/quiche/quic/moqt/moqt_parser.cc
index 6af3e37..847292f 100644
--- a/quiche/quic/moqt/moqt_parser.cc
+++ b/quiche/quic/moqt/moqt_parser.cc
@@ -1269,12 +1269,10 @@
   object_metadata = MoqtObject();
   if (!reader.ReadVarInt62(&type_raw) ||
       !reader.ReadVarInt62(&object_metadata.track_alias) ||
-      !reader.ReadVarInt62(&object_metadata.group_id) ||
-      !reader.ReadVarInt62(&object_metadata.object_id) ||
-      !reader.ReadUInt8(&object_metadata.publisher_priority)) {
+      !reader.ReadVarInt62(&object_metadata.group_id)) {
     return std::nullopt;
   }
-  object_metadata.subgroup_id = object_metadata.object_id;
+
   std::optional<MoqtDatagramType> datagram_type =
       MoqtDatagramType::FromValue(type_raw);
   if (!datagram_type.has_value()) {
@@ -1290,6 +1288,17 @@
   } else {
     object_metadata.object_status = MoqtObjectStatus::kNormal;
   }
+  if (datagram_type->has_object_id()) {
+    if (!reader.ReadVarInt62(&object_metadata.object_id)) {
+      return std::nullopt;
+    }
+  } else {
+    object_metadata.object_id = 0;
+  }
+  object_metadata.subgroup_id = object_metadata.object_id;
+  if (!reader.ReadUInt8(&object_metadata.publisher_priority)) {
+    return std::nullopt;
+  }
   if (datagram_type->has_extension()) {
     if (!reader.ReadStringPieceVarInt62(&extensions)) {
       return std::nullopt;
@@ -1481,7 +1490,13 @@
     case kObjectId: {
       std::optional<uint64_t> value_read = ReadVarInt62NoFin();
       if (value_read.has_value()) {
-        metadata_.object_id = *value_read;
+        if (type_.has_value() && type_->IsSubgroup() &&
+            last_object_id_.has_value()) {
+          metadata_.object_id = *value_read + *last_object_id_ + 1;
+        } else {
+          metadata_.object_id = *value_read;
+        }
+        last_object_id_ = metadata_.object_id;
         AdvanceParserState();
       }
       return;
diff --git a/quiche/quic/moqt/moqt_parser.h b/quiche/quic/moqt/moqt_parser.h
index aa4d367..9c287ba 100644
--- a/quiche/quic/moqt/moqt_parser.h
+++ b/quiche/quic/moqt/moqt_parser.h
@@ -11,6 +11,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <optional>
+#include <string>
 #include <vector>
 
 #include "absl/strings/string_view.h"
@@ -284,6 +285,7 @@
   std::optional<MoqtDataStreamType> type_ = std::nullopt;
   NextInput next_input_ = kStreamType;
   MoqtObject metadata_;
+  std::optional<uint64_t> last_object_id_;
   size_t payload_length_remaining_ = 0;
   size_t num_objects_read_ = 0;
 
diff --git a/quiche/quic/moqt/moqt_parser_test.cc b/quiche/quic/moqt/moqt_parser_test.cc
index 24e5657..039776f 100644
--- a/quiche/quic/moqt/moqt_parser_test.cc
+++ b/quiche/quic/moqt/moqt_parser_test.cc
@@ -1277,7 +1277,7 @@
 }
 
 TEST_F(MoqtMessageSpecificTest, DatagramSuccessful) {
-  for (MoqtDatagramType datagram_type : kMoqtDatagramTypes) {
+  for (MoqtDatagramType datagram_type : AllMoqtDatagramTypes()) {
     ObjectDatagramMessage message(datagram_type);
     MoqtObject object;
     std::optional<absl::string_view> payload =
@@ -1295,7 +1295,7 @@
 }
 
 TEST_F(MoqtMessageSpecificTest, DatagramSuccessfulExpandVarints) {
-  for (MoqtDatagramType datagram_type : kMoqtDatagramTypes) {
+  for (MoqtDatagramType datagram_type : AllMoqtDatagramTypes()) {
     ObjectDatagramMessage message(datagram_type);
     message.ExpandVarints();
     MoqtObject object;
@@ -1323,7 +1323,7 @@
 }
 
 TEST_F(MoqtMessageSpecificTest, TruncatedDatagram) {
-  ObjectDatagramMessage message(MoqtDatagramType(false, true, false));
+  ObjectDatagramMessage message(MoqtDatagramType(false, true, false, false));
   message.set_wire_image_size(4);
   MoqtObject object;
   std::optional<absl::string_view> payload =
diff --git a/quiche/quic/moqt/moqt_session.cc b/quiche/quic/moqt/moqt_session.cc
index 9ccb1db..7005f76 100644
--- a/quiche/quic/moqt/moqt_session.cc
+++ b/quiche/quic/moqt/moqt_session.cc
@@ -641,7 +641,9 @@
         if (fetch->session_->WriteObjectToStream(
                 stream_, fetch->request_id(), object.metadata,
                 std::move(object.payload), MoqtDataStreamType::Fetch(),
-                !stream_header_written_,
+                // last Object ID doesn't matter for FETCH, just use zero.
+                stream_header_written_ ? std::optional<uint64_t>(0)
+                                       : std::nullopt,
                 /*fin=*/false)) {
           stream_header_written_ = true;
         }
@@ -2393,14 +2395,14 @@
     }
     if (!session_->WriteObjectToStream(
             stream_, *subscription.track_alias(), object->metadata,
-            std::move(object->payload), stream_type_, !stream_header_written_,
+            std::move(object->payload), stream_type_, last_object_id_,
             object->fin_after_this)) {
       // WriteObjectToStream() closes the connection on error, meaning that
       // there is no need to process the stream any further.
       return;
     }
     ++next_object_;
-    stream_header_written_ = true;
+    last_object_id_ = object->metadata.location.object;
     subscription.OnObjectSent(object->metadata.location);
 
     if (object->fin_after_this && !delivery_timeout.IsInfinite() &&
@@ -2435,7 +2437,8 @@
                                       const PublishedObjectMetadata& metadata,
                                       quiche::QuicheMemSlice payload,
                                       MoqtDataStreamType type,
-                                      bool is_first_on_stream, bool fin) {
+                                      std::optional<uint64_t> last_id,
+                                      bool fin) {
   QUICHE_DCHECK(stream->CanWrite());
   MoqtObject header;
   header.track_alias = id;
@@ -2447,7 +2450,7 @@
   header.payload_length = payload.length();
 
   quiche::QuicheBuffer serialized_header =
-      framer_.SerializeObjectHeader(header, type, is_first_on_stream);
+      framer_.SerializeObjectHeader(header, type, last_id);
   // TODO(vasilvv): add a version of WebTransport write API that accepts
   // memslices so that we can avoid a copy here.
   std::array write_vector = {
diff --git a/quiche/quic/moqt/moqt_session.h b/quiche/quic/moqt/moqt_session.h
index 15d1d62..140de3f 100644
--- a/quiche/quic/moqt/moqt_session.h
+++ b/quiche/quic/moqt/moqt_session.h
@@ -563,7 +563,9 @@
     // exact ID of the next object in the stream because the next object could
     // be in a different subgroup or simply be skipped.
     uint64_t next_object_;
-    bool stream_header_written_ = false;
+    // Used in subgroup streams to compute the object ID diff. If nullopt, the
+    // stream header has not been written yet.
+    std::optional<uint64_t> last_object_id_;
     // If this data stream is for SUBSCRIBE, reset it if an object has been
     // excessively delayed per Section 7.1.1.2.
     std::unique_ptr<quic::QuicAlarm> delivery_timeout_alarm_;
@@ -755,8 +757,8 @@
   bool WriteObjectToStream(webtransport::Stream* stream, uint64_t id,
                            const PublishedObjectMetadata& metadata,
                            quiche::QuicheMemSlice payload,
-                           MoqtDataStreamType type, bool is_first_on_stream,
-                           bool fin);
+                           MoqtDataStreamType type,
+                           std::optional<uint64_t> last_id, bool fin);
 
   void CancelFetch(uint64_t request_id);
 
diff --git a/quiche/quic/moqt/moqt_session_test.cc b/quiche/quic/moqt/moqt_session_test.cc
index e57c1f4..56b3a73 100644
--- a/quiche/quic/moqt/moqt_session_test.cc
+++ b/quiche/quic/moqt/moqt_session_test.cc
@@ -193,7 +193,8 @@
         object,
         MoqtDataStreamType::Subgroup(object.subgroup_id, object.object_id,
                                      false),
-        visitor == nullptr);
+        (visitor == nullptr) ? std::nullopt
+                             : std::optional<uint64_t>(object.object_id - 1));
     size_t data_read = 0;
     if (visitor == nullptr) {  // It's the first object in the stream
       EXPECT_CALL(session, AcceptIncomingUnidirectionalStream())
@@ -1651,7 +1652,7 @@
   // Reopen the window.
   correct_message = false;
   // object id, extensions, payload length, status.
-  const std::string kExpectedMessage2 = {0x01, 0x00, 0x00, 0x03};
+  const std::string kExpectedMessage2 = {0x00, 0x00, 0x00, 0x03};
   EXPECT_CALL(mock_stream_, CanWrite()).WillRepeatedly([&] { return true; });
   EXPECT_CALL(*track, GetCachedObject(5, 0, 1)).WillRepeatedly([&] {
     return PublishedObject{PublishedObjectMetadata{
@@ -1963,7 +1964,7 @@
   // Publish in window.
   bool correct_message = false;
   uint8_t kExpectedMessage[] = {
-      0x00, 0x02, 0x05, 0x00, 0x80, 0x64, 0x65,
+      0x04, 0x02, 0x05, 0x80, 0x64, 0x65,
       0x61, 0x64, 0x62, 0x65, 0x65, 0x66,  // "deadbeef"
   };
   EXPECT_CALL(mock_session_, SendOrQueueDatagram(_))
@@ -2724,8 +2725,8 @@
       /*payload_length=*/3,
   };
   MoqtFramer framer(quiche::SimpleBufferAllocator::Get(), true);
-  quiche::QuicheBuffer header =
-      framer.SerializeObjectHeader(object, MoqtDataStreamType::Fetch(), true);
+  quiche::QuicheBuffer header = framer.SerializeObjectHeader(
+      object, MoqtDataStreamType::Fetch(), std::nullopt);
 
   // Open stream, deliver two objects before FETCH_OK. Neither should be read.
   webtransport::test::InMemoryStream data_stream(kIncomingUniStreamId);
@@ -2942,7 +2943,8 @@
   for (int i = 0; i < 4; ++i) {
     object.object_id = i;
     headers.push(framer_.SerializeObjectHeader(
-        object, MoqtDataStreamType::Fetch(), i == 0));
+        object, MoqtDataStreamType::Fetch(),
+        i == 0 ? std::nullopt : std::optional<uint64_t>(i - 1)));
     payloads.push("foo");
   }
 
@@ -3013,7 +3015,8 @@
   for (int i = 0; i < 4; ++i) {
     object.object_id = i;
     headers.push(framer_.SerializeObjectHeader(
-        object, MoqtDataStreamType::Fetch(), i == 0));
+        object, MoqtDataStreamType::Fetch(),
+        i == 0 ? std::nullopt : std::optional<uint64_t>(i - 1)));
     payloads.push("foo");
   }
 
@@ -3102,8 +3105,8 @@
       /*payload_length=*/6,
   };
   MoqtFramer framer_(quiche::SimpleBufferAllocator::Get(), true);
-  quiche::QuicheBuffer header =
-      framer_.SerializeObjectHeader(object, MoqtDataStreamType::Fetch(), true);
+  quiche::QuicheBuffer header = framer_.SerializeObjectHeader(
+      object, MoqtDataStreamType::Fetch(), std::nullopt);
   stream.Receive(header.AsStringView(), false);
   EXPECT_FALSE(task->HasObject());
   EXPECT_FALSE(object_ready);
diff --git a/quiche/quic/moqt/test_tools/moqt_test_message.h b/quiche/quic/moqt/test_tools/moqt_test_message.h
index bfe3d95..ad433a1 100644
--- a/quiche/quic/moqt/test_tools/moqt_test_message.h
+++ b/quiche/quic/moqt/test_tools/moqt_test_message.h
@@ -32,12 +32,20 @@
 inline constexpr absl::string_view kDefaultExtensionBlob(
     "\x00\x0c\x01\x03\x66\x6f\x6f", 7);
 
-const MoqtDatagramType kMoqtDatagramTypes[] = {
-    MoqtDatagramType(false, false, true), MoqtDatagramType(false, false, false),
-    MoqtDatagramType(false, true, true),  MoqtDatagramType(false, true, false),
-    MoqtDatagramType(true, false, false), MoqtDatagramType(true, true, false),
-    // Cannot have status and end_of_group both be true.
-};
+inline std::vector<MoqtDatagramType> AllMoqtDatagramTypes() {
+  std::vector<MoqtDatagramType> types;
+  for (bool payload : {false, true}) {
+    for (bool extension : {false, true}) {
+      for (bool end_of_group : {false, true}) {
+        for (bool zero_object_id : {false, true}) {
+          types.push_back(MoqtDatagramType(payload, extension, end_of_group,
+                                           zero_object_id));
+        }
+      }
+    }
+  }
+  return types;
+}
 
 inline std::vector<MoqtDataStreamType> AllMoqtDataStreamTypes() {
   std::vector<MoqtDataStreamType> types;
@@ -243,7 +251,6 @@
  public:
   ObjectDatagramMessage(MoqtDatagramType datagram_type)
       : ObjectMessage(), datagram_type_(datagram_type) {
-    object_.subgroup_id = object_.object_id;
     // Update ObjectMessage::object_ to match the datagram type.
     if (datagram_type.has_status()) {
       object_.object_status = MoqtObjectStatus::kObjectDoesNotExist;
@@ -254,15 +261,18 @@
                                   : MoqtObjectStatus::kNormal;
       object_.payload_length = 3;
     }
-    if (datagram_type.has_extension()) {
-      object_.extension_headers = std::string(kDefaultExtensionBlob);
-    } else {
-      object_.extension_headers = "";
-    }
+    object_.extension_headers =
+        datagram_type.has_extension() ? std::string(kDefaultExtensionBlob) : "";
+    object_.object_id = datagram_type.has_object_id() ? 6 : 0;
+    object_.subgroup_id = object_.object_id;
     quic::QuicDataWriter writer(sizeof(raw_packet_),
                                 reinterpret_cast<char*>(raw_packet_));
     EXPECT_TRUE(writer.WriteVarInt62(datagram_type.value()));
-    EXPECT_TRUE(writer.WriteStringPiece(kRawVarints));
+    EXPECT_TRUE(writer.WriteStringPiece(kRawAliasGroup));
+    if (datagram_type.has_object_id()) {
+      EXPECT_TRUE(writer.WriteStringPiece(kRawObject));
+    }
+    EXPECT_TRUE(writer.WriteStringPiece(kRawPriority));
     if (datagram_type.has_extension()) {
       EXPECT_TRUE(writer.WriteStringPiece(kRawExtensions));
     }
@@ -277,25 +287,26 @@
   }
 
   void ExpandVarints() override {
-    if (datagram_type_.has_extension()) {
-      if (datagram_type_.has_status()) {
-        ExpandVarintsImpl("vvvv-v-------v", false);
-      } else {
-        ExpandVarintsImpl("vvvv-v----------", false);
-      }
-    } else {
-      if (datagram_type_.has_status()) {
-        ExpandVarintsImpl("vvvv-v", false);
-      } else {
-        ExpandVarintsImpl("vvvv----", false);
-      }
+    std::string varints = "vvv";
+    if (datagram_type_.has_object_id()) {
+      varints += "v";
     }
+    varints += "-";  // priority
+    if (datagram_type_.has_extension()) {
+      varints += "v-------";
+    }
+    if (datagram_type_.has_status()) {
+      varints += "v";
+    }
+    ExpandVarintsImpl(varints, false);
   }
 
  private:
   uint8_t raw_packet_[17];
   MoqtDatagramType datagram_type_;
-  static constexpr absl::string_view kRawVarints = "\x04\x05\x06\x07";
+  static constexpr absl::string_view kRawAliasGroup = "\x04\x05";
+  static constexpr absl::string_view kRawObject = "\x06";
+  static constexpr absl::string_view kRawPriority = "\x07";
   static constexpr absl::string_view kRawExtensions{
       "\x07\x00\x0c\x01\x03\x66\x6f\x6f", 8};  // see kDefaultExtensionBlob
   static constexpr absl::string_view kRawPayload = "foo";
@@ -396,7 +407,7 @@
     }
     object_.object_id = 9;
     quic::QuicDataWriter writer(sizeof(raw_packet_), raw_packet_);
-    EXPECT_TRUE(writer.WriteVarInt62(object_.object_id));
+    EXPECT_TRUE(writer.WriteVarInt62(2));  // Object ID delta - 1
     if (type.AreExtensionHeadersPresent()) {
       EXPECT_TRUE(
           writer.WriteBytes(kRawExtensions.data(), kRawExtensions.length()));