Add RTP depacketizer for H265

1. Depacketize single nalu packet/AP/FU

2. Insert start code before each nalu

Bug: webrtc:13485
Change-Id: I8346f9c31e61e5d3c2c7e1bf5fdaae4018a1ff78
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/325660
Reviewed-by: Sergey Silkin <ssilkin@webrtc.org>
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Reviewed-by: Philip Eliasson <philipel@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#41628}
diff --git a/common_video/h265/h265_common.h b/common_video/h265/h265_common.h
index 643726f..d32de7f 100644
--- a/common_video/h265/h265_common.h
+++ b/common_video/h265/h265_common.h
@@ -55,11 +55,15 @@
   kAud = 35,
   kPrefixSei = 39,
   kSuffixSei = 40,
+  // Aggregation packets, refer to section 4.4.2 in RFC 7798.
   kAp = 48,
-  kFu = 49
+  // Fragmentation units, refer to section 4.4.3 in RFC 7798.
+  kFu = 49,
+  // PACI packets, refer to section 4.4.4 in RFC 7798.
+  kPaci = 50
 };
 
-// Slice type definition. See table 7-7 of the H265 spec
+// Slice type definition. See table 7-7 of the H.265 spec
 enum SliceType : uint8_t { kB = 0, kP = 1, kI = 2 };
 
 struct NaluIndex {
@@ -78,7 +82,7 @@
 // Get the NAL type from the header byte immediately following start sequence.
 NaluType ParseNaluType(uint8_t data);
 
-// Methods for parsing and writing RBSP. See section 7.4.2 of the H265 spec.
+// Methods for parsing and writing RBSP. See section 7.4.2 of the H.265 spec.
 //
 // The following sequences are illegal, and need to be escaped when encoding:
 // 00 00 00 -> 00 00 03 00
diff --git a/modules/rtp_rtcp/BUILD.gn b/modules/rtp_rtcp/BUILD.gn
index 2c0a19e..2c42e53 100644
--- a/modules/rtp_rtcp/BUILD.gn
+++ b/modules/rtp_rtcp/BUILD.gn
@@ -260,8 +260,11 @@
 
   if (rtc_use_h265) {
     sources += [
+      "source/rtp_packet_h265_common.h",
       "source/rtp_packetizer_h265.cc",
       "source/rtp_packetizer_h265.h",
+      "source/video_rtp_depacketizer_h265.cc",
+      "source/video_rtp_depacketizer_h265.h",
     ]
   }
 
@@ -632,7 +635,10 @@
       "source/video_rtp_depacketizer_vp9_unittest.cc",
     ]
     if (rtc_use_h265) {
-      sources += [ "source/rtp_packetizer_h265_unittest.cc" ]
+      sources += [
+        "source/rtp_packetizer_h265_unittest.cc",
+        "source/video_rtp_depacketizer_h265_unittest.cc",
+      ]
     }
 
     deps = [
diff --git a/modules/rtp_rtcp/source/create_video_rtp_depacketizer.cc b/modules/rtp_rtcp/source/create_video_rtp_depacketizer.cc
index 95db212..598a86d 100644
--- a/modules/rtp_rtcp/source/create_video_rtp_depacketizer.cc
+++ b/modules/rtp_rtcp/source/create_video_rtp_depacketizer.cc
@@ -19,6 +19,9 @@
 #include "modules/rtp_rtcp/source/video_rtp_depacketizer_h264.h"
 #include "modules/rtp_rtcp/source/video_rtp_depacketizer_vp8.h"
 #include "modules/rtp_rtcp/source/video_rtp_depacketizer_vp9.h"
+#ifdef RTC_ENABLE_H265
+#include "modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h"
+#endif
 
 namespace webrtc {
 
@@ -34,8 +37,11 @@
     case kVideoCodecAV1:
       return std::make_unique<VideoRtpDepacketizerAv1>();
     case kVideoCodecH265:
-      // TODO(bugs.webrtc.org/13485): Implement VideoRtpDepacketizerH265.
+#ifdef RTC_ENABLE_H265
+      return std::make_unique<VideoRtpDepacketizerH265>();
+#else
       return nullptr;
+#endif
     case kVideoCodecGeneric:
     case kVideoCodecMultiplex:
       return std::make_unique<VideoRtpDepacketizerGeneric>();
diff --git a/modules/rtp_rtcp/source/rtp_packet_h265_common.h b/modules/rtp_rtcp/source/rtp_packet_h265_common.h
new file mode 100644
index 0000000..8655a02
--- /dev/null
+++ b/modules/rtp_rtcp/source/rtp_packet_h265_common.h
@@ -0,0 +1,54 @@
+/*
+ *  Copyright (c) 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef MODULES_RTP_RTCP_SOURCE_RTP_PACKET_H265_COMMON_H_
+#define MODULES_RTP_RTCP_SOURCE_RTP_PACKET_H265_COMMON_H_
+
+#include <string>
+#include <vector>
+
+namespace webrtc {
+// The payload header consists of the same
+// fields (F, Type, LayerId and TID) as the NAL unit header. Refer to
+// section 4.4 in RFC 7798.
+constexpr size_t kH265PayloadHeaderSizeBytes = 2;
+// Unlike H.264, H.265 NAL header is 2-bytes.
+constexpr size_t kH265NalHeaderSizeBytes = 2;
+// H.265's FU is constructed of 2-byte payload header, 1-byte FU header and FU
+// payload.
+constexpr size_t kH265FuHeaderSizeBytes = 1;
+// The NALU size for H.265 RTP aggregated packet indicates the size of the NAL
+// unit is 2-bytes.
+constexpr size_t kH265LengthFieldSizeBytes = 2;
+constexpr size_t kH265ApHeaderSizeBytes =
+    kH265NalHeaderSizeBytes + kH265LengthFieldSizeBytes;
+
+// Bit masks for NAL headers.
+enum NalHdrMasks {
+  kH265FBit = 0x80,
+  kH265TypeMask = 0x7E,
+  kH265LayerIDHMask = 0x1,
+  kH265LayerIDLMask = 0xF8,
+  kH265TIDMask = 0x7,
+  kH265TypeMaskN = 0x81,
+  kH265TypeMaskInFuHeader = 0x3F
+};
+
+// Bit masks for FU headers.
+enum FuBitmasks {
+  kH265SBitMask = 0x80,
+  kH265EBitMask = 0x40,
+  kH265FuTypeBitMask = 0x3F
+};
+
+constexpr uint8_t kStartCode[] = {0, 0, 0, 1};
+
+}  // namespace webrtc
+
+#endif  // MODULES_RTP_RTCP_SOURCE_RTP_PACKET_H265_COMMON_H_
diff --git a/modules/rtp_rtcp/source/rtp_packetizer_h265.cc b/modules/rtp_rtcp/source/rtp_packetizer_h265.cc
index 313680c..5f10120 100644
--- a/modules/rtp_rtcp/source/rtp_packetizer_h265.cc
+++ b/modules/rtp_rtcp/source/rtp_packetizer_h265.cc
@@ -16,42 +16,10 @@
 #include "common_video/h264/h264_common.h"
 #include "common_video/h265/h265_common.h"
 #include "modules/rtp_rtcp/source/byte_io.h"
+#include "modules/rtp_rtcp/source/rtp_packet_h265_common.h"
 #include "rtc_base/logging.h"
 
 namespace webrtc {
-namespace {
-
-// The payload header consists of the same
-// fields (F, Type, LayerId and TID) as the NAL unit header. Refer to
-// section 4.2 in RFC 7798.
-constexpr size_t kH265PayloadHeaderSize = 2;
-// Unlike H.264, H265 NAL header is 2-bytes.
-constexpr size_t kH265NalHeaderSize = 2;
-// H265's FU is constructed of 2-byte payload header, 1-byte FU header and FU
-// payload.
-constexpr size_t kH265FuHeaderSize = 1;
-// The NALU size for H265 RTP aggregated packet indicates the size of the NAL
-// unit is 2-bytes.
-constexpr size_t kH265LengthFieldSize = 2;
-
-enum H265NalHdrMasks {
-  kH265FBit = 0x80,
-  kH265TypeMask = 0x7E,
-  kH265LayerIDHMask = 0x1,
-  kH265LayerIDLMask = 0xF8,
-  kH265TIDMask = 0x7,
-  kH265TypeMaskN = 0x81,
-  kH265TypeMaskInFuHeader = 0x3F
-};
-
-// Bit masks for FU headers.
-enum H265FuBitmasks {
-  kH265SBitMask = 0x80,
-  kH265EBitMask = 0x40,
-  kH265FuTypeBitMask = 0x3F
-};
-
-}  // namespace
 
 RtpPacketizerH265::RtpPacketizerH265(rtc::ArrayView<const uint8_t> payload,
                                      PayloadSizeLimits limits)
@@ -112,7 +80,8 @@
   // Refer to section 4.4.3 in RFC7798, each FU fragment will have a 2-bytes
   // payload header and a one-byte FU header. DONL is not supported so ignore
   // its size when calculating max_payload_len.
-  limits.max_payload_len -= kH265FuHeaderSize + kH265PayloadHeaderSize;
+  limits.max_payload_len -=
+      kH265FuHeaderSizeBytes + kH265PayloadHeaderSizeBytes;
 
   // Update single/first/last packet reductions unless it is single/first/last
   // fragment.
@@ -135,8 +104,8 @@
   }
 
   // Strip out the original header.
-  size_t payload_left = fragment.size() - kH265NalHeaderSize;
-  int offset = kH265NalHeaderSize;
+  size_t payload_left = fragment.size() - kH265NalHeaderSizeBytes;
+  int offset = kH265NalHeaderSizeBytes;
 
   std::vector<int> payload_sizes = SplitAboutEqually(payload_left, limits);
   if (payload_sizes.empty()) {
@@ -198,12 +167,13 @@
     payload_size_left -= fragment.size();
     payload_size_left -= fragment_headers_length;
 
-    fragment_headers_length = kH265LengthFieldSize;
+    fragment_headers_length = kH265LengthFieldSizeBytes;
     // If we are going to try to aggregate more fragments into this packet
     // we need to add the AP NALU header and a length field for the first
     // NALU of this packet.
     if (aggregated_fragments == 0) {
-      fragment_headers_length += kH265PayloadHeaderSize + kH265LengthFieldSize;
+      fragment_headers_length +=
+          kH265PayloadHeaderSizeBytes + kH265LengthFieldSizeBytes;
     }
     ++aggregated_fragments;
 
@@ -248,7 +218,7 @@
 
 void RtpPacketizerH265::NextAggregatePacket(RtpPacketToSend* rtp_packet) {
   size_t payload_capacity = rtp_packet->FreeCapacity();
-  RTC_CHECK_GE(payload_capacity, kH265PayloadHeaderSize);
+  RTC_CHECK_GE(payload_capacity, kH265PayloadHeaderSizeBytes);
   uint8_t* buffer = rtp_packet->AllocatePayload(payload_capacity);
   RTC_CHECK(buffer);
   PacketUnit* packet = &packets_.front();
@@ -272,13 +242,13 @@
   buffer[0] = payload_hdr_h;
   buffer[1] = payload_hdr_l;
 
-  int index = kH265PayloadHeaderSize;
+  int index = kH265PayloadHeaderSizeBytes;
   bool is_last_fragment = packet->last_fragment;
   while (packet->aggregated) {
     // Add NAL unit length field.
     rtc::ArrayView<const uint8_t> fragment = packet->source_fragment;
     ByteWriter<uint16_t>::WriteBigEndian(&buffer[index], fragment.size());
-    index += kH265LengthFieldSize;
+    index += kH265LengthFieldSizeBytes;
     // Add NAL unit.
     memcpy(&buffer[index], fragment.data(), fragment.size());
     index += fragment.size();
@@ -332,15 +302,15 @@
                   (H265::NaluType::kFu << 1) | layer_id_h;
   rtc::ArrayView<const uint8_t> fragment = packet->source_fragment;
   uint8_t* buffer = rtp_packet->AllocatePayload(
-      kH265FuHeaderSize + kH265PayloadHeaderSize + fragment.size());
+      kH265FuHeaderSizeBytes + kH265PayloadHeaderSizeBytes + fragment.size());
   RTC_CHECK(buffer);
   buffer[0] = payload_hdr_h;
   buffer[1] = payload_hdr_l;
   buffer[2] = fu_header;
 
   // Do not support DONL for fragmentation units, DONL field is not present.
-  memcpy(buffer + kH265FuHeaderSize + kH265PayloadHeaderSize, fragment.data(),
-         fragment.size());
+  memcpy(buffer + kH265FuHeaderSizeBytes + kH265PayloadHeaderSizeBytes,
+         fragment.data(), fragment.size());
   if (packet->last_fragment) {
     input_fragments_.pop_front();
   }
diff --git a/modules/rtp_rtcp/source/rtp_packetizer_h265_unittest.cc b/modules/rtp_rtcp/source/rtp_packetizer_h265_unittest.cc
index cb1de33..8f739e8 100644
--- a/modules/rtp_rtcp/source/rtp_packetizer_h265_unittest.cc
+++ b/modules/rtp_rtcp/source/rtp_packetizer_h265_unittest.cc
@@ -15,6 +15,7 @@
 #include "common_video/h265/h265_common.h"
 #include "modules/rtp_rtcp/mocks/mock_rtp_rtcp.h"
 #include "modules/rtp_rtcp/source/byte_io.h"
+#include "modules/rtp_rtcp/source/rtp_packet_h265_common.h"
 #include "test/gmock.h"
 #include "test/gtest.h"
 
@@ -29,18 +30,12 @@
 using ::testing::SizeIs;
 
 constexpr RtpPacketToSend::ExtensionManager* kNoExtensions = nullptr;
-constexpr size_t kMaxPayloadSize = 1200;
-constexpr size_t kLengthFieldLength = 2;
+constexpr size_t kMaxPayloadSizeBytes = 1200;
+constexpr size_t kH265LengthFieldSizeBytes = 2;
 constexpr RtpPacketizer::PayloadSizeLimits kNoLimits;
 
-constexpr size_t kNalHeaderSize = 2;
-constexpr size_t kFuHeaderSize = 3;
-
-constexpr uint8_t kNaluTypeMask = 0x7E;
-
-// Bit masks for FU headers.
-constexpr uint8_t kH265SBit = 0x80;
-constexpr uint8_t kH265EBit = 0x40;
+constexpr size_t kFuHeaderSizeBytes =
+    kH265FuHeaderSizeBytes + kH265PayloadHeaderSizeBytes;
 
 // Creates Buffer that looks like nal unit of given size.
 rtc::Buffer GenerateNalUnit(size_t size) {
@@ -127,8 +122,8 @@
 
 TEST(RtpPacketizerH265Test, SingleNaluTwoPackets) {
   RtpPacketizer::PayloadSizeLimits limits;
-  limits.max_payload_len = kMaxPayloadSize;
-  rtc::Buffer nalus[] = {GenerateNalUnit(kMaxPayloadSize),
+  limits.max_payload_len = kMaxPayloadSizeBytes;
+  rtc::Buffer nalus[] = {GenerateNalUnit(kMaxPayloadSizeBytes),
                          GenerateNalUnit(100)};
   rtc::Buffer frame = CreateFrame(nalus);
 
@@ -205,27 +200,28 @@
   ASSERT_THAT(packets, SizeIs(1));
   auto payload = packets[0].payload();
   int type = H265::ParseNaluType(payload[0]);
-  EXPECT_EQ(payload.size(),
-            kNalHeaderSize + 3 * kLengthFieldLength + 2 + 2 + 0x123);
+  EXPECT_EQ(payload.size(), kH265NalHeaderSizeBytes +
+                                3 * kH265LengthFieldSizeBytes + 2 + 2 + 0x123);
 
   EXPECT_EQ(type, H265::NaluType::kAp);
-  payload = payload.subview(kNalHeaderSize);
+  payload = payload.subview(kH265NalHeaderSizeBytes);
   // 1st fragment.
-  EXPECT_THAT(payload.subview(0, kLengthFieldLength),
+  EXPECT_THAT(payload.subview(0, kH265LengthFieldSizeBytes),
               ElementsAre(0, 2));  // Size.
-  EXPECT_THAT(payload.subview(kLengthFieldLength, 2),
+  EXPECT_THAT(payload.subview(kH265LengthFieldSizeBytes, 2),
               ElementsAreArray(nalus[0]));
-  payload = payload.subview(kLengthFieldLength + 2);
+  payload = payload.subview(kH265LengthFieldSizeBytes + 2);
   // 2nd fragment.
-  EXPECT_THAT(payload.subview(0, kLengthFieldLength),
+  EXPECT_THAT(payload.subview(0, kH265LengthFieldSizeBytes),
               ElementsAre(0, 2));  // Size.
-  EXPECT_THAT(payload.subview(kLengthFieldLength, 2),
+  EXPECT_THAT(payload.subview(kH265LengthFieldSizeBytes, 2),
               ElementsAreArray(nalus[1]));
-  payload = payload.subview(kLengthFieldLength + 2);
+  payload = payload.subview(kH265LengthFieldSizeBytes + 2);
   // 3rd fragment.
-  EXPECT_THAT(payload.subview(0, kLengthFieldLength),
+  EXPECT_THAT(payload.subview(0, kH265LengthFieldSizeBytes),
               ElementsAre(0x1, 0x23));  // Size.
-  EXPECT_THAT(payload.subview(kLengthFieldLength), ElementsAreArray(nalus[2]));
+  EXPECT_THAT(payload.subview(kH265LengthFieldSizeBytes),
+              ElementsAreArray(nalus[2]));
 }
 
 TEST(RtpPacketizerH265Test, ApRespectsFirstPacketReduction) {
@@ -284,7 +280,7 @@
   RtpPacketizer::PayloadSizeLimits limits;
   limits.max_payload_len = 1000;
   const size_t kLastFragmentSize =
-      limits.max_payload_len - 3 * kLengthFieldLength - 4;
+      limits.max_payload_len - 3 * kH265LengthFieldSizeBytes - 4;
   rtc::Buffer nalus[] = {GenerateNalUnit(/*size=*/2),
                          GenerateNalUnit(/*size=*/2),
                          GenerateNalUnit(/*size=*/kLastFragmentSize)};
