diff --git a/starboard/shared/starboard/media/iamf_util.cc b/starboard/shared/starboard/media/iamf_util.cc index f1be373fde26..eac3ac573376 100644 --- a/starboard/shared/starboard/media/iamf_util.cc +++ b/starboard/shared/starboard/media/iamf_util.cc @@ -16,13 +16,76 @@ #include #include +#include #include +#include #include #include "starboard/common/string.h" namespace starboard { namespace { + +constexpr uint8_t kIamfSequenceHeaderObu = 31; + +// A lightweight, forward-only reader for a raw byte buffer. +class BufferReader { + public: + explicit BufferReader(const uint8_t* data, size_t size) + : view_(reinterpret_cast(data), size) {} + + std::optional ReadByte() { + if (view_.empty()) { + return std::nullopt; + } + const uint8_t byte = static_cast(view_.front()); + view_.remove_prefix(1); + return byte; + } + + // Reads a LEB128-encoded unsigned integer. + std::optional ReadLeb128() { + uint32_t decoded_value = 0; + for (size_t i = 0; i < 5; ++i) { + auto byte_opt = ReadByte(); + if (!byte_opt.has_value()) { + // Not enough data. + return std::nullopt; + } + uint8_t byte = *byte_opt; + + if (i == 4 && (byte & 0x7f) > 0x0f) { + // Invalid 5-byte encoding. + return std::nullopt; + } + + decoded_value |= (uint32_t)(byte & 0x7f) << (i * 7); + + if (!(byte & 0x80)) { + return decoded_value; + } + } + return std::nullopt; // Value exceeds 5 bytes + } + + bool Skip(size_t bytes_to_skip) { + if (BytesRemaining() < bytes_to_skip) { + return false; + } + view_.remove_prefix(bytes_to_skip); + return true; + } + + const uint8_t* CurrentData() const { + return reinterpret_cast(view_.data()); + } + + size_t BytesRemaining() const { return view_.size(); } + + private: + std::string_view view_; +}; + // Checks if |input| is a valid IAMF profile value, and stores the converted // value in |*profile| if so. bool StringToProfile(const std::string& input, uint32_t* profile) { @@ -122,4 +185,52 @@ IamfMimeUtil::IamfMimeUtil(const std::string& mime_type) { additional_profile_ = additional_profile; } +// static. +Result IamfMimeUtil::ParseIamfSequenceHeaderObu( + const std::vector& data) { + BufferReader reader(data.data(), data.size()); + + auto header_byte_opt = reader.ReadByte(); + if (!header_byte_opt) { + return Failure("Truncated OBU header."); + } + + uint8_t obu_type = (*header_byte_opt >> 3) & 0x1f; + if (obu_type != kIamfSequenceHeaderObu) { + return Failure(FormatString( + "Tried to read OBU: %d instead of IA Sequence Header OBU " + "type %d in ParseIamfSequenceHeaderObu().", + static_cast(obu_type), static_cast(kIamfSequenceHeaderObu))); + } + + auto obu_size_opt = reader.ReadLeb128(); + if (!obu_size_opt) { + return Failure("Failed to parse OBU size."); + } + const uint32_t obu_size = *obu_size_opt; + + if (reader.BytesRemaining() < obu_size) { + return Failure(FormatString("Parsed OBU size %u exceeds the data size %zu.", + obu_size, reader.BytesRemaining())); + } + + // Create a sub-reader for the OBU payload to ensure we don't read past the + // specified OBU size. + BufferReader obu_reader(reader.CurrentData(), obu_size); + + // Skip ia_code. + if (!obu_reader.Skip(sizeof(uint32_t))) { + return Failure("Truncated OBU payload: missing ia_code."); + } + + auto primary_profile = obu_reader.ReadByte(); + auto additional_profile = obu_reader.ReadByte(); + + if (!primary_profile || !additional_profile) { + return Failure("Truncated OBU payload: missing profile info."); + } + + return Success(IamfProfileInfo{*primary_profile, *additional_profile}); +} + } // namespace starboard diff --git a/starboard/shared/starboard/media/iamf_util.h b/starboard/shared/starboard/media/iamf_util.h index e522061dabec..4802b9850378 100644 --- a/starboard/shared/starboard/media/iamf_util.h +++ b/starboard/shared/starboard/media/iamf_util.h @@ -16,9 +16,13 @@ #define STARBOARD_SHARED_STARBOARD_MEDIA_IAMF_UTIL_H_ #include +#include #include +#include #include "starboard/common/log.h" +#include "starboard/common/result.h" +#include "starboard/shared/internal_only.h" namespace starboard { @@ -41,8 +45,19 @@ constexpr uint32_t kIamfProfileMax = 255; // Always check is_valid() before calling the getter functions. class IamfMimeUtil { public: + struct IamfProfileInfo { + uint8_t primary_profile; + uint8_t additional_profile; + }; + explicit IamfMimeUtil(const std::string& mime_type); + // Parses IAMF Config OBUs for the primary and additional profiles, + // based on IAMF specification v1.0.0-errata. + // https://aomediacodec.github.io/iamf/v1.1.0.html#codecsparameter. + static Result ParseIamfSequenceHeaderObu( + const std::vector& data); + bool is_valid() const { return primary_profile_ <= kIamfProfileMax && additional_profile_ <= kIamfProfileMax && diff --git a/starboard/shared/starboard/media/iamf_util_test.cc b/starboard/shared/starboard/media/iamf_util_test.cc index 2126ec95f04f..402c864d47b8 100644 --- a/starboard/shared/starboard/media/iamf_util_test.cc +++ b/starboard/shared/starboard/media/iamf_util_test.cc @@ -15,6 +15,7 @@ #include "starboard/shared/starboard/media/iamf_util.h" #include +#include #include "testing/gtest/include/gtest/gtest.h" @@ -246,7 +247,111 @@ TEST(IamfUtilTest, Profile) { EXPECT_TRUE(util.is_valid()); ASSERT_NE(util.primary_profile(), kIamfProfileSimple); ASSERT_NE(util.primary_profile(), kIamfProfileBase); - ASSERT_EQ(util.primary_profile(), 2); + ASSERT_EQ(util.primary_profile(), 2U); +} + +TEST(IamfUtilTest, ParsesSequenceHeaderObu) { + // From iamf_simple_profile_5_1.dmp. + const std::vector kSimpleProfileSequenceHeaderObu = { + 0xF8, 0x06, 0x69, 0x61, 0x6D, 0x66, 0x00, 0x00}; + + // From iamf_base_profile_stereo_ambisonics.dmp. + const std::vector kBaseProfileSequenceHeaderObu = { + 0xF8, 0x06, 0x69, 0x61, 0x6D, 0x66, 0x01, 0x01}; + + auto result = + IamfMimeUtil::ParseIamfSequenceHeaderObu(kSimpleProfileSequenceHeaderObu); + + ASSERT_TRUE(result.has_value()) << result.error(); + EXPECT_EQ(result->primary_profile, kIamfProfileSimple); + EXPECT_EQ(result->additional_profile, kIamfProfileSimple); + + result = + IamfMimeUtil::ParseIamfSequenceHeaderObu(kBaseProfileSequenceHeaderObu); + + ASSERT_TRUE(result.has_value()) << result.error(); + EXPECT_EQ(result->primary_profile, kIamfProfileBase); + EXPECT_EQ(result->additional_profile, kIamfProfileBase); +} + +TEST(IamfUtilTest, ParseSequenceHeaderFailsOnInvalidObuType) { + // First byte 0x08 means OBU type 1, not 31. + const uint8_t kInvalidObuType[] = {0x08, 0x06, 0x69, 0x61, + 0x6D, 0x66, 0x00, 0x00}; + std::vector data(kInvalidObuType, + kInvalidObuType + SB_ARRAY_SIZE(kInvalidObuType)); + auto result = IamfMimeUtil::ParseIamfSequenceHeaderObu(data); + ASSERT_FALSE(result.has_value()) + << "Parsed IA Sequence Header OBU when an error was expected. Primary " + "profile: " + << result->primary_profile + << " additional profile: " << result->additional_profile; +} + +TEST(IamfUtilTest, ParseSequenceHeaderFailsOnTruncatedData) { + const uint8_t kTruncatedData[] = {0xF8, 0x06, 0x69, 0x61, + 0x6D}; // Truncated ia_code. + std::vector data(kTruncatedData, + kTruncatedData + SB_ARRAY_SIZE(kTruncatedData)); + auto result = IamfMimeUtil::ParseIamfSequenceHeaderObu(data); + ASSERT_FALSE(result.has_value()) + << "Parsed IA Sequence Header OBU when an error was expected. Primary " + "profile: " + << result->primary_profile + << " additional profile: " << result->additional_profile; +} + +TEST(IamfUtilTest, ParseSequenceHeaderFailsOnTruncatedLeb128) { + const uint8_t kTruncatedLeb128[] = {0xF8, 0x81}; // Incomplete LEB128 size. + std::vector data(kTruncatedLeb128, + kTruncatedLeb128 + SB_ARRAY_SIZE(kTruncatedLeb128)); + auto result = IamfMimeUtil::ParseIamfSequenceHeaderObu(data); + ASSERT_FALSE(result.has_value()) + << "Parsed IA Sequence Header OBU when an error was expected. Primary " + "profile: " + << result->primary_profile + << " additional profile: " << result->additional_profile; +} + +TEST(IamfUtilTest, ParseSequenceHeaderFailsOnInvalid5ByteLeb128) { + // This is an invalid encoding for a 32-bit value. + const uint8_t kInvalidLeb128[] = {0xF8, 0x81, 0x81, 0x81, 0x81, 0x10}; + std::vector data(kInvalidLeb128, + kInvalidLeb128 + SB_ARRAY_SIZE(kInvalidLeb128)); + auto result = IamfMimeUtil::ParseIamfSequenceHeaderObu(data); + ASSERT_FALSE(result.has_value()) + << "Parsed IA Sequence Header OBU when an error was expected. Primary " + "profile: " + << result->primary_profile + << " additional profile: " << result->additional_profile; +} + +TEST(IamfUtilTest, ParseSequenceHeaderFailsWhenObuSizeExceedsBufferSize) { + // LEB128 size is 7, but only 6 bytes remain. + const uint8_t kObuSizeTooLarge[] = {0xF8, 0x07, 0x69, 0x61, + 0x6D, 0x66, 0x00, 0x00}; + std::vector data(kObuSizeTooLarge, + kObuSizeTooLarge + SB_ARRAY_SIZE(kObuSizeTooLarge)); + auto result = IamfMimeUtil::ParseIamfSequenceHeaderObu(data); + ASSERT_FALSE(result.has_value()) + << "Parsed IA Sequence Header OBU when an error was expected. Primary " + "profile: " + << result->primary_profile + << " additional profile: " << result->additional_profile; +} + +TEST(IamfUtilTest, ParseSequenceHeaderFailsWhenObuSizeIsTooSmall) { + // LEB128 size is 5, but 6 bytes are needed for ia_code and profiles. + const uint8_t kObuSizeTooSmall[] = {0xF8, 0x05, 0x69, 0x61, + 0x6D, 0x66, 0x00, 0x00}; + std::vector data(kObuSizeTooSmall, + kObuSizeTooSmall + SB_ARRAY_SIZE(kObuSizeTooSmall)); + auto result = IamfMimeUtil::ParseIamfSequenceHeaderObu(data); + ASSERT_FALSE(result.has_value()) + << "Parsed IA Sequence Header OBU when an error was expected. Primary " + "profile: " + << result->primary_profile + << " additional profile: " << result->additional_profile; } } // namespace diff --git a/starboard/shared/starboard/player/video_dmp_reader.cc b/starboard/shared/starboard/player/video_dmp_reader.cc index 7033336e0e7a..cdcc20429476 100644 --- a/starboard/shared/starboard/player/video_dmp_reader.cc +++ b/starboard/shared/starboard/player/video_dmp_reader.cc @@ -16,8 +16,10 @@ #include #include +#include #include "starboard/common/check_op.h" +#include "starboard/shared/starboard/media/iamf_util.h" namespace starboard { @@ -157,7 +159,14 @@ std::string VideoDmpReader::audio_mime_type() const { ss << "audio/wav; codecs=\"1\";"; break; case kSbMediaAudioCodecIamf: - ss << "audio/mp4; codecs=\"iamf\";"; + SB_CHECK(dmp_info_.iamf_primary_profile.has_value()); + // Only Opus IAMF substreams are currently supported. + ss << "audio/mp4; codecs=\"iamf."; + ss << std::setw(3) << std::setfill('0') << std::hex + << static_cast(*dmp_info_.iamf_primary_profile) << "." + << std::setw(3) << std::setfill('0') << std::hex + << static_cast(dmp_info_.iamf_additional_profile.value_or(0)) + << ".Opus\";"; break; default: SB_NOTREACHED() << "Unsupported audio codec: " << dmp_info_.audio_codec; @@ -305,6 +314,15 @@ void VideoDmpReader::Parse() { while (ParseOneRecord()) { } + if (dmp_info_.audio_codec == kSbMediaAudioCodecIamf && + !dmp_info_.iamf_primary_profile.has_value()) { + auto result = + IamfMimeUtil::ParseIamfSequenceHeaderObu(audio_access_units_[0].data()); + SB_CHECK(result) << result.error(); + dmp_info_.iamf_primary_profile = result->primary_profile; + dmp_info_.iamf_additional_profile = result->additional_profile; + } + dmp_info_.audio_access_units_size = audio_access_units_.size(); dmp_info_.audio_bitrate = CalculateAverageBitrate(audio_access_units_); dmp_info_.video_access_units_size = video_access_units_.size(); diff --git a/starboard/shared/starboard/player/video_dmp_reader.h b/starboard/shared/starboard/player/video_dmp_reader.h index a15acb56aae2..26f9f4494c9b 100644 --- a/starboard/shared/starboard/player/video_dmp_reader.h +++ b/starboard/shared/starboard/player/video_dmp_reader.h @@ -138,6 +138,8 @@ class VideoDmpReader { size_t audio_access_units_size = 0; int64_t audio_bitrate = 0; int audio_duration = 0; + std::optional iamf_primary_profile; + std::optional iamf_additional_profile; SbMediaVideoCodec video_codec = kSbMediaVideoCodecNone; size_t video_access_units_size = 0;