Skip to content
Merged
1 change: 1 addition & 0 deletions starboard/shared/starboard/media/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ static_library("media_util") {
"//starboard/shared/starboard/media/vp9_util.h",
]

deps = [ "//third_party/abseil-cpp:absl" ]
public_deps = [ "//starboard/common" ]
configs += [ "//starboard/build/config:starboard_implementation" ]
}
108 changes: 108 additions & 0 deletions starboard/shared/starboard/media/iamf_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,73 @@

#include <cctype>
#include <cstdlib>
#include <optional>
#include <string>
#include <vector>

#include "starboard/common/string.h"
#include "third_party/abseil-cpp/absl/types/span.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) : span_(data, size) {}

std::optional<uint8_t> ReadByte() {
if (span_.empty()) {
return std::nullopt;
}
const uint8_t byte = span_.front();
span_.remove_prefix(1);
return byte;
}

// Reads a LEB128-encoded unsigned integer.
std::optional<uint32_t> 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;
}
span_.remove_prefix(bytes_to_skip);
return true;
}

const uint8_t* CurrentData() const { return span_.data(); }

size_t BytesRemaining() const { return span_.size(); }

private:
absl::Span<const uint8_t> span_;
};

// 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) {
Expand Down Expand Up @@ -122,4 +182,52 @@ IamfMimeUtil::IamfMimeUtil(const std::string& mime_type) {
additional_profile_ = additional_profile;
}

// static.
Result<IamfMimeUtil::IamfProfileInfo> IamfMimeUtil::ParseIamfSequenceHeaderObu(
const std::vector<uint8_t>& 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<int>(obu_type), static_cast<int>(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
15 changes: 15 additions & 0 deletions starboard/shared/starboard/media/iamf_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
#define STARBOARD_SHARED_STARBOARD_MEDIA_IAMF_UTIL_H_

#include <limits>
#include <optional>
#include <string>
#include <vector>

#include "starboard/common/log.h"
#include "starboard/common/result.h"
#include "starboard/shared/internal_only.h"

namespace starboard {

Expand All @@ -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<IamfProfileInfo> ParseIamfSequenceHeaderObu(
const std::vector<uint8_t>& data);

bool is_valid() const {
return primary_profile_ <= kIamfProfileMax &&
additional_profile_ <= kIamfProfileMax &&
Expand Down
107 changes: 106 additions & 1 deletion starboard/shared/starboard/media/iamf_util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "starboard/shared/starboard/media/iamf_util.h"

#include <string>
#include <vector>

#include "testing/gtest/include/gtest/gtest.h"

Expand Down Expand Up @@ -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<uint8_t> kSimpleProfileSequenceHeaderObu = {
0xF8, 0x06, 0x69, 0x61, 0x6D, 0x66, 0x00, 0x00};

// From iamf_base_profile_stereo_ambisonics.dmp.
const std::vector<uint8_t> 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<uint8_t> 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
<< " additonal profile: " << result->additional_profile;
}

TEST(IamfUtilTest, ParseSequenceHeaderFailsOnTruncatedData) {
const uint8_t kTruncatedData[] = {0xF8, 0x06, 0x69, 0x61,
0x6D}; // Truncated ia_code.
std::vector<uint8_t> 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
<< " additonal profile: " << result->additional_profile;
}

TEST(IamfUtilTest, ParseSequenceHeaderFailsOnTruncatedLeb128) {
const uint8_t kTruncatedLeb128[] = {0xF8, 0x81}; // Incomplete LEB128 size.
std::vector<uint8_t> 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
<< " additonal 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<uint8_t> 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
<< " additonal 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<uint8_t> 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
<< " additonal 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<uint8_t> 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
<< " additonal profile: " << result->additional_profile;
}

} // namespace
Expand Down
24 changes: 23 additions & 1 deletion starboard/shared/starboard/player/video_dmp_reader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

#include <algorithm>
#include <functional>
#include <iomanip>

#include "starboard/common/check_op.h"
#include "starboard/shared/starboard/media/iamf_util.h"

namespace starboard {

Expand Down Expand Up @@ -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<int>(*dmp_info_.iamf_primary_profile) << "."
<< std::setw(3) << std::setfill('0') << std::hex
<< static_cast<int>(dmp_info_.iamf_additional_profile.value_or(0))
<< ".Opus\";";
break;
default:
SB_NOTREACHED() << "Unsupported audio codec: " << dmp_info_.audio_codec;
Expand Down Expand Up @@ -305,6 +314,19 @@ void VideoDmpReader::Parse() {
while (ParseOneRecord()) {
}

if (dmp_info_.audio_codec == kSbMediaAudioCodecIamf &&
!dmp_info_.iamf_primary_profile.has_value()) {
dmp_info_.iamf_primary_profile = 0;
dmp_info_.iamf_additional_profile = 0;
std::vector<uint8_t> data(audio_access_units_[0].data().data(),
audio_access_units_[0].data().data() +
audio_access_units_[0].data().size());
auto result = IamfMimeUtil::ParseIamfSequenceHeaderObu(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();
Expand Down
2 changes: 2 additions & 0 deletions starboard/shared/starboard/player/video_dmp_reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ class VideoDmpReader {
size_t audio_access_units_size = 0;
int64_t audio_bitrate = 0;
int audio_duration = 0;
std::optional<uint8_t> iamf_primary_profile;
std::optional<uint8_t> iamf_additional_profile;

SbMediaVideoCodec video_codec = kSbMediaVideoCodecNone;
size_t video_access_units_size = 0;
Expand Down
Loading