MoQT Framer. Turns structured message data into the wire image.

PiperOrigin-RevId: 569530523
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 964887a..89f45e2 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -1476,11 +1476,14 @@
     "quic/load_balancer/load_balancer_server_id_test.cc",
 ]
 moqt_hdrs = [
+    "quic/moqt/moqt_framer.h",
     "quic/moqt/moqt_messages.h",
     "quic/moqt/moqt_parser.h",
     "quic/moqt/test_tools/moqt_test_message.h",
 ]
 moqt_srcs = [
+    "quic/moqt/moqt_framer.cc",
+    "quic/moqt/moqt_framer_test.cc",
     "quic/moqt/moqt_messages.cc",
     "quic/moqt/moqt_parser.cc",
     "quic/moqt/moqt_parser_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index e02373f..da834ac 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -1480,11 +1480,14 @@
     "src/quiche/quic/load_balancer/load_balancer_server_id_test.cc",
 ]
 moqt_hdrs = [
+    "src/quiche/quic/moqt/moqt_framer.h",
     "src/quiche/quic/moqt/moqt_messages.h",
     "src/quiche/quic/moqt/moqt_parser.h",
     "src/quiche/quic/moqt/test_tools/moqt_test_message.h",
 ]
 moqt_srcs = [
+    "src/quiche/quic/moqt/moqt_framer.cc",
+    "src/quiche/quic/moqt/moqt_framer_test.cc",
     "src/quiche/quic/moqt/moqt_messages.cc",
     "src/quiche/quic/moqt/moqt_parser.cc",
     "src/quiche/quic/moqt/moqt_parser_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index 4db9bc0..fdff71e 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -1479,11 +1479,14 @@
     "quiche/quic/load_balancer/load_balancer_server_id_test.cc"
   ],
   "moqt_hdrs": [
+    "quiche/quic/moqt/moqt_framer.h",
     "quiche/quic/moqt/moqt_messages.h",
     "quiche/quic/moqt/moqt_parser.h",
     "quiche/quic/moqt/test_tools/moqt_test_message.h"
   ],
   "moqt_srcs": [
+    "quiche/quic/moqt/moqt_framer.cc",
+    "quiche/quic/moqt/moqt_framer_test.cc",
     "quiche/quic/moqt/moqt_messages.cc",
     "quiche/quic/moqt/moqt_parser.cc",
     "quiche/quic/moqt/moqt_parser_test.cc"
diff --git a/quiche/quic/moqt/moqt_framer.cc b/quiche/quic/moqt/moqt_framer.cc
new file mode 100644
index 0000000..986e1ae
--- /dev/null
+++ b/quiche/quic/moqt/moqt_framer.cc
@@ -0,0 +1,297 @@
+// Copyright (c) 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/moqt/moqt_framer.h"
+
+#include <cstddef>
+#include <cstdint>
+
+#include "absl/strings/string_view.h"
+#include "quiche/quic/core/quic_data_writer.h"
+#include "quiche/quic/core/quic_time.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/common/quiche_buffer_allocator.h"
+
+namespace moqt {
+
+namespace {
+
+inline size_t NeededVarIntLen(uint64_t value) {
+  return static_cast<size_t>(quic::QuicDataWriter::GetVarInt62Len(value));
+}
+inline size_t ParameterLen(uint64_t type, uint64_t value_len) {
+  return NeededVarIntLen(type) + NeededVarIntLen(value_len) + value_len;
+}
+
+// This only supports values up to UINT8_MAX, as that's all that exists in the
+// standard.
+inline bool WriteIntParameter(quic::QuicDataWriter& writer, uint64_t type,
+                              uint8_t value) {
+  if (!writer.WriteVarInt62(type)) {
+    return false;
+  }
+  if (!writer.WriteVarInt62(1)) {
+    return false;
+  }
+  return writer.WriteUInt8(value);
+}
+
+inline bool WriteStringParameter(quic::QuicDataWriter& writer, uint64_t type,
+                                 absl::string_view value) {
+  if (!writer.WriteVarInt62(type)) {
+    return false;
+  }
+  return writer.WriteStringPieceVarInt62(value);
+}
+
+}  // namespace
+
+quiche::QuicheBuffer MoqtFramer::SerializeObject(
+    const MoqtObject& message, const absl::string_view payload,
+    const size_t known_payload_size) {
+  if (known_payload_size > 0 && known_payload_size < payload.length()) {
+    return quiche::QuicheBuffer();
+  }
+  size_t varint_len = NeededVarIntLen(message.track_id) +
+                      NeededVarIntLen(message.group_sequence) +
+                      NeededVarIntLen(message.object_sequence) +
+                      NeededVarIntLen(message.object_send_order);
+  size_t message_len =
+      known_payload_size == 0 ? 0 : (known_payload_size + varint_len);
+  size_t buffer_size =
+      varint_len + payload.length() +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kObject)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kObject));
+  writer.WriteVarInt62(message_len);
+  writer.WriteVarInt62(message.track_id);
+  writer.WriteVarInt62(message.group_sequence);
+  writer.WriteVarInt62(message.object_sequence);
+  writer.WriteVarInt62(message.object_send_order);
+  writer.WriteStringPiece(payload);
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeObjectPayload(
+    const absl::string_view payload) {
+  quiche::QuicheBuffer buffer(allocator_, payload.length());
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteStringPiece(payload);
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeSetup(const MoqtSetup& message) {
+  size_t message_len;
+  if (perspective_ == quic::Perspective::IS_CLIENT) {
+    message_len = NeededVarIntLen(message.number_of_supported_versions);
+    for (uint64_t i : message.supported_versions) {
+      message_len += NeededVarIntLen(i);
+    }
+    if (message.role.has_value()) {
+      message_len +=
+          ParameterLen(static_cast<uint64_t>(MoqtSetupParameter::kRole), 1);
+    }
+    if (!using_webtrans_ && message.path.has_value()) {
+      message_len +=
+          ParameterLen(static_cast<uint64_t>(MoqtSetupParameter::kPath),
+                       message.path->length());
+    }
+  } else {
+    message_len = NeededVarIntLen(message.supported_versions[0]);
+  }
+  size_t buffer_size =
+      message_len +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kSetup)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kSetup));
+  writer.WriteVarInt62(message_len);
+  if (perspective_ == quic::Perspective::IS_SERVER) {
+    writer.WriteVarInt62(message.supported_versions[0]);
+    return buffer;
+  }
+  writer.WriteVarInt62(message.number_of_supported_versions);
+  for (uint64_t i : message.supported_versions) {
+    writer.WriteVarInt62(i);
+  }
+  if (message.role.has_value()) {
+    WriteIntParameter(writer, static_cast<uint64_t>(MoqtSetupParameter::kRole),
+                      static_cast<uint8_t>(message.role.value()));
+  }
+  if (!using_webtrans_ && message.path.has_value()) {
+    WriteStringParameter(writer,
+                         static_cast<uint64_t>(MoqtSetupParameter::kPath),
+                         message.path.value());
+  }
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeSubscribeRequest(
+    const MoqtSubscribeRequest& message) {
+  size_t message_len = NeededVarIntLen(message.full_track_name.length()) +
+                       message.full_track_name.length();
+  if (message.group_sequence.has_value()) {
+    message_len += ParameterLen(
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kGroupSequence), 1);
+  }
+  if (message.object_sequence.has_value()) {
+    message_len += ParameterLen(
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kObjectSequence), 1);
+  }
+  if (message.authorization_info.has_value()) {
+    message_len += ParameterLen(
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kAuthorizationInfo),
+        message.authorization_info->length());
+  }
+  size_t buffer_size =
+      message_len +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kObject)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(
+      static_cast<uint64_t>(MoqtMessageType::kSubscribeRequest));
+  writer.WriteVarInt62(message_len);
+  writer.WriteStringPieceVarInt62(message.full_track_name);
+  if (message.group_sequence.has_value()) {
+    WriteIntParameter(
+        writer,
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kGroupSequence),
+        message.group_sequence.value());
+  }
+  if (message.object_sequence.has_value()) {
+    WriteIntParameter(
+        writer,
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kObjectSequence),
+        message.object_sequence.value());
+  }
+  if (message.authorization_info.has_value()) {
+    WriteStringParameter(
+        writer,
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kAuthorizationInfo),
+        message.authorization_info.value());
+  }
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeSubscribeOk(
+    const MoqtSubscribeOk& message) {
+  size_t message_len = NeededVarIntLen(message.full_track_name.length()) +
+                       message.full_track_name.length() +
+                       NeededVarIntLen(message.track_id) +
+                       NeededVarIntLen(message.expires.ToMilliseconds());
+  size_t buffer_size =
+      message_len +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kSubscribeOk)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kSubscribeOk));
+  writer.WriteVarInt62(message_len);
+  writer.WriteStringPieceVarInt62(message.full_track_name);
+  writer.WriteVarInt62(message.track_id);
+  writer.WriteVarInt62(message.expires.ToMilliseconds());
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeSubscribeError(
+    const MoqtSubscribeError& message) {
+  size_t message_len = NeededVarIntLen(message.full_track_name.length()) +
+                       message.full_track_name.length() +
+                       NeededVarIntLen(message.error_code) +
+                       NeededVarIntLen(message.reason_phrase.length()) +
+                       message.reason_phrase.length();
+  size_t buffer_size =
+      message_len +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kSubscribeError)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kSubscribeError));
+  writer.WriteVarInt62(message_len);
+  writer.WriteStringPieceVarInt62(message.full_track_name);
+  writer.WriteVarInt62(message.error_code);
+  writer.WriteStringPieceVarInt62(message.reason_phrase);
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeAnnounce(
+    const MoqtAnnounce& message) {
+  size_t message_len = NeededVarIntLen(message.track_namespace.length()) +
+                       message.track_namespace.length();
+  if (message.authorization_info.has_value()) {
+    message_len += ParameterLen(
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kAuthorizationInfo),
+        message.authorization_info->length());
+  }
+  size_t buffer_size =
+      message_len +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kAnnounce)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kAnnounce));
+  writer.WriteVarInt62(message_len);
+  writer.WriteStringPieceVarInt62(message.track_namespace);
+  if (message.authorization_info.has_value()) {
+    WriteStringParameter(
+        writer,
+        static_cast<uint64_t>(MoqtTrackRequestParameter::kAuthorizationInfo),
+        message.authorization_info.value());
+  }
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeAnnounceOk(
+    const MoqtAnnounceOk& message) {
+  size_t message_len = message.track_namespace.length();
+  size_t buffer_size =
+      message_len +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kAnnounceOk)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kAnnounceOk));
+  writer.WriteVarInt62(message_len);
+  writer.WriteStringPiece(message.track_namespace);
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeAnnounceError(
+    const MoqtAnnounceError& message) {
+  size_t message_len = NeededVarIntLen(message.track_namespace.length()) +
+                       message.track_namespace.length() +
+                       NeededVarIntLen(message.error_code) +
+                       NeededVarIntLen(message.reason_phrase.length()) +
+                       message.reason_phrase.length();
+  size_t buffer_size =
+      message_len +
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kAnnounceError)) +
+      NeededVarIntLen(message_len);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kAnnounceError));
+  writer.WriteVarInt62(message_len);
+  writer.WriteStringPieceVarInt62(message.track_namespace);
+  writer.WriteVarInt62(message.error_code);
+  writer.WriteStringPieceVarInt62(message.reason_phrase);
+  return buffer;
+}
+
+quiche::QuicheBuffer MoqtFramer::SerializeGoAway() {
+  size_t buffer_size =
+      NeededVarIntLen(static_cast<uint64_t>(MoqtMessageType::kGoAway)) +
+      NeededVarIntLen(0);
+  quiche::QuicheBuffer buffer(allocator_, buffer_size);
+  quic::QuicDataWriter writer(buffer.size(), buffer.data());
+  writer.WriteVarInt62(static_cast<uint64_t>(MoqtMessageType::kGoAway));
+  writer.WriteVarInt62(0);
+  return buffer;
+}
+
+}  // namespace moqt
diff --git a/quiche/quic/moqt/moqt_framer.h b/quiche/quic/moqt/moqt_framer.h
new file mode 100644
index 0000000..ba194a6
--- /dev/null
+++ b/quiche/quic/moqt/moqt_framer.h
@@ -0,0 +1,65 @@
+// Copyright (c) 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_QUIC_MOQT_MOQT_FRAMER_H_
+#define QUICHE_QUIC_MOQT_MOQT_FRAMER_H_
+
+#include <cstddef>
+
+#include "absl/strings/string_view.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/quiche_buffer_allocator.h"
+
+namespace moqt {
+
+// Serialize structured message data into a wire image. When the message format
+// is different per |perspective| or |using_webtrans|, it will omit unnecessary
+// fields. However, it does not enforce the presence of parameters that are
+// required for a particular mode.
+//
+// There can be one instance of this per session. This framer does not enforce
+// that these Serialize() calls are made in a logical order, as they can be on
+// different streams.
+class QUICHE_EXPORT MoqtFramer {
+ public:
+  MoqtFramer(quiche::QuicheBufferAllocator* allocator,
+             quic::Perspective perspective, bool using_webtrans)
+      : allocator_(allocator),
+        perspective_(perspective),
+        using_webtrans_(using_webtrans) {}
+
+  // Serialize functions. Takes structured data and serializes it into a
+  // QuicheBuffer for delivery to the stream.
+
+  // SerializeObject also takes a payload. |known_payload_size| is used in
+  // encoding the message length. If zero, the message length as also encoded as
+  // zero to indicate the message ends with the stream. If nonzero, and too
+  // small to fit the varints and the provided payload, returns an empty buffer.
+  quiche::QuicheBuffer SerializeObject(const MoqtObject& message,
+                                       absl::string_view payload,
+                                       size_t known_payload_size);
+  // Build a buffer for additional payload data.
+  quiche::QuicheBuffer SerializeObjectPayload(absl::string_view payload);
+  quiche::QuicheBuffer SerializeSetup(const MoqtSetup& message);
+  quiche::QuicheBuffer SerializeSubscribeRequest(
+      const MoqtSubscribeRequest& message);
+  quiche::QuicheBuffer SerializeSubscribeOk(const MoqtSubscribeOk& message);
+  quiche::QuicheBuffer SerializeSubscribeError(
+      const MoqtSubscribeError& message);
+  quiche::QuicheBuffer SerializeAnnounce(const MoqtAnnounce& message);
+  quiche::QuicheBuffer SerializeAnnounceOk(const MoqtAnnounceOk& message);
+  quiche::QuicheBuffer SerializeAnnounceError(const MoqtAnnounceError& message);
+  quiche::QuicheBuffer SerializeGoAway();
+
+ private:
+  quiche::QuicheBufferAllocator* allocator_;
+  quic::Perspective perspective_;
+  bool using_webtrans_;
+};
+
+}  // namespace moqt
+
+#endif  // QUICHE_QUIC_MOQT_MOQT_FRAMER_H_
diff --git a/quiche/quic/moqt/moqt_framer_test.cc b/quiche/quic/moqt/moqt_framer_test.cc
new file mode 100644
index 0000000..dd3cad4
--- /dev/null
+++ b/quiche/quic/moqt/moqt_framer_test.cc
@@ -0,0 +1,170 @@
+// Copyright (c) 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/quic/moqt/moqt_framer.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/moqt/moqt_messages.h"
+#include "quiche/quic/moqt/test_tools/moqt_test_message.h"
+#include "quiche/quic/platform/api/quic_test.h"
+#include "quiche/common/quiche_buffer_allocator.h"
+#include "quiche/common/simple_buffer_allocator.h"
+
+namespace moqt::test {
+
+struct MoqtFramerTestParams {
+  MoqtFramerTestParams(MoqtMessageType message_type,
+                       quic::Perspective perspective, bool uses_web_transport)
+      : message_type(message_type),
+        perspective(perspective),
+        uses_web_transport(uses_web_transport) {}
+  MoqtMessageType message_type;
+  quic::Perspective perspective;
+  bool uses_web_transport;
+};
+
+std::vector<MoqtFramerTestParams> GetMoqtFramerTestParams() {
+  std::vector<MoqtFramerTestParams> params;
+  std::vector<MoqtMessageType> message_types = {
+      MoqtMessageType::kObject,           MoqtMessageType::kSetup,
+      MoqtMessageType::kSubscribeRequest, MoqtMessageType::kSubscribeOk,
+      MoqtMessageType::kSubscribeError,   MoqtMessageType::kAnnounce,
+      MoqtMessageType::kAnnounceOk,       MoqtMessageType::kAnnounceError,
+      MoqtMessageType::kGoAway,
+  };
+  std::vector<quic::Perspective> perspectives = {
+      quic::Perspective::IS_SERVER,
+      quic::Perspective::IS_CLIENT,
+  };
+  std::vector<bool> uses_web_transport_bool = {
+      false,
+      true,
+  };
+  for (const MoqtMessageType message_type : message_types) {
+    if (message_type == MoqtMessageType::kSetup) {
+      for (const quic::Perspective perspective : perspectives) {
+        for (const bool uses_web_transport : uses_web_transport_bool) {
+          params.push_back(MoqtFramerTestParams(message_type, perspective,
+                                                uses_web_transport));
+        }
+      }
+    } else {
+      // All other types are processed the same for either perspective or
+      // transport.
+      params.push_back(MoqtFramerTestParams(
+          message_type, quic::Perspective::IS_SERVER, true));
+    }
+  }
+  return params;
+}
+
+std::string ParamNameFormatter(
+    const testing::TestParamInfo<MoqtFramerTestParams>& info) {
+  return MoqtMessageTypeToString(info.param.message_type) + "_" +
+         (info.param.perspective == quic::Perspective::IS_SERVER ? "Server"
+                                                                 : "Client") +
+         "_" + (info.param.uses_web_transport ? "WebTransport" : "QUIC");
+}
+
+class MoqtFramerTest
+    : public quic::test::QuicTestWithParam<MoqtFramerTestParams> {
+ public:
+  MoqtFramerTest()
+      : message_type_(GetParam().message_type),
+        is_client_(GetParam().perspective == quic::Perspective::IS_CLIENT),
+        webtrans_(GetParam().uses_web_transport),
+        buffer_allocator_(quiche::SimpleBufferAllocator::Get()),
+        framer_(buffer_allocator_, GetParam().perspective,
+                GetParam().uses_web_transport) {}
+
+  std::unique_ptr<TestMessageBase> MakeMessage(MoqtMessageType message_type) {
+    switch (message_type) {
+      case MoqtMessageType::kObject:
+        return std::make_unique<ObjectMessage>();
+      case MoqtMessageType::kSetup:
+        return std::make_unique<SetupMessage>(!is_client_, webtrans_);
+      case MoqtMessageType::kSubscribeRequest:
+        return std::make_unique<SubscribeRequestMessage>();
+      case MoqtMessageType::kSubscribeOk:
+        return std::make_unique<SubscribeOkMessage>();
+      case MoqtMessageType::kSubscribeError:
+        return std::make_unique<SubscribeErrorMessage>();
+      case MoqtMessageType::kAnnounce:
+        return std::make_unique<AnnounceMessage>();
+      case moqt::MoqtMessageType::kAnnounceOk:
+        return std::make_unique<AnnounceOkMessage>();
+      case moqt::MoqtMessageType::kAnnounceError:
+        return std::make_unique<AnnounceErrorMessage>();
+      case moqt::MoqtMessageType::kGoAway:
+        return std::make_unique<GoAwayMessage>();
+      default:
+        return nullptr;
+    }
+  }
+
+  quiche::QuicheBuffer SerializeMessage(
+      TestMessageBase::MessageStructuredData& structured_data) {
+    switch (message_type_) {
+      case MoqtMessageType::kObject: {
+        auto data = std::get<MoqtObject>(structured_data);
+        return framer_.SerializeObject(data, "foo", 3);
+      }
+      case MoqtMessageType::kSetup: {
+        auto data = std::get<MoqtSetup>(structured_data);
+        return framer_.SerializeSetup(data);
+      }
+      case MoqtMessageType::kSubscribeRequest: {
+        auto data = std::get<MoqtSubscribeRequest>(structured_data);
+        return framer_.SerializeSubscribeRequest(data);
+      }
+      case MoqtMessageType::kSubscribeOk: {
+        auto data = std::get<MoqtSubscribeOk>(structured_data);
+        return framer_.SerializeSubscribeOk(data);
+      }
+      case MoqtMessageType::kSubscribeError: {
+        auto data = std::get<MoqtSubscribeError>(structured_data);
+        return framer_.SerializeSubscribeError(data);
+      }
+      case MoqtMessageType::kAnnounce: {
+        auto data = std::get<MoqtAnnounce>(structured_data);
+        return framer_.SerializeAnnounce(data);
+      }
+      case moqt::MoqtMessageType::kAnnounceOk: {
+        auto data = std::get<MoqtAnnounceOk>(structured_data);
+        return framer_.SerializeAnnounceOk(data);
+      }
+      case moqt::MoqtMessageType::kAnnounceError: {
+        auto data = std::get<MoqtAnnounceError>(structured_data);
+        return framer_.SerializeAnnounceError(data);
+      }
+      case moqt::MoqtMessageType::kGoAway: {
+        return framer_.SerializeGoAway();
+      }
+    }
+  }
+
+  MoqtMessageType message_type_;
+  bool is_client_;
+  bool webtrans_;
+  quiche::SimpleBufferAllocator* buffer_allocator_;
+  MoqtFramer framer_;
+};
+
+INSTANTIATE_TEST_SUITE_P(MoqtFramerTests, MoqtFramerTest,
+                         testing::ValuesIn(GetMoqtFramerTestParams()),
+                         ParamNameFormatter);
+
+TEST_P(MoqtFramerTest, OneMessage) {
+  auto message = MakeMessage(message_type_);
+  auto structured_data = message->structured_data();
+  auto buffer = SerializeMessage(structured_data);
+  EXPECT_EQ(buffer.size(), message->total_message_size());
+  EXPECT_EQ(buffer.AsStringView(), message->PacketSample());
+}
+
+}  // namespace moqt::test
diff --git a/quiche/quic/moqt/test_tools/moqt_test_message.h b/quiche/quic/moqt/test_tools/moqt_test_message.h
index aa4c8c0..5d1ac90 100644
--- a/quiche/quic/moqt/test_tools/moqt_test_message.h
+++ b/quiche/quic/moqt/test_tools/moqt_test_message.h
@@ -63,6 +63,9 @@
     wire_image_size_ = wire_image_size;
   }
 
