Fix AdaptationSet switching signaling

Current mechanism of using AdaptationSet @group is not spec compliant.

The spec compliant mechanism is to use supplemental descriptor with
schemeIdUri: urn:mpeg:dash:adaptation-set-switching:2016 as specified
in ISO/IEC 23009-1:2014/Amd.4:2016.

Fixes #156

Change-Id: I4d97648547a23315ba9c09dcadb14e18c99a45fc
This commit is contained in:
Kongqun Yang 2016-12-13 15:48:54 -08:00 committed by KongQun Yang
parent 7a4a40acb8
commit af0904e428
10 changed files with 136 additions and 117 deletions

View File

@ -404,6 +404,19 @@ class PackagerAppTest(unittest.TestCase):
self._AssertStreamInfo(self.output[0], 'is_encrypted: true')
self._AssertStreamInfo(self.output[1], 'is_encrypted: true')
@unittest.skipUnless(test_env.has_aes_flags, 'Requires AES credentials.')
def testWidevineEncryptionWithAesAndDashIfIopWithMultFiles(self):
flags = self._GetFlags(widevine_encryption=True, dash_if_iop=True)
flags += ['--aes_signing_key=' + test_env.options.aes_signing_key,
'--aes_signing_iv=' + test_env.options.aes_signing_iv]
self.packager.Package(
self._GetStreams(['audio', 'video'],
test_files=['bear-1280x720.mp4', 'bear-640x360.mp4',
'bear-320x180.mp4']), flags)
with open(self.mpd_output, 'rb') as f:
print f.read()
# TODO(kqyang): Add some validations.
@unittest.skipUnless(test_env.has_aes_flags, 'Requires AES credentials.')
def testKeyRotationWithAes(self):
flags = self._GetFlags(widevine_encryption=True, key_rotation=True)

View File