@@ -326,7 +322,8 @@
 // Returns sizes of the payloads excluding FU headers.
 std::vector<int> TestFu(size_t frame_payload_size,
                         const RtpPacketizer::PayloadSizeLimits& limits) {
-  rtc::Buffer nalu[] = {GenerateNalUnit(kNalHeaderSize + frame_payload_size)};
+  rtc::Buffer nalu[] = {
+      GenerateNalUnit(kH265NalHeaderSizeBytes + frame_payload_size)};
   rtc::Buffer frame = CreateFrame(nalu);
 
   RtpPacketizerH265 packetizer(frame, limits);
@@ -338,18 +335,18 @@
 
   for (const RtpPacketToSend& packet : packets) {
     auto payload = packet.payload();
-    EXPECT_GT(payload.size(), kFuHeaderSize);
+    EXPECT_GT(payload.size(), kFuHeaderSizeBytes);
     // FU header is after the 2-bytes size PayloadHdr according to 4.4.3 in spec
     fu_header.push_back(payload[2]);
-    payload_sizes.push_back(payload.size() - kFuHeaderSize);
+    payload_sizes.push_back(payload.size() - kFuHeaderSizeBytes);
   }
 
-  EXPECT_TRUE(fu_header.front() & kH265SBit);
-  EXPECT_TRUE(fu_header.back() & kH265EBit);
+  EXPECT_TRUE(fu_header.front() & kH265SBitMask);
+  EXPECT_TRUE(fu_header.back() & kH265EBitMask);
   // Clear S and E bits before testing all are duplicating same original header.
-  fu_header.front() &= ~kH265SBit;
-  fu_header.back() &= ~kH265EBit;
-  uint8_t nalu_type = (nalu[0][0] & kNaluTypeMask) >> 1;
+  fu_header.front() &= ~kH265SBitMask;
+  fu_header.back() &= ~kH265EBitMask;
+  uint8_t nalu_type = (nalu[0][0] & kH265TypeMask) >> 1;
   EXPECT_THAT(fu_header, Each(Eq(nalu_type)));
 
   return payload_sizes;
@@ -403,7 +400,7 @@
   limits.max_payload_len = 1200;
   // Generate 10 full sized packets, leave room for FU headers.
   EXPECT_THAT(
-      TestFu(10 * (1200 - kFuHeaderSize), limits),
+      TestFu(10 * (1200 - kFuHeaderSizeBytes), limits),
       ElementsAre(1197, 1197, 1197, 1197, 1197, 1197, 1197, 1197, 1197, 1197));
 }
 
