From abadbce1fa8156cd6b742ed337d40aee7fdcec8a Mon Sep 17 00:00:00 2001 From: Qiu Jianlin Date: Thu, 2 Jan 2025 21:30:37 +0800 Subject: [PATCH] Update is_first_packet_in_frame flag in H.265 depacketizer Follow H.265 spec section 7.4.2.4.4 to set is_first_packet_in_frame flag in rtp header, to make sure we only set it to true when corresponding NALU can be the first NALU in a frame. Bug: chromium:384391181 Change-Id: I082c38513d9d213f8d354633539028b57777368f Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/372742 Reviewed-by: Danil Chapovalov Commit-Queue: Sergey Silkin Reviewed-by: Sergey Silkin Cr-Commit-Position: refs/heads/main@{#43651} --- common_video/h265/h265_bitstream_parser.cc | 14 + common_video/h265/h265_bitstream_parser.h | 5 + .../h265/h265_bitstream_parser_unittest.cc | 28 ++ common_video/h265/h265_common.h | 1 + .../source/video_rtp_depacketizer_h265.cc | 51 ++-- .../video_rtp_depacketizer_h265_unittest.cc | 247 +++++++++++++++++- 6 files changed, 327 insertions(+), 19 deletions(-) diff --git a/common_video/h265/h265_bitstream_parser.cc b/common_video/h265/h265_bitstream_parser.cc index 9ee6ff0bfb..f39cb51c87 100644 --- a/common_video/h265/h265_bitstream_parser.cc +++ b/common_video/h265/h265_bitstream_parser.cc @@ -519,6 +519,20 @@ H265BitstreamParser::ParsePpsIdFromSliceSegmentLayerRbsp( return slice_pic_parameter_set_id; } +std::optional H265BitstreamParser::IsFirstSliceSegmentInPic( + rtc::ArrayView data) { + std::vector unpacked_buffer = H265::ParseRbsp(data); + BitstreamReader slice_reader(unpacked_buffer); + + // first_slice_segment_in_pic_flag: u(1) + bool first_slice_segment_in_pic_flag = slice_reader.Read(); + if (!slice_reader.Ok()) { + return std::nullopt; + } + + return first_slice_segment_in_pic_flag; +} + void H265BitstreamParser::ParseBitstream( rtc::ArrayView bitstream) { std::vector nalu_indices = H265::FindNaluIndices(bitstream); diff --git a/common_video/h265/h265_bitstream_parser.h b/common_video/h265/h265_bitstream_parser.h index 3ac8a5a689..fbcf2c2a89 100644 --- a/common_video/h265/h265_bitstream_parser.h +++ b/common_video/h265/h265_bitstream_parser.h @@ -43,6 +43,11 @@ class RTC_EXPORT H265BitstreamParser : public BitstreamParser { rtc::ArrayView data, uint8_t nalu_type); + // Returns true if the slice segment is the first in the picture; otherwise + // return false. If parse failed, return nullopt. + static std::optional IsFirstSliceSegmentInPic( + rtc::ArrayView data); + protected: enum Result { kOk, diff --git a/common_video/h265/h265_bitstream_parser_unittest.cc b/common_video/h265/h265_bitstream_parser_unittest.cc index aed779c306..087d0e0916 100644 --- a/common_video/h265/h265_bitstream_parser_unittest.cc +++ b/common_video/h265/h265_bitstream_parser_unittest.cc @@ -11,8 +11,12 @@ #include "common_video/h265/h265_bitstream_parser.h" #include "common_video/h265/h265_common.h" +#include "test/gmock.h" #include "test/gtest.h" +using ::testing::Eq; +using ::testing::Optional; + namespace webrtc { // VPS/SPS/PPS part of below chunk. @@ -53,6 +57,12 @@ const uint8_t kH265SliceChunk[] = { 0x26, 0x0f, 0x7b, 0x30, 0x1c, 0xd7, 0xd4, 0x3a, 0xec, 0xad, 0xef, 0x73, }; +// Contains enough of data for the second slice of a frame. +const uint8_t kH265SecondSliceChunkInAFrame[] = { + 0x02, 0x01, 0x23, 0xfc, 0x20, 0x22, 0xad, 0x13, 0x68, 0xce, 0xc3, 0x5a, + 0x00, 0xdc, 0xeb, 0x86, 0x4b, 0x0b, 0xa7, 0x6a, 0xe1, 0x9c, 0x5c, 0xea, +}; + // Contains short term ref pic set slice to verify Log2Ceiling path. const uint8_t kH265SliceStrChunk[] = { 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x00, @@ -144,4 +154,22 @@ TEST(H265BitstreamParserTest, ReportsLastSliceQpInvalidQPSlices) { ASSERT_FALSE(qp.has_value()); } +TEST(H265BitstreamParserTest, ReportsFirstSliceSegmentInPic) { + EXPECT_THAT(H265BitstreamParser::IsFirstSliceSegmentInPic(kH265SliceChunk), + Optional(Eq(true))); +} + +TEST(H265BitstreamParserTest, ReportsFirstSliceSegmentInPicFalse) { + EXPECT_THAT(H265BitstreamParser::IsFirstSliceSegmentInPic( + kH265SecondSliceChunkInAFrame), + Optional(Eq(false))); +} + +TEST(H265BitstreamParserTest, ReportsFirstSliceSegmentInPicParseInvalidSlice) { + rtc::ArrayView slice_data(kH265SliceChunk); + EXPECT_THAT( + H265BitstreamParser::IsFirstSliceSegmentInPic(slice_data.subview(50)), + Eq(std::nullopt)); +} + } // namespace webrtc diff --git a/common_video/h265/h265_common.h b/common_video/h265/h265_common.h index 50ca1b3073..373e53b1dc 100644 --- a/common_video/h265/h265_common.h +++ b/common_video/h265/h265_common.h @@ -50,6 +50,7 @@ enum NaluType : uint8_t { kIdrNLp = 20, kCra = 21, kRsvIrapVcl23 = 23, + kRsvVcl31 = 31, kVps = 32, kSps = 33, kPps = 34, diff --git a/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc index e0ed2215af..a100f478d1 100644 --- a/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc +++ b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265.cc @@ -63,10 +63,8 @@ bool ParseApStartOffsets(const uint8_t* nalu_ptr, // https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2 std::optional 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."; + if (rtp_payload.size() < kH265PayloadHeaderSizeBytes) { + RTC_LOG(LS_ERROR) << "RTP payload truncated."; return std::nullopt; } const uint8_t* const payload_data = rtp_payload.cdata(); @@ -75,7 +73,7 @@ std::optional ProcessApOrSingleNalu( 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; + parsed_payload->video_header.is_first_packet_in_frame = false; const uint8_t* nalu_start = payload_data + kH265PayloadHeaderSizeBytes; const size_t nalu_length = rtp_payload.size() - kH265PayloadHeaderSizeBytes; @@ -129,20 +127,13 @@ std::optional ProcessApOrSingleNalu( case H265::NaluType::kIdrWRadl: case H265::NaluType::kIdrNLp: case H265::NaluType::kCra: - case H265::NaluType::kRsvIrapVcl23: // Mark IRAP(Intra Random Access Point) frames as key frames. Their NALU - // types are in the range of BLA_W_LP (16) to RSV_IRAP_VCL23 (23), - // inclusive. + // types are in the range of BLA_W_LP (16) to CRA (21), inclusive. // https://datatracker.ietf.org/doc/html/rfc7798#section-3.1.1 parsed_payload->video_header.frame_type = VideoFrameType::kVideoFrameKey; break; case H265::NaluType::kSps: { - // Copy any previous data first (likely just the first header). - std::unique_ptr output_buffer(new rtc::Buffer()); - if (start_offset) - output_buffer->AppendData(payload_data, start_offset); - std::optional sps = H265SpsParser::ParseSps(nalu_data); @@ -160,14 +151,14 @@ std::optional ProcessApOrSingleNalu( 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: + // Slices below don't contain SPS or PPS ids. + case H265::NaluType::kAud: case H265::NaluType::kPrefixSei: case H265::NaluType::kSuffixSei: break; @@ -177,6 +168,20 @@ std::optional ProcessApOrSingleNalu( RTC_LOG(LS_WARNING) << "Unexpected AP, FU or PACI received."; return std::nullopt; } + + // Spec 7.4.2.4.4: Order of NAL units and codec pictures. + if ((nalu_type >= H265::NaluType::kVps && + nalu_type <= H265::NaluType::kAud) || + nalu_type == H265::NaluType::kPrefixSei) { + parsed_payload->video_header.is_first_packet_in_frame = true; + } else if (nalu_type >= H265::NaluType::kTrailN && + nalu_type <= H265::NaluType::kRsvVcl31) { + std::optional first_slice_segment_in_pic_flag = + H265BitstreamParser::IsFirstSliceSegmentInPic(nalu_data); + if (first_slice_segment_in_pic_flag.value_or(false)) { + parsed_payload->video_header.is_first_packet_in_frame = true; + } + } } parsed_payload->video_payload = video_payload; return parsed_payload; @@ -200,7 +205,20 @@ std::optional ParseFuNalu( uint8_t original_nal_type = rtp_payload.cdata()[2] & kH265TypeMaskInFuHeader; bool first_fragment = rtp_payload.cdata()[2] & kH265SBitMask; + bool is_first_packet_in_frame = false; if (first_fragment) { + if (original_nal_type >= H265::NaluType::kTrailN && + original_nal_type <= H265::NaluType::kRsvVcl31) { + size_t slice_offset = + kH265FuHeaderSizeBytes + kH265PayloadHeaderSizeBytes; + std::optional first_slice_segment_in_pic_flag = + H265BitstreamParser::IsFirstSliceSegmentInPic( + rtc::ArrayView(rtp_payload.cdata() + slice_offset, + rtp_payload.size() - slice_offset)); + if (first_slice_segment_in_pic_flag.value_or(false)) { + is_first_packet_in_frame = true; + } + } rtp_payload = rtp_payload.Slice( kH265FuHeaderSizeBytes, rtp_payload.size() - kH265FuHeaderSizeBytes); rtp_payload.MutableData()[0] = f | original_nal_type << 1 | layer_id_h; @@ -227,7 +245,8 @@ std::optional ParseFuNalu( 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; + parsed_payload->video_header.is_first_packet_in_frame = + is_first_packet_in_frame; return parsed_payload; } diff --git a/modules/rtp_rtcp/source/video_rtp_depacketizer_h265_unittest.cc b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265_unittest.cc index 8b7d080fd0..a4d713e88d 100644 --- a/modules/rtp_rtcp/source/video_rtp_depacketizer_h265_unittest.cc +++ b/modules/rtp_rtcp/source/video_rtp_depacketizer_h265_unittest.cc @@ -388,14 +388,255 @@ TEST(VideoRtpDepacketizerH265Test, InvalidNaluSizeApNalu) { EXPECT_FALSE(depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload))); } -TEST(VideoRtpDepacketizerH265Test, SeiPacket) { +TEST(VideoRtpDepacketizerH265Test, PrefixSeiSetsFirstPacketInFrame) { const uint8_t kPayload[] = { - 0x4e, 0x02, // F=0, Type=39 (kPrefixSei). + 0x4e, 0x02, // F=0, Type=39 (H265::kPrefixSei). 0x03, 0x03, 0x03, 0x03 // Payload. }; VideoRtpDepacketizerH265 depacketizer; auto parsed = depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); - ASSERT_TRUE(parsed); + ASSERT_TRUE(parsed.has_value()); + EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, ApVpsSpsPpsMultiIdrSlices) { + 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. + // Slices are truncated to contain enough data for test. + 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_slice1[] = {0x28, 0x01, 0xac, 0x6d, 0xa0, + 0x7b, 0x4c, 0xe2, 0x09, 0xef}; + uint8_t idr_slice2[] = {0x28, 0x01, 0x27, 0xf8, 0x63, + 0x6d, 0x7b, 0x6f, 0xcf, 0xff}; + + rtc::CopyOnWriteBuffer rtp_payload; + rtp_payload.AppendData(payload_header); + rtp_payload.AppendData(vps_nalu_size); + rtp_payload.AppendData(vps); + rtp_payload.AppendData(sps_nalu_size); + rtp_payload.AppendData(sps); + rtp_payload.AppendData(pps_nalu_size); + rtp_payload.AppendData(pps); + rtp_payload.AppendData(slice_nalu_size); + rtp_payload.AppendData(idr_slice1); + rtp_payload.AppendData(slice_nalu_size); + rtp_payload.AppendData(idr_slice2); + + 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_slice1); + expected_packet.AppendData(start_code); + expected_packet.AppendData(idr_slice2); + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtp_payload); + ASSERT_TRUE(parsed.has_value()); + + 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_TRUE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, ApMultiNonFirstSlicesFromSingleNonIdrFrame) { + uint8_t payload_header[] = {0x60, 0x02}; + uint8_t slice_nalu_size[] = {0, 0xa}; + uint8_t start_code[] = {0x00, 0x00, 0x00, 0x01}; + // First few bytes of two non-IDR slices from the same frame, both with the + // first_slice_segment_in_pic_flag set to 0. + uint8_t non_idr_slice1[] = {0x02, 0x01, 0x23, 0xfc, 0x20, + 0x42, 0xad, 0x1b, 0x68, 0xdf}; + uint8_t non_idr_slice2[] = {0x02, 0x01, 0x27, 0xf8, 0x20, + 0x42, 0xad, 0x1b, 0x68, 0xe0}; + + rtc::CopyOnWriteBuffer rtp_payload; + rtp_payload.AppendData(payload_header); + rtp_payload.AppendData(slice_nalu_size); + rtp_payload.AppendData(non_idr_slice1); + rtp_payload.AppendData(slice_nalu_size); + rtp_payload.AppendData(non_idr_slice2); + + rtc::Buffer expected_packet; + expected_packet.AppendData(start_code); + expected_packet.AppendData(non_idr_slice1); + expected_packet.AppendData(start_code); + expected_packet.AppendData(non_idr_slice2); + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtp_payload); + ASSERT_TRUE(parsed.has_value()); + + 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_FALSE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, ApFirstTwoSlicesFromSingleNonIdrFrame) { + uint8_t payload_header[] = {0x60, 0x02}; + uint8_t slice_nalu_size[] = {0, 0xa}; + uint8_t start_code[] = {0x00, 0x00, 0x00, 0x01}; + // First few bytes of two non-IDR slices from the same frame, with the first + // slice's first_slice_segment_in_pic_flag set to 1, and second set to 0. + uint8_t non_idr_slice1[] = {0x02, 0x01, 0xa4, 0x08, 0x55, + 0xa3, 0x6d, 0xcc, 0xcf, 0x26}; + uint8_t non_idr_slice2[] = {0x02, 0x01, 0x23, 0xfc, 0x20, + 0x42, 0xad, 0x1b, 0x68, 0xdf}; + + rtc::CopyOnWriteBuffer rtp_payload; + rtp_payload.AppendData(payload_header); + rtp_payload.AppendData(slice_nalu_size); + rtp_payload.AppendData(non_idr_slice1); + rtp_payload.AppendData(slice_nalu_size); + rtp_payload.AppendData(non_idr_slice2); + + rtc::Buffer expected_packet; + expected_packet.AppendData(start_code); + expected_packet.AppendData(non_idr_slice1); + expected_packet.AppendData(start_code); + expected_packet.AppendData(non_idr_slice2); + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtp_payload); + ASSERT_TRUE(parsed.has_value()); + + 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_TRUE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, SingleNaluFromIdrSecondSlice) { + // First few bytes of the second slice of an IDR_N_LP nalu with + // first_slice_segment_in_pic_flag set to 0. + const uint8_t kPayload[] = {0x28, 0x01, 0x27, 0xf8, 0x63, 0x6d, 0x7b, 0x6f, + 0xcf, 0xff, 0x0d, 0xf5, 0xc7, 0xfe, 0x57, 0x77, + 0xdc, 0x29, 0x24, 0x89, 0x89, 0xea, 0xd1, 0x88}; + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameKey); + EXPECT_FALSE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, SingleNaluFromNonIdrSecondSlice) { + // First few bytes of the second slice of an TRAIL_R nalu with + // first_slice_segment_in_pic_flag set to 0. + const uint8_t kPayload[] = {0x02, 0x01, 0x23, 0xfc, 0x20, 0x22, 0xad, 0x13, + 0x68, 0xce, 0xc3, 0x5a, 0x00, 0xdc, 0xeb, 0x86, + 0x4b, 0x0b, 0xa7, 0x6a, 0xe1, 0x9c, 0x5c, 0xea}; + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameDelta); + EXPECT_FALSE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, FuFromIdrFrameSecondSlice) { + // First few bytes of the second slice of an IDR_N_LP nalu with + // first_slice_segment_in_pic_flag set to 0. + const uint8_t kPayload[] = { + 0x62, 0x02, // F=0, Type=49 (H265::kFu). + 0x93, // FU header kH265SBitMask | H265::kIdrWRadl. + 0x23, 0xfc, 0x20, 0x22, 0xad, 0x13, 0x68, 0xce, 0xc3, 0x5a, 0x00, 0xdc}; + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameKey); + EXPECT_FALSE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, FuFromNonIdrFrameSecondSlice) { + // First few bytes of the second slice of an TRAIL_R nalu with + // first_slice_segment_in_pic_flag set to 0. + const uint8_t kPayload[] = {0x62, 0x02, // F=0, Type=49 (H265::kFu). + 0x80, // FU header kH265SBitMask | H265::kTrailR. + 0x23, 0xfc, 0x20, 0x22, 0xad, 0x13, + 0x68, 0xce, 0xc3, 0x5a, 0x00, 0xdc}; + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(parsed->video_header.frame_type, VideoFrameType::kVideoFrameDelta); + EXPECT_FALSE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, AudSetsFirstPacketInFrame) { + const uint8_t kPayload[] = {0x46, 0x01, 0x10}; + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, PpsSetsFirstPacketInFrame) { + const uint8_t kPayload[] = { + 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}; + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_TRUE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, SuffixSeiNotSetFirstPacketInFrame) { + const uint8_t kPayload[] = {0x50, 0x01, 0x81, 0x01, 0x03, 0x80}; + + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); + EXPECT_FALSE(parsed->video_header.is_first_packet_in_frame); +} + +TEST(VideoRtpDepacketizerH265Test, EmptyNaluPayload) { + const uint8_t kPayload[] = {0x48, 0x00}; // F=0, Type=36 (H265::kEos). + VideoRtpDepacketizerH265 depacketizer; + std::optional parsed = + depacketizer.Parse(rtc::CopyOnWriteBuffer(kPayload)); + ASSERT_TRUE(parsed.has_value()); } } // namespace