@ -15,8 +15,6 @@ namespace shaka {
namespace {
const int kStartingGroupId = 1;
// The easiest way to check whether two protobufs are equal, is to compare the
// serialized version.
bool ProtectedContentEq(
@ -50,8 +48,7 @@ DashIopMpdNotifier::DashIopMpdNotifier(
mpd_builder_(new MpdBuilder(dash_profile == kLiveProfile
? MpdBuilder::kDynamic
: MpdBuilder::kStatic,
mpd_options)),
next_group_id_(kStartingGroupId) {
mpd_options)) {
DCHECK(dash_profile == kLiveProfile || dash_profile == kOnDemandProfile);
for (size_t i = 0; i < base_urls.size(); ++i)
mpd_builder_->AddBaseUrl(base_urls[i]);
@ -91,7 +88,7 @@ bool DashIopMpdNotifier::NotifyNewContainer(const MediaInfo& media_info,
representation_id_to_adaptation_set_[representation->id()] = adaptation_set;
SetGroupId(key, adaptation_set);
SetAdaptationSetSwitching(key, adaptation_set);
*container_id = representation->id();
DCHECK(!ContainsKey(representation_map_, representation->id()));
@ -197,16 +194,18 @@ AdaptationSet* DashIopMpdNotifier::GetAdaptationSetForMediaInfo(
}
// Get all the UUIDs of the AdaptationSet. If another AdaptationSet has the
// same UUIDs then those should be groupable.
void DashIopMpdNotifier::SetGroupId(const std::string& key,
AdaptationSet* adaptation_set) {
if (adaptation_set->Group() >= 0) // @group already assigned.
// same UUIDs then those are switchable.
void DashIopMpdNotifier::SetAdaptationSetSwitching(
const std::string& key,
AdaptationSet* adaptation_set) {
// This adaptation set is already visited.
if (!adaptation_set->adaptation_set_switching_ids().empty())
return;
ProtectedContentMap::const_iterator protected_content_it =
protected_content_map_.find(adaptation_set->id());
// Clear contents should be in one AdaptationSet, so no need to assign
// @group.
// Clear contents should be in one AdaptationSet and may not be switchable
// with encrypted contents.
if (protected_content_it == protected_content_map_.end()) {
DVLOG(1) << "No content protection set for AdaptationSet@id="
<< adaptation_set->id();
@ -237,21 +236,15 @@ void DashIopMpdNotifier::SetGroupId(const std::string& key,
protected_content_map_[loop_adaptation_set_id];
if (static_cast<int>(adaptation_set_uuids.size()) !=
loop_protected_content.content_protection_entry().size()) {
// Different number of UUIDs, cannot be grouped.
// Different number of UUIDs, may not be switchable.
continue;
}
if (adaptation_set_uuids == GetUUIDs(loop_protected_content)) {
AdaptationSet& uuid_match_adaptation_set = **adaptation_set_it;
// They match. These AdaptationSets can be in the same group. Break out.
if (uuid_match_adaptation_set.Group() >= 0) {
adaptation_set->SetGroup(uuid_match_adaptation_set.Group());
} else {
const int group_id = next_group_id_++;
uuid_match_adaptation_set.SetGroup(group_id);
adaptation_set->SetGroup(group_id);
}
break;
// They match. These AdaptationSets are switchable.
uuid_match_adaptation_set.AddAdaptationSetSwitching(adaptation_set->id());
adaptation_set->AddAdaptationSetSwitching(uuid_match_adaptation_set.id());
}
}
}

View File

@ -73,9 +73,10 @@ class DashIopMpdNotifier : public MpdNotifier {
AdaptationSet* GetAdaptationSetForMediaInfo(const std::string& key,
const MediaInfo& media_info);
// Sets a group id for |adaptation_set| if applicable.
// If a group ID is already assigned, then this returns immediately.
void SetGroupId(const std::string& key, AdaptationSet* adaptation_set);
// Sets adaptation set switching. If adaptation set switching is already
// set, then this returns immediately.
void SetAdaptationSetSwitching(const std::string& key,
AdaptationSet* adaptation_set);
// Helper function to get a new AdaptationSet; registers the values
// to the fields (maps) of the instance.
@ -104,9 +105,6 @@ class DashIopMpdNotifier : public MpdNotifier {
std::unique_ptr<MpdBuilder> mpd_builder_;
base::Lock lock_;
// Next group ID to use for AdapationSets that can be grouped.
int next_group_id_;
// Maps Representation ID to AdaptationSet. This is for updating the PSSH.
std::map<uint32_t, AdaptationSet*> representation_id_to_adaptation_set_;
};

View File

@ -18,9 +18,11 @@ namespace shaka {
using ::testing::_;
using ::testing::Eq;
using ::testing::ElementsAre;
using ::testing::InSequence;
using ::testing::Return;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;
namespace {
@ -37,7 +39,6 @@ const char kValidMediaInfo[] =
"container_type: 1\n";
const uint32_t kDefaultAdaptationSetId = 0u;
const uint32_t kDefaultRepresentationId = 1u;
const int kDefaultGroupId = -1;
bool ElementEqual(const Element& lhs, const Element& rhs) {
const bool all_equal_except_sublement_check =
@ -102,8 +103,6 @@ class DashIopMpdNotifierTest
void SetUp() override {
ASSERT_TRUE(base::CreateTemporaryFile(&temp_file_path_));
output_path_ = temp_file_path_.AsUTF8Unsafe();
ON_CALL(*default_mock_adaptation_set_, Group())
.WillByDefault(Return(kDefaultGroupId));
}
void TearDown() override {
@ -218,7 +217,7 @@ TEST_P(DashIopMpdNotifierTest, NotifyNewTextContainer) {
// Verify VOD NotifyNewContainer() operation works with different
// MediaInfo::ProtectedContent.
// Two AdaptationSets should be created.
// Different DRM so they won't be grouped.
// AdaptationSets with different DRM won't be switchable.
TEST_P(DashIopMpdNotifierTest,
NotifyNewContainersWithDifferentProtectedContent) {
DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_,
@ -295,11 +294,6 @@ TEST_P(DashIopMpdNotifierTest,
std::unique_ptr<MockAdaptationSet> hd_adaptation_set(
new MockAdaptationSet(kHdAdaptationSetId));
ON_CALL(*sd_adaptation_set, Group()).WillByDefault(Return(kDefaultGroupId));
ON_CALL(*hd_adaptation_set, Group()).WillByDefault(Return(kDefaultGroupId));
EXPECT_CALL(*sd_adaptation_set, SetGroup(_)).Times(0);
EXPECT_CALL(*hd_adaptation_set, SetGroup(_)).Times(0);
const uint32_t kSdRepresentation = 4u;
const uint32_t kHdRepresentation = 5u;
std::unique_ptr<MockRepresentation> sd_representation(
@ -336,6 +330,9 @@ TEST_P(DashIopMpdNotifierTest,
ConvertToMediaInfo(kSdProtectedContent), &unused_container_id));
EXPECT_TRUE(notifier.NotifyNewContainer(
ConvertToMediaInfo(kHdProtectedContent), &unused_container_id));
EXPECT_THAT(sd_adaptation_set->adaptation_set_switching_ids(), ElementsAre());
EXPECT_THAT(hd_adaptation_set->adaptation_set_switching_ids(), ElementsAre());
}
// Verify VOD NotifyNewContainer() operation works with same
@ -410,9 +407,6 @@ TEST_P(DashIopMpdNotifierTest, NotifyNewContainersWithSameProtectedContent) {
std::unique_ptr<MockRepresentation> hd_representation(
new MockRepresentation(kHdRepresentation));
// No reason to set @group if there is only one AdaptationSet.
EXPECT_CALL(*default_mock_adaptation_set_, SetGroup(_)).Times(0);
InSequence in_sequence;
EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_))
.WillOnce(Return(default_mock_adaptation_set_.get()));
@ -440,6 +434,10 @@ TEST_P(DashIopMpdNotifierTest, NotifyNewContainersWithSameProtectedContent) {
ConvertToMediaInfo(kSdProtectedContent), &unused_container_id));
EXPECT_TRUE(notifier.NotifyNewContainer(
ConvertToMediaInfo(kHdProtectedContent), &unused_container_id));
// No adaptation set switching if there is only one AdaptationSet.
EXPECT_THAT(default_mock_adaptation_set_->adaptation_set_switching_ids(),
ElementsAre());
}
// AddContentProtection() should not work and should always return false.
@ -466,17 +464,16 @@ TEST_P(DashIopMpdNotifierTest, AddContentProtection) {
}
// Default Key IDs are different but if the content protection UUIDs match, then
// they can be in the same group.
// the AdaptationSet they belong to should be switchable.
// This is a long test.
// Basically this
// 1. Add an SD protected content. This should make an AdaptationSet.
// 2. Add an HD protected content. This should make another AdaptationSet that
// is different from the SD version. Both SD and HD should have the same
// group ID assigned.
// is different from the SD version. SD AdaptationSet and HD AdaptationSet
// should be switchable.
// 3. Add a 4k protected content. This should also make a new AdaptationSet.
// The group ID should also match the SD and HD (but this takes a slightly
// different path).
TEST_P(DashIopMpdNotifierTest, SetGroup) {
// It should be switchable with SD/HD AdaptationSet.
TEST_P(DashIopMpdNotifierTest, SetAdaptationSetSwitching) {
DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_,
empty_base_urls_, output_path_);
std::unique_ptr<MockMpdBuilder> mock_mpd_builder(
@ -530,9 +527,6 @@ TEST_P(DashIopMpdNotifierTest, SetGroup) {
std::unique_ptr<MockAdaptationSet> hd_adaptation_set(
new MockAdaptationSet(kHdAdaptationSetId));
ON_CALL(*sd_adaptation_set, Group()).WillByDefault(Return(kDefaultGroupId));
ON_CALL(*hd_adaptation_set, Group()).WillByDefault(Return(kDefaultGroupId));
const uint32_t kSdRepresentation = 4u;
const uint32_t kHdRepresentation = 5u;
std::unique_ptr<MockRepresentation> sd_representation(
@ -553,11 +547,6 @@ TEST_P(DashIopMpdNotifierTest, SetGroup) {
EXPECT_CALL(*hd_adaptation_set, AddRepresentation(_))
.WillOnce(Return(hd_representation.get()));
// Both AdaptationSets' groups should be set to the same value.
const int kExpectedGroupId = 1;
EXPECT_CALL(*sd_adaptation_set, SetGroup(kExpectedGroupId));
EXPECT_CALL(*hd_adaptation_set, SetGroup(kExpectedGroupId));
// This is not very nice but we need it for settings expectations later.
MockMpdBuilder* mock_mpd_builder_raw = mock_mpd_builder.get();
uint32_t unused_container_id;
@ -567,12 +556,13 @@ TEST_P(DashIopMpdNotifierTest, SetGroup) {
EXPECT_TRUE(notifier.NotifyNewContainer(
ConvertToMediaInfo(kHdProtectedContent), &unused_container_id));
// Now that the group IDs are set Group() returns kExpectedGroupId.
ON_CALL(*sd_adaptation_set, Group()).WillByDefault(Return(kExpectedGroupId));
ON_CALL(*hd_adaptation_set, Group()).WillByDefault(Return(kExpectedGroupId));
EXPECT_THAT(sd_adaptation_set->adaptation_set_switching_ids(),
ElementsAre(kHdAdaptationSetId));
EXPECT_THAT(hd_adaptation_set->adaptation_set_switching_ids(),
ElementsAre(kSdAdaptationSetId));
// Add another content that has the same protected content and make sure that
// it gets added to the existing group.
// adaptation set switching is set correctly.
const char k4kProtectedContent[] =
"video_info {\n"
" codec: 'avc1'\n"
@ -596,8 +586,6 @@ TEST_P(DashIopMpdNotifierTest, SetGroup) {
const uint32_t k4kAdaptationSetId = 4000u;
std::unique_ptr<MockAdaptationSet> fourk_adaptation_set(
new MockAdaptationSet(k4kAdaptationSetId));
ON_CALL(*fourk_adaptation_set, Group())
.WillByDefault(Return(kDefaultGroupId));
const uint32_t k4kRepresentationId = 4001u;
std::unique_ptr<MockRepresentation> fourk_representation(
@ -609,16 +597,21 @@ TEST_P(DashIopMpdNotifierTest, SetGroup) {
EXPECT_CALL(*fourk_adaptation_set, AddRepresentation(_))
.WillOnce(Return(fourk_representation.get()));
// Same group ID should be set.
EXPECT_CALL(*fourk_adaptation_set, SetGroup(kExpectedGroupId));
EXPECT_TRUE(notifier.NotifyNewContainer(
ConvertToMediaInfo(k4kProtectedContent), &unused_container_id));
EXPECT_THAT(sd_adaptation_set->adaptation_set_switching_ids(),
UnorderedElementsAre(kHdAdaptationSetId, k4kAdaptationSetId));
EXPECT_THAT(hd_adaptation_set->adaptation_set_switching_ids(),
UnorderedElementsAre(kSdAdaptationSetId, k4kAdaptationSetId));
EXPECT_THAT(fourk_adaptation_set->adaptation_set_switching_ids(),
ElementsAre(kSdAdaptationSetId, kHdAdaptationSetId));
}
// Even if the UUIDs match, video and audio AdaptationSets should not be grouped
// together.
TEST_P(DashIopMpdNotifierTest, DoNotSetGroupIfContentTypesDifferent) {
// Even if the UUIDs match, video and audio AdaptationSets should not be
// switchable.
TEST_P(DashIopMpdNotifierTest,
DoNotSetAdaptationSetSwitchingIfContentTypesDifferent) {
DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_,
empty_base_urls_, output_path_);
std::unique_ptr<MockMpdBuilder> mock_mpd_builder(
@ -671,15 +664,6 @@ TEST_P(DashIopMpdNotifierTest, DoNotSetGroupIfContentTypesDifferent) {
std::unique_ptr<MockAdaptationSet> audio_adaptation_set(
new MockAdaptationSet(kAudioAdaptationSetId));
ON_CALL(*video_adaptation_set, Group())
.WillByDefault(Return(kDefaultGroupId));
ON_CALL(*audio_adaptation_set, Group())
.WillByDefault(Return(kDefaultGroupId));
// Both AdaptationSets' groups should NOT be set.
EXPECT_CALL(*video_adaptation_set, SetGroup(_)).Times(0);
EXPECT_CALL(*audio_adaptation_set, SetGroup(_)).Times(0);
const uint32_t kVideoRepresentation = 8u;
const uint32_t kAudioRepresentation = 9u;
std::unique_ptr<MockRepresentation> video_representation(
@ -708,6 +692,11 @@ TEST_P(DashIopMpdNotifierTest, DoNotSetGroupIfContentTypesDifferent) {
ConvertToMediaInfo(kVideoContent), &unused_container_id));
EXPECT_TRUE(notifier.NotifyNewContainer(
ConvertToMediaInfo(kAudioContent), &unused_container_id));
EXPECT_THAT(video_adaptation_set->adaptation_set_switching_ids(),
ElementsAre());
EXPECT_THAT(audio_adaptation_set->adaptation_set_switching_ids(),
ElementsAre());
}
TEST_P(DashIopMpdNotifierTest, UpdateEncryption) {

View File

@ -41,9 +41,6 @@ class MockAdaptationSet : public AdaptationSet {
MOCK_METHOD1(AddRole, void(AdaptationSet::Role role));
MOCK_METHOD1(ForceSetSegmentAlignment, void(bool segment_alignment));
MOCK_METHOD1(SetGroup, void(int group_number));
MOCK_CONST_METHOD0(Group, int());
private:
// Only for constructing the super class. Not used for testing.
base::AtomicSequenceNumber sequence_counter_;

View File

@ -40,8 +40,6 @@ using xml::AdaptationSetXmlNode;
namespace {
const int kAdaptationSetGroupNotSet = -1;
AdaptationSet::Role MediaInfoTextTypeToRole(
MediaInfo::TextInfo::TextType type) {
switch (type) {
@ -682,7 +680,6 @@ AdaptationSet::AdaptationSet(uint32_t adaptation_set_id,
lang_(lang),
mpd_options_(mpd_options),
mpd_type_(mpd_type),
group_(kAdaptationSetGroupNotSet),
segments_aligned_(kSegmentAlignmentUnknown),
force_set_segment_alignment_(false) {
DCHECK(counter);
@ -808,13 +805,22 @@ xml::scoped_xml_ptr<xmlNode> AdaptationSet::GetXml() {
if (picture_aspect_ratio_.size() == 1)
adaptation_set.SetStringAttribute("par", *picture_aspect_ratio_.begin());
if (group_ >= 0)
adaptation_set.SetIntegerAttribute("group", group_);
if (!adaptation_set.AddContentProtectionElements(
content_protection_elements_)) {
return xml::scoped_xml_ptr<xmlNode>();
}
std::string switching_ids;
for (uint32_t id : adaptation_set_switching_ids_) {
if (!switching_ids.empty())
switching_ids += ',';
switching_ids += base::UintToString(id);
}
if (!switching_ids.empty()) {
adaptation_set.AddSupplementalProperty(
"urn:mpeg:dash:adaptation-set-switching:2016", switching_ids);
}
for (AdaptationSet::Role role : roles_)
adaptation_set.AddRoleElement("urn:mpeg:dash:role:2011", RoleToText(role));
@ -840,12 +846,8 @@ void AdaptationSet::ForceSetSegmentAlignment(bool segment_alignment) {
force_set_segment_alignment_ = true;
}
void AdaptationSet::SetGroup(int group_number) {
group_ = group_number;
}
int AdaptationSet::Group() const {
return group_;
void AdaptationSet::AddAdaptationSetSwitching(uint32_t adaptation_set_id) {
adaptation_set_switching_ids_.push_back(adaptation_set_id);
}
// Check segmentAlignment for Live here. Storing all start_time and duration

View File

@ -232,15 +232,14 @@ class AdaptationSet {
/// attribute.
virtual void ForceSetSegmentAlignment(bool segment_alignment);
/// Sets the AdaptationSet@group attribute.
/// Passing a negative value to this method will unset the attribute.
/// Note that group=0 is a special group, as mentioned in the DASH MPD
/// specification.
/// @param group_number is the value of AdaptatoinSet@group.
virtual void SetGroup(int group_number);
/// Adds the id of the adaptation set this adaptation set can switch to.
/// @param adaptation_set_id is the id of the switchable adaptation set.
void AddAdaptationSetSwitching(uint32_t adaptation_set_id);
/// @return Returns the value for group. If not set, returns a negative value.
virtual int Group() const;
/// @return the ids of the adaptation sets this adaptation set can switch to.
const std::vector<uint32_t>& adaptation_set_switching_ids() const {
return adaptation_set_switching_ids_;
}
// Must be unique in the Period.
uint32_t id() const { return id_; }
@ -348,10 +347,8 @@ class AdaptationSet {
const MpdOptions& mpd_options_;
const MpdBuilder::MpdType mpd_type_;
// The group attribute for the AdaptationSet. If the value is negative,
// no group number is specified.
// Note that group 0 is a special group number.
int group_;
// The ids of the adaptation sets this adaptation set can switch to.
std::vector<uint32_t> adaptation_set_switching_ids_;
// Video widths and heights of Representations. Note that this is a set; if
// there is only 1 resolution, then @width & @height should be set, otherwise

View File

@ -404,23 +404,39 @@ class TimeShiftBufferDepthTest : public SegmentTemplateTest {
}
};
// Verify that AdaptationSet@group can be set and unset.
TEST_F(CommonMpdBuilderTest, SetAdaptationSetGroup) {
base::AtomicSequenceNumber sequence_counter;
auto adaptation_set =
CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(),
MpdBuilder::kStatic, &sequence_counter);
adaptation_set->SetGroup(1);
TEST_F(CommonMpdBuilderTest, AddAdaptationSetSwitching) {
MpdBuilder mpd_builder(MpdBuilder::kStatic, MpdOptions());
AdaptationSet* adaptation_set = mpd_builder.AddAdaptationSet("");
adaptation_set->AddAdaptationSetSwitching(1);
adaptation_set->AddAdaptationSetSwitching(2);
adaptation_set->AddAdaptationSetSwitching(8);
xml::scoped_xml_ptr<xmlNode> xml_with_group(adaptation_set->GetXml());
EXPECT_NO_FATAL_FAILURE(
ExpectAttributeEqString("group", "1", xml_with_group.get()));
// Unset by passing a negative value.
adaptation_set->SetGroup(-1);
xml::scoped_xml_ptr<xmlNode> xml_without_group(adaptation_set->GetXml());
EXPECT_NO_FATAL_FAILURE(
ExpectAttributeNotSet("group", xml_without_group.get()));
xml::scoped_xml_ptr<xmlNode> adaptation_set_xml(adaptation_set->GetXml());
// The empty contentType is sort of a side effect of being able to generate an
// MPD without adding any Representations.
const char kExpectedOutput[] =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\"\n"
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
" xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"
" xsi:schemaLocation=\"urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd\"\n"
" minBufferTime=\"PT2S\" type=\"static\"\n"
" profiles=\"urn:mpeg:dash:profile:isoff-on-demand:2011\"\n"
" mediaPresentationDuration=\"PT0S\">\n"
" <Period id=\"0\">\n"
" <AdaptationSet id=\"0\" contentType=\"\">\n"
" <SupplementalProperty "
"schemeIdUri=\"urn:mpeg:dash:adaptation-set-switching:2016\" "
"value=\"1,2,8\"/>\n"
" </AdaptationSet>\n"
" </Period>\n"
"</MPD>";
std::string mpd_output;
EXPECT_TRUE(mpd_builder.ToString(&mpd_output));
ASSERT_TRUE(ValidateMpdSchema(mpd_output));
EXPECT_TRUE(XmlEqual(kExpectedOutput, mpd_output))
<< "Expected " << kExpectedOutput << std::endl
<< "Actual: " << mpd_output;
}
// Verify that Representation::Init() works with all "required" fields of

View File

@ -166,6 +166,15 @@ bool RepresentationBaseXmlNode::AddContentProtectionElements(
return true;
}
void RepresentationBaseXmlNode::AddSupplementalProperty(
const std::string& scheme_id_uri,
const std::string& value) {
XmlNode supplemental_property("SupplementalProperty");
supplemental_property.SetStringAttribute("schemeIdUri", scheme_id_uri);
supplemental_property.SetStringAttribute("value", value);
AddChild(supplemental_property.PassScopedPtr());
}
bool RepresentationBaseXmlNode::AddContentProtectionElement(
const ContentProtectionElement& content_protection_element) {
XmlNode content_protection_node("ContentProtection");

View File

@ -100,6 +100,11 @@ class RepresentationBaseXmlNode : public XmlNode {
bool AddContentProtectionElements(
const std::list<ContentProtectionElement>& content_protection_elements);
/// @param scheme_id_uri is content of the schemeIdUri attribute.
/// @param value is the content of value attribute.
void AddSupplementalProperty(const std::string& scheme_id_uri,
const std::string& value);
protected:
explicit RepresentationBaseXmlNode(const char* name);