@@ -449,30 +446,30 @@
     if (expected_packet.aggregated) {
       int type = H265::ParseNaluType(packets[i].payload()[0]);
       EXPECT_THAT(type, H265::NaluType::kAp);
-      auto payload = packets[i].payload().subview(kNalHeaderSize);
+      auto payload = packets[i].payload().subview(kH265NalHeaderSizeBytes);
       int offset = 0;
       // Generated AP packet header and payload align
       for (int j = expected_packet.nalu_index; j < expected_packet.nalu_number;
            j++) {
-        EXPECT_THAT(payload.subview(0, kLengthFieldLength),
+        EXPECT_THAT(payload.subview(0, kH265LengthFieldSizeBytes),
                     ElementsAre(0, nalus[j].size()));
-        EXPECT_THAT(
-            payload.subview(offset + kLengthFieldLength, nalus[j].size()),
-            ElementsAreArray(nalus[j]));
-        offset += kLengthFieldLength + nalus[j].size();
+        EXPECT_THAT(payload.subview(offset + kH265LengthFieldSizeBytes,
+                                    nalus[j].size()),
+                    ElementsAreArray(nalus[j]));
+        offset += kH265LengthFieldSizeBytes + nalus[j].size();
       }
     } else {
       uint8_t fu_header = 0;
-      fu_header |= (expected_packet.first_fragment ? kH265SBit : 0);
-      fu_header |= (expected_packet.last_fragment ? kH265EBit : 0);
+      fu_header |= (expected_packet.first_fragment ? kH265SBitMask : 0);
+      fu_header |= (expected_packet.last_fragment ? kH265EBitMask : 0);
       fu_header |= H265::NaluType::kTrailR;
-      EXPECT_THAT(packets[i].payload().subview(0, kFuHeaderSize),
+      EXPECT_THAT(packets[i].payload().subview(0, kFuHeaderSizeBytes),
                   ElementsAre(98, 2, fu_header));
-      EXPECT_THAT(
-          packets[i].payload().subview(kFuHeaderSize),
-          ElementsAreArray(nalus[expected_packet.nalu_index].data() +
-                               kNalHeaderSize + expected_packet.start_offset,
-                           expected_packet.payload_size));
+      EXPECT_THAT(packets[i].payload().subview(kFuHeaderSizeBytes),
+                  ElementsAreArray(nalus[expected_packet.nalu_index].data() +
+                                       kH265NalHeaderSizeBytes +
+                                       expected_packet.start_offset,
+                                   expected_packet.payload_size));
     }
   }
 }
