diff --git a/modules/rtp_rtcp/BUILD.gn b/modules/rtp_rtcp/BUILD.gn index 0892a32747..766cb56d41 100644 --- a/modules/rtp_rtcp/BUILD.gn +++ b/modules/rtp_rtcp/BUILD.gn @@ -523,6 +523,21 @@ rtc_library("rtp_packetizer_av1_test_helper") { ] } +rtc_library("corruption_detection_extension") { + visibility = [ "*" ] + sources = [ + "source/corruption_detection_extension.cc", + "source/corruption_detection_extension.h", + ] + deps = [ + ":rtp_rtcp_format", + "../../api:array_view", + "../../common_video:common_video", + "//third_party/abseil-cpp/absl/container:inlined_vector", + "//third_party/abseil-cpp/absl/strings:string_view", + ] +} + if (rtc_include_tests) { if (!build_with_chromium) { rtc_executable("test_packet_masks_metrics") { @@ -652,6 +667,7 @@ if (rtc_include_tests) { } deps = [ + ":corruption_detection_extension_unittest", ":fec_test_helper", ":frame_transformer_factory_unittest", ":leb128", @@ -752,4 +768,15 @@ if (rtc_include_tests) { "//third_party/abseil-cpp/absl/memory", ] } + + rtc_library("corruption_detection_extension_unittest") { + testonly = true + sources = [ "source/corruption_detection_extension_unittest.cc" ] + deps = [ + ":corruption_detection_extension", + "../../common_video:common_video", + "../../test:test_support", + "//third_party/abseil-cpp/absl/types:optional", + ] + } } diff --git a/modules/rtp_rtcp/source/corruption_detection_extension.cc b/modules/rtp_rtcp/source/corruption_detection_extension.cc new file mode 100644 index 0000000000..8bfc050b88 --- /dev/null +++ b/modules/rtp_rtcp/source/corruption_detection_extension.cc @@ -0,0 +1,126 @@ +/* + * Copyright 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/corruption_detection_extension.h" + +#include +#include +#include + +#include "absl/container/inlined_vector.h" +#include "api/array_view.h" +#include "common_video/corruption_detection_message.h" + +namespace webrtc { +namespace { + +constexpr size_t kMandatoryPayloadBytes = 1; +constexpr size_t kConfigurationBytes = 3; +constexpr double kMaxValueForStdDev = 40.0; + +} // namespace + +// The message format of the header extension: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |B| seq# index | kernel size | Y err | UV err| sample 0 | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | sample 1 | sample 2 | … up to sample <=12 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// * B (1 bit): If the sequence number should be interpreted as the MSB or LSB +// of the full size 14 bit sequence index described in the next point. +// * seq# index (7 bits): The index into the Halton sequence (used to locate +// where the samples should be drawn from). +// * If B is set: the 7 most significant bits of the true index. The 7 least +// significant bits of the true index shall be interpreted as 0. This is +// because this is the point where we can guarantee that the sender and +// receiver has the same full index). For this reason, B must only be set +// for key frames. +// * If B is not set: The 7 LSB of the true index. The 7 most significant bits +// should be inferred based on the most recent message. +// * kernel size (8 bits): The standard deviation of the gaussian filter used +// to weigh the samples. The value is scaled using a linear map: +// 0 = 0.0 to 255 = 40.0. A kernel size of 0 is interpreted as directly using +// just the sample value at the desired coordinate, without any weighting. +// * Y err (4 bits): The allowed error for the luma channel. +// * UV err (4 bits): The allowed error for the chroma channels. +// * Sample N (8 bits): The N:th filtered sample from the input image. Each +// sample represents a new point in one of the image planes, the plane and +// coordinates being determined by index into the Halton sequence (starting at +// seq# index and is incremented by one for each sample). Each sample has gone +// through a Gaussian filter with the kernel size specified above. The samples +// have been floored to the nearest integer. +// +// A special case is so called "synchronization" messages. These are messages +// that only contains the first byte. They always have B set and are used to +// keep the sender and receiver in sync even if no "full" messages have been +// sent for a while. + +bool CorruptionDetectionExtension::Parse(rtc::ArrayView data, + CorruptionDetectionMessage* message) { + if (message == nullptr) { + return false; + } + if ((data.size() != kMandatoryPayloadBytes && + data.size() <= kConfigurationBytes) || + data.size() > 16) { + return false; + } + message->interpret_sequence_index_as_most_significant_bits_ = data[0] >> 7; + message->sequence_index_ = data[0] & 0b0111'1111; + if (data.size() == kMandatoryPayloadBytes) { + return true; + } + message->std_dev_ = data[1] * kMaxValueForStdDev / 255.0; + uint8_t channel_error_thresholds = data[2]; + message->luma_error_threshold_ = channel_error_thresholds >> 4; + message->chroma_error_threshold_ = channel_error_thresholds & 0xF; + message->sample_values_.assign(data.cbegin() + kConfigurationBytes, + data.cend()); + return true; +} + +bool CorruptionDetectionExtension::Write( + rtc::ArrayView data, + const CorruptionDetectionMessage& message) { + if (data.size() != ValueSize(message)) { + return false; + } + + data[0] = message.sequence_index() & 0b0111'1111; + if (message.interpret_sequence_index_as_most_significant_bits()) { + data[0] |= 0b1000'0000; + } + if (message.sample_values().empty()) { + return true; + } + data[1] = static_cast( + std::round(message.std_dev() / kMaxValueForStdDev * 255.0)); + data[2] = (message.luma_error_threshold() << 4) | + (message.chroma_error_threshold() & 0xF); + rtc::ArrayView sample_values = data.subview(kConfigurationBytes); + for (size_t i = 0; i < message.sample_values().size(); ++i) { + sample_values[i] = std::floor(message.sample_values()[i]); + } + return true; +} + +size_t CorruptionDetectionExtension::ValueSize( + const CorruptionDetectionMessage& message) { + if (message.sample_values_.empty()) { + return kMandatoryPayloadBytes; + } + return kConfigurationBytes + message.sample_values_.size(); +} + +} // namespace webrtc diff --git a/modules/rtp_rtcp/source/corruption_detection_extension.h b/modules/rtp_rtcp/source/corruption_detection_extension.h new file mode 100644 index 0000000000..d77e91898f --- /dev/null +++ b/modules/rtp_rtcp/source/corruption_detection_extension.h @@ -0,0 +1,50 @@ +/* + * Copyright 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_CORRUPTION_DETECTION_EXTENSION_H_ +#define MODULES_RTP_RTCP_SOURCE_CORRUPTION_DETECTION_EXTENSION_H_ + +#include +#include + +#include "absl/strings/string_view.h" +#include "api/array_view.h" +#include "common_video/corruption_detection_message.h" +#include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" + +namespace webrtc { + +// RTP Corruption Detection Header Extension. +// +// The class reads and writes the corruption detection RTP header extension. +// The class implements traits so that the class is compatible with being an +// argument to the templated `RtpPacket::GetExtension` and +// `RtpPacketToSend::SetExtension` methods. +class CorruptionDetectionExtension { + public: + using value_type = CorruptionDetectionMessage; + + // TODO: b/358039777 - Change to our `RTPExtensionType` when available. + static constexpr RTPExtensionType kId = kRtpExtensionNumberOfExtensions; + static constexpr char kCorruptionDetectionUri[] = + "http://www.webrtc.org/experiments/rtp-hdrext/corruption-detection"; + + static constexpr absl::string_view Uri() { return kCorruptionDetectionUri; } + static bool Parse(rtc::ArrayView data, + CorruptionDetectionMessage* message); + static bool Write(rtc::ArrayView data, + const CorruptionDetectionMessage& message); + // Size of the header extension in bytes. + static size_t ValueSize(const CorruptionDetectionMessage& message); +}; + +} // namespace webrtc + +#endif // MODULES_RTP_RTCP_SOURCE_CORRUPTION_DETECTION_EXTENSION_H_ diff --git a/modules/rtp_rtcp/source/corruption_detection_extension_unittest.cc b/modules/rtp_rtcp/source/corruption_detection_extension_unittest.cc new file mode 100644 index 0000000000..be47e843eb --- /dev/null +++ b/modules/rtp_rtcp/source/corruption_detection_extension_unittest.cc @@ -0,0 +1,254 @@ +/* + * Copyright 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/corruption_detection_extension.h" + +#include +#include + +#include "absl/types/optional.h" +#include "common_video/corruption_detection_message.h" +#include "test/gmock.h" +#include "test/gtest.h" + +namespace webrtc { +namespace { + +using ::testing::DoubleEq; +using ::testing::ElementsAre; + +TEST(CorruptionDetectionExtensionTest, ValueSizeIs1UnlessSamplesAreSpecified) { + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSequenceIndex(0b0110'1111) + .WithInterpretSequenceIndexAsMostSignificantBits(true) + .WithStdDev(8.0) + .WithSampleValues({}) + .Build(); + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_EQ(CorruptionDetectionExtension::ValueSize(*kMessage), size_t{1}); +} + +TEST(CorruptionDetectionExtensionTest, + GivenSamplesTheValueSizeIsTheSumOfTheNumberOfSamplesPlus3) { + const double kSampleValues[] = {1.0, 2.0, 3.0, 4.0}; + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSampleValues(kSampleValues) + .Build(); + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_EQ(CorruptionDetectionExtension::ValueSize(*kMessage), size_t{7}); +} + +TEST(CorruptionDetectionExtensionTest, + WritesMandatoryWhenEnoughMemoryIsAllocatedWithoutSamples) { + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSequenceIndex(0b0110'1111) + .WithInterpretSequenceIndexAsMostSignificantBits(true) + .Build(); + uint8_t data[] = {0}; + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_TRUE(CorruptionDetectionExtension::Write(data, *kMessage)); + EXPECT_THAT(data, ElementsAre(0b1110'1111)); +} + +TEST(CorruptionDetectionExtensionTest, + FailsToWriteWhenTooMuchMemoryIsAllocatedWithoutSamples) { + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSequenceIndex(0b0110'1111) + .WithInterpretSequenceIndexAsMostSignificantBits(true) + .Build(); + uint8_t data[] = {0, 0, 0}; + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_FALSE(CorruptionDetectionExtension::Write(data, *kMessage)); +} + +TEST(CorruptionDetectionExtensionTest, + FailsToWriteWhenTooMuchMemoryIsAllocatedWithSamples) { + const double kSampleValues[] = {1.0}; + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSequenceIndex(0b0110'1111) + .WithInterpretSequenceIndexAsMostSignificantBits(true) + .WithStdDev(8.0) + .WithSampleValues(kSampleValues) + .Build(); + uint8_t data[] = {0, 0, 0, 0, 0}; + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_FALSE(CorruptionDetectionExtension::Write(data, *kMessage)); +} + +TEST(CorruptionDetectionExtensionTest, + WritesEverythingWhenEnoughMemoryIsAllocatedWithSamples) { + const double kSampleValues[] = {1.0}; + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSequenceIndex(0b0110'1111) + .WithInterpretSequenceIndexAsMostSignificantBits(true) + .WithStdDev(8.0) + .WithSampleValues(kSampleValues) + .Build(); + uint8_t data[] = {0, 0, 0, 0}; + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_TRUE(CorruptionDetectionExtension::Write(data, *kMessage)); + EXPECT_THAT(data, ElementsAre(0b1110'1111, 51, 0, 1)); +} + +TEST(CorruptionDetectionExtensionTest, + WritesEverythingToExtensionWhenUpperBitsAreUsedForSequenceIndex) { + const double kSampleValues[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, + 8.0, 9.0, 10.0, 11.0, 12.0, 13.0}; + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSequenceIndex(0b0110'1111) + .WithInterpretSequenceIndexAsMostSignificantBits(true) + .WithStdDev(34.5098) // 220 / (255.0 * 40.0) + .WithLumaErrorThreshold(0b1110) + .WithChromaErrorThreshold(0b1111) + .WithSampleValues(kSampleValues) + .Build(); + uint8_t data[16]; + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_TRUE(CorruptionDetectionExtension::Write(data, *kMessage)); + EXPECT_THAT(data, ElementsAre(0b1110'1111, 220, 0b1110'1111, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13)); +} + +TEST(CorruptionDetectionExtensionTest, + WritesEverythingToExtensionWhenLowerBitsAreUsedForSequenceIndex) { + const double kSampleValues[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, + 8.0, 9.0, 10.0, 11.0, 12.0, 13.0}; + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSequenceIndex(0b0110'1111) + .WithInterpretSequenceIndexAsMostSignificantBits(false) + .WithStdDev(34.5098) // 220 / (255.0 * 40.0) + .WithLumaErrorThreshold(0b1110) + .WithChromaErrorThreshold(0b1111) + .WithSampleValues(kSampleValues) + .Build(); + uint8_t data[16]; + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_TRUE(CorruptionDetectionExtension::Write(data, *kMessage)); + EXPECT_THAT(data, ElementsAre(0b0110'1111, 220, 0b1110'1111, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13)); +} + +TEST(CorruptionDetectionExtensionTest, TruncatesSampleValuesWhenWriting) { + const double kSampleValues[] = {1.4, 2.5, 3.6}; + const absl::optional kMessage = + CorruptionDetectionMessage::Builder() + .WithSampleValues(kSampleValues) + .Build(); + uint8_t data[6]; + + ASSERT_NE(kMessage, absl::nullopt); + EXPECT_TRUE(CorruptionDetectionExtension::Write(data, *kMessage)); + EXPECT_THAT(data, ElementsAre(0, 0, 0, 1, 2, 3)); +} + +TEST(CorruptionDetectionExtensionTest, ParsesMandatoryFieldsFromExtension) { + CorruptionDetectionMessage message; + const uint8_t kData[] = {0b1110'1111}; + + EXPECT_TRUE(CorruptionDetectionExtension::Parse(kData, &message)); + EXPECT_EQ(message.sequence_index(), 0b0110'1111); + EXPECT_TRUE(message.interpret_sequence_index_as_most_significant_bits()); + EXPECT_THAT(message.std_dev(), DoubleEq(0.0)); + EXPECT_EQ(message.luma_error_threshold(), 0); + EXPECT_EQ(message.chroma_error_threshold(), 0); + EXPECT_THAT(message.sample_values(), ElementsAre()); +} + +TEST(CorruptionDetectionExtensionTest, FailsToParseWhenGivenTooFewFields) { + CorruptionDetectionMessage message; + const uint8_t kData[] = {0b1110'1111, 8, 0}; + + EXPECT_FALSE(CorruptionDetectionExtension::Parse(kData, &message)); +} + +TEST(CorruptionDetectionExtensionTest, + ParsesEverythingFromExtensionWhenUpperBitsAreUsedForSequenceIndex) { + CorruptionDetectionMessage message; + const uint8_t kSampleValues[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + const uint8_t kData[] = {0b1100'0100, 220, + 0b1110'1111, kSampleValues[0], + kSampleValues[1], kSampleValues[2], + kSampleValues[3], kSampleValues[4], + kSampleValues[5], kSampleValues[6], + kSampleValues[7], kSampleValues[8], + kSampleValues[9], kSampleValues[10], + kSampleValues[11], kSampleValues[12]}; + + EXPECT_TRUE(CorruptionDetectionExtension::Parse(kData, &message)); + EXPECT_EQ(message.sequence_index(), 0b0100'0100); + EXPECT_TRUE(message.interpret_sequence_index_as_most_significant_bits()); + EXPECT_THAT(message.std_dev(), + DoubleEq(34.509803921568626)); // 220 / (255.0 * 40.0) + EXPECT_EQ(message.luma_error_threshold(), 0b1110); + EXPECT_EQ(message.chroma_error_threshold(), 0b1111); + EXPECT_EQ(message.sample_values().size(), sizeof(kSampleValues)); + for (size_t i = 0; i < sizeof(kSampleValues); ++i) { + EXPECT_EQ(message.sample_values()[i], kSampleValues[i]); + } +} + +TEST(CorruptionDetectionExtensionTest, + ParsesEverythingFromExtensionWhenLowerBitsAreUsedForSequenceIndex) { + CorruptionDetectionMessage message; + const uint8_t kSampleValues[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + const uint8_t kData[] = {0b0100'0100, 220, + 0b1110'1111, kSampleValues[0], + kSampleValues[1], kSampleValues[2], + kSampleValues[3], kSampleValues[4], + kSampleValues[5], kSampleValues[6], + kSampleValues[7], kSampleValues[8], + kSampleValues[9], kSampleValues[10], + kSampleValues[11], kSampleValues[12]}; + + EXPECT_TRUE(CorruptionDetectionExtension::Parse(kData, &message)); + EXPECT_EQ(message.sequence_index(), 0b0100'0100); + EXPECT_FALSE(message.interpret_sequence_index_as_most_significant_bits()); + EXPECT_THAT(message.std_dev(), + DoubleEq(34.509803921568626)); // 220 / (255.0 * 40.0) + EXPECT_EQ(message.luma_error_threshold(), 0b1110); + EXPECT_EQ(message.chroma_error_threshold(), 0b1111); + EXPECT_EQ(message.sample_values().size(), sizeof(kSampleValues)); + for (size_t i = 0; i < sizeof(kSampleValues); ++i) { + EXPECT_EQ(message.sample_values()[i], kSampleValues[i]); + } +} + +TEST(CorruptionDetectionExtensionTest, FailsToParseWhenGivenNullptrAsOutput) { + const uint8_t kData[] = {0, 0, 0}; + + EXPECT_FALSE(CorruptionDetectionExtension::Parse(kData, nullptr)); +} + +TEST(CorruptionDetectionExtensionTest, + FailsToParseWhenTooManySamplesAreSpecified) { + CorruptionDetectionMessage message; + uint8_t data[17]; + + EXPECT_FALSE(CorruptionDetectionExtension::Parse(data, &message)); +} + +} // namespace +} // namespace webrtc