From c48a94d60fedd80214483bbb02f226f1dc4ba577 Mon Sep 17 00:00:00 2001 From: Rintaro Kuroiwa Date: Tue, 21 Jul 2015 23:57:21 -0700 Subject: [PATCH] Add MpdNotifier that tries to generate IOP compliant MPD - Add DashIopMpdNotifier for generating DASH-IF IOP v3 compliant MPD. Change-Id: I201b4cdafde6bb963f74d4bbaee3fecc432cb9d7 --- packager/mpd/base/dash_iop_mpd_notifier.cc | 254 +++++++ packager/mpd/base/dash_iop_mpd_notifier.h | 113 +++ .../base/dash_iop_mpd_notifier_unittest.cc | 692 ++++++++++++++++++ packager/mpd/base/mock_mpd_builder.cc | 5 +- packager/mpd/base/mock_mpd_builder.h | 10 +- packager/mpd/base/mpd_builder.cc | 8 + packager/mpd/base/mpd_builder.h | 10 +- packager/mpd/base/mpd_builder_unittest.cc | 4 +- packager/mpd/base/mpd_notifier.h | 2 + packager/mpd/base/mpd_notifier_util.cc | 190 +++++ packager/mpd/base/mpd_notifier_util.h | 63 ++ packager/mpd/base/simple_mpd_notifier.cc | 172 +---- packager/mpd/base/simple_mpd_notifier.h | 13 +- .../mpd/base/simple_mpd_notifier_unittest.cc | 6 +- packager/mpd/mpd.gyp | 7 +- 15 files changed, 1358 insertions(+), 191 deletions(-) create mode 100644 packager/mpd/base/dash_iop_mpd_notifier.cc create mode 100644 packager/mpd/base/dash_iop_mpd_notifier.h create mode 100644 packager/mpd/base/dash_iop_mpd_notifier_unittest.cc create mode 100644 packager/mpd/base/mpd_notifier_util.cc create mode 100644 packager/mpd/base/mpd_notifier_util.h diff --git a/packager/mpd/base/dash_iop_mpd_notifier.cc b/packager/mpd/base/dash_iop_mpd_notifier.cc new file mode 100644 index 0000000000..2b3daa5f62 --- /dev/null +++ b/packager/mpd/base/dash_iop_mpd_notifier.cc @@ -0,0 +1,254 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/mpd/base/dash_iop_mpd_notifier.h" + +#include "packager/mpd/base/media_info.pb.h" +#include "packager/mpd/base/mpd_notifier_util.h" + +namespace edash_packager { + +namespace { + +const int kStartingGroupId = 1; + +// The easiest way to check whether two protobufs are equal, is to compare the +// serialized version. +bool ProtectedContentEq( + const MediaInfo::ProtectedContent& content_protection1, + const MediaInfo::ProtectedContent& content_protection2) { + std::string s1; + std::string s2; + return content_protection1.SerializeToString(&s1) && + content_protection2.SerializeToString(&s2) && + s1 == s2; +} + +std::set GetUUIDs( + const MediaInfo::ProtectedContent& protected_content) { + std::set uuids; + for (int i = 0; i < protected_content.content_protection_entry().size(); + ++i) { + const MediaInfo::ProtectedContent::ContentProtectionEntry& entry = + protected_content.content_protection_entry(i); + uuids.insert(entry.uuid()); + } + return uuids; +} + +} // namespace + +DashIopMpdNotifier::DashIopMpdNotifier( + DashProfile dash_profile, + const MpdOptions& mpd_options, + const std::vector& base_urls, + const std::string& output_path) + : MpdNotifier(dash_profile), + output_path_(output_path), + mpd_builder_(new MpdBuilder(dash_profile == kLiveProfile + ? MpdBuilder::kDynamic + : MpdBuilder::kStatic, + mpd_options)), + next_group_id_(kStartingGroupId) { + DCHECK(dash_profile == kLiveProfile || dash_profile == kOnDemandProfile); + for (size_t i = 0; i < base_urls.size(); ++i) + mpd_builder_->AddBaseUrl(base_urls[i]); +} + +DashIopMpdNotifier::~DashIopMpdNotifier() {} + +bool DashIopMpdNotifier::Init() { + return true; +} + +bool DashIopMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, + uint32_t* container_id) { + DCHECK(container_id); + + ContentType content_type = GetContentType(media_info); + if (content_type == kContentTypeUnknown) + return false; + + base::AutoLock auto_lock(lock_); + std::string lang; + if (media_info.has_audio_info()) { + lang = media_info.audio_info().language(); + } + + AdaptationSet* adaptation_set = + GetAdaptationSetForMediaInfo(media_info, content_type, lang); + DCHECK(adaptation_set); + + MediaInfo adjusted_media_info(media_info); + MpdBuilder::MakePathsRelativeToMpd(output_path_, &adjusted_media_info); + Representation* representation = + adaptation_set->AddRepresentation(adjusted_media_info); + if (!representation) + return false; + + SetGroupId(content_type, lang, adaptation_set); + + *container_id = representation->id(); + DCHECK(!ContainsKey(representation_map_, representation->id())); + representation_map_[representation->id()] = representation; + + if (mpd_builder_->type() == MpdBuilder::kStatic) + return WriteMpdToFile(output_path_, mpd_builder_.get()); + return true; +} + +bool DashIopMpdNotifier::NotifySampleDuration(uint32_t container_id, + uint32_t sample_duration) { + base::AutoLock auto_lock(lock_); + RepresentationMap::iterator it = representation_map_.find(container_id); + if (it == representation_map_.end()) { + LOG(ERROR) << "Unexpected container_id: " << container_id; + return false; + } + it->second->SetSampleDuration(sample_duration); + return WriteMpdToFile(output_path_, mpd_builder_.get()); +} + +bool DashIopMpdNotifier::NotifyNewSegment(uint32_t container_id, + uint64_t start_time, + uint64_t duration, + uint64_t size) { + base::AutoLock auto_lock(lock_); + RepresentationMap::iterator it = representation_map_.find(container_id); + if (it == representation_map_.end()) { + LOG(ERROR) << "Unexpected container_id: " << container_id; + return false; + } + it->second->AddNewSegment(start_time, duration, size); + return WriteMpdToFile(output_path_, mpd_builder_.get()); +} + +bool DashIopMpdNotifier::AddContentProtectionElement( + uint32_t container_id, + const ContentProtectionElement& content_protection_element) { + // Intentionally not implemented because if a Representation gets a new + // element, then it might require moving the + // Representation out of the AdaptationSet. There's no logic to do that + // yet. + return false; +} + +AdaptationSet* DashIopMpdNotifier::GetAdaptationSetForMediaInfo( + const MediaInfo& media_info, + ContentType content_type, + const std::string& language) { + std::list& adaptation_sets = + adaptation_set_list_map_[content_type][language]; + if (adaptation_sets.empty()) + return NewAdaptationSet(media_info, language, &adaptation_sets); + + const bool has_protected_content = media_info.has_protected_content(); + + for (std::list::const_iterator adaptation_set_it = + adaptation_sets.begin(); + adaptation_set_it != adaptation_sets.end(); ++adaptation_set_it) { + ProtectedContentMap::const_iterator protected_content_it = + protected_content_map_.find((*adaptation_set_it)->id()); + + // If the AdaptationSet ID is not registered in the map, then it is clear + // content (or encrypted but doesn't need element + // possibly because the player knows how to handle it). + if (protected_content_it == protected_content_map_.end()) { + // Can reuse the AdaptationSet without content protection. + if (!has_protected_content) + return *adaptation_set_it; + continue; + } + + if (ProtectedContentEq(protected_content_it->second, + media_info.protected_content())) { + // Content protection info matches. Reuse the AdaptationSet. + return *adaptation_set_it; + } + } + + // None of the adaptation sets match with the new content protection. + // Need a new one. + return NewAdaptationSet(media_info, language, &adaptation_sets); +} + +// Get all the UUIDs of the AdaptationSet. If another AdaptationSet has the +// same UUIDs then those should be groupable. +void DashIopMpdNotifier::SetGroupId(ContentType type, + const std::string& language, + AdaptationSet* adaptation_set) { + if (adaptation_set->Group() >= 0) // @group already assigned. + 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. + if (protected_content_it == protected_content_map_.end()) { + DVLOG(1) << "No content protection set for AdaptationSet@id=" + << adaptation_set->id(); + return; + } + + // Get all the UUIDs of the ContentProtections in AdaptationSet. + std::set adaptation_set_uuids = + GetUUIDs(protected_content_it->second); + + std::list& same_type_adapatation_sets = + adaptation_set_list_map_[type][language]; + DCHECK(!same_type_adapatation_sets.empty()) + << "same_type_adapatation_sets should not be null, it should at least " + "contain adaptation_set"; + + for (std::list::iterator adaptation_set_it = + same_type_adapatation_sets.begin(); + adaptation_set_it != same_type_adapatation_sets.end(); + ++adaptation_set_it) { + const uint32_t loop_adaptation_set_id = (*adaptation_set_it)->id(); + if (loop_adaptation_set_id == adaptation_set->id() || + !ContainsKey(protected_content_map_, loop_adaptation_set_id)) { + continue; + } + + const MediaInfo::ProtectedContent& loop_protected_content = + protected_content_map_[loop_adaptation_set_id]; + if (static_cast(adaptation_set_uuids.size()) != + loop_protected_content.content_protection_entry().size()) { + // Different number of UUIDs, cannot be grouped. + 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; + } + } +} + +AdaptationSet* DashIopMpdNotifier::NewAdaptationSet( + const MediaInfo& media_info, + const std::string& language, + std::list* adaptation_sets) { + AdaptationSet* new_adaptation_set = mpd_builder_->AddAdaptationSet(language); + if (media_info.has_protected_content()) { + DCHECK(!ContainsKey(protected_content_map_, new_adaptation_set->id())); + protected_content_map_[new_adaptation_set->id()] = + media_info.protected_content(); + AddContentProtectionElements(media_info, new_adaptation_set); + } + adaptation_sets->push_back(new_adaptation_set); + return new_adaptation_set; +} + +} // namespace edash_packager diff --git a/packager/mpd/base/dash_iop_mpd_notifier.h b/packager/mpd/base/dash_iop_mpd_notifier.h new file mode 100644 index 0000000000..7d4c00cc8c --- /dev/null +++ b/packager/mpd/base/dash_iop_mpd_notifier.h @@ -0,0 +1,113 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MPD_BASE_DASH_IOP_MPD_NOTIFIER_H_ +#define MPD_BASE_DASH_IOP_MPD_NOTIFIER_H_ + +#include "packager/mpd/base/mpd_notifier.h" + +#include +#include +#include +#include + +#include "packager/mpd/base/mpd_builder.h" +#include "packager/mpd/base/mpd_notifier_util.h" +#include "packager/mpd/base/mpd_options.h" + +namespace edash_packager { + +/// This class is an MpdNotifier which will try its best to generate a +/// DASH IF IOPv3 compliant MPD. +/// For example, all elements must be right under +/// and cannot be under . +class DashIopMpdNotifier : public MpdNotifier { + public: + DashIopMpdNotifier(DashProfile dash_profile, + const MpdOptions& mpd_options, + const std::vector& base_urls, + const std::string& output_path); + virtual ~DashIopMpdNotifier() OVERRIDE; + + /// @name MpdNotifier implemetation overrides. + /// @{ + virtual bool Init() OVERRIDE; + virtual bool NotifyNewContainer(const MediaInfo& media_info, + uint32_t* id) OVERRIDE; + virtual bool NotifySampleDuration(uint32_t container_id, + uint32_t sample_duration) OVERRIDE; + virtual bool NotifyNewSegment(uint32_t id, + uint64_t start_time, + uint64_t duration, + uint64_t size) OVERRIDE; + virtual bool AddContentProtectionElement( + uint32_t id, + const ContentProtectionElement& content_protection_element) OVERRIDE; + /// @} + + private: + friend class DashIopMpdNotifierTest; + + // Maps representation ID to Representation. + typedef std::map RepresentationMap; + + // Maps AdaptationSet ID to ProtectedContent. + typedef std::map ProtectedContentMap; + + // Checks the protected_content field of media_info and returns a non-null + // AdaptationSet* for a new Representation. + // This does not necessarily return a new AdaptationSet. If + // media_info.protected_content completely matches with an existing + // AdaptationSet, then it will return the pointer. + AdaptationSet* GetAdaptationSetForMediaInfo(const MediaInfo& media_info, + ContentType type, + const std::string& language); + + // Sets a group id for |adaptation_set| if applicable. + // If a group ID is already assigned, then this returns immediately. + // |type| and |language| are the type and language of |adaptation_set|. + void SetGroupId(ContentType type, + const std::string& language, + AdaptationSet* adaptation_set); + + // Helper function to get a new AdaptationSet; registers the values + // to the fields (maps) of the instance. + // If the media is encrypted, registers data to protected_content_map_. + AdaptationSet* NewAdaptationSet(const MediaInfo& media_info, + const std::string& language, + std::list* adaptation_sets); + + // Testing only method. Returns a pointer to MpdBuilder. + MpdBuilder* MpdBuilderForTesting() const { + return mpd_builder_.get(); + } + + // Testing only method. Sets mpd_builder_. + void SetMpdBuilderForTesting(scoped_ptr mpd_builder) { + mpd_builder_ = mpd_builder.Pass(); + } + + // [type][lang] = list + // Note: lang can be empty, e.g. for video. + std::map > > + adaptation_set_list_map_; + RepresentationMap representation_map_; + + // Used to check whether a Representation should be added to an AdaptationSet. + ProtectedContentMap protected_content_map_; + + // MPD output path. + std::string output_path_; + scoped_ptr mpd_builder_; + base::Lock lock_; + + // Next group ID to use for AdapationSets that can be grouped. + int next_group_id_; +}; + +} // namespace edash_packager + +#endif // MPD_BASE_DASH_IOP_MPD_NOTIFIER_H_ diff --git a/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc b/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc new file mode 100644 index 0000000000..b25bc4ba30 --- /dev/null +++ b/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc @@ -0,0 +1,692 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include +#include + +#include "packager/base/file_util.h" +#include "packager/base/files/file_path.h" +#include "packager/mpd/base/dash_iop_mpd_notifier.h" +#include "packager/mpd/base/mock_mpd_builder.h" +#include "packager/mpd/base/mpd_builder.h" +#include "packager/mpd/test/mpd_builder_test_helper.h" + +namespace edash_packager { + +using ::testing::_; +using ::testing::Eq; +using ::testing::InSequence; +using ::testing::Return; + +namespace { + +const char kValidMediaInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "container_type: 1\n"; +const uint32_t kDefaultAdaptationSetId = 0u; +const uint32_t kDefaultRepresentationId = 1u; +const int kDefaultGroupId = -1; + +bool ElementEqual(const edash_packager::Element& lhs, + const edash_packager::Element& rhs) { + const bool all_equal_except_sublement_check = + lhs.name == rhs.name && lhs.attributes.size() == rhs.attributes.size() && + std::equal(lhs.attributes.begin(), lhs.attributes.end(), + rhs.attributes.begin()) && + lhs.content == rhs.content && + lhs.subelements.size() == rhs.subelements.size(); + + if (!all_equal_except_sublement_check) { + return false; + } + + for (size_t i = 0; i < lhs.subelements.size(); ++i) { + if (!ElementEqual(lhs.subelements[i], rhs.subelements[i])) + return false; + } + return true; +} + +bool ContentProtectionElementEqual( + const edash_packager::ContentProtectionElement& lhs, + const edash_packager::ContentProtectionElement& rhs) { + const bool all_equal_except_sublement_check = + lhs.value == rhs.value && lhs.scheme_id_uri == rhs.scheme_id_uri && + lhs.additional_attributes.size() == rhs.additional_attributes.size() && + std::equal(lhs.additional_attributes.begin(), + lhs.additional_attributes.end(), + rhs.additional_attributes.begin()) && + lhs.subelements.size() == rhs.subelements.size(); + + if (!all_equal_except_sublement_check) + return false; + + for (size_t i = 0; i < lhs.subelements.size(); ++i) { + if (!ElementEqual(lhs.subelements[i], rhs.subelements[i])) + return false; + } + return true; +} + +MATCHER_P(ContentProtectionElementEq, expected, "") { + return ContentProtectionElementEqual(arg, expected); +} + +} // namespace + +// TODO(rkuroiwa): This is almost exactly the same as SimpleMpdNotifierTest but +// replaced all SimpleMpd with DashIopMpd, +// use typed tests +// (https://code.google.com/p/googletest/wiki/AdvancedGuide#Typed_Tests); +// also because SimpleMpdNotifier and DashIopMpdNotifier have common behavior +// for most of the public functions. +class DashIopMpdNotifierTest + : public ::testing::TestWithParam { + protected: + DashIopMpdNotifierTest() + : default_mock_adaptation_set_( + new MockAdaptationSet(kDefaultAdaptationSetId)), + default_mock_representation_( + new MockRepresentation(kDefaultRepresentationId)) {} + + virtual void SetUp() OVERRIDE { + ASSERT_TRUE(base::CreateTemporaryFile(&temp_file_path_)); + output_path_ = temp_file_path_.value(); + ON_CALL(*default_mock_adaptation_set_, Group()) + .WillByDefault(Return(kDefaultGroupId)); + } + + virtual void TearDown() OVERRIDE { + base::DeleteFile(temp_file_path_, false /* non recursive, just 1 file */); + } + + MpdBuilder::MpdType GetMpdBuilderType(const DashIopMpdNotifier& notifier) { + return notifier.MpdBuilderForTesting()->type(); + } + + void SetMpdBuilder(DashIopMpdNotifier* notifier, + scoped_ptr mpd_builder) { + notifier->SetMpdBuilderForTesting(mpd_builder.Pass()); + } + + MpdBuilder::MpdType mpd_type() { + return GetParam(); + } + + DashProfile dash_profile() { + return mpd_type() == MpdBuilder::kStatic ? kOnDemandProfile : kLiveProfile; + } + + // Use output_path_ for specifying the MPD output path so that + // WriteMpdToFile() doesn't crash. + std::string output_path_; + const MpdOptions empty_mpd_option_; + const std::vector empty_base_urls_; + + // Default AdaptationSet mock. + scoped_ptr default_mock_adaptation_set_; + scoped_ptr default_mock_representation_; + + private: + base::FilePath temp_file_path_; +}; + +// Verify that it creates the correct MpdBuilder type using DashProfile passed +// to the constructor. +TEST_F(DashIopMpdNotifierTest, CreateCorrectMpdBuilderType) { + DashIopMpdNotifier on_demand_notifier(kOnDemandProfile, empty_mpd_option_, + empty_base_urls_, output_path_); + EXPECT_TRUE(on_demand_notifier.Init()); + EXPECT_EQ(MpdBuilder::kStatic, GetMpdBuilderType(on_demand_notifier)); + DashIopMpdNotifier live_notifier(kLiveProfile, empty_mpd_option_, + empty_base_urls_, output_path_); + EXPECT_TRUE(live_notifier.Init()); + EXPECT_EQ(MpdBuilder::kDynamic, GetMpdBuilderType(live_notifier)); +} + +// Verify that basic VOD NotifyNewContainer() operation works. +// No encrypted contents. +TEST_P(DashIopMpdNotifierTest, NotifyNewContainer) { + DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_, + empty_base_urls_, output_path_); + + scoped_ptr mock_mpd_builder(new MockMpdBuilder(mpd_type())); + + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(default_mock_adaptation_set_.get())); + EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) + .WillOnce(Return(default_mock_representation_.get())); + + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + uint32_t unused_container_id; + SetMpdBuilder(¬ifier, mock_mpd_builder.PassAs()); + EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), + &unused_container_id)); +} + +// Verify VOD NotifyNewContainer() operation works with different +// MediaInfo::ProtectedContent. +// Two AdaptationSets should be created. +// Different DRM so they won't be grouped. +TEST_P(DashIopMpdNotifierTest, + NotifyNewContainersWithDifferentProtectedContent) { + DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_, + empty_base_urls_, output_path_); + scoped_ptr mock_mpd_builder(new MockMpdBuilder(mpd_type())); + + // Note they both have different (bogus) pssh, like real use case. + // default Key ID = _default_key_id_ + const char kSdProtectedContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 640\n" + " height: 360\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'pssh1'\n" + " }\n" + " default_key_id: '_default_key_id_'\n" + "}\n" + "container_type: 1\n"; + + // default Key ID = .default.key.id. + const char kHdProtectedContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'anotheruuid'\n" + " name_version: 'SomeOtherProtection version 3'\n" + " pssh: 'pssh2'\n" + " }\n" + " default_key_id: '.default.key.id.'\n" + "}\n" + "container_type: 1\n"; + + // Check that the right ContentProtectionElements for SD is created. + // HD is the same case, so not checking. + ContentProtectionElement mp4_protection; + mp4_protection.scheme_id_uri = "urn:mpeg:dash:mp4protection:2011"; + mp4_protection.value = "cenc"; + // This should match the "_default_key_id_" above, but taking it as hex data + // and converted to UUID format. + mp4_protection.additional_attributes["cenc:default_KID"] = + "5f646566-6175-6c74-5f6b-65795f69645f"; + ContentProtectionElement sd_my_drm; + sd_my_drm.scheme_id_uri = "urn:uuid:myuuid"; + sd_my_drm.value = "MyContentProtection version 1"; + Element cenc_pssh; + cenc_pssh.name = "cenc:pssh"; + cenc_pssh.content = "cHNzaDE="; // Base64 encoding of 'pssh1'. + sd_my_drm.subelements.push_back(cenc_pssh); + + // Not using default mocks in this test so that we can keep track of + // mocks by named mocks. + const uint32_t kSdAdaptationSetId = 2u; + const uint32_t kHdAdaptationSetId = 3u; + scoped_ptr sd_adaptation_set( + new MockAdaptationSet(kSdAdaptationSetId)); + scoped_ptr 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; + scoped_ptr sd_representation( + new MockRepresentation(kSdRepresentation)); + scoped_ptr hd_representation( + new MockRepresentation(kHdRepresentation)); + + InSequence in_sequence; + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(sd_adaptation_set.get())); + EXPECT_CALL( + *sd_adaptation_set, + AddContentProtectionElement(ContentProtectionElementEq(mp4_protection))); + EXPECT_CALL(*sd_adaptation_set, AddContentProtectionElement( + ContentProtectionElementEq(sd_my_drm))); + EXPECT_CALL(*sd_adaptation_set, AddRepresentation(_)) + .WillOnce(Return(sd_representation.get())); + + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(hd_adaptation_set.get())); + // Called twice for the same reason as above. + EXPECT_CALL(*hd_adaptation_set, AddContentProtectionElement(_)).Times(2); + EXPECT_CALL(*hd_adaptation_set, AddRepresentation(_)) + .WillOnce(Return(hd_representation.get())); + + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + uint32_t unused_container_id; + SetMpdBuilder(¬ifier, mock_mpd_builder.PassAs()); + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(kSdProtectedContent), &unused_container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(kHdProtectedContent), &unused_container_id)); +} + +// Verify VOD NotifyNewContainer() operation works with same +// MediaInfo::ProtectedContent. Only one AdaptationSet should be +// created. +TEST_P(DashIopMpdNotifierTest, NotifyNewContainersWithSameProtectedContent) { + DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_, + empty_base_urls_, output_path_); + scoped_ptr mock_mpd_builder(new MockMpdBuilder(mpd_type())); + + // These have the same default key ID and PSSH. + const char kSdProtectedContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 640\n" + " height: 360\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'psshbox'\n" + " }\n" + " default_key_id: '.DEFAULT.KEY.ID.'\n" + "}\n" + "container_type: 1\n"; + + const char kHdProtectedContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'psshbox'\n" + " }\n" + " default_key_id: '.DEFAULT.KEY.ID.'\n" + "}\n" + "container_type: 1\n"; + + ContentProtectionElement mp4_protection; + mp4_protection.scheme_id_uri = "urn:mpeg:dash:mp4protection:2011"; + mp4_protection.value = "cenc"; + // This should match the ".DEFAULT.KEY.ID." above, but taking it as hex data + // and converted to UUID format. + mp4_protection.additional_attributes["cenc:default_KID"] = + "2e444546-4155-4c54-2e4b-45592e49442e"; + ContentProtectionElement my_drm; + my_drm.scheme_id_uri = "urn:uuid:myuuid"; + my_drm.value = "MyContentProtection version 1"; + Element cenc_pssh; + cenc_pssh.name = "cenc:pssh"; + cenc_pssh.content = "cHNzaGJveA=="; // Base64 encoding of 'psshbox'. + my_drm.subelements.push_back(cenc_pssh); + + const uint32_t kSdRepresentation = 6u; + const uint32_t kHdRepresentation = 7u; + scoped_ptr sd_representation( + new MockRepresentation(kSdRepresentation)); + scoped_ptr 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())); + EXPECT_CALL( + *default_mock_adaptation_set_, + AddContentProtectionElement(ContentProtectionElementEq(mp4_protection))); + EXPECT_CALL(*default_mock_adaptation_set_, + AddContentProtectionElement(ContentProtectionElementEq(my_drm))); + EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) + .WillOnce(Return(sd_representation.get())); + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + // For second representation, no new AddAdaptationSet(). + // And make sure that AddContentProtection() is not called. + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)).Times(0); + EXPECT_CALL(*default_mock_adaptation_set_, AddContentProtectionElement(_)) + .Times(0); + EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) + .WillOnce(Return(hd_representation.get())); + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + uint32_t unused_container_id; + SetMpdBuilder(¬ifier, mock_mpd_builder.PassAs()); + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(kSdProtectedContent), &unused_container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(kHdProtectedContent), &unused_container_id)); +} + +// AddContentProtection() should not work and should always return false. +TEST_P(DashIopMpdNotifierTest, AddContentProtection) { + DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_, + empty_base_urls_, output_path_); + + scoped_ptr mock_mpd_builder(new MockMpdBuilder(mpd_type())); + + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(default_mock_adaptation_set_.get())); + EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) + .WillOnce(Return(default_mock_representation_.get())); + + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + uint32_t container_id; + SetMpdBuilder(¬ifier, mock_mpd_builder.PassAs()); + EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), + &container_id)); + + ContentProtectionElement empty_content_protection_element; + EXPECT_FALSE(notifier.AddContentProtectionElement( + container_id, empty_content_protection_element)); +} + +// Default Key IDs are different but if the content protection UUIDs match, then +// they can be in the same group. +// 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. +// 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) { + DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_, + empty_base_urls_, output_path_); + scoped_ptr mock_mpd_builder(new MockMpdBuilder(mpd_type())); + + // These have the same default key ID and PSSH. + const char kSdProtectedContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 640\n" + " height: 360\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'pssh_sd'\n" + " }\n" + " default_key_id: '_default_key_id_'\n" + "}\n" + "container_type: 1\n"; + + const char kHdProtectedContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'pssh_hd'\n" + " }\n" + " default_key_id: '.DEFAULT.KEY.ID.'\n" + "}\n" + "container_type: 1\n"; + + const uint32_t kSdAdaptationSetId = 6u; + const uint32_t kHdAdaptationSetId = 7u; + scoped_ptr sd_adaptation_set( + new MockAdaptationSet(kSdAdaptationSetId)); + scoped_ptr 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; + scoped_ptr sd_representation( + new MockRepresentation(kSdRepresentation)); + scoped_ptr hd_representation( + new MockRepresentation(kHdRepresentation)); + + InSequence in_sequence; + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(sd_adaptation_set.get())); + EXPECT_CALL(*sd_adaptation_set, AddContentProtectionElement(_)).Times(2); + EXPECT_CALL(*sd_adaptation_set, AddRepresentation(_)) + .WillOnce(Return(sd_representation.get())); + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(hd_adaptation_set.get())); + EXPECT_CALL(*hd_adaptation_set, AddContentProtectionElement(_)).Times(2); + 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)); + + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + // 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; + SetMpdBuilder(¬ifier, mock_mpd_builder.PassAs()); + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(kSdProtectedContent), &unused_container_id)); + 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)); + + // Add another content that has the same protected content and make sure that + // it gets added to the existing group. + const char k4kProtectedContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 4096\n" + " height: 2160\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'pssh_4k'\n" + " }\n" + " default_key_id: 'some16bytestring'\n" + "}\n" + "container_type: 1\n"; + + const uint32_t k4kAdaptationSetId = 4000u; + scoped_ptr fourk_adaptation_set( + new MockAdaptationSet(k4kAdaptationSetId)); + ON_CALL(*fourk_adaptation_set, Group()) + .WillByDefault(Return(kDefaultGroupId)); + + const uint32_t k4kRepresentationId = 4001u; + scoped_ptr fourk_representation( + new MockRepresentation(k4kRepresentationId)); + + EXPECT_CALL(*mock_mpd_builder_raw, AddAdaptationSet(_)) + .WillOnce(Return(fourk_adaptation_set.get())); + EXPECT_CALL(*fourk_adaptation_set, AddContentProtectionElement(_)).Times(2); + EXPECT_CALL(*fourk_adaptation_set, AddRepresentation(_)) + .WillOnce(Return(fourk_representation.get())); + + // Same group ID should be set. + EXPECT_CALL(*fourk_adaptation_set, SetGroup(kExpectedGroupId)); + + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder_raw, ToString(_)).WillOnce(Return(true)); + + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(k4kProtectedContent), &unused_container_id)); +} + +// Even if the UUIDs match, video and audio AdaptationSets should not be grouped +// together. +TEST_P(DashIopMpdNotifierTest, DoNotSetGroupIfContentTypesDifferent) { + DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_, + empty_base_urls_, output_path_); + scoped_ptr mock_mpd_builder(new MockMpdBuilder(mpd_type())); + + // These have the same default key ID and PSSH. + const char kVideoContent[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 640\n" + " height: 360\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'pssh_video'\n" + " }\n" + " default_key_id: '_default_key_id_'\n" + "}\n" + "container_type: 1\n"; + + const char kAudioContent[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + "}\n" + "protected_content {\n" + " content_protection_entry {\n" + " uuid: 'myuuid'\n" + " name_version: 'MyContentProtection version 1'\n" + " pssh: 'pssh_audio'\n" + " }\n" + " default_key_id: '_default_key_id_'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: 1\n" + "media_duration_seconds: 10.5\n"; + + const uint32_t kSdAdaptationSetId = 6u; + const uint32_t kHdAdaptationSetId = 7u; + scoped_ptr video_adaptation_set( + new MockAdaptationSet(kSdAdaptationSetId)); + scoped_ptr audio_adaptation_set( + new MockAdaptationSet(kHdAdaptationSetId)); + + 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 kSdRepresentation = 8u; + const uint32_t kHdRepresentation = 9u; + scoped_ptr sd_representation( + new MockRepresentation(kSdRepresentation)); + scoped_ptr hd_representation( + new MockRepresentation(kHdRepresentation)); + + InSequence in_sequence; + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(video_adaptation_set.get())); + EXPECT_CALL(*video_adaptation_set, AddContentProtectionElement(_)).Times(2); + EXPECT_CALL(*video_adaptation_set, AddRepresentation(_)) + .WillOnce(Return(sd_representation.get())); + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + .WillOnce(Return(audio_adaptation_set.get())); + EXPECT_CALL(*audio_adaptation_set, AddContentProtectionElement(_)).Times(2); + EXPECT_CALL(*audio_adaptation_set, AddRepresentation(_)) + .WillOnce(Return(hd_representation.get())); + + if (mpd_type() == MpdBuilder::kStatic) + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + uint32_t unused_container_id; + SetMpdBuilder(¬ifier, mock_mpd_builder.PassAs()); + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(kVideoContent), &unused_container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer( + ConvertToMediaInfo(kAudioContent), &unused_container_id)); +} + +INSTANTIATE_TEST_CASE_P(StaticAndDynamic, + DashIopMpdNotifierTest, + ::testing::Values(MpdBuilder::kStatic, + MpdBuilder::kDynamic)); + +} // namespace edash_packager diff --git a/packager/mpd/base/mock_mpd_builder.cc b/packager/mpd/base/mock_mpd_builder.cc index 2d430b745e..b9bbf747ce 100644 --- a/packager/mpd/base/mock_mpd_builder.cc +++ b/packager/mpd/base/mock_mpd_builder.cc @@ -4,7 +4,6 @@ namespace edash_packager { namespace { -const uint32_t kAnyAdaptationSetId = 1; const char kEmptyLang[] = ""; const MpdOptions kDefaultMpdOptions; const MpdBuilder::MpdType kDefaultMpdType = MpdBuilder::kStatic; @@ -16,8 +15,8 @@ MockMpdBuilder::MockMpdBuilder(MpdType type) : MpdBuilder(type, kDefaultMpdOptions) {} MockMpdBuilder::~MockMpdBuilder() {} -MockAdaptationSet::MockAdaptationSet() - : AdaptationSet(kAnyAdaptationSetId, +MockAdaptationSet::MockAdaptationSet(uint32_t adaptation_set_id) + : AdaptationSet(adaptation_set_id, kEmptyLang, kDefaultMpdOptions, kDefaultMpdType, diff --git a/packager/mpd/base/mock_mpd_builder.h b/packager/mpd/base/mock_mpd_builder.h index 67bc251fc2..76e66d3d46 100644 --- a/packager/mpd/base/mock_mpd_builder.h +++ b/packager/mpd/base/mock_mpd_builder.h @@ -17,7 +17,7 @@ namespace edash_packager { class MockMpdBuilder : public MpdBuilder { public: - // type indicates whether the MPD should be for VOD or live content (kStatic + // |type| indicates whether the MPD should be for VOD or live content (kStatic // for VOD profile, or kDynamic for live profile). explicit MockMpdBuilder(MpdType type); virtual ~MockMpdBuilder() OVERRIDE; @@ -28,10 +28,16 @@ class MockMpdBuilder : public MpdBuilder { class MockAdaptationSet : public AdaptationSet { public: - MockAdaptationSet(); + // |adaptation_set_id| is the id for the AdaptationSet. + explicit MockAdaptationSet(uint32_t adaptation_set_id); virtual ~MockAdaptationSet() OVERRIDE; MOCK_METHOD1(AddRepresentation, Representation*(const MediaInfo& media_info)); + MOCK_METHOD1(AddContentProtectionElement, + void(const ContentProtectionElement& element)); + + MOCK_METHOD1(SetGroup, void(int group_number)); + MOCK_CONST_METHOD0(Group, int()); private: // Only for constructing the super class. Not used for testing. diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index 29186f4665..522c9ef84d 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -757,6 +757,14 @@ 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::OnNewSegmentForRepresentation(uint32_t representation_id, uint64_t start_time, uint64_t duration) { diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index be507a15b5..17ed80853c 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -187,7 +187,8 @@ class AdaptationSet { /// If @a element has {value, schemeIdUri} set and has /// {“value”, “schemeIdUri”} as key for @a additional_attributes, /// then the former is used. - void AddContentProtectionElement(const ContentProtectionElement& element); + virtual void AddContentProtectionElement( + const ContentProtectionElement& element); /// Set the Role element for this AdaptationSet. /// The Role element's is schemeIdUri='urn:mpeg:dash:role:2011'. @@ -213,9 +214,10 @@ class AdaptationSet { /// Note that group=0 is a special group, as mentioned in the DASH MPD /// specification. /// @param group_number is the value of AdaptatoinSet@group. - void set_group(int group_number) { - group_ = group_number; - } + virtual void SetGroup(int group_number); + + /// @return Returns the value for group. If not set, returns a negative value. + virtual int Group() const; // Must be unique in the Period. uint32_t id() const { return id_; } diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index 20c0c3fe92..99b9c27b69 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -351,14 +351,14 @@ TEST_F(CommonMpdBuilderTest, SetAdaptationSetGroup) { base::AtomicSequenceNumber sequence_counter; AdaptationSet adaptation_set(kAnyAdaptationSetId, "", MpdOptions(), MpdBuilder::kStatic, &sequence_counter); - adaptation_set.set_group(1); + adaptation_set.SetGroup(1); xml::ScopedXmlPtr::type 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.set_group(-1); + adaptation_set.SetGroup(-1); xml::ScopedXmlPtr::type xml_without_group(adaptation_set.GetXml()); EXPECT_NO_FATAL_FAILURE( ExpectAttributeNotSet("group", xml_without_group.get())); diff --git a/packager/mpd/base/mpd_notifier.h b/packager/mpd/base/mpd_notifier.h index fbf0891a29..27b0a29543 100644 --- a/packager/mpd/base/mpd_notifier.h +++ b/packager/mpd/base/mpd_notifier.h @@ -12,6 +12,8 @@ #include +#include "packager/base/macros.h" + namespace edash_packager { class MediaInfo; diff --git a/packager/mpd/base/mpd_notifier_util.cc b/packager/mpd/base/mpd_notifier_util.cc new file mode 100644 index 0000000000..0f83e4df68 --- /dev/null +++ b/packager/mpd/base/mpd_notifier_util.cc @@ -0,0 +1,190 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/mpd/base/mpd_notifier_util.h" + +#include "packager/base/strings/string_number_conversions.h" +#include "packager/base/strings/string_util.h" +#include "packager/media/file/file_closer.h" +#include "packager/media/file/file.h" +#include "packager/mpd/base/mpd_utils.h" + +namespace edash_packager { + +using media::File; +using media::FileCloser; + +namespace { + +// Helper function for adding ContentProtection for AdaptatoinSet or +// Representation. +// Works because both classes have AddContentProtectionElement(). +template +void AddContentProtectionElementsHelper(const MediaInfo& media_info, + ContentProtectionParent* parent) { + DCHECK(parent); + 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."; + } + } + + parent->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); + } + + parent->AddContentProtectionElement(drm_content_protection); + } + + LOG_IF(WARNING, protected_content.content_protection_entry().size() == 0) + << "The media is encrypted but no content protection specified."; +} + +} // 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) << "UUID size is expected to be " << kExpectedUUIDSize + << " but is " << data.size() << " and the data in hex is " + << base::HexEncode(data.data(), 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; +} + +bool WriteMpdToFile(const std::string& output_path, MpdBuilder* mpd_builder) { + CHECK(!output_path.empty()); + + std::string mpd; + if (!mpd_builder->ToString(&mpd)) { + LOG(ERROR) << "Failed to write MPD to string."; + return false; + } + + scoped_ptr file(File::Open(output_path.c_str(), "w")); + if (!file) { + LOG(ERROR) << "Failed to open file for writing: " << output_path; + return false; + } + + const char* mpd_char_ptr = mpd.data(); + size_t mpd_bytes_left = mpd.size(); + while (mpd_bytes_left > 0) { + int64_t length = file->Write(mpd_char_ptr, mpd_bytes_left); + if (length <= 0) { + LOG(ERROR) << "Failed to write to file '" << output_path << "' (" + << length << ")."; + return false; + } + mpd_char_ptr += length; + mpd_bytes_left -= length; + } + // Release the pointer because Close() destructs itself. + return file.release()->Close(); +} + +ContentType GetContentType(const MediaInfo& media_info) { + const bool has_video = media_info.has_video_info(); + const bool has_audio = media_info.has_audio_info(); + const bool has_text = media_info.has_text_info(); + + if (MoreThanOneTrue(has_video, has_audio, has_text)) { + NOTIMPLEMENTED() << "MediaInfo with more than one stream is not supported."; + return kContentTypeUnknown; + } + if (!AtLeastOneTrue(has_video, has_audio, has_text)) { + LOG(ERROR) << "MediaInfo should contain one audio, video, or text stream."; + return kContentTypeUnknown; + } + return has_video ? kContentTypeVideo + : (has_audio ? kContentTypeAudio : kContentTypeText); +} + +void AddContentProtectionElements(const MediaInfo& media_info, + AdaptationSet* adaptation_set) { + AddContentProtectionElementsHelper(media_info, adaptation_set); +} + +void AddContentProtectionElements(const MediaInfo& media_info, + Representation* representation) { + AddContentProtectionElementsHelper(media_info, representation); +} + +} // namespace edash_packager diff --git a/packager/mpd/base/mpd_notifier_util.h b/packager/mpd/base/mpd_notifier_util.h new file mode 100644 index 0000000000..5016a19840 --- /dev/null +++ b/packager/mpd/base/mpd_notifier_util.h @@ -0,0 +1,63 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +/// This file contains helper functions and enums for MpdNotifier +/// implementations. + +#ifndef MPD_BASE_MPD_NOTIFIER_UTIL_H_ +#define MPD_BASE_MPD_NOTIFIER_UTIL_H_ + +#include + +#include "packager/base/base64.h" +#include "packager/mpd/base/media_info.pb.h" +#include "packager/mpd/base/mpd_builder.h" + +namespace edash_packager{ + +enum ContentType { + kContentTypeUnknown, + kContentTypeVideo, + kContentTypeAudio, + kContentTypeText +}; + +/// Converts hex data to UUID format. Hex data must be size 16. +/// @param data input hex data. +/// @param uuid_format is the UUID format of the input. +bool HexToUUID(const std::string& data, std::string* uuid_format); + +/// Outputs MPD to @a output_path. +/// @param output_path is the path to the MPD output location. +/// @param mpd_builder is the MPD builder instance. +bool WriteMpdToFile(const std::string& output_path, MpdBuilder* mpd_builder); + +/// Determines the content type of |media_info|. +/// @param media_info is the information about the media. +/// @return content type of the @a media_info. +ContentType GetContentType(const MediaInfo& media_info); + +/// Adds elements specified by @a media_info to +/// @a adaptation_set. +/// Note that this will add the elements as direct chlidren of AdaptationSet. +/// @param media_info may or may not have protected_content field. +/// @param adaptation_set is the parent element that owns the ContentProtection +/// elements. +void AddContentProtectionElements(const MediaInfo& media_info, + AdaptationSet* adaptation_set); + +/// Adds elements specified by @a media_info to +/// @a representation. +/// @param media_info may or may not have protected_content field. +/// @param representation is the parent element that owns the ContentProtection +/// elements. +void AddContentProtectionElements(const MediaInfo& media_info, + Representation* representation); + + +} // namespace edash_packager + +#endif // MPD_BASE_MPD_NOTIFIER_UTIL_H_ diff --git a/packager/mpd/base/simple_mpd_notifier.cc b/packager/mpd/base/simple_mpd_notifier.cc index 6ee4abc689..4241da533f 100644 --- a/packager/mpd/base/simple_mpd_notifier.cc +++ b/packager/mpd/base/simple_mpd_notifier.cc @@ -6,128 +6,13 @@ #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_notifier_util.h" #include "packager/mpd/base/mpd_utils.h" -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, @@ -155,7 +40,7 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, DCHECK(container_id); ContentType content_type = GetContentType(media_info); - if (content_type == kUnknown) + if (content_type == kContentTypeUnknown) return false; base::AutoLock auto_lock(lock_); @@ -177,11 +62,13 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, if (representation == NULL) return false; + // For SimpleMpdNotifier, just put it in Representation. It should still + // generate a valid MPD. AddContentProtectionElements(media_info, representation); *container_id = representation->id(); if (mpd_builder_->type() == MpdBuilder::kStatic) - return WriteMpdToFile(); + return WriteMpdToFile(output_path_, mpd_builder_.get()); DCHECK(!ContainsKey(representation_map_, representation->id())); representation_map_[representation->id()] = representation; @@ -212,7 +99,7 @@ bool SimpleMpdNotifier::NotifyNewSegment(uint32_t container_id, return false; } it->second->AddNewSegment(start_time, duration, size); - return WriteMpdToFile(); + return WriteMpdToFile(output_path_, mpd_builder_.get()); } bool SimpleMpdNotifier::AddContentProtectionElement( @@ -222,51 +109,4 @@ bool SimpleMpdNotifier::AddContentProtectionElement( return false; } -SimpleMpdNotifier::ContentType SimpleMpdNotifier::GetContentType( - const MediaInfo& media_info) { - const bool has_video = media_info.has_video_info(); - const bool has_audio = media_info.has_audio_info(); - const bool has_text = media_info.has_text_info(); - - if (MoreThanOneTrue(has_video, has_audio, has_text)) { - NOTIMPLEMENTED() << "MediaInfo with more than one stream is not supported."; - return kUnknown; - } - if (!AtLeastOneTrue(has_video, has_audio, has_text)) { - LOG(ERROR) << "MediaInfo should contain one audio, video, or text stream."; - return kUnknown; - } - return has_video ? kVideo : (has_audio ? kAudio : kText); -} - -bool SimpleMpdNotifier::WriteMpdToFile() { - CHECK(!output_path_.empty()); - - std::string mpd; - if (!mpd_builder_->ToString(&mpd)) { - LOG(ERROR) << "Failed to write MPD to string."; - return false; - } - - File* file = File::Open(output_path_.c_str(), "w"); - if (!file) { - LOG(ERROR) << "Failed to open file for writing: " << output_path_; - return false; - } - - const char* mpd_char_ptr = mpd.data(); - size_t mpd_bytes_left = mpd.size(); - while (mpd_bytes_left > 0) { - int64_t length = file->Write(mpd_char_ptr, mpd_bytes_left); - if (length <= 0) { - LOG(ERROR) << "Failed to write to file '" << output_path_ << "' (" - << length << ")."; - return false; - } - mpd_char_ptr += length; - mpd_bytes_left -= length; - } - return file->Close(); -} - } // namespace edash_packager diff --git a/packager/mpd/base/simple_mpd_notifier.h b/packager/mpd/base/simple_mpd_notifier.h index 49bb499314..7df6cd4b8d 100644 --- a/packager/mpd/base/simple_mpd_notifier.h +++ b/packager/mpd/base/simple_mpd_notifier.h @@ -15,6 +15,7 @@ #include "packager/base/memory/scoped_ptr.h" #include "packager/base/synchronization/lock.h" #include "packager/mpd/base/mpd_notifier.h" +#include "packager/mpd/base/mpd_notifier_util.h" namespace edash_packager { @@ -64,19 +65,9 @@ class SimpleMpdNotifier : public MpdNotifier { mpd_builder_ = mpd_builder.Pass(); } - enum ContentType { - kUnknown, - kVideo, - kAudio, - kText - }; - ContentType GetContentType(const MediaInfo& media_info); - bool WriteMpdToFile(); - + // MPD output path. std::string output_path_; - scoped_ptr mpd_builder_; - base::Lock lock_; // [type][lang] = AdaptationSet diff --git a/packager/mpd/base/simple_mpd_notifier_unittest.cc b/packager/mpd/base/simple_mpd_notifier_unittest.cc index 23ef90f61e..f8a1294643 100644 --- a/packager/mpd/base/simple_mpd_notifier_unittest.cc +++ b/packager/mpd/base/simple_mpd_notifier_unittest.cc @@ -31,13 +31,15 @@ const char kValidMediaInfo[] = " pixel_height: 1\n" "}\n" "container_type: 1\n"; +const uint32_t kDefaultAdaptationSetId = 0u; } // namespace class SimpleMpdNotifierTest : public ::testing::TestWithParam { protected: SimpleMpdNotifierTest() - : default_mock_adaptation_set_(new MockAdaptationSet()) {} + : default_mock_adaptation_set_( + new MockAdaptationSet(kDefaultAdaptationSetId)) {} virtual void SetUp() OVERRIDE { ASSERT_TRUE(base::CreateTemporaryFile(&temp_file_path_)); @@ -66,7 +68,7 @@ class SimpleMpdNotifierTest } // Use output_path_ for specifying the MPD output path so that - // SimpleMpdNotifier::WriteMpdToFile() doesn't crash. + // WriteMpdToFile() doesn't crash. std::string output_path_; const MpdOptions empty_mpd_option_; const std::vector empty_base_urls_; diff --git a/packager/mpd/mpd.gyp b/packager/mpd/mpd.gyp index 1c7f994396..faa61299f9 100644 --- a/packager/mpd/mpd.gyp +++ b/packager/mpd/mpd.gyp @@ -37,10 +37,14 @@ 'base/bandwidth_estimator.h', 'base/content_protection_element.cc', 'base/content_protection_element.h', + 'base/dash_iop_mpd_notifier.cc', + 'base/dash_iop_mpd_notifier.h', 'base/language_utils.cc', 'base/language_utils.h', 'base/mpd_builder.cc', 'base/mpd_builder.h', + 'base/mpd_notifier_util.cc', + 'base/mpd_notifier_util.h', 'base/mpd_notifier.h', 'base/mpd_options.h', 'base/mpd_utils.cc', @@ -68,8 +72,9 @@ 'type': '<(gtest_target_type)', 'sources': [ 'base/bandwidth_estimator_unittest.cc', - 'base/mock_mpd_builder.h', + 'base/dash_iop_mpd_notifier_unittest.cc', 'base/mock_mpd_builder.cc', + 'base/mock_mpd_builder.h', 'base/mpd_builder_unittest.cc', 'base/simple_mpd_notifier_unittest.cc', 'base/xml/xml_node_unittest.cc',