diff --git a/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc
new file mode 100644
index 0000000..b54df7c
--- /dev/null
+++ b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc
@@ -0,0 +1,244 @@
+/*
+ *  Copyright (c) 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "absl/base/macros.h"
+#include "absl/types/optional.h"
+#include "absl/types/variant.h"
+#include "api/video/video_codec_type.h"
+#include "common_video/h264/h264_common.h"
+#include "common_video/h265/h265_bitstream_parser.h"
+#include "common_video/h265/h265_common.h"
+#include "modules/rtp_rtcp/source/byte_io.h"
+#include "modules/rtp_rtcp/source/rtp_packet_h265_common.h"
+#include "modules/rtp_rtcp/source/video_rtp_depacketizer.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
+
+namespace webrtc {
+namespace {
+
+bool ParseApStartOffsets(const uint8_t* nalu_ptr,
+                         size_t length_remaining,
+                         std::vector<size_t>* offsets) {
+  size_t offset = 0;
+  while (length_remaining > 0) {
+    // Buffer doesn't contain room for additional NALU length.
+    if (length_remaining < kH265LengthFieldSizeBytes)
+      return false;
+    // Read 16-bit NALU size defined in RFC7798 section 4.4.2.
+    uint16_t nalu_size = ByteReader<uint16_t>::ReadBigEndian(nalu_ptr);
+    nalu_ptr += kH265LengthFieldSizeBytes;
+    length_remaining -= kH265LengthFieldSizeBytes;
+    if (nalu_size > length_remaining)
+      return false;
+    nalu_ptr += nalu_size;
+    length_remaining -= nalu_size;
+
+    offsets->push_back(offset + kH265ApHeaderSizeBytes);
+    offset += kH265LengthFieldSizeBytes + nalu_size;
+  }
+  return true;
+}
+
+absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> ProcessApOrSingleNalu(
+    rtc::CopyOnWriteBuffer rtp_payload) {
+  // Skip the single NALU header (payload header), aggregated packet case will
+  // be checked later.
+  if (rtp_payload.size() <= kH265PayloadHeaderSizeBytes) {
+    RTC_LOG(LS_ERROR) << "Single NALU header truncated.";
+    return absl::nullopt;
+  }
+  const uint8_t* const payload_data = rtp_payload.cdata();
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed_payload(
+      absl::in_place);
+  parsed_payload->video_header.width = 0;
+  parsed_payload->video_header.height = 0;
+  parsed_payload->video_header.codec = kVideoCodecH265;
+  parsed_payload->video_header.is_first_packet_in_frame = true;
+
+  const uint8_t* nalu_start = payload_data + kH265PayloadHeaderSizeBytes;
+  const size_t nalu_length = rtp_payload.size() - kH265PayloadHeaderSizeBytes;
+  uint8_t nal_type = (payload_data[0] & kH265TypeMask) >> 1;
+  std::vector<size_t> nalu_start_offsets;
+  rtc::CopyOnWriteBuffer video_payload;
+  if (nal_type == H265::NaluType::kAp) {
+    // Skip the aggregated packet header (Aggregated packet NAL type + length).
+    if (rtp_payload.size() <= kH265ApHeaderSizeBytes) {
+      RTC_LOG(LS_ERROR) << "Aggregated packet header truncated.";
+      return absl::nullopt;
+    }
+
+    if (!ParseApStartOffsets(nalu_start, nalu_length, &nalu_start_offsets)) {
+      RTC_LOG(LS_ERROR)
+          << "Aggregated packet with incorrect NALU packet lengths.";
+      return absl::nullopt;
+    }
+
+    nal_type = (payload_data[kH265ApHeaderSizeBytes] & kH265TypeMask) >> 1;
+  } else {
+    nalu_start_offsets.push_back(0);
+  }
+  parsed_payload->video_header.frame_type = VideoFrameType::kVideoFrameDelta;
+
+  nalu_start_offsets.push_back(rtp_payload.size() +
+                               kH265LengthFieldSizeBytes);  // End offset.
+  for (size_t i = 0; i < nalu_start_offsets.size() - 1; ++i) {
+    size_t start_offset = nalu_start_offsets[i];
+    // End offset is actually start offset for next unit, excluding length field
+    // so remove that from this units length.
+    size_t end_offset = nalu_start_offsets[i + 1] - kH265LengthFieldSizeBytes;
+    if (end_offset - start_offset < kH265NalHeaderSizeBytes) {
+      RTC_LOG(LS_ERROR) << "Aggregated packet too short";
+      return absl::nullopt;
+    }
+
+    // Insert start code before each NALU in aggregated packet.
+    video_payload.AppendData(kStartCode);
+    video_payload.AppendData(&payload_data[start_offset],
+                             end_offset - start_offset);
+
+    uint8_t nalu_type = (payload_data[start_offset] & kH265TypeMask) >> 1;
+    start_offset += kH265NalHeaderSizeBytes;
+    switch (nalu_type) {
+      case H265::NaluType::kBlaWLp:
+      case H265::NaluType::kBlaWRadl:
+      case H265::NaluType::kBlaNLp:
+      case H265::NaluType::kIdrWRadl:
+      case H265::NaluType::kIdrNLp:
+      case H265::NaluType::kCra:
+      case H265::NaluType::kRsvIrapVcl23:
+        parsed_payload->video_header.frame_type =
+            VideoFrameType::kVideoFrameKey;
+        ABSL_FALLTHROUGH_INTENDED;
+      case H265::NaluType::kSps: {
+        // Copy any previous data first (likely just the first header).
+        std::unique_ptr<rtc::Buffer> output_buffer(new rtc::Buffer());
+        if (start_offset)
+          output_buffer->AppendData(payload_data, start_offset);
+
+        absl::optional<H265SpsParser::SpsState> sps = H265SpsParser::ParseSps(
+            &payload_data[start_offset], end_offset - start_offset);
+
+        if (sps) {
+          // TODO(bugs.webrtc.org/13485): Implement the size calculation taking
+          // VPS->vui_parameters.def_disp_win_xx_offset into account.
+          parsed_payload->video_header.width = sps->width;
+          parsed_payload->video_header.height = sps->height;
+        } else {
+          RTC_LOG(LS_WARNING) << "Failed to parse SPS from SPS slice.";
+        }
+      }
+        ABSL_FALLTHROUGH_INTENDED;
+      case H265::NaluType::kVps:
+      case H265::NaluType::kPps:
+      case H265::NaluType::kTrailN:
+      case H265::NaluType::kTrailR:
+      // Slices below don't contain SPS or PPS ids.
+      case H265::NaluType::kAud:
+      case H265::NaluType::kTsaN:
+      case H265::NaluType::kTsaR:
+      case H265::NaluType::kStsaN:
+      case H265::NaluType::kStsaR:
+      case H265::NaluType::kRadlN:
+      case H265::NaluType::kRadlR:
+      case H265::NaluType::kPrefixSei:
+      case H265::NaluType::kSuffixSei:
+        break;
+      case H265::NaluType::kAp:
+      case H265::NaluType::kFu:
+      case H265::NaluType::kPaci:
+        RTC_LOG(LS_WARNING) << "Unexpected AP, FU or PACI received.";
+        return absl::nullopt;
+    }
+  }
+  parsed_payload->video_payload = video_payload;
+  return parsed_payload;
+}
+
+absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> ParseFuNalu(
+    rtc::CopyOnWriteBuffer rtp_payload) {
+  if (rtp_payload.size() < kH265FuHeaderSizeBytes + kH265NalHeaderSizeBytes) {
+    RTC_LOG(LS_ERROR) << "FU NAL units truncated.";
+    return absl::nullopt;
+  }
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed_payload(
+      absl::in_place);
+
+  uint8_t f = rtp_payload.cdata()[0] & kH265FBit;
+  uint8_t layer_id_h = rtp_payload.cdata()[0] & kH265LayerIDHMask;
+  uint8_t layer_id_l_unshifted = rtp_payload.cdata()[1] & kH265LayerIDLMask;
+  uint8_t tid = rtp_payload.cdata()[1] & kH265TIDMask;
+
+  uint8_t original_nal_type = rtp_payload.cdata()[2] & kH265TypeMaskInFuHeader;
+  bool first_fragment = rtp_payload.cdata()[2] & kH265SBitMask;
+  if (first_fragment) {
+    rtp_payload = rtp_payload.Slice(
+        kH265FuHeaderSizeBytes, rtp_payload.size() - kH265FuHeaderSizeBytes);
+    rtp_payload.MutableData()[0] = f | original_nal_type << 1 | layer_id_h;
+    rtp_payload.MutableData()[1] = layer_id_l_unshifted | tid;
+    rtc::CopyOnWriteBuffer video_payload;
+    // Insert start code before the first fragment in FU.
+    video_payload.AppendData(kStartCode);
+    video_payload.AppendData(rtp_payload);
+    parsed_payload->video_payload = video_payload;
+  } else {
+    parsed_payload->video_payload = rtp_payload.Slice(
+        kH265NalHeaderSizeBytes + kH265FuHeaderSizeBytes,
+        rtp_payload.size() - kH265NalHeaderSizeBytes - kH265FuHeaderSizeBytes);
+  }
+
+  if (original_nal_type == H265::NaluType::kIdrWRadl ||
+      original_nal_type == H265::NaluType::kIdrNLp ||
+      original_nal_type == H265::NaluType::kCra) {
+    parsed_payload->video_header.frame_type = VideoFrameType::kVideoFrameKey;
+  } else {
+    parsed_payload->video_header.frame_type = VideoFrameType::kVideoFrameDelta;
+  }
+  parsed_payload->video_header.width = 0;
+  parsed_payload->video_header.height = 0;
+  parsed_payload->video_header.codec = kVideoCodecH265;
+  parsed_payload->video_header.is_first_packet_in_frame = first_fragment;
+
+  return parsed_payload;
+}
+
+}  // namespace
+
+absl::optional<VideoRtpDepacketizer::ParsedRtpPayload>
+VideoRtpDepacketizerH265::Parse(rtc::CopyOnWriteBuffer rtp_payload) {
+  if (rtp_payload.empty()) {
+    RTC_LOG(LS_ERROR) << "Empty payload.";
+    return absl::nullopt;
+  }
+
+  uint8_t nal_type = (rtp_payload.cdata()[0] & kH265TypeMask) >> 1;
+
+  if (nal_type == H265::NaluType::kFu) {
+    // Fragmented NAL units (FU).
+    return ParseFuNalu(std::move(rtp_payload));
+  } else if (nal_type == H265::NaluType::kPaci) {
+    // TODO(bugs.webrtc.org/13485): Implement PACI parse for H265
+    RTC_LOG(LS_ERROR) << "Not support type:" << nal_type;
+    return absl::nullopt;
+  } else {
+    // Single NAL unit packet or Aggregated packets (AP).
+    return ProcessApOrSingleNalu(std::move(rtp_payload));
+  }
+}
+
+}  // namespace webrtc
diff --git a/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h
new file mode 100644
index 0000000..ed5290d
--- /dev/null
+++ b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h
@@ -0,0 +1,28 @@
+/*
+ *  Copyright (c) 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_RTP_RTCP_SOURCE_VIDEO_RTP_DEPACKETIZER_H265_H_
+#define MODULES_RTP_RTCP_SOURCE_VIDEO_RTP_DEPACKETIZER_H265_H_
+
+#include "absl/types/optional.h"
+#include "modules/rtp_rtcp/source/video_rtp_depacketizer.h"
+#include "rtc_base/copy_on_write_buffer.h"
+
+namespace webrtc {
+class VideoRtpDepacketizerH265 : public VideoRtpDepacketizer {
+ public:
+  ~VideoRtpDepacketizerH265() override = default;
+
+  absl::optional<ParsedRtpPayload> Parse(
+      rtc::CopyOnWriteBuffer rtp_payload) override;
+};
+}  // namespace webrtc
+
+#endif  // MODULES_RTP_RTCP_SOURCE_VIDEO_RTP_DEPACKETIZER_H265_H_
diff --git a/modules/rtp_rtcp/source/video_rtp_depacketizer_h265_unittest.cc b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265_unittest.cc
new file mode 100644
index 0000000..a630671
--- /dev/null
+++ b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265_unittest.cc
@@ -0,0 +1,400 @@
+/*
+ *  Copyright (c) 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h"
+
+#include <cstdint>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/array_view.h"
+#include "common_video/h265/h265_common.h"
+#include "modules/rtp_rtcp/mocks/mock_rtp_rtcp.h"
+#include "modules/rtp_rtcp/source/byte_io.h"
+#include "modules/rtp_rtcp/source/rtp_packet_h265_common.h"
+#include "rtc_base/copy_on_write_buffer.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+using ::testing::Each;
+using ::testing::ElementsAre;
+using ::testing::ElementsAreArray;
+using ::testing::Eq;
+using ::testing::IsEmpty;
+using ::testing::SizeIs;
+
+TEST(VideoRtpDepacketizerH265Test, SingleNalu) {
+  uint8_t packet[3] = {0x26, 0x02,
+                       0xFF};  // F=0, Type=19 (Idr), LayerId=0, TID=2.
+  uint8_t expected_packet[] = {0x00, 0x00, 0x00, 0x01, 0x26, 0x02, 0xff};
+  rtc::CopyOnWriteBuffer rtp_payload(packet);
+
+  VideoRtpDepacketizerH265 depacketizer;
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed =
+      depacketizer.Parse(rtp_payload);
+  ASSERT_TRUE(parsed);
+
+  EXPECT_THAT(rtc::MakeArrayView(parsed->video_payload.cdata(),
+                                 parsed->video_payload.size()),
+              ElementsAreArray(expected_packet));
+  EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameKey);
+  EXPECT_EQ(parsed->video_header.codec, kVideoCodecH265);
+  EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame);
+}
+
+TEST(VideoRtpDepacketizerH265Test, SingleNaluSpsWithResolution) {
+  // SPS for a 1280x720 camera capture from ffmpeg on linux. Contains
+  // emulation bytes but no cropping. This buffer is generated
+  // with following command:
+  // 1) ffmpeg -i /dev/video0 -r 30 -c:v libx265 -s 1280x720 camera.h265
+  //
+  // 2) Open camera.h265 and find the SPS, generally everything between the
+  // second and third start codes (0 0 0 1 or 0 0 1). The first two bytes
+  // 0x42 and 0x02 shows the nal header of SPS.
+  uint8_t packet[] = {0x42, 0x02, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03,
+                      0x00, 0x9d, 0x08, 0x00, 0x00, 0x03, 0x00, 0x00,
+                      0x5d, 0xb0, 0x02, 0x80, 0x80, 0x2d, 0x16, 0x59,
+                      0x59, 0xa4, 0x93, 0x2b, 0x80, 0x40, 0x00, 0x00,
+                      0x03, 0x00, 0x40, 0x00, 0x00, 0x07, 0x82};
+  uint8_t expected_packet[] = {
+      0x00, 0x00, 0x00, 0x01, 0x42, 0x02, 0x01, 0x04, 0x08, 0x00, 0x00,
+      0x03, 0x00, 0x9d, 0x08, 0x00, 0x00, 0x03, 0x00, 0x00, 0x5d, 0xb0,
+      0x02, 0x80, 0x80, 0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0x80,
+      0x40, 0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x07, 0x82};
+  rtc::CopyOnWriteBuffer rtp_payload(packet);
+
+  VideoRtpDepacketizerH265 depacketizer;
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed =
+      depacketizer.Parse(rtp_payload);
+  ASSERT_TRUE(parsed);
+
+  EXPECT_THAT(rtc::MakeArrayView(parsed->video_payload.cdata(),
+                                 parsed->video_payload.size()),
+              ElementsAreArray(expected_packet));
+  EXPECT_EQ(parsed->video_header.codec, kVideoCodecH265);
+  EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame);
+  EXPECT_EQ(parsed->video_header.width, 1280u);
+  EXPECT_EQ(parsed->video_header.height, 720u);
+}
+
+TEST(VideoRtpDepacketizerH265Test, PaciPackets) {
+  uint8_t packet[2] = {0x64, 0x02};  // F=0, Type=50 (PACI), LayerId=0, TID=2.
+  rtc::CopyOnWriteBuffer rtp_payload(packet);
+
+  VideoRtpDepacketizerH265 depacketizer;
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed =
+      depacketizer.Parse(rtp_payload);
+  ASSERT_FALSE(parsed);
+}
+
+TEST(VideoRtpDepacketizerH265Test, ApKey) {
+  uint8_t payload_header[] = {0x60, 0x02};
+  uint8_t vps_nalu_size[] = {0, 0x17};
+  uint8_t sps_nalu_size[] = {0, 0x27};
+  uint8_t pps_nalu_size[] = {0, 0x32};
+  uint8_t slice_nalu_size[] = {0, 0xa};
+  uint8_t start_code[] = {0x00, 0x00, 0x00, 0x01};
+  // VPS/SPS/PPS/IDR for a 1280x720 camera capture from ffmpeg on linux.
+  // Contains emulation bytes but no cropping. This buffer is generated with
+  // following command: 1) ffmpeg -i /dev/video0 -r 30 -c:v libx265 -s 1280x720
+  // camera.h265
+  //
+  // 2) Open camera.h265 and find:
+  // VPS - generally everything between the first and second start codes (0 0 0
+  // 1 or 0 0 1). The first two bytes 0x40 and 0x02 shows the nal header of VPS.
+  // SPS - generally everything between the
+  // second and third start codes (0 0 0 1 or 0 0 1). The first two bytes
+  // 0x42 and 0x02 shows the nal header of SPS.
+  // PPS - generally everything between the third and fourth start codes (0 0 0
+  // 1 or 0 0 1). The first two bytes 0x44 and 0x02 shows the nal header of PPS.
+  // IDR - Part of the keyframe bitstream (no need to show all the bytes for
+  // depacketizer testing). The first two bytes 0x26 and 0x02 shows the nal
+  // header of IDR frame.
+  uint8_t vps[] = {
+      0x40, 0x02, 0x1c, 0x01, 0xff, 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00,
+      0x9d, 0x08, 0x00, 0x00, 0x03, 0x00, 0x00, 0x78, 0x95, 0x98, 0x09,
+  };
+  uint8_t sps[] = {0x42, 0x02, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9d,
+                   0x08, 0x00, 0x00, 0x03, 0x00, 0x00, 0x5d, 0xb0, 0x02, 0x80,
+                   0x80, 0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0x80, 0x40,
+                   0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x07, 0x82};
+  uint8_t pps[] = {0x44, 0x02, 0xa4, 0x04, 0x55, 0xa2, 0x6d, 0xce, 0xc0, 0xc3,
+                   0xed, 0x0b, 0xac, 0xbc, 0x00, 0xc4, 0x44, 0x2e, 0xf7, 0x55,
+                   0xfd, 0x05, 0x86, 0x92, 0x19, 0xdf, 0x58, 0xec, 0x38, 0x36,
+                   0xb7, 0x7c, 0x00, 0x15, 0x33, 0x78, 0x03, 0x67, 0x26, 0x0f,
+                   0x7b, 0x30, 0x1c, 0xd7, 0xd4, 0x3a, 0xec, 0xad, 0xef, 0x73};
+  uint8_t idr[] = {0x26, 0x02, 0xaf, 0x08, 0x4a, 0x31, 0x11, 0x15, 0xe5, 0xc0};
+
+  rtc::Buffer packet;
+  packet.AppendData(payload_header);
+  packet.AppendData(vps_nalu_size);
+  packet.AppendData(vps);
+  packet.AppendData(sps_nalu_size);
+  packet.AppendData(sps);
+  packet.AppendData(pps_nalu_size);
+  packet.AppendData(pps);
+  packet.AppendData(slice_nalu_size);
+  packet.AppendData(idr);
+
+  rtc::Buffer expected_packet;
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(vps);
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(sps);
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(pps);
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(idr);
+
+  // clang-format on
+  rtc::CopyOnWriteBuffer rtp_payload(packet);
+
+  VideoRtpDepacketizerH265 depacketizer;
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed =
+      depacketizer.Parse(rtp_payload);
+  ASSERT_TRUE(parsed);
+
+  EXPECT_THAT(rtc::MakeArrayView(parsed->video_payload.cdata(),
+                                 parsed->video_payload.size()),
+              ElementsAreArray(expected_packet));
+  EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameKey);
+  EXPECT_EQ(parsed->video_header.codec, kVideoCodecH265);
+  EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame);
+}
+
+TEST(VideoRtpDepacketizerH265Test, ApNaluSpsWithResolution) {
+  uint8_t payload_header[] = {0x60, 0x02};
+  uint8_t vps_nalu_size[] = {0, 0x17};
+  uint8_t sps_nalu_size[] = {0, 0x27};
+  uint8_t pps_nalu_size[] = {0, 0x32};
+  uint8_t slice_nalu_size[] = {0, 0xa};
+  uint8_t start_code[] = {0x00, 0x00, 0x00, 0x01};
+  // The VPS/SPS/PPS/IDR bytes are generated using the same way as above case.
+  uint8_t vps[] = {
+      0x40, 0x02, 0x1c, 0x01, 0xff, 0xff, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00,
+      0x9d, 0x08, 0x00, 0x00, 0x03, 0x00, 0x00, 0x78, 0x95, 0x98, 0x09,
+  };
+  uint8_t sps[] = {0x42, 0x02, 0x01, 0x04, 0x08, 0x00, 0x00, 0x03, 0x00, 0x9d,
+                   0x08, 0x00, 0x00, 0x03, 0x00, 0x00, 0x5d, 0xb0, 0x02, 0x80,
+                   0x80, 0x2d, 0x16, 0x59, 0x59, 0xa4, 0x93, 0x2b, 0x80, 0x40,
+                   0x00, 0x00, 0x03, 0x00, 0x40, 0x00, 0x00, 0x07, 0x82};
+  uint8_t pps[] = {0x44, 0x02, 0xa4, 0x04, 0x55, 0xa2, 0x6d, 0xce, 0xc0, 0xc3,
+                   0xed, 0x0b, 0xac, 0xbc, 0x00, 0xc4, 0x44, 0x2e, 0xf7, 0x55,
+                   0xfd, 0x05, 0x86, 0x92, 0x19, 0xdf, 0x58, 0xec, 0x38, 0x36,
+                   0xb7, 0x7c, 0x00, 0x15, 0x33, 0x78, 0x03, 0x67, 0x26, 0x0f,
+                   0x7b, 0x30, 0x1c, 0xd7, 0xd4, 0x3a, 0xec, 0xad, 0xef, 0x73};
+  uint8_t idr[] = {0x26, 0x02, 0xaf, 0x08, 0x4a, 0x31, 0x11, 0x15, 0xe5, 0xc0};
+
+  rtc::Buffer packet;
+  packet.AppendData(payload_header);
+  packet.AppendData(vps_nalu_size);
+  packet.AppendData(vps);
+  packet.AppendData(sps_nalu_size);
+  packet.AppendData(sps);
+  packet.AppendData(pps_nalu_size);
+  packet.AppendData(pps);
+  packet.AppendData(slice_nalu_size);
+  packet.AppendData(idr);
+
+  rtc::Buffer expected_packet;
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(vps);
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(sps);
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(pps);
+  expected_packet.AppendData(start_code);
+  expected_packet.AppendData(idr);
+
+  rtc::CopyOnWriteBuffer rtp_payload(packet);
+
+  VideoRtpDepacketizerH265 depacketizer;
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed =
+      depacketizer.Parse(rtp_payload);
+  ASSERT_TRUE(parsed);
+
+  EXPECT_THAT(rtc::MakeArrayView(parsed->video_payload.cdata(),
+                                 parsed->video_payload.size()),
+              ElementsAreArray(expected_packet));
+  EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameKey);
+  EXPECT_EQ(parsed->video_header.codec, kVideoCodecH265);
+  EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame);
+  EXPECT_EQ(parsed->video_header.width, 1280u);
+  EXPECT_EQ(parsed->video_header.height, 720u);
+}
+
+TEST(VideoRtpDepacketizerH265Test, EmptyApRejected) {
+  uint8_t lone_empty_packet[] = {0x60, 0x02,  // F=0, Type=48 (kH265Ap).
+                                 0x00, 0x00};
+  uint8_t leading_empty_packet[] = {0x60, 0x02,  // F=0, Type=48 (kH265Ap).
+                                    0x00, 0x00, 0x00, 0x05, 0x26,
+                                    0x02, 0xFF, 0x00, 0x11};  // kIdrWRadl
+  uint8_t middle_empty_packet[] = {0x60, 0x02,  // F=0, Type=48 (kH265Ap).
+                                   0x00, 0x04, 0x26, 0x02, 0xFF,
+                                   0x00, 0x00, 0x00, 0x00, 0x05,
+                                   0x26, 0x02, 0xFF, 0x00, 0x11};  // kIdrWRadl
+  uint8_t trailing_empty_packet[] = {0x60, 0x02,  // F=0, Type=48 (kH265Ap).
+                                     0x00, 0x04, 0x26,
+                                     0x02, 0xFF, 0x00,  // kIdrWRadl
+                                     0x00, 0x00};
+
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(lone_empty_packet)));
+  EXPECT_FALSE(
+      depacketizer.Parse(rtc::CopyOnWriteBuffer(leading_empty_packet)));
+  EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(middle_empty_packet)));
+  EXPECT_FALSE(
+      depacketizer.Parse(rtc::CopyOnWriteBuffer(trailing_empty_packet)));
+}
+
+TEST(VideoRtpDepacketizerH265Test, ApDelta) {
+  uint8_t packet[20] = {0x60, 0x02,  // F=0, Type=48 (kH265Ap).
+                                     // Length, nal header, payload.
+                        0, 0x03, 0x02, 0x02, 0xFF,               // TrailR
+                        0, 0x04, 0x02, 0x02, 0xFF, 0x00,         // TrailR
+                        0, 0x05, 0x02, 0x02, 0xFF, 0x00, 0x11};  // TrailR
+  uint8_t expected_packet[] = {
+      0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0xFF,               // TrailR
+      0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0xFF, 0x00,         // TrailR
+      0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0xFF, 0x00, 0x11};  // TrailR
+  rtc::CopyOnWriteBuffer rtp_payload(packet);
+
+  VideoRtpDepacketizerH265 depacketizer;
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed =
+      depacketizer.Parse(rtp_payload);
+  ASSERT_TRUE(parsed);
+
+  EXPECT_THAT(rtc::MakeArrayView(parsed->video_payload.cdata(),
+                                 parsed->video_payload.size()),
+              ElementsAreArray(expected_packet));
+
+  EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameDelta);
+  EXPECT_EQ(parsed->video_header.codec, kVideoCodecH265);
+  EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame);
+}
+
+TEST(VideoRtpDepacketizerH265Test, Fu) {
+  // clang-format off
+  uint8_t packet1[] = {
+      0x62, 0x02,  // F=0, Type=49 (kH265Fu).
+      0x93,  // FU header kH265SBitMask | H265::kIdrWRadl.
+      0xaf, 0x08, 0x4a, 0x31, 0x11, 0x15, 0xe5, 0xc0  // Payload.
+  };
+  // clang-format on
+  // F=0, Type=19, (kIdrWRadl), tid=1, nalu header: 00100110 00000010, which is
+  // 0x26, 0x02
+  const uint8_t kExpected1[] = {0x00, 0x00, 0x00, 0x01, 0x26, 0x02, 0xaf,
+                                0x08, 0x4a, 0x31, 0x11, 0x15, 0xe5, 0xc0};
+
+  uint8_t packet2[] = {
+      0x62, 0x02,       // F=0, Type=49 (kH265Fu).
+      H265::kIdrWRadl,  // FU header.
+      0x02              // Payload.
+  };
+  const uint8_t kExpected2[] = {0x02};
+
+  uint8_t packet3[] = {
+      0x62, 0x02,  // F=0, Type=49 (kH265Fu).
+      0x33,        // FU header kH265EBitMask | H265::kIdrWRadl.
+      0x03         // Payload.
+  };
+  const uint8_t kExpected3[] = {0x03};
+
+  VideoRtpDepacketizerH265 depacketizer;
+  absl::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed1 =
+      depacketizer.Parse(rtc::CopyOnWriteBuffer(packet1));
+  ASSERT_TRUE(parsed1);
+  // We expect that the first packet is one byte shorter since the FU header
+  // has been replaced by the original nal header.
+  EXPECT_THAT(rtc::MakeArrayView(parsed1->video_payload.cdata(),
+                                 parsed1->video_payload.size()),
+              ElementsAreArray(kExpected1));
+  EXPECT_EQ(parsed1->video_header.frame_type, VideoFrameType::kVideoFrameKey);
+  EXPECT_EQ(parsed1->video_header.codec, kVideoCodecH265);
+  EXPECT_TRUE(parsed1->video_header.is_first_packet_in_frame);
+
+  // Following packets will be 2 bytes shorter since they will only be appended
+  // onto the first packet.
+  auto parsed2 = depacketizer.Parse(rtc::CopyOnWriteBuffer(packet2));
+  EXPECT_THAT(rtc::MakeArrayView(parsed2->video_payload.cdata(),
+                                 parsed2->video_payload.size()),
+              ElementsAreArray(kExpected2));
+  EXPECT_FALSE(parsed2->video_header.is_first_packet_in_frame);
+  EXPECT_EQ(parsed2->video_header.codec, kVideoCodecH265);
+
+  auto parsed3 = depacketizer.Parse(rtc::CopyOnWriteBuffer(packet3));
+  EXPECT_THAT(rtc::MakeArrayView(parsed3->video_payload.cdata(),
+                                 parsed3->video_payload.size()),
+              ElementsAreArray(kExpected3));
+  EXPECT_FALSE(parsed3->video_header.is_first_packet_in_frame);
+  EXPECT_EQ(parsed3->video_header.codec, kVideoCodecH265);
+}
+
+TEST(VideoRtpDepacketizerH265Test, EmptyPayload) {
+  rtc::CopyOnWriteBuffer empty;
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_FALSE(depacketizer.Parse(empty));
+}
+
+TEST(VideoRtpDepacketizerH265Test, TruncatedFuNalu) {
+  const uint8_t kPayload[] = {0x62};
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)));
+}
+
+TEST(VideoRtpDepacketizerH265Test, TruncatedSingleApNalu) {
+  const uint8_t kPayload[] = {0xe0, 0x02, 0x40};
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)));
+}
+
+TEST(VideoRtpDepacketizerH265Test, ApPacketWithTruncatedNalUnits) {
+  const uint8_t kPayload[] = {0x60, 0x02, 0xED, 0xDF};
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)));
+}
+
+TEST(VideoRtpDepacketizerH265Test, TruncationJustAfterSingleApNalu) {
+  const uint8_t kPayload[] = {0x60, 0x02, 0x40, 0x40};
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)));
+}
+
+TEST(VideoRtpDepacketizerH265Test, ShortSpsPacket) {
+  const uint8_t kPayload[] = {0x40, 0x80, 0x00};
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_TRUE(depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)));
+}
+
+TEST(VideoRtpDepacketizerH265Test, InvalidNaluSizeApNalu) {
+  const uint8_t kPayload[] = {0x60, 0x02,  // F=0, Type=48 (kH265Ap).
+                                           // Length, nal header, payload.
+                              0, 0xff, 0x02, 0x02, 0xFF,  // TrailR
+                              0, 0x05, 0x02, 0x02, 0xFF, 0x00,
+                              0x11};  // TrailR;
+  VideoRtpDepacketizerH265 depacketizer;
+  EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)));
+}
+
+TEST(VideoRtpDepacketizerH265Test, SeiPacket) {
+  const uint8_t kPayload[] = {
+      0x4e, 0x02,             // F=0, Type=39 (kPrefixSei).
+      0x03, 0x03, 0x03, 0x03  // Payload.
+  };
+  VideoRtpDepacketizerH265 depacketizer;
+  auto parsed = depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload));
+  ASSERT_TRUE(parsed);
+}
+
+}  // namespace
+}  // namespace webrtc
diff --git a/test/fuzzers/BUILD.gn b/test/fuzzers/BUILD.gn
index 083c20c..4f6a542 100644
--- a/test/fuzzers/BUILD.gn
+++ b/test/fuzzers/BUILD.gn
@@ -132,6 +132,11 @@
       "../../modules/video_coding/",
     ]
   }
+
+  webrtc_fuzzer_test("h265_depacketizer_fuzzer") {
+    sources = [ "h265_depacketizer_fuzzer.cc" ]
+    deps = [ "../../modules/rtp_rtcp" ]
+  }
 }
 
 webrtc_fuzzer_test("forward_error_correction_fuzzer") {
diff --git a/test/fuzzers/h265_depacketizer_fuzzer.cc b/test/fuzzers/h265_depacketizer_fuzzer.cc
new file mode 100644
index 0000000..00025ef
--- /dev/null
+++ b/test/fuzzers/h265_depacketizer_fuzzer.cc
@@ -0,0 +1,19 @@
+/*
+ *  Copyright (c) 2024 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "modules/rtp_rtcp/source/video_rtp_depacketizer_h265.h"
+
+namespace webrtc {
+void FuzzOneInput(const uint8_t* data, size_t size) {
+  if (size > 200000)
+    return;
+  VideoRtpDepacketizerH265 depacketizer;
+  depacketizer.Parse(rtc::CopyOnWriteBuffer(data, size));
+}
+}  // namespace webrtc
diff --git a/video/rtp_video_stream_receiver2.cc b/video/rtp_video_stream_receiver2.cc
index badb942..7f65653 100644
--- a/video/rtp_video_stream_receiver2.cc
+++ b/video/rtp_video_stream_receiver2.cc
@@ -803,6 +803,7 @@
     if (packet->is_last_packet_in_frame()) {
       auto depacketizer_it = payload_type_map_.find(first_packet->payload_type);
       RTC_CHECK(depacketizer_it != payload_type_map_.end());
+      RTC_CHECK(depacketizer_it->second);
 
       rtc::scoped_refptr<EncodedImageBuffer> bitstream =
           depacketizer_it->second->AssembleFrame(payloads);