+  // Returns a copy of the structured data for the message.
+  virtual MessageStructuredData structured_data() const = 0;
+
   // Sets the message length field. If |message_size| == 0, just change the
   // field in the wire image. If another value, this will either truncate the
   // message or increase its length (which adds uninitialized bytes). This can
@@ -194,6 +197,10 @@
     ExpandVarintsImpl("vvvvvv");  // first six fields are varints
   }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(object_);
+  }
+
  private:
   uint8_t raw_packet_[9] = {
       0x00, 0x07, 0x04, 0x05, 0x06, 0x07,  // varints
@@ -257,6 +264,13 @@
     }
   }
 
+  MessageStructuredData structured_data() const override {
+    if (client_) {
+      return TestMessageBase::MessageStructuredData(server_setup_);
+    }
+    return TestMessageBase::MessageStructuredData(client_setup_);
+  }
+
  private:
   bool client_;
   uint8_t client_raw_packet_[13] = {
@@ -312,6 +326,10 @@
 
   void ExpandVarints() override { ExpandVarintsImpl("vvv---vv-vv-vv"); }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(subscribe_request_);
+  }
+
  private:
   uint8_t raw_packet_[17] = {
       0x03, 0x0f, 0x03, 0x66, 0x6f, 0x6f,  // track_name = "foo"
@@ -350,6 +368,10 @@
 
   void ExpandVarints() override { ExpandVarintsImpl("vvv---vv"); }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(subscribe_ok_);
+  }
+
  private:
   uint8_t raw_packet_[8] = {
       0x04, 0x06, 0x03, 0x66, 0x6f, 0x6f,  // track_name = "foo"
@@ -389,6 +411,10 @@
 
   void ExpandVarints() override { ExpandVarintsImpl("vvv---vv---"); }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(subscribe_error_);
+  }
+
  private:
   uint8_t raw_packet_[11] = {
       0x05, 0x09, 0x03, 0x66, 0x6f, 0x6f,  // track_name = "foo"
@@ -424,6 +450,10 @@
 
   void ExpandVarints() override { ExpandVarintsImpl("vvv---vv---"); }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(announce_);
+  }
+
  private:
   uint8_t raw_packet_[11] = {
       0x06, 0x09, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
@@ -453,6 +483,10 @@
 
   void ExpandVarints() override { ExpandVarintsImpl("vv---"); }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(announce_ok_);
+  }
+
  private:
   uint8_t raw_packet_[5] = {
       0x07, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
@@ -488,6 +522,10 @@
 
   void ExpandVarints() override { ExpandVarintsImpl("vvv---vv---"); }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(announce_error_);
+  }
+
  private:
   uint8_t raw_packet_[11] = {
       0x08, 0x09, 0x03, 0x66, 0x6f, 0x6f,  // track_namespace = "foo"
@@ -514,11 +552,17 @@
 
   void ExpandVarints() override { ExpandVarintsImpl("vv"); }
 
+  MessageStructuredData structured_data() const override {
+    return TestMessageBase::MessageStructuredData(goaway_);
+  }
+
  private:
   uint8_t raw_packet_[2] = {
       0x10,
       0x00,
   };
+
+  MoqtGoAway goaway_ = {};
 };
 
 }  // namespace moqt::test