From 60c54975d7507905ba8f75ccd0b5981c7b6e549a Mon Sep 17 00:00:00 2001 From: Rintaro Kuroiwa Date: Wed, 15 Jul 2015 14:57:47 -0700 Subject: [PATCH] MpdBuilder should not automatically add ContentProtection elements - ContentProtection elements should be added explicitly by the AddContentProtection() methods. - This is because some MPDs should have ContentProtection at AdaptationSet level instead of Representation. - Change SimpleMpdNotifier, which uses MpdBuilder, to add ContentProtectionElements. The logic is moved from MuxerListener. - Add Element class for specifying subelements for ContentProtectionElement. Change-Id: I9bedfb3e5a5ac0b3d5c702f1e6e4a8608c978d1d --- .../media/event/mpd_notify_muxer_listener.cc | 12 -- .../media/event/muxer_listener_internal.cc | 38 ---- .../media/event/muxer_listener_internal.h | 9 - .../vod_media_info_dump_muxer_listener.cc | 8 - ...media_info_dump_muxer_listener_unittest.cc | 7 - .../mpd/base/content_protection_element.cc | 2 + .../mpd/base/content_protection_element.h | 18 +- packager/mpd/base/mpd_builder.cc | 11 +- packager/mpd/base/mpd_builder.h | 9 + packager/mpd/base/mpd_builder_unittest.cc | 129 +++++++++++- packager/mpd/base/simple_mpd_notifier.cc | 114 ++++++++++ packager/mpd/base/xml/xml_node.cc | 195 ++++-------------- packager/mpd/base/xml/xml_node.h | 14 +- packager/mpd/base/xml/xml_node_unittest.cc | 75 ++++--- ...ypted_audio_media_info_expected_output.txt | 1 - packager/mpd/test/xml_compare.cc | 36 +++- 16 files changed, 393 insertions(+), 285 deletions(-) diff --git a/packager/media/event/mpd_notify_muxer_listener.cc b/packager/media/event/mpd_notify_muxer_listener.cc index 58ff4fbdac..9931e17324 100644 --- a/packager/media/event/mpd_notify_muxer_listener.cc +++ b/packager/media/event/mpd_notify_muxer_listener.cc @@ -65,18 +65,6 @@ void MpdNotifyMuxerListener::OnMediaStart( default_key_id_, pssh_, media_info.get()); } - if (is_encrypted_) { - // TODO(rkuroiwa): When MediaInfo's content protection fields are processed - // in MpdBuilder (e.g. content_protection_uuid, default_key_id) then skip - // this step if scheme_id_uri_'s UUID == content_protection_uuid_. - // Also consider removing SetContentProtectionSchemeIdUri(). - if (!internal::AddContentProtectionElements( - container_type, scheme_id_uri_, media_info.get())) { - LOG(ERROR) << "Failed to add content protection elements."; - return; - } - } - if (mpd_notifier_->dash_profile() == kLiveProfile) { // TODO(kqyang): Check return result. mpd_notifier_->NotifyNewContainer(*media_info, ¬ification_id_); diff --git a/packager/media/event/muxer_listener_internal.cc b/packager/media/event/muxer_listener_internal.cc index a0539c0f61..6b6d205a8d 100644 --- a/packager/media/event/muxer_listener_internal.cc +++ b/packager/media/event/muxer_listener_internal.cc @@ -201,44 +201,6 @@ bool SetVodInformation(bool has_init_range, return true; } -// TODO(rkuroiwa): Move this logic to MpdBuilder? MuxerListener probably doesn't -// need to know this. -bool AddContentProtectionElements(MuxerListener::ContainerType container_type, - const std::string& user_scheme_id_uri, - MediaInfo* media_info) { - DCHECK(media_info); - - const char kEncryptedMp4Uri[] = "urn:mpeg:dash:mp4protection:2011"; - const char kEncryptedMp4Value[] = "cenc"; - - // DASH MPD spec specifies a default ContentProtection element for ISO BMFF - // (MP4) files. - const bool is_mp4_container = container_type == MuxerListener::kContainerMp4; - if (is_mp4_container) { - MediaInfo::ContentProtectionXml* mp4_protection = - media_info->add_content_protections(); - mp4_protection->set_scheme_id_uri(kEncryptedMp4Uri); - mp4_protection->set_value(kEncryptedMp4Value); - } - - if (!user_scheme_id_uri.empty()) { - MediaInfo::ContentProtectionXml* content_protection = - media_info->add_content_protections(); - content_protection->set_scheme_id_uri(user_scheme_id_uri); - } else if (is_mp4_container) { - LOG(WARNING) << "schemeIdUri is not specified. Added default " - "ContentProtection only."; - } - - if (media_info->content_protections_size() == 0) { - LOG(ERROR) << "The stream is encrypted but no schemeIdUri specified for " - "ContentProtection."; - return false; - } - - return true; -} - void SetContentProtectionFields( const std::string& content_protection_uuid, const std::string& content_protection_name_version, diff --git a/packager/media/event/muxer_listener_internal.h b/packager/media/event/muxer_listener_internal.h index 201eda62ed..ba4a57b94c 100644 --- a/packager/media/event/muxer_listener_internal.h +++ b/packager/media/event/muxer_listener_internal.h @@ -45,15 +45,6 @@ bool SetVodInformation(bool has_init_range, uint64_t file_size, MediaInfo* media_info); -/// @param container_type specifies container type. A default ContentProtection -/// element will be added if the container is MP4. -/// @param user_scheme_id_uri is the user specified schemeIdUri for -/// ContentProtection. -/// @return true if a ContentProtectionXml is added, false otherwise. -bool AddContentProtectionElements(MuxerListener::ContainerType container_type, - const std::string& user_scheme_id_uri, - MediaInfo* media_info); - /// @param content_protection_uuid is the UUID of the content protection /// in human readable form. /// @param content_protection_name_version is the DRM name and verion. diff --git a/packager/media/event/vod_media_info_dump_muxer_listener.cc b/packager/media/event/vod_media_info_dump_muxer_listener.cc index e9e020e870..bf63e0a3e8 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener.cc @@ -62,14 +62,6 @@ void VodMediaInfoDumpMuxerListener::OnMediaStart( content_protection_uuid_, content_protection_name_version_, default_key_id_, pssh_, media_info_.get()); } - - if (is_encrypted_) { - if (!internal::AddContentProtectionElements( - container_type, scheme_id_uri_, media_info_.get())) { - LOG(ERROR) << "Failed to add content protection elements."; - return; - } - } } void VodMediaInfoDumpMuxerListener::OnSampleDurationReady( diff --git a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc index 3fd535533f..cd854a8414 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc @@ -290,13 +290,6 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, EncryptedStream_Normal) { " pixel_width: 1\n" " pixel_height: 1\n" "}\n" - "content_protections {\n" - " scheme_id_uri: 'urn:mpeg:dash:mp4protection:2011'\n" - " value: 'cenc'\n" - "}\n" - "content_protections {\n" - " scheme_id_uri: 'http://foo.com/bar'\n" - "}\n" "init_range {\n" " begin: 0\n" " end: 120\n" diff --git a/packager/mpd/base/content_protection_element.cc b/packager/mpd/base/content_protection_element.cc index 57e100037e..908e491f0e 100644 --- a/packager/mpd/base/content_protection_element.cc +++ b/packager/mpd/base/content_protection_element.cc @@ -7,6 +7,8 @@ #include "packager/mpd/base/content_protection_element.h" namespace edash_packager { +Element::Element() {} +Element::~Element() {} ContentProtectionElement::ContentProtectionElement() {} ContentProtectionElement::~ContentProtectionElement() {} } // namespace edash_packager diff --git a/packager/mpd/base/content_protection_element.h b/packager/mpd/base/content_protection_element.h index da73e0b445..8792249e72 100644 --- a/packager/mpd/base/content_protection_element.h +++ b/packager/mpd/base/content_protection_element.h @@ -14,9 +14,23 @@ #include #include +#include namespace edash_packager { +// This is any (XML) element. +struct Element { + Element(); + ~Element(); + // Name of this element. + std::string name; + // attributes for this element. + std::map attributes; + // Content of this element. + std::string content; + std::vector subelements; +}; + /// Structure to represent element in DASH MPD spec (ISO /// 23009-1:2012 MPD and Segment Formats). struct ContentProtectionElement { @@ -29,8 +43,8 @@ struct ContentProtectionElement { // Other attributes for this element. std::map additional_attributes; - // The elements that will be in this element. - std::string subelements; + // The subelements that will be in this element. + std::vector subelements; }; } // namespace edash_packager diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index 8e5247fc87..86e56a472b 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -60,15 +60,18 @@ void AddMpdNameSpaceInfo(XmlNode* mpd) { DCHECK(mpd); static const char kXmlNamespace[] = "urn:mpeg:DASH:schema:MPD:2011"; - mpd->SetStringAttribute("xmlns", kXmlNamespace); static const char kXmlNamespaceXsi[] = "http://www.w3.org/2001/XMLSchema-instance"; - mpd->SetStringAttribute("xmlns:xsi", kXmlNamespaceXsi); static const char kXmlNamespaceXlink[] = "http://www.w3.org/1999/xlink"; - mpd->SetStringAttribute("xmlns:xlink", kXmlNamespaceXlink); static const char kDashSchemaMpd2011[] = "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"; + static const char kCencNamespace[] = "urn:mpeg:cenc:2013"; + + mpd->SetStringAttribute("xmlns", kXmlNamespace); + mpd->SetStringAttribute("xmlns:xsi", kXmlNamespaceXsi); + mpd->SetStringAttribute("xmlns:xlink", kXmlNamespaceXlink); mpd->SetStringAttribute("xsi:schemaLocation", kDashSchemaMpd2011); + mpd->SetStringAttribute("xmlns:cenc", kCencNamespace); } bool IsPeriodNode(xmlNodePtr node) { @@ -864,8 +867,6 @@ xml::ScopedXmlPtr::type Representation::GetXml() { content_protection_elements_)) { return xml::ScopedXmlPtr::type(); } - if (!representation.AddContentProtectionElementsFromMediaInfo(media_info_)) - return xml::ScopedXmlPtr::type(); if (HasVODOnlyFields(media_info_) && !representation.AddVODOnlyInfo(media_info_)) { diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index 28bb3250aa..c220996823 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -178,6 +178,10 @@ class AdaptationSet { Representation* AddRepresentation(const MediaInfo& media_info); /// Add a ContenProtection element to the adaptation set. + /// AdaptationSet does not add elements + /// automatically to itself even if @a media_info.protected_content is + /// populated. This is because some MPDs should have the elements at + /// AdaptationSet level and some at Representation level. /// @param element contains the ContentProtection element contents. /// If @a element has {value, schemeIdUri} set and has /// {“value”, “schemeIdUri”} as key for @a additional_attributes, @@ -294,6 +298,11 @@ class Representation { bool Init(); /// Add a ContenProtection element to the representation. + /// Representation does not add elements + /// automatically to itself even if @a media_info passed to + /// AdaptationSet::AddRepresentation() has @a media_info.protected_content + /// populated. This is because some MPDs should have the elements at + /// AdaptationSet level and some at Representation level. /// @param element contains the ContentProtection element contents. /// If @a element has {value, schemeIdUri} set and has /// {“value”, “schemeIdUri”} as key for @a additional_attributes, diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index 885bc0aa1a..e4b3e142a6 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -856,6 +856,58 @@ TEST_F(CommonMpdBuilderTest, SetSampleDuration) { representation.media_info_.video_info().frame_duration()); } +// Verify that AdaptationSet::AddContentProtection() works. +TEST_F(CommonMpdBuilderTest, AdaptationSetAddContentProtection) { + const char kVideoMediaInfo1080p[] = + "video_info {\n" + " codec: \"avc1\"\n" + " width: 1920\n" + " height: 1080\n" + " time_scale: 3000\n" + " frame_duration: 100\n" + "}\n" + "container_type: 1\n"; + ContentProtectionElement content_protection; + content_protection.scheme_id_uri = "someuri"; + content_protection.value = "some value"; + Element pssh; + pssh.name = "cenc:pssh"; + pssh.content = "any value"; + content_protection.subelements.push_back(pssh); + + AdaptationSet* video_adaptation_set = mpd_.AddAdaptationSet(""); + ASSERT_TRUE(video_adaptation_set); + ASSERT_TRUE(video_adaptation_set->AddRepresentation( + ConvertToMediaInfo(kVideoMediaInfo1080p))); + video_adaptation_set->AddContentProtectionElement(content_protection); + + const char kExpectedOutput[] = + "\n" + "" + " " + " " + " " + " any value" + " " + " " + " " + " " + ""; + std::string mpd_output; + ASSERT_TRUE(mpd_.ToString(&mpd_output)); + EXPECT_TRUE(XmlEqual(kExpectedOutput, mpd_output)); +} + // Add one video check the output. TEST_F(StaticMpdBuilderTest, Video) { MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); @@ -888,18 +940,78 @@ TEST_F(StaticMpdBuilderTest, VideoAndAudio) { // MPD schema has strict ordering. AudioChannelConfiguration must appear before // ContentProtection. +// Also test that Representation::AddContentProtection() works. TEST_F(StaticMpdBuilderTest, AudioChannelConfigurationWithContentProtection) { - MediaInfo encrypted_audio_media_info = - GetTestMediaInfo(kFileNameEncytpedAudioMediaInfo); + const char kTestMediaInfo[] = + "bandwidth: 195857\n" + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 44100\n" + " num_channels: 2\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 863\n" + "}\n" + "index_range {\n" + " begin: 864\n" + " end: 931\n" + "}\n" + "media_file_name: 'encrypted_audio.mp4'\n" + "media_duration_seconds: 24.009434\n" + "reference_time_scale: 44100\n" + "container_type: CONTAINER_MP4\n"; + const char kExpectedOutput[] = + "\n" + "" + " " + " " + " " + " " + " " + " anything" + " " + " encrypted_audio.mp4" + " " + " " + " " + " " + " " + " " + ""; + + ContentProtectionElement content_protection; + content_protection.scheme_id_uri = "http://foo.com/"; + Element pssh; + pssh.name = "cenc:pssh"; + pssh.content = "anything"; + content_protection.subelements.push_back(pssh); + + MediaInfo audio_media_info = ConvertToMediaInfo(kTestMediaInfo); AdaptationSet* audio_adaptation_set = mpd_.AddAdaptationSet(""); ASSERT_TRUE(audio_adaptation_set); Representation* audio_representation = - audio_adaptation_set->AddRepresentation(encrypted_audio_media_info); + audio_adaptation_set->AddRepresentation(audio_media_info); ASSERT_TRUE(audio_representation); + audio_representation->AddContentProtectionElement(content_protection); - EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputEncryptedAudio)); + std::string mpd_output; + ASSERT_TRUE(mpd_.ToString(&mpd_output)); + EXPECT_TRUE(XmlEqual(kExpectedOutput, mpd_output)); } // Static profile requires bandwidth to be set because it has no other way to @@ -939,14 +1051,19 @@ TEST_F(StaticMpdBuilderTest, WriteToFile) { } // Check whether the attributes are set correctly for dynamic element. +// This test must use ASSERT_EQ for comparison because XmlEqual() cannot +// handle namespaces correctly yet. TEST_F(DynamicMpdBuilderTest, CheckMpdAttributes) { static const char kExpectedOutput[] = "\n" "\n" " \n" diff --git a/packager/mpd/base/simple_mpd_notifier.cc b/packager/mpd/base/simple_mpd_notifier.cc index e7288c49db..6ee4abc689 100644 --- a/packager/mpd/base/simple_mpd_notifier.cc +++ b/packager/mpd/base/simple_mpd_notifier.cc @@ -6,7 +6,10 @@ #include "packager/mpd/base/simple_mpd_notifier.h" +#include "packager/base/base64.h" #include "packager/base/logging.h" +#include "packager/base/strings/string_number_conversions.h" +#include "packager/base/strings/string_util.h" #include "packager/media/file/file.h" #include "packager/mpd/base/mpd_builder.h" #include "packager/mpd/base/mpd_utils.h" @@ -15,6 +18,116 @@ using edash_packager::media::File; namespace edash_packager { +namespace { + +// Coverts binary data into human readable UUID format. +bool HexToUUID(const std::string& data, std::string* uuid_format) { + DCHECK(uuid_format); + const size_t kExpectedUUIDSize = 16; + if (data.size() != kExpectedUUIDSize) { + LOG(ERROR) << "Default key ID size is expected to be " << kExpectedUUIDSize + << " but is " << data.size(); + return false; + } + + const std::string hex_encoded = + StringToLowerASCII(base::HexEncode(data.data(), data.size())); + DCHECK_EQ(hex_encoded.size(), kExpectedUUIDSize * 2); + base::StringPiece all(hex_encoded); + // Note UUID has 5 parts separated with dashes. + // e.g. 123e4567-e89b-12d3-a456-426655440000 + // These StringPieces have each part. + base::StringPiece first = all.substr(0, 8); + base::StringPiece second = all.substr(8, 4); + base::StringPiece third = all.substr(12, 4); + base::StringPiece fourth = all.substr(16, 4); + base::StringPiece fifth= all.substr(20, 12); + + // 32 hexadecimal characters with 4 hyphens. + const size_t kHumanReadableUUIDSize = 36; + uuid_format->reserve(kHumanReadableUUIDSize); + first.CopyToString(uuid_format); + uuid_format->append("-"); + second.AppendToString(uuid_format); + uuid_format->append("-"); + third.AppendToString(uuid_format); + uuid_format->append("-"); + fourth.AppendToString(uuid_format); + uuid_format->append("-"); + fifth.AppendToString(uuid_format); + return true; +} + +// This might be useful for DashIopCompliantMpdNotifier. If so it might make +// sense to template this so that it accepts Representation and AdaptationSet. +// For SimpleMpdNotifier, just put it in Representation. It should still +// generate a valid MPD. +void AddContentProtectionElements(const MediaInfo& media_info, + Representation* representation) { + DCHECK(representation); + if (!media_info.has_protected_content()) + return; + + const MediaInfo::ProtectedContent& protected_content = + media_info.protected_content(); + + const char kEncryptedMp4Uri[] = "urn:mpeg:dash:mp4protection:2011"; + const char kEncryptedMp4Value[] = "cenc"; + + // DASH MPD spec specifies a default ContentProtection element for ISO BMFF + // (MP4) files. + const bool is_mp4_container = + media_info.container_type() == MediaInfo::CONTAINER_MP4; + if (is_mp4_container) { + ContentProtectionElement mp4_content_protection; + mp4_content_protection.scheme_id_uri = kEncryptedMp4Uri; + mp4_content_protection.value = kEncryptedMp4Value; + if (protected_content.has_default_key_id()) { + std::string key_id_uuid_format; + if (HexToUUID(protected_content.default_key_id(), &key_id_uuid_format)) { + mp4_content_protection.additional_attributes["cenc:default_KID"] = + key_id_uuid_format; + } else { + LOG(ERROR) << "Failed to convert default key ID into UUID format."; + } + } + + representation->AddContentProtectionElement(mp4_content_protection); + } + + for (int i = 0; i < protected_content.content_protection_entry().size(); + ++i) { + const MediaInfo::ProtectedContent::ContentProtectionEntry& entry = + protected_content.content_protection_entry(i); + if (!entry.has_uuid()) { + LOG(WARNING) + << "ContentProtectionEntry was specified but no UUID is set for " + << entry.name_version() << ", skipping."; + continue; + } + + ContentProtectionElement drm_content_protection; + drm_content_protection.scheme_id_uri = "urn:uuid:" + entry.uuid(); + if (entry.has_name_version()) + drm_content_protection.value = entry.name_version(); + + if (entry.has_pssh()) { + std::string base64_encoded_pssh; + base::Base64Encode(entry.pssh(), &base64_encoded_pssh); + Element cenc_pssh; + cenc_pssh.name = "cenc:pssh"; + cenc_pssh.content = base64_encoded_pssh; + drm_content_protection.subelements.push_back(cenc_pssh); + } + + representation->AddContentProtectionElement(drm_content_protection); + } + + LOG_IF(WARNING, protected_content.content_protection_entry().size() == 0) + << "The media is encrypted but no content protection specified."; +} +} // namespace + SimpleMpdNotifier::SimpleMpdNotifier(DashProfile dash_profile, const MpdOptions& mpd_options, const std::vector& base_urls, @@ -64,6 +177,7 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, if (representation == NULL) return false; + AddContentProtectionElements(media_info, representation); *container_id = representation->id(); if (mpd_builder_->type() == MpdBuilder::kStatic) diff --git a/packager/mpd/base/xml/xml_node.cc b/packager/mpd/base/xml/xml_node.cc index b97cc8629a..533d8571be 100644 --- a/packager/mpd/base/xml/xml_node.cc +++ b/packager/mpd/base/xml/xml_node.cc @@ -33,132 +33,6 @@ std::string RangeToString(const Range& range) { base::Uint64ToString(range.end()); } -bool SetAttributes(const google::protobuf::RepeatedPtrField< - AttributeNameValuePair>& attributes, - XmlNode* xml_node) { - DCHECK(xml_node); - for (int i = 0; i < attributes.size(); ++i) { - const AttributeNameValuePair& attribute = attributes.Get(i); - const std::string& name = attribute.name(); - const std::string& value = attribute.value(); - - if (name.empty()) { - LOG(ERROR) << "For element " - << reinterpret_cast(xml_node->GetRawPtr()->name) - << ", no name specified for attribute with value: " << value; - return false; - } - - xml_node->SetStringAttribute(name.c_str(), value); - } - - return true; -} - -// This function is recursive. Note that elements.size() == 0 is a terminating -// condition. -bool AddSubelements(const google::protobuf::RepeatedPtrField< - ContentProtectionXml::Element>& elements, - XmlNode* xml_node) { - DCHECK(xml_node); - for (int i = 0; i < elements.size(); ++i) { - const ContentProtectionXml::Element& subelement = elements.Get(i); - const std::string& subelement_name = subelement.name(); - if (subelement_name.empty()) { - LOG(ERROR) << "Subelement name was not specified for node " - << reinterpret_cast(xml_node->GetRawPtr()->name); - return false; - } - - XmlNode subelement_xml_node(subelement_name.c_str()); - if (!SetAttributes(subelement.attributes(), &subelement_xml_node)) { - LOG(ERROR) << "Failed to set attributes for " << subelement_name; - return false; - } - - if (!AddSubelements(subelement.subelements(), &subelement_xml_node)) { - LOG(ERROR) << "Failed to add subelements to " << subelement_name; - return false; - } - - if (!xml_node->AddChild(subelement_xml_node.PassScopedPtr())) { - LOG(ERROR) << "Failed to add subelement " << subelement_name << " to " - << reinterpret_cast(xml_node->GetRawPtr()->name); - return false; - } - } - - return true; -} - -// Returns true if 'schemeIdUri' is set in |content_protection_xml| and sets -// |scheme_id_uri_output|. This function checks -// ContentProtectionXml::scheme_id_uri before searching thru attributes. -bool GetSchemeIdAttribute(const ContentProtectionXml& content_protection_xml, - std::string* scheme_id_uri_output) { - // Common case where 'schemeIdUri' is set directly. - if (content_protection_xml.has_scheme_id_uri()) { - scheme_id_uri_output->assign(content_protection_xml.scheme_id_uri()); - return true; - } - - // 'schemeIdUri' is one of the attributes. - for (int i = 0; i < content_protection_xml.attributes().size(); ++i) { - const AttributeNameValuePair& attribute = - content_protection_xml.attributes(i); - const std::string& name = attribute.name(); - const std::string& value = attribute.value(); - if (name == "schemeIdUri") { - if (value.empty()) - LOG(WARNING) << "schemeIdUri is specified with an empty string."; - - // 'schemeIdUri' is a mandatory field but MPD doesn't care what the actual - // value is, proceed. - scheme_id_uri_output->assign(value); - return true; - } - } - - return false; -} - -// Translates ContentProtectionXml to XmlNode. -// content_protection_xml.scheme_id_uri and content_protection_xml.value takes -// precedence over attributes in content_protection_xml.attributes. -bool TranslateToContentProtectionXmlNode( - const ContentProtectionXml& content_protection_xml, - XmlNode* xml_node_content_protection) { - std::string scheme_id_uri; - if (!GetSchemeIdAttribute(content_protection_xml, &scheme_id_uri)) { - LOG(ERROR) << "ContentProtection element requires schemeIdUri."; - return false; - } - - if (!SetAttributes(content_protection_xml.attributes(), - xml_node_content_protection)) { - LOG(ERROR) << "Failed to set attributes for ContentProtection."; - return false; - } - - if (!AddSubelements(content_protection_xml.subelements(), - xml_node_content_protection)) { - LOG(ERROR) << "Failed to add sublements to ContentProtection."; - return false; - } - - // Add 'schemeIdUri' and 'value' attributes after SetAttributes() to avoid - // being overridden by content_protection_xml.attributes(). - xml_node_content_protection->SetStringAttribute("schemeIdUri", scheme_id_uri); - - if (content_protection_xml.has_value()) { - // Note that |value| is an optional field. - xml_node_content_protection->SetStringAttribute( - "value", content_protection_xml.value()); - } - - return true; -} - bool PopulateSegmentTimeline(const std::list& segment_infos, XmlNode* segment_timeline) { for (std::list::const_iterator it = segment_infos.begin(); @@ -199,6 +73,36 @@ bool XmlNode::AddChild(ScopedXmlPtr::type child) { return true; } +bool XmlNode::AddElements(const std::vector& elements) { + for (size_t element_index = 0; element_index < elements.size(); + ++element_index) { + const Element& child_element = elements[element_index]; + XmlNode child_node(child_element.name.c_str()); + for (std::map::const_iterator attribute_it = + child_element.attributes.begin(); + attribute_it != child_element.attributes.end(); ++attribute_it) { + child_node.SetStringAttribute(attribute_it->first.c_str(), + attribute_it->second); + } + // Recursively set children for the child. + if (!child_node.AddElements(child_element.subelements)) + return false; + + child_node.SetContent(child_element.content); + + if (!xmlAddChild(node_.get(), child_node.GetRawPtr())) { + LOG(ERROR) << "Failed to set child " << child_element.name + << " to parent element " + << reinterpret_cast(node_->name); + return false; + } + // Reaching here means the ownership of |child_node| transfered to |node_|. + // Release the pointer so that it doesn't get destructed in this scope. + ignore_result(child_node.Release()); + } + return true; +} + void XmlNode::SetStringAttribute(const char* attribute_name, const std::string& attribute) { DCHECK(node_); @@ -265,39 +169,15 @@ bool RepresentationBaseXmlNode::AddContentProtectionElements( return true; } -bool RepresentationBaseXmlNode::AddContentProtectionElementsFromMediaInfo( - const MediaInfo& media_info) { - const bool has_content_protections = - media_info.content_protections().size() > 0; - - if (!has_content_protections) - return true; - - for (int i = 0; i < media_info.content_protections().size(); ++i) { - const ContentProtectionXml& content_protection_xml = - media_info.content_protections(i); - XmlNode content_protection_node("ContentProtection"); - if (!TranslateToContentProtectionXmlNode(content_protection_xml, - &content_protection_node)) { - LOG(ERROR) << "Failed to make ContentProtection element from MediaInfo."; - return false; - } - - if (!AddChild(content_protection_node.PassScopedPtr())) { - LOG(ERROR) << "Failed to add ContentProtection to Representation."; - return false; - } - } - - return true; -} - bool RepresentationBaseXmlNode::AddContentProtectionElement( const ContentProtectionElement& content_protection_element) { XmlNode content_protection_node("ContentProtection"); - content_protection_node.SetStringAttribute("value", - content_protection_element.value); + // @value is an optional attribute. + if (!content_protection_element.value.empty()) { + content_protection_node.SetStringAttribute( + "value", content_protection_element.value); + } content_protection_node.SetStringAttribute( "schemeIdUri", content_protection_element.scheme_id_uri); @@ -312,7 +192,10 @@ bool RepresentationBaseXmlNode::AddContentProtectionElement( attributes_it->second); } - content_protection_node.SetContent(content_protection_element.subelements); + if (!content_protection_node.AddElements( + content_protection_element.subelements)) { + return false; + } return AddChild(content_protection_node.PassScopedPtr()); } diff --git a/packager/mpd/base/xml/xml_node.h b/packager/mpd/base/xml/xml_node.h index c94b03bb9c..ff980abddf 100644 --- a/packager/mpd/base/xml/xml_node.h +++ b/packager/mpd/base/xml/xml_node.h @@ -41,6 +41,9 @@ class XmlNode { /// @return true on success, false otherwise. bool AddChild(ScopedXmlPtr::type child); + /// Adds Elements to this node using the Element struct. + bool AddElements(const std::vector& elements); + /// Set a string attribute. /// @param attribute_name The name (lhs) of the attribute. /// @param attribute The value (rhs) of the attribute. @@ -62,8 +65,9 @@ class XmlNode { void SetId(uint32_t id); /// Set the contents of an XML element using a string. - /// Note: This function does not work well with AddChild(). Use either - /// AddChild() or SetContent() when setting the content of this node. + /// This cannot set child elements because <> will become < and &rt; + /// This should be used to set the text for the element, e.g. setting + /// a URL for element. /// @param content is a string containing the text-encoded child elements to /// be added to the element. void SetContent(const std::string& content); @@ -95,12 +99,6 @@ class RepresentationBaseXmlNode : public XmlNode { bool AddContentProtectionElements( const std::list& content_protection_elements); - /// Add a ContentProtection elements to this element. - /// @param media_info is a MediaInfo containing the ContentProtection - /// elements to add. - /// @return true on success, false otherwise. - bool AddContentProtectionElementsFromMediaInfo(const MediaInfo& media_info); - protected: explicit RepresentationBaseXmlNode(const char* name); diff --git a/packager/mpd/base/xml/xml_node_unittest.cc b/packager/mpd/base/xml/xml_node_unittest.cc index d44a386177..bb6a8eb1ef 100644 --- a/packager/mpd/base/xml/xml_node_unittest.cc +++ b/packager/mpd/base/xml/xml_node_unittest.cc @@ -84,7 +84,7 @@ class RepresentationTest : public ::testing::Test { // Make sure XmlEqual() is functioning correctly. // TODO(rkuroiwa): Move this to a separate file. This requires it to be TEST_F // due to gtest /test -TEST_F(RepresentationTest, MetaTest_XmlEqual) { +TEST_F(RepresentationTest, MetaTestXmlElementsEqual) { static const char kXml1[] = "\n" " ) (for both s) will return "content1content2". +// But if it is run on for the first XML, it will return "content1", but +// for second XML will return "c". +TEST_F(RepresentationTest, MetaTestXmlEqualDifferentContent) { + ASSERT_FALSE(XmlEqual( + "content1content2", + "content1content2")); +} + +// Verify that AddContentProtectionElements work. +// xmlReadMemory() (used in XmlEqual()) doesn't like XML fragments that have +// namespaces without context, e.g. element. +// The MpdBuilderTests work because the MPD element has xmlns:cenc attribute. +// Tests that have is in mpd_builder_unittest. +TEST_F(RepresentationTest, AddContentProtectionElements) { + std::list content_protections; + ContentProtectionElement content_protection_widevine; + content_protection_widevine.scheme_id_uri = + "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; + content_protection_widevine.value = "SOME bogus Widevine DRM version"; + Element any_element; + any_element.name = "AnyElement"; + any_element.content = "any content"; + content_protection_widevine.subelements.push_back(any_element); + content_protections.push_back(content_protection_widevine); + + ContentProtectionElement content_protection_clearkey; + content_protection_clearkey.scheme_id_uri = + "urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b"; + content_protections.push_back(content_protection_clearkey); + + representation_.AddContentProtectionElements(content_protections); + ScopedXmlPtr::type doc(MakeDoc(representation_.PassScopedPtr())); + ASSERT_TRUE(XmlEqual( "\n" " \n" - " \n" + " schemeIdUri=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\"\n" + " value=\"SOME bogus Widevine DRM version\">\n" + " any content\n" " \n" - ""; - - MediaInfo media_info; - MediaInfo::ContentProtectionXml* content_protection_xml = - media_info.add_content_protections(); - content_protection_xml->set_scheme_id_uri("http://www.foo.com/drm"); - content_protection_xml->set_value("somevalue"); - AddAttribute("a", "1", content_protection_xml); - AddAttribute("b", "2", content_protection_xml); - - MediaInfo::ContentProtectionXml::Element* subelement = - content_protection_xml->add_subelements(); - subelement->set_name("TestSubElement"); - AddAttribute("c", "3", subelement); - AddAttribute("d", "4", subelement); - - ASSERT_TRUE( - representation_.AddContentProtectionElementsFromMediaInfo(media_info)); - ScopedXmlPtr::type doc(MakeDoc(representation_.PassScopedPtr())); - ASSERT_TRUE( - XmlEqual(kExpectedRepresentaionString, doc.get())); + " " + " \n" + "", + doc.get())); } // Some template names cannot be used for init segment name. diff --git a/packager/mpd/test/data/encrypted_audio_media_info_expected_output.txt b/packager/mpd/test/data/encrypted_audio_media_info_expected_output.txt index 27990319da..0922e6838d 100644 --- a/packager/mpd/test/data/encrypted_audio_media_info_expected_output.txt +++ b/packager/mpd/test/data/encrypted_audio_media_info_expected_output.txt @@ -4,7 +4,6 @@ - encrypted_audio.mp4 diff --git a/packager/mpd/test/xml_compare.cc b/packager/mpd/test/xml_compare.cc index f916f95250..afb025c059 100644 --- a/packager/mpd/test/xml_compare.cc +++ b/packager/mpd/test/xml_compare.cc @@ -8,15 +8,16 @@ #include #include "packager/base/logging.h" +#include "packager/base/strings/string_util.h" #include "packager/mpd/base/xml/scoped_xml_ptr.h" +#include "packager/third_party/libxml/src/include/libxml/parser.h" namespace edash_packager { namespace { xml::ScopedXmlPtr::type GetDocFromString(const std::string& xml_str) { - xml::ScopedXmlPtr::type schema_as_doc( - xmlReadMemory(xml_str.data(), xml_str.size(), NULL, NULL, 0)); - + xml::ScopedXmlPtr::type schema_as_doc(xmlReadMemory( + xml_str.data(), xml_str.size(), NULL, NULL, 0)); return schema_as_doc.Pass(); } @@ -74,12 +75,33 @@ bool CompareNames(xmlNodePtr node1, xmlNodePtr node2) { return xmlStrcmp(node1->name, node2->name) == 0; } +bool CompareContents(xmlNodePtr node1, xmlNodePtr node2) { + std::string node1_content = + reinterpret_cast(xmlNodeGetContent(node1)); + std::string node2_content = + reinterpret_cast(xmlNodeGetContent(node2)); + base::ReplaceChars(node1_content, "\n", "", &node1_content); + base::TrimString(node1_content, " ", &node1_content); + base::ReplaceChars(node2_content, "\n", "", &node2_content); + base::TrimString(node2_content, " ", &node2_content); + DVLOG(2) << "Comparing contents of " + << reinterpret_cast(node1->name) << "\n" + << "First node's content:\n" << node1_content << "\n" + << "Second node's content:\n" << node2_content; + const bool same_content = node1_content == node2_content; + LOG_IF(ERROR, !same_content) + << "Contents of " << reinterpret_cast(node1->name) + << " do not match.\n" + << "First node's content:\n" << node1_content << "\n" + << "Second node's content:\n" << node2_content; + return same_content; +} + // Recursively check the elements. // Note that the terminating condition of the recursion is when the children do // not match (inside the loop). bool CompareNodes(xmlNodePtr node1, xmlNodePtr node2) { DCHECK(node1 && node2); - if (!CompareNames(node1, node2)) { LOG(ERROR) << "Names of the nodes do not match: " << reinterpret_cast(node1->name) << " " @@ -96,6 +118,12 @@ bool CompareNodes(xmlNodePtr node1, xmlNodePtr node2) { xmlNodePtr node1_child = xmlFirstElementChild(node1); xmlNodePtr node2_child = xmlFirstElementChild(node2); + if (!node1_child && !node2_child) { + // Note that xmlFirstElementChild() returns NULL if there are only + // text type children. + return CompareContents(node1, node2); + } + do { if (!node1_child || !node2_child) return node1_child == node2_child;