From 99a18d0c4c18f382509ecd06ac725ecb071b4bce Mon Sep 17 00:00:00 2001 From: jwcullen Date: Thu, 15 Aug 2024 19:31:27 -0400 Subject: [PATCH] Add low-level support for writing and validating `MixPresentationTags`. - The IAMF spec has an asymmetric requirement where there SHALL be at most one `content_language`, but if they are present the remaining ones can be ignored. - Like other asymmetric requirements the encoder typically refuses to produce data which seemingly violates the requirement. - On read we will process and store duplicate `content_language` tags. - This means we will store them, even if they are not "really" valid. - Add a `CreateFromBuffer/SucceedsWithDuplicateContentLanguageTags` test to guard this behavior. - Although for this CL, the read falls into the `ObuBase::footer_`. - On the write side we will typically refuse to write data which seemingly violates the requirement. - Following the existing style - this gets enforced in `ValidateAndWrite`. - Add tests to guard this behavior, and a parallel test that *other* tags MAY be duplicated. PiperOrigin-RevId: 663496061 --- iamf/obu/BUILD | 1 - iamf/obu/mix_presentation.cc | 24 ++- iamf/obu/mix_presentation.h | 28 ++++ iamf/obu/tests/mix_presentation_test.cc | 187 +++++++++++++++++++++++- 4 files changed, 235 insertions(+), 5 deletions(-) diff --git a/iamf/obu/BUILD b/iamf/obu/BUILD index 67ba5066..55521e11 100644 --- a/iamf/obu/BUILD +++ b/iamf/obu/BUILD @@ -134,7 +134,6 @@ cc_library( "//iamf/common:write_bit_buffer", "@com_google_absl//absl/base:no_destructor", "@com_google_absl//absl/container:flat_hash_map", - "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/log", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", diff --git a/iamf/obu/mix_presentation.cc b/iamf/obu/mix_presentation.cc index 5725672c..a8b2fc2d 100644 --- a/iamf/obu/mix_presentation.cc +++ b/iamf/obu/mix_presentation.cc @@ -17,7 +17,6 @@ #include "absl/base/no_destructor.h" #include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" #include "absl/log/log.h" #include "absl/status/status.h" #include "absl/strings/str_cat.h" @@ -328,6 +327,29 @@ absl::Status MixPresentationSubMix::ReadAndValidate(const int32_t& count_label, return absl::OkStatus(); } +absl::Status MixPresentationTags::ValidateAndWrite(WriteBitBuffer& wb) const { + RETURN_IF_NOT_OK(wb.WriteUnsignedLiteral(num_tags, 8)); + RETURN_IF_NOT_OK(ValidateVectorSizeEqual("tags", tags.size(), num_tags)); + + int count_content_language_tag = 0; + + for (const auto& tag : tags) { + if (tag.tag_name == "content_language") { + count_content_language_tag++; + } + RETURN_IF_NOT_OK(wb.WriteString(tag.tag_name)); + RETURN_IF_NOT_OK(wb.WriteString(tag.tag_value)); + } + // Tags are freeform and may be duplicated. Except for the "content_language" + // tag which SHALL appear at most once. + if (count_content_language_tag > 1) { + return absl::InvalidArgumentError( + "Expected zero or one content_language tag."); + } + + return absl::OkStatus(); +} + // Validates and writes a `LoudspeakersSsConventionLayout` and sets // `found_stereo_layout` to true if it is a stereo layout. absl::Status LoudspeakersSsConventionLayout::Write(bool& found_stereo_layout, diff --git a/iamf/obu/mix_presentation.h b/iamf/obu/mix_presentation.h index 6288082f..7934364a 100644 --- a/iamf/obu/mix_presentation.h +++ b/iamf/obu/mix_presentation.h @@ -13,6 +13,7 @@ #define OBU_MIX_PRESENTATION_H_ #include +#include #include #include #include @@ -305,6 +306,29 @@ struct MixPresentationSubMix { std::vector layouts; }; +struct MixPresentationTags { + struct Tag { + friend bool operator==(const Tag& lhs, const Tag& rhs) = default; + + std::string tag_name; + std::string tag_value; + }; + + friend bool operator==(const MixPresentationTags& lhs, + const MixPresentationTags& rhs) = default; + + /*!\brief Writes the MixPresentationTags to the buffer. + * + * \param wb Buffer to write to. + * \return `absl::OkStatus()` if the MixPresentationTags is valid. A specific + * status if the write fails. + */ + absl::Status ValidateAndWrite(WriteBitBuffer& wb) const; + + uint8_t num_tags; + std::vector tags; +}; + /*!\brief Metadata required for post-processing the mixed audio signal. * * The metadata specifies how to render, process and mix one or more audio @@ -412,6 +436,10 @@ class MixPresentationObu : public ObuBase { std::vector sub_mixes_; + // Implicitly included based on `obu_size` after writing the v1.0.0-errata + // payload. + std::optional mix_presentation_tags_; + private: DecodedUleb128 mix_presentation_id_; DecodedUleb128 count_label_; diff --git a/iamf/obu/tests/mix_presentation_test.cc b/iamf/obu/tests/mix_presentation_test.cc index 40eddf26..1137fe04 100644 --- a/iamf/obu/tests/mix_presentation_test.cc +++ b/iamf/obu/tests/mix_presentation_test.cc @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -933,7 +934,7 @@ TEST(CreateFromBufferTest, RejectEmptyBitstream) { MixPresentationObu::CreateFromBuffer(header, source.size(), buffer).ok()); } -TEST(CreateFromBufferTest, RejectNoSubMix) { +TEST(CreateFromBuffer, InvalidWithNoSubMixes) { std::vector source = { // Start Mix OBU. // mix_presentation_id @@ -955,7 +956,7 @@ TEST(CreateFromBufferTest, RejectNoSubMix) { MixPresentationObu::CreateFromBuffer(header, source.size(), buffer).ok()); } -TEST(CreateFromBufferTest, OneSubMix) { +TEST(CreateFromBuffer, ReadsOneSubMix) { const std::vector kAnnotationsLanguage = {"en-us"}; const std::vector kLocalizedPresentationAnnotations = {"Mix 1"}; const std::vector kAudioElementLocalizedElementAnnotations = { @@ -996,7 +997,7 @@ TEST(CreateFromBufferTest, OneSubMix) { ObuHeader header; auto obu = MixPresentationObu::CreateFromBuffer(header, source.size(), buffer); - EXPECT_THAT(obu, IsOk()); + ASSERT_THAT(obu, IsOk()); EXPECT_EQ(obu->header_.obu_type, kObuIaMixPresentation); EXPECT_EQ(obu->GetMixPresentationId(), 10); EXPECT_EQ(obu->GetAnnotationsLanguage(), kAnnotationsLanguage); @@ -1008,6 +1009,96 @@ TEST(CreateFromBufferTest, OneSubMix) { kAudioElementLocalizedElementAnnotations); } +TEST(CreateFromBufferTest, ReadsMixPresentationTagsIntoFooter) { + const std::vector kMixPresentationTags = { + // Start MixPresentationTags. + 1, + // Start Tag1. + 'A', 'B', 'C', '\0', '1', '2', '3', '\0', + // End Tag1. + }; + std::vector source = { + // Start Mix OBU. + // mix_presentation_id + 10, + // count_label + 0, + // num_submixes + 1, + // Start Submix. + 1, 21, + // Start RenderingConfig. + RenderingConfig::kHeadphonesRenderingModeStereo << 6, 0, + // End RenderingConfig. + 22, 23, 0x80, 0, 24, 25, 26, 0x80, 0, 27, + // num_layouts + 1, + // Start Layout0. + (Layout::kLayoutTypeLoudspeakersSsConvention << 6) | + (LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0 << 2), + 0, 0, 31, 0, 32, + // End SubMix. + }; + source.insert(source.end(), kMixPresentationTags.begin(), + kMixPresentationTags.end()); + ReadBitBuffer buffer(1024, &source); + ObuHeader header; + auto obu = + MixPresentationObu::CreateFromBuffer(header, source.size(), buffer); + ASSERT_THAT(obu, IsOk()); + + EXPECT_FALSE(obu->mix_presentation_tags_.has_value()); + EXPECT_EQ(obu->footer_, kMixPresentationTags); +} + +TEST(CreateFromBufferTest, SucceedsWithDuplicateContentLanguageTags) { + const std::vector kDuplicateContentLanguageTags = { + // Start MixPresentationTags. + 2, + // `tag_name[0]`. + 'c', 'o', 'n', 't', 'e', 'n', 't', '_', 'l', 'a', 'n', 'g', 'u', 'a', 'g', + 'e', '\0', + // `tag_value[0]`. + 'e', 'n', '-', 'u', 's', '\0', + // `tag_name[1]`. + 'c', 'o', 'n', 't', 'e', 'n', 't', '_', 'l', 'a', 'n', 'g', 'u', 'a', 'g', + 'e', '\0', + // `tag_value[1]`. + 'e', 'n', '-', 'g', 'b', '\0'}; + std::vector source = { + // Start Mix OBU. + // mix_presentation_id + 10, + // count_label + 0, + // num_submixes + 1, + // Start Submix. + 1, 21, + // Start RenderingConfig. + RenderingConfig::kHeadphonesRenderingModeStereo << 6, 0, + // End RenderingConfig. + 22, 23, 0x80, 0, 24, 25, 26, 0x80, 0, 27, + // num_layouts + 1, + // Start Layout0. + (Layout::kLayoutTypeLoudspeakersSsConvention << 6) | + (LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0 << 2), + 0, 0, 31, 0, 32, + // End SubMix. + }; + source.insert(source.end(), kDuplicateContentLanguageTags.begin(), + kDuplicateContentLanguageTags.end()); + ReadBitBuffer buffer(1024, &source); + ObuHeader header; + auto obu = + MixPresentationObu::CreateFromBuffer(header, source.size(), buffer); + ASSERT_THAT(obu, IsOk()); + + EXPECT_FALSE(obu->mix_presentation_tags_.has_value()); + EXPECT_EQ(obu->footer_, kDuplicateContentLanguageTags); +} + TEST(ReadSubMixAudioElementTest, AllFieldsPresent) { std::vector source = { // Start SubMixAudioElement. @@ -1220,5 +1311,95 @@ TEST(ReadMixPresentationSubMixTest, AudioElementAndMultipleLayouts) { LoudspeakersSsConventionLayout::kSoundSystemA_0_2_0})); } +TEST(MixPresentationTagsWriteAndValidate, WritesWithZeroTags) { + constexpr uint8_t kZeroNumTags = 0; + const MixPresentationTags kMixPresentationTagsWithZeroTags = { + .num_tags = kZeroNumTags}; + const std::vector kExpectedBuffer = { + // `num_tags`. + kZeroNumTags, + }; + WriteBitBuffer wb(1024); + + EXPECT_THAT(kMixPresentationTagsWithZeroTags.ValidateAndWrite(wb), IsOk()); + + EXPECT_EQ(wb.bit_buffer(), kExpectedBuffer); +} + +TEST(MixPresentationTagsWriteAndValidate, WritesContentLanguageTag) { + constexpr uint8_t kOneTag = 1; + const MixPresentationTags kMixPresentationTagsWithContentLanguageTag = { + .num_tags = kOneTag, .tags = {{"content_language", "en-us"}}}; + const std::vector kExpectedBuffer = { + // `num_tags`. + kOneTag, + // `tag_name[0]`. + 'c', 'o', 'n', 't', 'e', 'n', 't', '_', 'l', 'a', 'n', 'g', 'u', 'a', 'g', + 'e', '\0', + // `tag_value[0]`. + 'e', 'n', '-', 'u', 's', '\0'}; + WriteBitBuffer wb(1024); + + EXPECT_THAT(kMixPresentationTagsWithContentLanguageTag.ValidateAndWrite(wb), + IsOk()); + + EXPECT_EQ(wb.bit_buffer(), kExpectedBuffer); +} + +TEST(MixPresentationTagsWriteAndValidate, WritesArbitraryTags) { + constexpr uint8_t kNumTags = 1; + const MixPresentationTags kMixPresentationTagsWithArbitraryTag = { + .num_tags = kNumTags, .tags = {{"ABC", "123"}}}; + const std::vector kExpectedBuffer = {// `num_tags`. + kNumTags, + // `tag_name[0]`. + 'A', 'B', 'C', '\0', + // `tag_value[1]`. + '1', '2', '3', '\0'}; + WriteBitBuffer wb(1024); + + EXPECT_THAT(kMixPresentationTagsWithArbitraryTag.ValidateAndWrite(wb), + IsOk()); + + EXPECT_EQ(wb.bit_buffer(), kExpectedBuffer); +} + +TEST(MixPresentationTagsWriteAndValidate, WritesDuplicateArbitraryTags) { + constexpr uint8_t kTwoTags = 2; + const MixPresentationTags kMixPresentationTagsWithArbitraryTag = { + .num_tags = kTwoTags, .tags = {{"tag", "value"}, {"tag", "value"}}}; + const std::vector kExpectedBuffer = {// `num_tags`. + kTwoTags, + // `tag_name[0]`. + 't', 'a', 'g', '\0', + // `tag_value[0]`. + 'v', 'a', 'l', 'u', 'e', '\0', + // `tag_name[1]`. + 't', 'a', 'g', '\0', + // `tag_value[1]`. + 'v', 'a', 'l', 'u', 'e', '\0'}; + WriteBitBuffer wb(1024); + + EXPECT_THAT(kMixPresentationTagsWithArbitraryTag.ValidateAndWrite(wb), + IsOk()); + + EXPECT_EQ(wb.bit_buffer(), kExpectedBuffer); +} + +TEST(MixPresentationTagsWriteAndValidate, InvalidForDuplicateContentIdTag) { + constexpr uint8_t kTwoTags = 2; + const MixPresentationTags + kMixPresentationTagsWithDuplicateContentLanguageTag = { + .num_tags = kTwoTags, + .tags = {{"content_language", "en-us"}, + {"content_language", "en-gb"}}}; + + WriteBitBuffer wb(1024); + + EXPECT_FALSE( + kMixPresentationTagsWithDuplicateContentLanguageTag.ValidateAndWrite(wb) + .ok()); +} + } // namespace } // namespace iamf_tools